From f9f83531354af424dc7fea1b3894fdce21f5559b Mon Sep 17 00:00:00 2001 From: 69k4xmdfm2-blip Date: Tue, 21 Jul 2026 22:25:12 -0300 Subject: [PATCH] fix(codex): gate Pro models by account catalog --- agent/error_classifier.py | 13 +++++++++++ hermes_cli/codex_models.py | 11 +++------ tests/agent/test_error_classifier.py | 18 +++++++++++++++ tests/hermes_cli/test_codex_models.py | 33 +++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 9d830cdc996f..e8ee016dc29a 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -1199,6 +1199,19 @@ def _classify_400( ) -> ClassifiedError: """Classify 400 Bad Request — context overflow, format error, or generic.""" + # ChatGPT OAuth rejects account-ineligible Codex models with HTTP 400 + # rather than 404. This is a model-route failure, not a malformed request: + # retrying the same slug cannot help, while a configured fallback can. + if ( + provider == "openai-codex" + and "model is not supported when using codex" in error_msg + ): + return result_fn( + FailoverReason.model_not_found, + retryable=False, + should_fallback=True, + ) + # Multimodal tool content rejected from 400. Must be checked BEFORE # image_too_large because the recovery is different (strip image parts # from tool messages, mark the model as no-list-tool-content for the diff --git a/hermes_cli/codex_models.py b/hermes_cli/codex_models.py index 021d31918bc3..5084bddf1a7f 100644 --- a/hermes_cli/codex_models.py +++ b/hermes_cli/codex_models.py @@ -13,14 +13,12 @@ logger = logging.getLogger(__name__) DEFAULT_CODEX_MODELS: List[str] = [ - # GPT-5.6 series (Sol/Terra/Luna + -pro high-effort modes) — GA 2026-07-09 - # (previewed 2026-06-26). + # GPT-5.6 series — GA 2026-07-09 (previewed 2026-06-26). ``-pro`` + # variants are entitlement-specific and must come from live discovery; + # synthesizing them for ChatGPT OAuth accounts produces deterministic 400s. "gpt-5.6-sol", - "gpt-5.6-sol-pro", "gpt-5.6-terra", - "gpt-5.6-terra-pro", "gpt-5.6-luna", - "gpt-5.6-luna-pro", "gpt-5.5", "gpt-5.4-mini", "gpt-5.4", @@ -54,11 +52,8 @@ _FORWARD_COMPAT_TEMPLATE_MODELS: List[tuple[str, tuple[str, ...]]] = [ ("gpt-5.6-sol", ("gpt-5.5", "gpt-5.4")), - ("gpt-5.6-sol-pro", ("gpt-5.5", "gpt-5.4")), ("gpt-5.6-terra", ("gpt-5.5", "gpt-5.4")), - ("gpt-5.6-terra-pro", ("gpt-5.5", "gpt-5.4")), ("gpt-5.6-luna", ("gpt-5.5", "gpt-5.4")), - ("gpt-5.6-luna-pro", ("gpt-5.5", "gpt-5.4")), ("gpt-5.5", ("gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex")), ("gpt-5.4-mini", ("gpt-5.3-codex",)), ("gpt-5.4", ("gpt-5.3-codex",)), diff --git a/tests/agent/test_error_classifier.py b/tests/agent/test_error_classifier.py index 8ef407f50fb2..d2780816fbba 100644 --- a/tests/agent/test_error_classifier.py +++ b/tests/agent/test_error_classifier.py @@ -571,6 +571,24 @@ def test_404_model_not_found(self): assert result.should_fallback is True assert result.retryable is False + @pytest.mark.parametrize( + "message", + [ + "The 'gpt-5.6-sol-pro' model is not supported when using Codex " + "with a ChatGPT account.", + "ERROR: MODEL IS NOT SUPPORTED WHEN USING CODEX with a ChatGPT " + "account; choose another model.", + ], + ) + def test_400_codex_chatgpt_unsupported_model_falls_back(self, message): + e = MockAPIError(message, status_code=400) + + result = classify_api_error(e, provider="openai-codex") + + assert result.reason == FailoverReason.model_not_found + assert result.should_fallback is True + assert result.retryable is False + def test_404_generic(self): # Generic 404 with no "model not found" signal — common for local # llama.cpp/Ollama/vLLM endpoints with slightly wrong paths. Treat diff --git a/tests/hermes_cli/test_codex_models.py b/tests/hermes_cli/test_codex_models.py index abefbc12c9f3..5485fd57a881 100644 --- a/tests/hermes_cli/test_codex_models.py +++ b/tests/hermes_cli/test_codex_models.py @@ -57,6 +57,16 @@ def test_get_codex_model_ids_falls_back_to_curated_defaults(tmp_path, monkeypatc assert "gpt-5.3-codex-spark" in models +def test_curated_fallback_excludes_unverified_pro_variants(tmp_path, monkeypatch): + codex_home = tmp_path / "codex-home" + codex_home.mkdir(parents=True, exist_ok=True) + monkeypatch.setenv("CODEX_HOME", str(codex_home)) + + models = get_codex_model_ids() + + assert not any(model.endswith("-pro") for model 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", @@ -77,6 +87,29 @@ def test_get_codex_model_ids_adds_forward_compat_models_from_templates(monkeypat ] +def test_live_catalog_only_surfaces_pro_variants_when_advertised(monkeypatch): + advertised = [ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-5.5", + "gpt-5.4", + ] + monkeypatch.setattr( + "hermes_cli.codex_models._fetch_models_from_api", + lambda access_token: list(advertised), + ) + + models = get_codex_model_ids(access_token="codex-access-token") + + assert not any(model.endswith("-pro") for model in models) + + advertised.append("gpt-5.6-sol-pro") + models = get_codex_model_ids(access_token="codex-access-token") + + assert "gpt-5.6-sol-pro" in models + + def test_fetch_from_api_keeps_supported_in_api_false_models(monkeypatch): """Regression: gpt-5.3-codex-spark is returned by the live Codex backend with ``supported_in_api: false`` because it isn't in the public OpenAI