mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
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:
parent
a7784f11fb
commit
97375e0f06
2 changed files with 52 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue