fix(models): a vendor's own id on its first-party provider never re-routes

215fd0ecb9 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.
This commit is contained in:
Teknium
2026-09-10 06:03:40 -07:00
parent 7e0b5cd235
commit 9a3a15c87a
2 changed files with 31 additions and 1 deletions

View File

@@ -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):

View File

@@ -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