Skip to content
Closed
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
47 changes: 45 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions tests/test_cli_provider_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down