From 9a3a15c87ae228f2f89f6d4e40285f0f92f45e3a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 10 Sep 2026 06:03:40 -0700 Subject: [PATCH] fix(models): a vendor's own id on its first-party provider never re-routes 215fd0ecb9a made the current provider's live catalog outrank static guesses, but a live catalog that could not be fetched (Codex outage, cold cache) looked identical to "not served", so `/model gpt-6-astra` on openai-codex still walked to OpenRouter (which relists every vendor) whenever an OpenRouter key existed. Same shape for grok-* on xai-oauth and claude-* on anthropic. detect_provider_for_model now treats a vendor id typed on that vendor's single-vendor first-party provider as a selection: stay, let the vendor accept or reject it (the existing "not found in listing" note still fires). Cross-vendor remaps (claude id on Codex -> keyed OpenRouter) and aggregator / custom / multi-vendor reseller sessions are unchanged. --- hermes_cli/models.py | 7 ++++++- hermes_cli/models_detect.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/hermes_cli/models.py b/hermes_cli/models.py index fc70420a09..bdf0ce3240 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -938,7 +938,8 @@ def detect_provider_for_model( skipped and the ladder continues (``None`` = stay on the current provider). Exceptions: the user NAMED the provider (``/model nous``), or there is no current provider yet (``auto``) — then the first guess is returned so the credential step fails loudly instead of silently ignoring input.""" - from hermes_cli.models_detect import current_provider_catalog_match, provider_has_credentials + from hermes_cli.models_detect import ( + current_provider_catalog_match, current_provider_owns_vendor, provider_has_credentials) name = (model_name or "").strip() if not name: @@ -950,6 +951,10 @@ def detect_provider_for_model( served = current_provider_catalog_match(name, current_provider) if served is not None: return (current_provider, served) if served != name else None + # Live catalog unavailable or lagging: the vendor's own id on the vendor's first-party provider + # is still a selection — an aggregator relisting it is not grounds to switch. + if current_provider_owns_vendor(name, current_provider): + return None no_selection = (current_provider or "").strip().lower() in {"", "auto"} for candidate in _detection_candidates(name, current_provider): diff --git a/hermes_cli/models_detect.py b/hermes_cli/models_detect.py index 19ebb44699..0ff0f9c6e2 100644 --- a/hermes_cli/models_detect.py +++ b/hermes_cli/models_detect.py @@ -44,6 +44,31 @@ def current_provider_catalog_match(model_name: str, current_provider: str) -> Op (mid for mid in catalog if "/" in mid and mid.split("/", 1)[1].lower() == wanted), None) +def current_provider_owns_vendor(model_name: str, current_provider: str) -> bool: + """True when *model_name* belongs to the vendor a single-vendor first-party provider natively + serves (``gpt-6-astra`` on ``openai-codex``, ``grok-4.6`` on ``xai-oauth``). + + A first-party session plus that vendor's own id is a selection, not a guess: when the live + catalog could not confirm the id (fetch failed, static fallback lags an early-access rollout) + the answer is "stay and let the vendor accept or reject it", never "a reseller lists it, so + switch there". Aggregators, custom endpoints and multi-vendor resellers (nvidia, alibaba, ...) + have no single native vendor and are skipped.""" + from hermes_cli.model_normalize import detect_vendor + from hermes_cli.models import _AGGREGATOR_PROVIDERS, _PROVIDER_MODELS, normalize_provider + + provider = (current_provider or "").strip().lower() + if provider in _SKIP or provider.startswith("custom:"): + return False + normalized = normalize_provider(provider) + if normalized in _SKIP or normalized in _AGGREGATOR_PROVIDERS: + return False + vendor = detect_vendor(model_name or "") + if not vendor: + return False + native = {detect_vendor(mid) for mid in _PROVIDER_MODELS.get(normalized, ())} - {None} + return native == {vendor} + + def provider_has_credentials(provider: str) -> bool: """Whether *provider* can be switched to without the user typing a key: env/.env key, auth store login, or a usable credential-pool entry. ``custom``/``custom:*`` targets only come out