Skip to content

Commit e9c47c7

Browse files
committed
fix(tui): honor launch model overrides
1 parent ee0728c commit e9c47c7

4 files changed

Lines changed: 152 additions & 9 deletions

File tree

hermes_cli/main.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,12 @@ def _node_bin(bin: str) -> str:
10281028
return [node, str(root / "dist" / "entry.js")], root
10291029

10301030

1031-
def _launch_tui(resume_session_id: Optional[str] = None, tui_dev: bool = False):
1031+
def _launch_tui(
1032+
resume_session_id: Optional[str] = None,
1033+
tui_dev: bool = False,
1034+
model: Optional[str] = None,
1035+
provider: Optional[str] = None,
1036+
):
10321037
"""Replace current process with the TUI."""
10331038
tui_dir = PROJECT_ROOT / "ui-tui"
10341039

@@ -1038,6 +1043,12 @@ def _launch_tui(resume_session_id: Optional[str] = None, tui_dev: bool = False):
10381043
)
10391044
env.setdefault("HERMES_PYTHON", sys.executable)
10401045
env.setdefault("HERMES_CWD", os.getcwd())
1046+
if model:
1047+
env["HERMES_MODEL"] = model
1048+
env["HERMES_INFERENCE_MODEL"] = model
1049+
if provider:
1050+
env["HERMES_TUI_PROVIDER"] = provider
1051+
env["HERMES_INFERENCE_PROVIDER"] = provider
10411052
# Guarantee an 8GB V8 heap + exposed GC for the TUI. Default node cap is
10421053
# ~1.5–4GB depending on version and can fatal-OOM on long sessions with
10431054
# large transcripts / reasoning blobs. Token-level merge: respect any
@@ -1176,6 +1187,8 @@ def cmd_chat(args):
11761187
_launch_tui(
11771188
getattr(args, "resume", None),
11781189
tui_dev=getattr(args, "tui_dev", False),
1190+
model=getattr(args, "model", None),
1191+
provider=getattr(args, "provider", None),
11791192
)
11801193

11811194
# Import and run the CLI
@@ -6913,15 +6926,15 @@ def main():
69136926
default=None,
69146927
help=(
69156928
"Model override for this invocation (e.g. anthropic/claude-sonnet-4.6). "
6916-
"Applies to -z/--oneshot. Also settable via HERMES_INFERENCE_MODEL env var."
6929+
"Applies to -z/--oneshot and --tui. Also settable via HERMES_INFERENCE_MODEL env var."
69176930
),
69186931
)
69196932
parser.add_argument(
69206933
"--provider",
69216934
default=None,
69226935
help=(
69236936
"Provider override for this invocation (e.g. openrouter, anthropic). "
6924-
"Applies to -z/--oneshot. Also settable via HERMES_INFERENCE_PROVIDER env var."
6937+
"Applies to -z/--oneshot and --tui. Also settable via HERMES_INFERENCE_PROVIDER env var."
69256938
),
69266939
)
69276940
parser.add_argument(

tests/hermes_cli/test_tui_resume_flow.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from argparse import Namespace
2+
from pathlib import Path
23
import sys
34
import types
45

@@ -8,8 +9,11 @@
89
def _args(**overrides):
910
base = {
1011
"continue_last": None,
12+
"model": None,
13+
"provider": None,
1114
"resume": None,
1215
"tui": True,
16+
"tui_dev": False,
1317
}
1418
base.update(overrides)
1519
return Namespace(**base)
@@ -31,7 +35,7 @@ def fake_resolve_last(source="cli"):
3135
calls.append(source)
3236
return "20260408_235959_a1b2c3" if source == "tui" else None
3337

34-
def fake_launch(resume_session_id=None, tui_dev=False):
38+
def fake_launch(resume_session_id=None, tui_dev=False, model=None, provider=None):
3539
captured["resume"] = resume_session_id
3640
raise SystemExit(0)
3741

@@ -58,7 +62,7 @@ def fake_resolve_last(source="cli"):
5862
return "20260408_235959_d4e5f6"
5963
return None
6064

61-
def fake_launch(resume_session_id=None, tui_dev=False):
65+
def fake_launch(resume_session_id=None, tui_dev=False, model=None, provider=None):
6266
captured["resume"] = resume_session_id
6367
raise SystemExit(0)
6468

@@ -76,7 +80,7 @@ def fake_launch(resume_session_id=None, tui_dev=False):
7680
def test_cmd_chat_tui_resume_resolves_title_before_launch(monkeypatch, main_mod):
7781
captured = {}
7882

79-
def fake_launch(resume_session_id=None, tui_dev=False):
83+
def fake_launch(resume_session_id=None, tui_dev=False, model=None, provider=None):
8084
captured["resume"] = resume_session_id
8185
raise SystemExit(0)
8286

@@ -89,6 +93,60 @@ def fake_launch(resume_session_id=None, tui_dev=False):
8993
assert captured["resume"] == "20260409_000000_aa11bb"
9094

9195

96+
def test_cmd_chat_tui_passes_model_and_provider(monkeypatch, main_mod):
97+
captured = {}
98+
99+
def fake_launch(resume_session_id=None, tui_dev=False, model=None, provider=None):
100+
captured.update(
101+
{
102+
"model": model,
103+
"provider": provider,
104+
"resume": resume_session_id,
105+
"tui_dev": tui_dev,
106+
}
107+
)
108+
raise SystemExit(0)
109+
110+
monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)
111+
112+
with pytest.raises(SystemExit):
113+
main_mod.cmd_chat(
114+
_args(model="anthropic/claude-sonnet-4.6", provider="anthropic")
115+
)
116+
117+
assert captured == {
118+
"model": "anthropic/claude-sonnet-4.6",
119+
"provider": "anthropic",
120+
"resume": None,
121+
"tui_dev": False,
122+
}
123+
124+
125+
def test_launch_tui_exports_model_and_provider(monkeypatch, main_mod):
126+
captured = {}
127+
128+
monkeypatch.setattr(
129+
main_mod,
130+
"_make_tui_argv",
131+
lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
132+
)
133+
134+
def fake_call(argv, cwd=None, env=None):
135+
captured.update({"argv": argv, "cwd": cwd, "env": env})
136+
return 1
137+
138+
monkeypatch.setattr(main_mod.subprocess, "call", fake_call)
139+
140+
with pytest.raises(SystemExit):
141+
main_mod._launch_tui(model="nous/hermes-test", provider="nous")
142+
143+
env = captured["env"]
144+
assert env["HERMES_MODEL"] == "nous/hermes-test"
145+
assert env["HERMES_INFERENCE_MODEL"] == "nous/hermes-test"
146+
assert env["HERMES_TUI_PROVIDER"] == "nous"
147+
assert env["HERMES_INFERENCE_PROVIDER"] == "nous"
148+
149+
92150
def test_print_tui_exit_summary_includes_resume_and_token_totals(monkeypatch, capsys):
93151
import hermes_cli.main as main_mod
94152

tests/test_tui_gateway_server.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,40 @@ def test_status_callback_accepts_single_message_argument():
8383
)
8484

8585

86+
def test_resolve_model_uses_inference_model_env(monkeypatch):
87+
monkeypatch.delenv("HERMES_MODEL", raising=False)
88+
monkeypatch.setenv("HERMES_INFERENCE_MODEL", "anthropic/claude-sonnet-4.6")
89+
90+
assert server._resolve_model() == "anthropic/claude-sonnet-4.6"
91+
92+
93+
def test_startup_runtime_uses_tui_provider_env(monkeypatch):
94+
monkeypatch.setenv("HERMES_MODEL", "nous/hermes-test")
95+
monkeypatch.setenv("HERMES_TUI_PROVIDER", "nous")
96+
monkeypatch.delenv("HERMES_INFERENCE_PROVIDER", raising=False)
97+
98+
assert server._resolve_startup_runtime() == ("nous/hermes-test", "nous")
99+
100+
101+
def test_startup_runtime_detects_provider_for_model_env(monkeypatch):
102+
monkeypatch.setenv("HERMES_MODEL", "sonnet")
103+
monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False)
104+
monkeypatch.delenv("HERMES_INFERENCE_PROVIDER", raising=False)
105+
monkeypatch.setattr(server, "_load_cfg", lambda: {"model": {"provider": "auto"}})
106+
107+
def fake_detect(model, current_provider):
108+
assert model == "sonnet"
109+
assert current_provider == "auto"
110+
return "anthropic", "anthropic/claude-sonnet-4.6"
111+
112+
monkeypatch.setattr("hermes_cli.models.detect_provider_for_model", fake_detect)
113+
114+
assert server._resolve_startup_runtime() == (
115+
"anthropic/claude-sonnet-4.6",
116+
"anthropic",
117+
)
118+
119+
86120
def _session(agent=None, **extra):
87121
return {
88122
"agent": agent if agent is not None else types.SimpleNamespace(),

tui_gateway/server.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ def resolve_skin() -> dict:
560560

561561

562562
def _resolve_model() -> str:
563-
env = os.environ.get("HERMES_MODEL", "")
563+
env = os.environ.get("HERMES_MODEL", "") or os.environ.get("HERMES_INFERENCE_MODEL", "")
564564
if env:
565565
return env
566566
m = _load_cfg().get("model", "")
@@ -571,6 +571,40 @@ def _resolve_model() -> str:
571571
return "anthropic/claude-sonnet-4"
572572

573573

574+
def _resolve_startup_runtime() -> tuple[str, str | None]:
575+
model = _resolve_model()
576+
explicit_provider = (
577+
os.environ.get("HERMES_TUI_PROVIDER", "")
578+
or os.environ.get("HERMES_INFERENCE_PROVIDER", "")
579+
).strip()
580+
if explicit_provider:
581+
return model, explicit_provider
582+
583+
explicit_model = (
584+
os.environ.get("HERMES_MODEL", "")
585+
or os.environ.get("HERMES_INFERENCE_MODEL", "")
586+
).strip()
587+
if not explicit_model:
588+
return model, None
589+
590+
try:
591+
from hermes_cli.models import detect_provider_for_model
592+
593+
cfg = _load_cfg().get("model") or {}
594+
current_provider = (
595+
str(cfg.get("provider") or "").strip().lower()
596+
if isinstance(cfg, dict)
597+
else ""
598+
) or "auto"
599+
detected = detect_provider_for_model(explicit_model, current_provider)
600+
if detected:
601+
provider, detected_model = detected
602+
return detected_model, provider
603+
except Exception:
604+
pass
605+
return model, None
606+
607+
574608
def _write_config_key(key_path: str, value):
575609
cfg = _load_cfg()
576610
current = cfg
@@ -1277,9 +1311,13 @@ def _make_agent(sid: str, key: str, session_id: str | None = None):
12771311

12781312
cfg = _load_cfg()
12791313
system_prompt = ((cfg.get("agent") or {}).get("system_prompt", "") or "").strip()
1280-
runtime = resolve_runtime_provider(requested=None)
1314+
model, requested_provider = _resolve_startup_runtime()
1315+
runtime = resolve_runtime_provider(
1316+
requested=requested_provider,
1317+
target_model=model or None,
1318+
)
12811319
return AIAgent(
1282-
model=_resolve_model(),
1320+
model=model,
12831321
provider=runtime.get("provider"),
12841322
base_url=runtime.get("base_url"),
12851323
api_key=runtime.get("api_key"),

0 commit comments

Comments
 (0)