Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3320,11 +3320,26 @@ def _ensure_runtime_credentials(self) -> bool:

_primary_exc = None
runtime = None
direct_alias = None
resolved_model_from_alias = None
requested_provider = self.requested_provider
explicit_base_url = self._explicit_base_url
try:
from hermes_cli.model_switch import resolve_direct_alias
direct_alias = resolve_direct_alias(self.model)
except Exception:
direct_alias = None
if direct_alias is not None:
resolved_model_from_alias = direct_alias.model
requested_provider = direct_alias.provider or requested_provider
explicit_base_url = explicit_base_url or direct_alias.base_url or None

try:
runtime = resolve_runtime_provider(
requested=self.requested_provider,
requested=requested_provider,
explicit_api_key=self._explicit_api_key,
explicit_base_url=self._explicit_base_url,
explicit_base_url=explicit_base_url,
target_model=resolved_model_from_alias or self.model or None,
)
except Exception as exc:
_primary_exc = exc
Expand Down Expand Up @@ -3403,6 +3418,8 @@ def _ensure_runtime_credentials(self) -> bool:
self._provider_source = runtime.get("source")
self.api_key = api_key
self.base_url = base_url
if resolved_model_from_alias:
self.model = resolved_model_from_alias

# When a custom_provider entry carries an explicit `model` field,
# use it as the effective model name. Without this, running
Expand Down
33 changes: 32 additions & 1 deletion gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1461,7 +1461,38 @@ def _resolve_session_agent_runtime(
list(self._session_model_overrides.keys())[:5] if self._session_model_overrides else "[]",
)

runtime_kwargs = _resolve_runtime_agent_kwargs()
direct_alias = None
try:
from hermes_cli.model_switch import resolve_direct_alias
direct_alias = resolve_direct_alias(model)
except Exception:
direct_alias = None

if direct_alias is not None:
from hermes_cli.runtime_provider import (
resolve_runtime_provider,
format_runtime_provider_error,
)
try:
runtime = resolve_runtime_provider(
requested=direct_alias.provider,
explicit_base_url=direct_alias.base_url or None,
target_model=direct_alias.model,
)
except Exception as exc:
raise RuntimeError(format_runtime_provider_error(exc)) from exc
model = direct_alias.model
runtime_kwargs = {
"api_key": runtime.get("api_key"),
"base_url": runtime.get("base_url"),
"provider": runtime.get("provider"),
"api_mode": runtime.get("api_mode"),
"command": runtime.get("command"),
"args": list(runtime.get("args") or []),
"credential_pool": runtime.get("credential_pool"),
}
else:
runtime_kwargs = _resolve_runtime_agent_kwargs()
if override and resolved_session_key:
model, runtime_kwargs = self._apply_session_model_override(
resolved_session_key, model, runtime_kwargs
Expand Down
8 changes: 8 additions & 0 deletions hermes_cli/model_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ def _ensure_direct_aliases() -> None:
DIRECT_ALIASES.update(_load_direct_aliases())


def resolve_direct_alias(raw_input: str) -> Optional[DirectAlias]:
"""Return an exact config-defined model alias without catalog fallback."""
if not isinstance(raw_input, str) or not raw_input.strip():
return None
_ensure_direct_aliases()
return DIRECT_ALIASES.get(raw_input.strip().lower())


# ---------------------------------------------------------------------------
# Result dataclasses
# ---------------------------------------------------------------------------
Expand Down
11 changes: 11 additions & 0 deletions hermes_cli/oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ def _run_agent(
# the caller just asked for.
effective_provider = (provider or "").strip() or None
explicit_base_url_from_alias: Optional[str] = None
try:
from hermes_cli.model_switch import resolve_direct_alias
direct_alias = resolve_direct_alias(effective_model)
except Exception:
direct_alias = None
if direct_alias is not None:
effective_model = direct_alias.model
effective_provider = effective_provider or direct_alias.provider
if direct_alias.base_url:
explicit_base_url_from_alias = direct_alias.base_url.rstrip("/")

if effective_provider is None and (model or env_model):
# Only auto-detect when the model was explicitly requested via arg or
# env var (not when it came from config — that's the "use my defaults"
Expand Down
34 changes: 34 additions & 0 deletions tests/cli/test_cli_provider_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,40 @@ def _runtime_resolve(**kwargs):
assert shell.api_mode == "codex_responses"


def test_runtime_resolution_resolves_direct_model_alias(monkeypatch):
cli = _import_cli()
calls = {}

def _runtime_resolve(**kwargs):
calls.update(kwargs)
return {
"provider": "custom",
"api_mode": "chat_completions",
"base_url": kwargs["explicit_base_url"],
"api_key": "test-key",
"source": "direct-alias",
}

monkeypatch.setattr(
"hermes_cli.model_switch.resolve_direct_alias",
lambda model: SimpleNamespace(
model="claude-opus-4-6",
provider="custom",
base_url="https://custom.example/v1",
) if model == "opus" else None,
)
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))

shell = cli.HermesCLI(model="opus", provider="custom", compact=True, max_turns=1)

assert shell._ensure_runtime_credentials() is True
assert calls["requested"] == "custom"
assert calls["explicit_base_url"] == "https://custom.example/v1"
assert calls["target_model"] == "claude-opus-4-6"
assert shell.model == "claude-opus-4-6"


def test_cli_turn_routing_uses_primary_when_disabled(monkeypatch):
cli = _import_cli()
shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1)
Expand Down
35 changes: 35 additions & 0 deletions tests/gateway/test_session_model_override_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,41 @@ def _explode_runtime_resolution():
)


def test_gateway_runtime_resolves_direct_model_alias(monkeypatch):
runner = _make_runner()
calls = {}

monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "opus")
monkeypatch.setattr(
"hermes_cli.model_switch.resolve_direct_alias",
lambda model: types.SimpleNamespace(
model="claude-opus-4-6",
provider="custom",
base_url="https://custom.example/v1",
) if model == "opus" else None,
)

def _runtime_resolve(**kwargs):
calls.update(kwargs)
return {
"provider": "custom",
"api_mode": "chat_completions",
"base_url": kwargs["explicit_base_url"],
"api_key": "test-key",
}

monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)

model, runtime = runner._resolve_session_agent_runtime()

assert model == "claude-opus-4-6"
assert calls["requested"] == "custom"
assert calls["explicit_base_url"] == "https://custom.example/v1"
assert calls["target_model"] == "claude-opus-4-6"
assert runtime["provider"] == "custom"
assert runtime["base_url"] == "https://custom.example/v1"


def test_run_agent_prefers_session_override_over_global_runtime(monkeypatch):
monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
monkeypatch.setattr(gateway_run, "load_dotenv", lambda *args, **kwargs: None)
Expand Down
53 changes: 53 additions & 0 deletions tests/hermes_cli/test_oneshot_model_aliases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import sys
import types
from types import SimpleNamespace


def test_oneshot_resolves_direct_model_alias_with_explicit_provider(monkeypatch):
captured = {}

class _Agent:
def __init__(self, **kwargs):
captured.update(kwargs)

def chat(self, prompt):
return "ok"

fake_run_agent = types.ModuleType("run_agent")
fake_run_agent.AIAgent = _Agent
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
monkeypatch.setattr("hermes_cli.config.load_config", lambda: {"model": {}})
monkeypatch.setattr(
"hermes_cli.model_switch.resolve_direct_alias",
lambda model: SimpleNamespace(
model="claude-opus-4-6",
provider="custom",
base_url="https://custom.example/v1",
) if model == "opus" else None,
)

def _runtime_resolve(**kwargs):
assert kwargs["requested"] == "custom"
assert kwargs["explicit_base_url"] == "https://custom.example/v1"
assert kwargs["target_model"] == "claude-opus-4-6"
return {
"provider": "custom",
"api_mode": "chat_completions",
"base_url": "https://custom.example/v1",
"api_key": "test-key",
}

monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)

from hermes_cli.oneshot import _run_agent

assert _run_agent(
"hello",
model="opus",
provider="custom",
toolsets=[],
use_config_toolsets=False,
) == "ok"
assert captured["model"] == "claude-opus-4-6"
assert captured["provider"] == "custom"
assert captured["base_url"] == "https://custom.example/v1"
Loading