diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index 988983dbad..a257de48b7 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -660,12 +660,12 @@ def switch_model( api_key=api_key, base_url=base_url, ) - except Exception: + except Exception as e: validation = { - "accepted": True, - "persist": True, + "accepted": False, + "persist": False, "recognized": False, - "message": None, + "message": f"Could not validate `{new_model}`: {e}", } if not validation.get("accepted"): diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 8577769832..18b62bb489 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -1842,12 +1842,11 @@ def validate_requested_model( if suggestions: suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions) return { - "accepted": True, - "persist": True, + "accepted": False, + "persist": False, "recognized": False, "message": ( - f"Note: `{requested}` was not found in the OpenAI Codex model listing. " - f"It may still work if your account has access to it." + f"Model `{requested}` was not found in the OpenAI Codex model listing." f"{suggestion_text}" ), } @@ -1864,26 +1863,20 @@ def validate_requested_model( "recognized": True, "message": None, } - else: - # API responded but model is not listed. Accept anyway — - # the user may have access to models not shown in the public - # listing (e.g. Z.AI Pro/Max plans can use glm-5 on coding - # endpoints even though it's not in /models). Warn but allow. - suggestions = get_close_matches(requested, api_models, n=3, cutoff=0.5) - suggestion_text = "" - if suggestions: - suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions) + suggestions = get_close_matches(requested, api_models, n=3, cutoff=0.5) + suggestion_text = "" + if suggestions: + suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions) - return { - "accepted": True, - "persist": True, - "recognized": False, - "message": ( - f"Note: `{requested}` was not found in this provider's model listing. " - f"It may still work if your plan supports it." - f"{suggestion_text}" - ), - } + return { + "accepted": False, + "persist": False, + "recognized": False, + "message": ( + f"Model `{requested}` was not found in this provider's model listing." + f"{suggestion_text}" + ), + } # api_models is None — couldn't reach API. Accept and persist, # but warn so typos don't silently break things. diff --git a/tests/hermes_cli/test_model_validation.py b/tests/hermes_cli/test_model_validation.py index af1d89ae8d..be08ca034f 100644 --- a/tests/hermes_cli/test_model_validation.py +++ b/tests/hermes_cli/test_model_validation.py @@ -401,7 +401,8 @@ class TestValidateFormatChecks: def test_no_slash_model_rejected_if_not_in_api(self): result = _validate("gpt-5.4", api_models=["openai/gpt-5.4"]) - assert result["accepted"] is True + assert result["accepted"] is False + assert result["persist"] is False assert "not found" in result["message"] @@ -427,15 +428,15 @@ class TestValidateApiFound: # -- validate — API not found ------------------------------------------------ class TestValidateApiNotFound: - def test_model_not_in_api_accepted_with_warning(self): + def test_model_not_in_api_rejected_with_guidance(self): result = _validate("anthropic/claude-nonexistent") - assert result["accepted"] is True - assert result["persist"] is True + assert result["accepted"] is False + assert result["persist"] is False assert "not found" in result["message"] def test_warning_includes_suggestions(self): result = _validate("anthropic/claude-opus-4.5") - assert result["accepted"] is True + assert result["accepted"] is False assert "Similar models" in result["message"] diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 137a5de084..9ef4398e95 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -155,7 +155,7 @@ def test_config_set_model_uses_live_switch_path(monkeypatch): def _fake_apply(sid, session, raw): seen["args"] = (sid, session["session_key"], raw) - return "new/model" + return {"value": "new/model", "warning": "catalog unreachable"} monkeypatch.setattr(server, "_apply_model_switch", _fake_apply) resp = server.handle_request( @@ -163,6 +163,7 @@ def test_config_set_model_uses_live_switch_path(monkeypatch): ) assert resp["result"]["value"] == "new/model" + assert resp["result"]["warning"] == "catalog unreachable" assert seen["args"] == ("sid", "session-key", "new/model") diff --git a/tui_gateway/server.py b/tui_gateway/server.py index e74313178e..f90b881e8a 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -327,11 +327,11 @@ def _restart_slash_worker(session: dict): session["slash_worker"] = None -def _apply_model_switch(sid: str, session: dict, raw_input: str) -> str: +def _apply_model_switch(sid: str, session: dict, raw_input: str) -> dict: agent = session.get("agent") if not agent: os.environ["HERMES_MODEL"] = raw_input - return raw_input + return {"value": raw_input, "warning": ""} from hermes_cli.model_switch import switch_model @@ -355,7 +355,7 @@ def _apply_model_switch(sid: str, session: dict, raw_input: str) -> str: os.environ["HERMES_MODEL"] = result.new_model _restart_slash_worker(session) _emit("session.info", sid, _session_info(agent)) - return result.new_model + return {"value": result.new_model, "warning": result.warning_message or ""} def _compress_session_history(session: dict) -> tuple[int, dict]: @@ -1273,10 +1273,11 @@ def _(rid, params: dict) -> dict: if not value: return _err(rid, 4002, "model value required") if session: - value = _apply_model_switch(params.get("session_id", ""), session, value) + result = _apply_model_switch(params.get("session_id", ""), session, value) else: os.environ["HERMES_MODEL"] = value - return _ok(rid, {"key": key, "value": value}) + result = {"value": value, "warning": ""} + return _ok(rid, {"key": key, "value": result["value"], "warning": result["warning"]}) except Exception as e: return _err(rid, 5001, str(e)) diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx index 80a4ea4a4f..e9152f1b3b 100644 --- a/ui-tui/src/app.tsx +++ b/ui-tui/src/app.tsx @@ -119,6 +119,7 @@ const stripTokens = (text: string, re: RegExp) => const imageTokenMeta = (info: { height?: number; token_estimate?: number; width?: number } | null | undefined) => { const dims = info?.width && info?.height ? `${info.width}x${info.height}` : '' + const tok = typeof info?.token_estimate === 'number' && info.token_estimate > 0 ? `~${fmtK(info.token_estimate)} tok` : '' @@ -1988,6 +1989,11 @@ export function App({ gw }: { gw: GatewayClient }) { } sys(`model → ${r.value}`) + + if (r.warning) { + sys(`warning: ${r.warning}`) + } + setInfo(prev => (prev ? { ...prev, model: r.value } : prev)) } )