diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 9941760aad10..40b64fb58204 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -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 diff --git a/tests/test_empty_model_fallback.py b/tests/test_empty_model_fallback.py index 410211f06faa..335d38df1c10 100644 --- a/tests/test_empty_model_fallback.py +++ b/tests/test_empty_model_fallback.py @@ -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."""