fix(models): route bare-provider /model switch through the cost-safe default

`detect_static_provider_for_model` handles a bare provider name typed as a
model (e.g. `/model nous`) by returning `_PROVIDER_MODELS[provider][0]` — the
first curated entry. For metered aggregators whose curated list is ordered
most-capable-first (Nous Portal), entry [0] is the priciest flagship, so
`/model nous` silently switched to it.

This is exactly the billing footgun `_PROVIDER_SILENT_DEFAULT_OVERRIDES` /
`get_default_model_for_provider` exist to prevent (per their docstring, a
missing model "escalated to Opus and billed 863 requests before the user
noticed"). The non-interactive fallback already routes through that cost-safe
helper; the interactive `/model <provider>` path did not.

Route this path through `get_default_model_for_provider` too. Providers
without a silent-default override are unchanged (the helper returns
`models[0]`), so only overridden providers (currently `nous`) change — from
the flagship to the low-cost default.

Adds regression tests: `/model nous` resolves to the cost-safe default, and
non-overridden providers still resolve to their first catalog model.
This commit is contained in:
HumphreySun98 2026-07-08 16:34:36 -05:00 committed by Teknium
parent a7784f11fb
commit 97375e0f06
2 changed files with 52 additions and 1 deletions

View file

@ -1976,7 +1976,18 @@ def detect_static_provider_for_model(
and default_models
and resolved_provider not in current_keys
):
return (resolved_provider, default_models[0])
# Route through the cost-safe default rather than picking
# ``default_models[0]`` directly. For metered aggregators whose
# curated list is ordered most-capable-first (e.g. Nous Portal),
# entry [0] is the priciest flagship, and typing ``/model nous``
# would silently escalate to it — the exact billing footgun
# ``_PROVIDER_SILENT_DEFAULT_OVERRIDES`` exists to prevent. For
# providers without an override this is unchanged (it returns
# ``models[0]``).
return (
resolved_provider,
get_default_model_for_provider(resolved_provider) or default_models[0],
)
# Aggregators list other providers' models — never auto-switch TO them
# If the model belongs to the current provider's catalog, don't suggest switching

View file

@ -76,6 +76,46 @@ class TestGetDefaultModelForProvider:
assert result == models_mod._PROVIDER_MODELS["openai-codex"][0]
class TestDetectStaticProviderCostSafeDefault:
"""detect_static_provider_for_model must apply the same cost-safe default
as get_default_model_for_provider when a bare provider name is typed as a
model (e.g. ``/model nous``)."""
def test_bare_nous_does_not_escalate_to_flagship(self):
from hermes_cli.models import (
_PROVIDER_MODELS,
get_default_model_for_provider,
detect_static_provider_for_model,
)
result = detect_static_provider_for_model("nous", "openrouter")
assert result is not None
provider, model = result
assert provider == "nous"
# Must match the cost-safe silent default, NOT the priciest catalog
# entry [0]. Regression: this path returned _PROVIDER_MODELS["nous"][0]
# directly, re-introducing the billing footgun on the interactive
# ``/model nous`` path.
assert model == get_default_model_for_provider("nous")
assert "opus" not in model.lower()
assert model != _PROVIDER_MODELS["nous"][0]
def test_provider_without_override_still_uses_first_model(self):
"""Providers with no silent-default override are unchanged."""
from hermes_cli.models import (
_PROVIDER_MODELS,
_PROVIDER_SILENT_DEFAULT_OVERRIDES,
detect_static_provider_for_model,
)
for provider in ("anthropic", "xai"):
if provider in _PROVIDER_SILENT_DEFAULT_OVERRIDES:
continue
result = detect_static_provider_for_model(provider, "openrouter")
assert result is not None
assert result[1] == _PROVIDER_MODELS[provider][0]
class TestGatewayEmptyModelFallback:
"""Test that _resolve_session_agent_runtime fills in empty model from provider catalog."""