diff --git a/cli.py b/cli.py index 6c44ef61b7f7..4081e04ef786 100755 --- a/cli.py +++ b/cli.py @@ -1176,6 +1176,47 @@ def _invalidate(self, min_interval: float = 0.25) -> None: self._last_invalidate = now self._app.invalidate() + def _normalize_model_for_provider(self, resolved_provider: str) -> bool: + """Normalize obviously incompatible model/provider pairings. + + Returns True when the active model changed. + """ + if resolved_provider != "openai-codex": + return False + + current_model = (self.model or "").strip() + current_slug = current_model.split("/")[-1] if current_model else "" + + # Keep explicit Codex models, but strip any provider prefix that the + # Codex Responses API does not expect. + if current_slug and "codex" in current_slug.lower(): + if current_slug != current_model: + self.model = current_slug + return True + return False + + fallback_model = "gpt-5.3-codex" + try: + from hermes_cli.codex_models import get_codex_model_ids + + fallback_model = next( + (model_id for model_id in get_codex_model_ids() if "codex" in model_id.lower()), + fallback_model, + ) + except Exception: + pass + + if current_model != fallback_model: + if current_model: + self.console.print( + f"[yellow]Model '{current_model}' is not supported with OpenAI Codex; " + f"using '{fallback_model}' instead.[/]" + ) + self.model = fallback_model + return True + + return False + def _ensure_runtime_credentials(self) -> bool: """ Ensure runtime credentials are resolved before agent use. @@ -1220,9 +1261,11 @@ def _ensure_runtime_credentials(self) -> bool: self._provider_source = runtime.get("source") self.api_key = api_key self.base_url = base_url + model_changed = self._normalize_model_for_provider(resolved_provider) - # AIAgent/OpenAI client holds auth at init time, so rebuild if key rotated - if (credentials_changed or routing_changed) and self.agent is not None: + # AIAgent/OpenAI client holds auth at init time, so rebuild if key, + # routing, or the effective model changed. + if (credentials_changed or routing_changed or model_changed) and self.agent is not None: self.agent = None return True diff --git a/tests/test_cli_provider_resolution.py b/tests/test_cli_provider_resolution.py index 3c8fe14a5ed9..faf6592c8e39 100644 --- a/tests/test_cli_provider_resolution.py +++ b/tests/test_cli_provider_resolution.py @@ -162,6 +162,54 @@ def _runtime_resolve(**kwargs): assert shell.api_mode == "codex_responses" +def test_runtime_resolution_normalizes_non_codex_model_for_codex_provider(monkeypatch): + cli = _import_cli() + + def _runtime_resolve(**kwargs): + return { + "provider": "openai-codex", + "api_mode": "codex_responses", + "base_url": "https://chatgpt.com/backend-api/codex", + "api_key": "codex-token", + "source": "env/config", + } + + 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)) + monkeypatch.setattr( + "hermes_cli.codex_models.get_codex_model_ids", + lambda access_token=None: ["gpt-5.2-codex", "gpt-5.1-codex-mini"], + ) + + shell = cli.HermesCLI(model="anthropic/claude-opus-4.6", compact=True, max_turns=1) + + assert shell._ensure_runtime_credentials() is True + assert shell.provider == "openai-codex" + assert shell.api_mode == "codex_responses" + assert shell.model == "gpt-5.2-codex" + + +def test_runtime_resolution_strips_provider_prefix_for_codex_model(monkeypatch): + cli = _import_cli() + + def _runtime_resolve(**kwargs): + return { + "provider": "openai-codex", + "api_mode": "codex_responses", + "base_url": "https://chatgpt.com/backend-api/codex", + "api_key": "codex-token", + "source": "env/config", + } + + 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="openai/gpt-5.2-codex", compact=True, max_turns=1) + + assert shell._ensure_runtime_credentials() is True + assert shell.model == "gpt-5.2-codex" + + def test_cmd_model_falls_back_to_auto_on_invalid_provider(monkeypatch, capsys): monkeypatch.setattr( "hermes_cli.config.load_config",