From 2aad4035a5b8c8a00d63e7827a1f8284ab1d53f3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:07:38 -0700 Subject: [PATCH] Port pattern from zed-industries/zed#62729: request ungated Codex model catalog (clean-room) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ChatGPT Codex models endpoint interprets client_version as a Codex CLI compatibility version and filters out any model whose minimal_client_version is newer than the value sent. Hermes hardcoded client_version=1.0.0 at both catalog request sites, so model visibility was accidentally coupled to a version scheme Hermes doesn't follow — future models gated behind a higher minimal version would silently vanish from the account catalog. The backend accepts the exact sentinel 0.0.0 as an ungated request returning the complete account catalog (verified live: 0.0.0 and current versions return identical model sets today, while omitting the parameter is HTTP 400 and out-of-sequence values like 0.0.1 return no models). Both request sites (hermes_cli/codex_models.py and the context-length probe in agent/model_metadata.py) now share one CODEX_UNGATED_CLIENT_VERSION constant. Clean-room port of the observed behavior in zed-industries/zed#62729; no GPL code translated. --- agent/model_metadata.py | 9 +++++- hermes_cli/codex_models.py | 6 ++-- tests/hermes_cli/test_codex_models.py | 44 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/agent/model_metadata.py b/agent/model_metadata.py index a255063e94..c9a7824a81 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -1595,6 +1595,13 @@ def _verified_codex_ctx_for_slug(model_bare: str) -> Optional[int]: _codex_oauth_context_cache: Dict[str, Tuple[Dict[str, int], float]] = {} _CODEX_OAUTH_CONTEXT_CACHE_TTL = 3600 # 1 hour +# The Codex models endpoint reads ``client_version`` as a Codex CLI compatibility version and +# hides models whose ``minimal_client_version`` is newer, so a made-up version (the old +# "1.0.0") silently drops future models. "0.0.0" is the backend's ungated sentinel returning +# the full account catalog; other out-of-sequence values return an empty catalog and omitting +# the parameter is HTTP 400. +CODEX_UNGATED_CLIENT_VERSION = "0.0.0" +CODEX_MODELS_CATALOG_URL = f"https://chatgpt.com/backend-api/codex/models?client_version={CODEX_UNGATED_CLIENT_VERSION}" def _codex_oauth_token_fingerprint(access_token: str) -> str: @@ -1630,7 +1637,7 @@ def _fetch_codex_oauth_context_lengths_with_source(access_token: str) -> Tuple[D headers["ChatGPT-Account-Id"] = acct_id try: _ensure_requests() - resp = requests.get("https://chatgpt.com/backend-api/codex/models?client_version=1.0.0", headers=headers, timeout=(5, 10), verify=_resolve_requests_verify()) + resp = requests.get(CODEX_MODELS_CATALOG_URL, headers=headers, timeout=(5, 10), verify=_resolve_requests_verify()) if resp.status_code != 200: logger.debug("Codex /models probe returned HTTP %s; falling back to hardcoded defaults", resp.status_code) return {}, False diff --git a/hermes_cli/codex_models.py b/hermes_cli/codex_models.py index 7d728a92f3..a652f10541 100644 --- a/hermes_cli/codex_models.py +++ b/hermes_cli/codex_models.py @@ -155,10 +155,8 @@ def _fetch_models_from_api(access_token: str) -> List[str]: acct_id = _extract_chatgpt_account_id(access_token) if acct_id: headers["ChatGPT-Account-Id"] = acct_id - resp = httpx.get( - "https://chatgpt.com/backend-api/codex/models?client_version=1.0.0", - headers=headers, - timeout=10) + from agent.model_metadata import CODEX_MODELS_CATALOG_URL + resp = httpx.get(CODEX_MODELS_CATALOG_URL, headers=headers, timeout=10) if resp.status_code != 200: return [] data = resp.json() diff --git a/tests/hermes_cli/test_codex_models.py b/tests/hermes_cli/test_codex_models.py index 653d402bd5..78a057711f 100644 --- a/tests/hermes_cli/test_codex_models.py +++ b/tests/hermes_cli/test_codex_models.py @@ -269,3 +269,47 @@ class TestNormalizeModelForProvider: assert changed is True # Uses first from available list assert cli.model == "gpt-5.3-codex" + + +def test_catalog_requests_use_ungated_client_version(monkeypatch): + """Both catalog request sites send the backend's ungated ``0.0.0`` sentinel: the endpoint + hides models whose ``minimal_client_version`` is newer than ``client_version``, so a + made-up version silently drops future models.""" + import sys + from urllib.parse import parse_qs, urlparse + + from agent import model_metadata + from hermes_cli import codex_models + + seen_urls = [] + + class _FakeResp: + status_code = 200 + + def json(self): + return {"models": []} + + class _FakeHttpx: + @staticmethod + def get(url, headers=None, timeout=None): + seen_urls.append(url) + return _FakeResp() + + class _FakeRequests: + @staticmethod + def get(url, headers=None, timeout=None, verify=None): + seen_urls.append(url) + return _FakeResp() + + monkeypatch.setitem(sys.modules, "httpx", _FakeHttpx) + codex_models._fetch_models_from_api(access_token="tok") + monkeypatch.setattr(model_metadata, "requests", _FakeRequests) + monkeypatch.setattr(model_metadata, "_ensure_requests", lambda: None) + monkeypatch.setattr(model_metadata, "_codex_oauth_context_cache", {}) + model_metadata._fetch_codex_oauth_context_lengths_with_source("tok") + + assert len(seen_urls) == 2 + for url in seen_urls: + parsed = urlparse(url) + assert parsed.netloc == "chatgpt.com" and parsed.path == "/backend-api/codex/models" + assert parse_qs(parsed.query)["client_version"] == ["0.0.0"]