From 57e98fe6c9f0a6f15d22f0c8bc51cf6c636f1d16 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Fri, 13 Mar 2026 21:06:06 -0700 Subject: [PATCH 1/4] fix: surface gpt-5.4 in codex setup --- hermes_cli/codex_models.py | 1 + hermes_cli/models.py | 2 ++ hermes_cli/setup.py | 10 +++++++- tests/hermes_cli/test_setup.py | 47 ++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/hermes_cli/codex_models.py b/hermes_cli/codex_models.py index 9fe34671..43722124 100644 --- a/hermes_cli/codex_models.py +++ b/hermes_cli/codex_models.py @@ -13,6 +13,7 @@ logger = logging.getLogger(__name__) DEFAULT_CODEX_MODELS: List[str] = [ "gpt-5.3-codex", + "gpt-5.4", "gpt-5.2-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 3b3d0ab4..85c248c1 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -40,6 +40,8 @@ _PROVIDER_MODELS: dict[str, list[str]] = { "deepseek-v3.2", ], "openai-codex": [ + "gpt-5.3-codex", + "gpt-5.4", "gpt-5.2-codex", "gpt-5.1-codex-mini", "gpt-5.1-codex-max", diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index b2e53c87..789f2b09 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -654,6 +654,7 @@ def setup_model_provider(config: dict): _update_config_for_provider, _login_openai_codex, get_codex_auth_status, + resolve_codex_runtime_credentials, DEFAULT_CODEX_BASE_URL, detect_external_credentials, ) @@ -1266,7 +1267,14 @@ def setup_model_provider(config: dict): elif selected_provider == "openai-codex": from hermes_cli.codex_models import get_codex_model_ids - codex_models = get_codex_model_ids() + codex_token = None + try: + codex_creds = resolve_codex_runtime_credentials() + codex_token = codex_creds.get("api_key") + except Exception as exc: + logger.debug("Could not resolve Codex runtime credentials for model list: %s", exc) + + codex_models = get_codex_model_ids(access_token=codex_token) model_choices = codex_models + [f"Keep current ({current_model})"] default_codex = 0 if current_model in codex_models: diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index 54a82e4b..7e2443ab 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -95,3 +95,50 @@ def test_custom_setup_clears_active_oauth_provider(tmp_path, monkeypatch): assert reloaded["model"]["provider"] == "custom" assert reloaded["model"]["base_url"] == "https://custom.example/v1" assert reloaded["model"]["default"] == "custom/model" + + +def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, monkeypatch): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.setenv("OPENROUTER_API_KEY", "or-test-key") + _clear_provider_env(monkeypatch) + monkeypatch.setenv("OPENROUTER_API_KEY", "or-test-key") + + config = load_config() + + prompt_choices = iter([1, 0]) + monkeypatch.setattr( + "hermes_cli.setup.prompt_choice", + lambda *args, **kwargs: next(prompt_choices), + ) + monkeypatch.setattr("hermes_cli.setup.prompt", lambda *args, **kwargs: "") + monkeypatch.setattr("hermes_cli.auth.detect_external_credentials", lambda: []) + monkeypatch.setattr("hermes_cli.auth._login_openai_codex", lambda *args, **kwargs: None) + monkeypatch.setattr( + "hermes_cli.auth.resolve_codex_runtime_credentials", + lambda *args, **kwargs: { + "base_url": "https://chatgpt.com/backend-api/codex", + "api_key": "codex-access-token", + }, + ) + + captured = {} + + def _fake_get_codex_model_ids(access_token=None): + captured["access_token"] = access_token + return ["gpt-5.4", "gpt-5.3-codex"] + + monkeypatch.setattr( + "hermes_cli.codex_models.get_codex_model_ids", + _fake_get_codex_model_ids, + ) + + setup_model_provider(config) + save_config(config) + + reloaded = load_config() + + assert captured["access_token"] == "codex-access-token" + assert isinstance(reloaded["model"], dict) + assert reloaded["model"]["provider"] == "openai-codex" + assert reloaded["model"]["default"] == "gpt-5.4" + assert reloaded["model"]["base_url"] == "https://chatgpt.com/backend-api/codex" From 529729831c2b168637db439fcb09107e29c466a3 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Fri, 13 Mar 2026 21:12:55 -0700 Subject: [PATCH 2/4] fix: explain codex oauth gpt-5.4 limits --- hermes_cli/codex_models.py | 1 - hermes_cli/main.py | 6 +++++ hermes_cli/models.py | 1 - hermes_cli/setup.py | 5 +++++ tests/hermes_cli/test_setup.py | 8 ++++--- tests/test_codex_models.py | 41 ++++++++++++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/hermes_cli/codex_models.py b/hermes_cli/codex_models.py index 43722124..9fe34671 100644 --- a/hermes_cli/codex_models.py +++ b/hermes_cli/codex_models.py @@ -13,7 +13,6 @@ logger = logging.getLogger(__name__) DEFAULT_CODEX_MODELS: List[str] = [ "gpt-5.3-codex", - "gpt-5.4", "gpt-5.2-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 14706f23..52a2b98b 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1057,7 +1057,12 @@ def _model_flow_openai_codex(config, current_model=""): _codex_token = _codex_creds.get("api_key") except Exception: pass + codex_models = get_codex_model_ids(access_token=_codex_token) + if "gpt-5.4" not in codex_models: + print("Note: `gpt-5.4` is not currently supported for ChatGPT/Codex OAuth accounts.") + print("Use OpenRouter if you need GPT-5.4 specifically.") + print() selected = _prompt_model_selection(codex_models, current_model=current_model) if selected: @@ -1072,6 +1077,7 @@ def _model_flow_openai_codex(config, current_model=""): print("No change.") + def _model_flow_custom(config): """Custom endpoint: collect URL, API key, and model name. diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 85c248c1..d2d1bf46 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -41,7 +41,6 @@ _PROVIDER_MODELS: dict[str, list[str]] = { ], "openai-codex": [ "gpt-5.3-codex", - "gpt-5.4", "gpt-5.2-codex", "gpt-5.1-codex-mini", "gpt-5.1-codex-max", diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 789f2b09..3e9ebee6 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -1275,6 +1275,11 @@ def setup_model_provider(config: dict): logger.debug("Could not resolve Codex runtime credentials for model list: %s", exc) codex_models = get_codex_model_ids(access_token=codex_token) + if "gpt-5.4" not in codex_models: + print_warning("`gpt-5.4` is not currently supported for ChatGPT/Codex OAuth accounts.") + print_info("Use OpenRouter if you need GPT-5.4 specifically.") + print() + model_choices = codex_models + [f"Keep current ({current_model})"] default_codex = 0 if current_model in codex_models: diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index 7e2443ab..12f70999 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -97,7 +97,7 @@ def test_custom_setup_clears_active_oauth_provider(tmp_path, monkeypatch): assert reloaded["model"]["default"] == "custom/model" -def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, monkeypatch): +def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, monkeypatch, capsys): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OPENROUTER_API_KEY", "or-test-key") _clear_provider_env(monkeypatch) @@ -125,7 +125,7 @@ def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, mon def _fake_get_codex_model_ids(access_token=None): captured["access_token"] = access_token - return ["gpt-5.4", "gpt-5.3-codex"] + return ["gpt-5.2-codex", "gpt-5.2"] monkeypatch.setattr( "hermes_cli.codex_models.get_codex_model_ids", @@ -136,9 +136,11 @@ def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, mon save_config(config) reloaded = load_config() + output = capsys.readouterr().out assert captured["access_token"] == "codex-access-token" + assert "not currently supported for ChatGPT/Codex OAuth accounts" in output assert isinstance(reloaded["model"], dict) assert reloaded["model"]["provider"] == "openai-codex" - assert reloaded["model"]["default"] == "gpt-5.4" + assert reloaded["model"]["default"] == "gpt-5.2-codex" assert reloaded["model"]["base_url"] == "https://chatgpt.com/backend-api/codex" diff --git a/tests/test_codex_models.py b/tests/test_codex_models.py index 5e85e46a..85ed6faa 100644 --- a/tests/test_codex_models.py +++ b/tests/test_codex_models.py @@ -54,6 +54,47 @@ def test_get_codex_model_ids_falls_back_to_curated_defaults(tmp_path, monkeypatc assert models[: len(DEFAULT_CODEX_MODELS)] == DEFAULT_CODEX_MODELS +def test_model_command_warns_when_gpt_5_4_is_unavailable_for_codex(monkeypatch, capsys): + from hermes_cli.main import _model_flow_openai_codex + + captured = {} + + monkeypatch.setattr( + "hermes_cli.auth.get_codex_auth_status", + lambda: {"logged_in": True}, + ) + monkeypatch.setattr( + "hermes_cli.auth.resolve_codex_runtime_credentials", + lambda *args, **kwargs: {"api_key": "codex-access-token"}, + ) + + def _fake_get_codex_model_ids(access_token=None): + captured["access_token"] = access_token + return ["gpt-5.2-codex", "gpt-5.2"] + + def _fake_prompt_model_selection(model_ids, current_model=""): + captured["model_ids"] = list(model_ids) + captured["current_model"] = current_model + return None + + monkeypatch.setattr( + "hermes_cli.codex_models.get_codex_model_ids", + _fake_get_codex_model_ids, + ) + monkeypatch.setattr( + "hermes_cli.auth._prompt_model_selection", + _fake_prompt_model_selection, + ) + + _model_flow_openai_codex({}, current_model="openai/gpt-5.4") + output = capsys.readouterr().out + + assert captured["access_token"] == "codex-access-token" + assert captured["model_ids"] == ["gpt-5.2-codex", "gpt-5.2"] + assert "not currently supported for ChatGPT/Codex OAuth accounts" in output + assert "Use OpenRouter if you need GPT-5.4 specifically." in output + + # ── Tests for _normalize_model_for_provider ────────────────────────── From 899cb52e7abd17310a86866db828a55d445545b1 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Fri, 13 Mar 2026 21:18:29 -0700 Subject: [PATCH 3/4] refactor: drop codex oauth model warning --- hermes_cli/main.py | 4 ---- hermes_cli/setup.py | 4 ---- tests/hermes_cli/test_setup.py | 4 +--- tests/test_codex_models.py | 6 ++---- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 52a2b98b..6daf1562 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1059,10 +1059,6 @@ def _model_flow_openai_codex(config, current_model=""): pass codex_models = get_codex_model_ids(access_token=_codex_token) - if "gpt-5.4" not in codex_models: - print("Note: `gpt-5.4` is not currently supported for ChatGPT/Codex OAuth accounts.") - print("Use OpenRouter if you need GPT-5.4 specifically.") - print() selected = _prompt_model_selection(codex_models, current_model=current_model) if selected: diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 3e9ebee6..4f1a1c24 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -1275,10 +1275,6 @@ def setup_model_provider(config: dict): logger.debug("Could not resolve Codex runtime credentials for model list: %s", exc) codex_models = get_codex_model_ids(access_token=codex_token) - if "gpt-5.4" not in codex_models: - print_warning("`gpt-5.4` is not currently supported for ChatGPT/Codex OAuth accounts.") - print_info("Use OpenRouter if you need GPT-5.4 specifically.") - print() model_choices = codex_models + [f"Keep current ({current_model})"] default_codex = 0 diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index 12f70999..4d0ab887 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -97,7 +97,7 @@ def test_custom_setup_clears_active_oauth_provider(tmp_path, monkeypatch): assert reloaded["model"]["default"] == "custom/model" -def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, monkeypatch, capsys): +def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("OPENROUTER_API_KEY", "or-test-key") _clear_provider_env(monkeypatch) @@ -136,10 +136,8 @@ def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, mon save_config(config) reloaded = load_config() - output = capsys.readouterr().out assert captured["access_token"] == "codex-access-token" - assert "not currently supported for ChatGPT/Codex OAuth accounts" in output assert isinstance(reloaded["model"], dict) assert reloaded["model"]["provider"] == "openai-codex" assert reloaded["model"]["default"] == "gpt-5.2-codex" diff --git a/tests/test_codex_models.py b/tests/test_codex_models.py index 85ed6faa..7148c659 100644 --- a/tests/test_codex_models.py +++ b/tests/test_codex_models.py @@ -54,7 +54,7 @@ def test_get_codex_model_ids_falls_back_to_curated_defaults(tmp_path, monkeypatc assert models[: len(DEFAULT_CODEX_MODELS)] == DEFAULT_CODEX_MODELS -def test_model_command_warns_when_gpt_5_4_is_unavailable_for_codex(monkeypatch, capsys): +def test_model_command_uses_runtime_access_token_for_codex_list(monkeypatch): from hermes_cli.main import _model_flow_openai_codex captured = {} @@ -87,12 +87,10 @@ def test_model_command_warns_when_gpt_5_4_is_unavailable_for_codex(monkeypatch, ) _model_flow_openai_codex({}, current_model="openai/gpt-5.4") - output = capsys.readouterr().out assert captured["access_token"] == "codex-access-token" assert captured["model_ids"] == ["gpt-5.2-codex", "gpt-5.2"] - assert "not currently supported for ChatGPT/Codex OAuth accounts" in output - assert "Use OpenRouter if you need GPT-5.4 specifically." in output + assert captured["current_model"] == "openai/gpt-5.4" # ── Tests for _normalize_model_for_provider ────────────────────────── From 607689095ebda71c189f4956741e4f8ea6e7a81d Mon Sep 17 00:00:00 2001 From: teknium1 Date: Fri, 13 Mar 2026 21:34:01 -0700 Subject: [PATCH 4/4] fix: add codex forward-compat model listing --- hermes_cli/codex_models.py | 36 +++++++++++++++++++++++++++++++++--- tests/test_codex_models.py | 13 +++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/hermes_cli/codex_models.py b/hermes_cli/codex_models.py index 9fe34671..169c63e8 100644 --- a/hermes_cli/codex_models.py +++ b/hermes_cli/codex_models.py @@ -18,6 +18,36 @@ DEFAULT_CODEX_MODELS: List[str] = [ "gpt-5.1-codex-mini", ] +_FORWARD_COMPAT_TEMPLATE_MODELS: List[tuple[str, tuple[str, ...]]] = [ + ("gpt-5.3-codex", ("gpt-5.2-codex",)), + ("gpt-5.4", ("gpt-5.3-codex", "gpt-5.2-codex")), + ("gpt-5.3-codex-spark", ("gpt-5.3-codex", "gpt-5.2-codex")), +] + + +def _add_forward_compat_models(model_ids: List[str]) -> List[str]: + """Add Clawdbot-style synthetic forward-compat Codex models. + + If a newer Codex slug isn't returned by live discovery, surface it when an + older compatible template model is present. This mirrors Clawdbot's + synthetic catalog / forward-compat behavior for GPT-5 Codex variants. + """ + ordered: List[str] = [] + seen: set[str] = set() + for model_id in model_ids: + if model_id not in seen: + ordered.append(model_id) + seen.add(model_id) + + for synthetic_model, template_models in _FORWARD_COMPAT_TEMPLATE_MODELS: + if synthetic_model in seen: + continue + if any(template in seen for template in template_models): + ordered.append(synthetic_model) + seen.add(synthetic_model) + + return ordered + def _fetch_models_from_api(access_token: str) -> List[str]: """Fetch available models from the Codex API. Returns visible models sorted by priority.""" @@ -54,7 +84,7 @@ def _fetch_models_from_api(access_token: str) -> List[str]: sortable.append((rank, slug)) sortable.sort(key=lambda x: (x[0], x[1])) - return [slug for _, slug in sortable] + return _add_forward_compat_models([slug for _, slug in sortable]) def _read_default_model(codex_home: Path) -> Optional[str]: @@ -125,7 +155,7 @@ def get_codex_model_ids(access_token: Optional[str] = None) -> List[str]: if access_token: api_models = _fetch_models_from_api(access_token) if api_models: - return api_models + return _add_forward_compat_models(api_models) # Fall back to local sources default_model = _read_default_model(codex_home) @@ -140,4 +170,4 @@ def get_codex_model_ids(access_token: Optional[str] = None) -> List[str]: if model_id not in ordered: ordered.append(model_id) - return ordered + return _add_forward_compat_models(ordered) diff --git a/tests/test_codex_models.py b/tests/test_codex_models.py index 7148c659..32fe6315 100644 --- a/tests/test_codex_models.py +++ b/tests/test_codex_models.py @@ -52,6 +52,19 @@ def test_get_codex_model_ids_falls_back_to_curated_defaults(tmp_path, monkeypatc models = get_codex_model_ids() assert models[: len(DEFAULT_CODEX_MODELS)] == DEFAULT_CODEX_MODELS + assert "gpt-5.4" in models + assert "gpt-5.3-codex-spark" in models + + +def test_get_codex_model_ids_adds_forward_compat_models_from_templates(monkeypatch): + monkeypatch.setattr( + "hermes_cli.codex_models._fetch_models_from_api", + lambda access_token: ["gpt-5.2-codex"], + ) + + models = get_codex_model_ids(access_token="codex-access-token") + + assert models == ["gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.3-codex-spark"] def test_model_command_uses_runtime_access_token_for_codex_list(monkeypatch):