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
13 changes: 13 additions & 0 deletions agent/error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 3 additions & 8 deletions hermes_cli/codex_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",)),
Expand Down
18 changes: 18 additions & 0 deletions tests/agent/test_error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions tests/hermes_cli/test_codex_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down