mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 01:31:41 +00:00
fix(tui): resolve startup model aliases statically
- expand short model aliases like sonnet/opus via static catalogs during startup runtime resolution - keep startup alias resolution network-free and add regression tests in models and tui gateway suites
This commit is contained in:
parent
48bdd2445e
commit
fdcbd2257b
3 changed files with 87 additions and 3 deletions
|
|
@ -1393,6 +1393,59 @@ def _model_in_provider_catalog(name_lower: str, providers: set[str]) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_AGGREGATOR_PROVIDERS = frozenset(
|
||||||
|
{"nous", "openrouter", "ai-gateway", "copilot", "kilocode"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_static_model_alias(
|
||||||
|
name_lower: str,
|
||||||
|
current_keys: set[str],
|
||||||
|
) -> Optional[tuple[str, str]]:
|
||||||
|
"""Resolve short aliases (e.g. sonnet/opus) using static catalogs only."""
|
||||||
|
try:
|
||||||
|
from hermes_cli.model_switch import MODEL_ALIASES
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
identity = MODEL_ALIASES.get(name_lower)
|
||||||
|
if identity is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
vendor = identity.vendor
|
||||||
|
family = identity.family
|
||||||
|
|
||||||
|
def _match(provider: str) -> Optional[str]:
|
||||||
|
models = _PROVIDER_MODELS.get(provider, [])
|
||||||
|
if not models:
|
||||||
|
return None
|
||||||
|
prefix = (
|
||||||
|
f"{vendor}/{family}"
|
||||||
|
if provider in _AGGREGATOR_PROVIDERS
|
||||||
|
else family
|
||||||
|
).lower()
|
||||||
|
for model in models:
|
||||||
|
if model.lower().startswith(prefix):
|
||||||
|
return model
|
||||||
|
return None
|
||||||
|
|
||||||
|
for provider in current_keys:
|
||||||
|
if matched := _match(provider):
|
||||||
|
return provider, matched
|
||||||
|
|
||||||
|
for provider in _PROVIDER_MODELS:
|
||||||
|
if provider in current_keys or provider in _AGGREGATOR_PROVIDERS:
|
||||||
|
continue
|
||||||
|
if matched := _match(provider):
|
||||||
|
return provider, matched
|
||||||
|
|
||||||
|
for provider in _AGGREGATOR_PROVIDERS:
|
||||||
|
if provider in current_keys and (matched := _match(provider)):
|
||||||
|
return provider, matched
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def detect_static_provider_for_model(
|
def detect_static_provider_for_model(
|
||||||
model_name: str,
|
model_name: str,
|
||||||
current_provider: str,
|
current_provider: str,
|
||||||
|
|
@ -1409,6 +1462,10 @@ def detect_static_provider_for_model(
|
||||||
name_lower = name.lower()
|
name_lower = name.lower()
|
||||||
current_keys = _provider_keys(current_provider)
|
current_keys = _provider_keys(current_provider)
|
||||||
|
|
||||||
|
alias_match = _resolve_static_model_alias(name_lower, current_keys)
|
||||||
|
if alias_match:
|
||||||
|
return alias_match
|
||||||
|
|
||||||
# --- Step 0: bare provider name typed as model ---
|
# --- Step 0: bare provider name typed as model ---
|
||||||
# If someone types `/model nous` or `/model anthropic`, treat it as a
|
# If someone types `/model nous` or `/model anthropic`, treat it as a
|
||||||
# provider switch and pick the first model from that provider's catalog.
|
# provider switch and pick the first model from that provider's catalog.
|
||||||
|
|
@ -1425,15 +1482,13 @@ def detect_static_provider_for_model(
|
||||||
return (resolved_provider, default_models[0])
|
return (resolved_provider, default_models[0])
|
||||||
|
|
||||||
# Aggregators list other providers' models — never auto-switch TO them
|
# Aggregators list other providers' models — never auto-switch TO them
|
||||||
_AGGREGATORS = {"nous", "openrouter", "ai-gateway", "copilot", "kilocode"}
|
|
||||||
|
|
||||||
# If the model belongs to the current provider's catalog, don't suggest switching
|
# If the model belongs to the current provider's catalog, don't suggest switching
|
||||||
if _model_in_provider_catalog(name_lower, current_keys):
|
if _model_in_provider_catalog(name_lower, current_keys):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- Step 1: check static provider catalogs for a direct match ---
|
# --- Step 1: check static provider catalogs for a direct match ---
|
||||||
for pid, models in _PROVIDER_MODELS.items():
|
for pid, models in _PROVIDER_MODELS.items():
|
||||||
if pid in current_keys or pid in _AGGREGATORS:
|
if pid in current_keys or pid in _AGGREGATOR_PROVIDERS:
|
||||||
continue
|
continue
|
||||||
if any(name_lower == m.lower() for m in models):
|
if any(name_lower == m.lower() for m in models):
|
||||||
return (pid, name)
|
return (pid, name)
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,17 @@ class TestDetectProviderForModel:
|
||||||
"""Models belonging to the current provider should not trigger a switch."""
|
"""Models belonging to the current provider should not trigger a switch."""
|
||||||
assert detect_provider_for_model("gpt-5.3-codex", "openai-codex") is None
|
assert detect_provider_for_model("gpt-5.3-codex", "openai-codex") is None
|
||||||
|
|
||||||
|
def test_short_alias_resolves_to_static_model(self):
|
||||||
|
"""Short aliases (e.g. sonnet) should resolve without network lookups."""
|
||||||
|
with patch(
|
||||||
|
"hermes_cli.models.fetch_openrouter_models",
|
||||||
|
side_effect=AssertionError("network lookup should not run"),
|
||||||
|
):
|
||||||
|
result = detect_provider_for_model("sonnet", "auto")
|
||||||
|
assert result is not None
|
||||||
|
assert result[0] == "anthropic"
|
||||||
|
assert result[1].startswith("claude-sonnet")
|
||||||
|
|
||||||
def test_openrouter_slug_match(self):
|
def test_openrouter_slug_match(self):
|
||||||
"""Models in the OpenRouter catalog should be found."""
|
"""Models in the OpenRouter catalog should be found."""
|
||||||
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
|
with patch("hermes_cli.models.fetch_openrouter_models", return_value=LIVE_OPENROUTER_MODELS):
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,24 @@ def test_startup_runtime_detects_provider_for_model_env(monkeypatch):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_startup_runtime_resolves_short_alias_without_network(monkeypatch):
|
||||||
|
monkeypatch.setenv("HERMES_MODEL", "sonnet")
|
||||||
|
monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False)
|
||||||
|
monkeypatch.delenv("HERMES_INFERENCE_PROVIDER", raising=False)
|
||||||
|
monkeypatch.setattr(server, "_load_cfg", lambda: {"model": {"provider": "auto"}})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"hermes_cli.models.fetch_openrouter_models",
|
||||||
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||||
|
AssertionError("network lookup should not run")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
model, provider = server._resolve_startup_runtime()
|
||||||
|
|
||||||
|
assert provider == "anthropic"
|
||||||
|
assert model.startswith("claude-sonnet")
|
||||||
|
|
||||||
|
|
||||||
def test_startup_runtime_does_not_call_network_detector(monkeypatch):
|
def test_startup_runtime_does_not_call_network_detector(monkeypatch):
|
||||||
monkeypatch.setenv("HERMES_MODEL", "sonnet")
|
monkeypatch.setenv("HERMES_MODEL", "sonnet")
|
||||||
monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False)
|
monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue