fix(models): never auto-switch to a provider the user has no credentials for

/model <name> on provider A, where the name is only known to provider B
(static catalog or OpenRouter), switched the session to B even when B had
no key: an immediate 401 for most vendors, and for OpenRouter — whose
runtime resolves with an EMPTY key instead of raising — a silent switch
onto a metered aggregator. The dashboard's flat Model field had two more
copies of the same guess ("vendor/model on a native provider" → openrouter).

detect_provider_for_model() now walks its ladder as candidates and skips any
target without credentials (env/.env key, auth-store login, or a usable
credential pool entry). Exceptions: the user NAMED the provider (/model nous)
or there is no current provider yet ("auto") — then the guess is handed back
so the credential step fails loudly instead of silently ignoring input. A
vendor/ prefix naming a provider declared in `providers:` is a selection, not
a guess, and always routes. The dashboard fallbacks apply the same gate.

Tests that pinned "switch to OpenRouter/vendor with no key" now grant the
credential they assumed; two new invariants cover the gate.
This commit is contained in:
Teknium
2026-09-10 03:37:28 -07:00
parent a0749d583a
commit 2466684db5
9 changed files with 169 additions and 31 deletions
+13 -4
View File
@@ -426,7 +426,12 @@ def _normalize_main_model_assignment(provider: str, model: str) -> tuple[str, st
canonical = normalize_provider(cur_provider)
prov_in = cur_provider
else:
canonical = prov_in = "openrouter"
from hermes_cli.models_detect import provider_has_credentials
# Only guess OpenRouter when the user actually holds a key for it; otherwise keep the
# pair as sent rather than persisting a provider they never selected.
if provider_has_credentials("openrouter"):
canonical = prov_in = "openrouter"
if canonical in _KNOWN_PROVIDER_NAMES and not canonical.startswith("custom"):
try:
@@ -770,11 +775,15 @@ def _infer_provider_on_model_change(model_val: str, prev_provider: str) -> tuple
if "/" in name:
try:
from hermes_cli.models_detect import provider_has_credentials
cur_is_aggregator = normalize_provider(prev_provider) in _AGGREGATOR_PROVIDERS
# A vendor slug on a native provider is a guess at an aggregator; never guess one the
# user has no key for — that silently writes a metered provider into config.yaml.
if not cur_is_aggregator and provider_has_credentials("openrouter"):
return "openrouter", name
except Exception:
cur_is_aggregator = False
if not cur_is_aggregator:
return "openrouter", name
pass
return "", name