mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(codex): send ChatGPT-Account-Id on /models probes
The Codex backend returns the per-account model catalog only when the
ChatGPT-Account-Id header is present. Without it, GET /backend-api/codex/models
responds 200 OK with {"models":[]} and the picker silently degrades to the
hardcoded fallback list — which is stale or wrong for the active plan
(no GPT-5.6 family, wrong context windows).
This was the upstream bug behind slow first responses and HTTP 520/120s SSE
hangs: Hermes was sending invalid slugs because the probe never saw them in
the catalog, and Codex's request builder also depends on the same JWT claim
that's now being threaded through both probe paths.
Fixes the probe-side paths in hermes_cli/codex_models.py and
agent/model_metadata.py by extracting chatgpt_account_id from the OAuth JWT
(mirroring the request-side logic already in auxiliary_client.py) and sending
it as a header.
Verified live:
- _fetch_models_from_api now returns the 10-model catalog (gpt-5.6-sol,
gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gpt-5.4-mini,
gpt-5.3-codex-spark, 3x -pro variants) instead of [].
- _fetch_codex_oauth_context_lengths resolves all 8 account models to 272K
context (matches direct API probes of the same account).
- end-to-end: hermes chat -m gpt-5.6-sol -q 'Reply with one word: pong'
returns 'pong' cleanly via the openai-codex route.
Same class of bug as PR #64760.
This commit is contained in:
parent
64702f8f91
commit
c44c2fbb0b
3 changed files with 72 additions and 2 deletions
|
|
@ -4,6 +4,7 @@ Pure utility functions with no AIAgent dependency. Used by ContextCompressor
|
|||
and run_agent.py for pre-flight context checks.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import json
|
||||
|
|
@ -1933,6 +1934,34 @@ def _codex_oauth_token_fingerprint(access_token: str) -> str:
|
|||
return hashlib.sha256(access_token.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def _extract_chatgpt_account_id(access_token: str) -> Optional[str]:
|
||||
"""Extract ``chatgpt_account_id`` from the Codex OAuth JWT.
|
||||
|
||||
The Codex ``/backend-api/codex/models`` endpoint returns the per-account
|
||||
catalog only when the ``ChatGPT-Account-Id`` header is present; without
|
||||
it, the endpoint returns ``{"models":[]}`` (HTTP 200) and the context
|
||||
probe falls back to the hardcoded defaults — which can be stale or
|
||||
wrong for the active account's plan. Mirrors the same extraction done
|
||||
in ``auxiliary_client.py`` for the request path.
|
||||
|
||||
Returns ``None`` on any parse error rather than raising, so a bad
|
||||
token still surfaces as a normal probe failure instead of crashing
|
||||
the metadata resolver.
|
||||
"""
|
||||
try:
|
||||
parts = access_token.split(".")
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4)
|
||||
claims = json.loads(base64.urlsafe_b64decode(payload_b64))
|
||||
if not isinstance(claims, dict):
|
||||
return None
|
||||
acct_id = claims.get("https://api.openai.com/auth", {}).get("chatgpt_account_id")
|
||||
return acct_id if isinstance(acct_id, str) and acct_id else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_codex_oauth_context_lengths_with_source(
|
||||
access_token: str,
|
||||
) -> Tuple[Dict[str, int], bool]:
|
||||
|
|
@ -1953,10 +1982,15 @@ def _fetch_codex_oauth_context_lengths_with_source(
|
|||
if now - cached_at < _CODEX_OAUTH_CONTEXT_CACHE_TTL:
|
||||
return cached_models, False
|
||||
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
acct_id = _extract_chatgpt_account_id(access_token)
|
||||
if acct_id:
|
||||
headers["ChatGPT-Account-Id"] = acct_id
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
"https://chatgpt.com/backend-api/codex/models?client_version=1.0.0",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
headers=headers,
|
||||
timeout=(5, 10),
|
||||
verify=_resolve_requests_verify(),
|
||||
)
|
||||
|
|
|
|||
1
contributors/emails/tars@users.noreply.github.com
Normal file
1
contributors/emails/tars@users.noreply.github.com
Normal file
|
|
@ -0,0 +1 @@
|
|||
MLcogTech
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
|
@ -93,13 +94,47 @@ def _add_forward_compat_models(model_ids: List[str]) -> List[str]:
|
|||
return ordered
|
||||
|
||||
|
||||
def _extract_chatgpt_account_id(access_token: str) -> Optional[str]:
|
||||
"""Best-effort extraction of ``chatgpt_account_id`` from the OAuth JWT.
|
||||
|
||||
The Codex backend requires the ``ChatGPT-Account-Id`` header for the
|
||||
per-account catalog. Without it, ``GET /backend-api/codex/models``
|
||||
returns ``{"models":[]}`` (HTTP 200) — which masquerades as "no
|
||||
models available" and silently degrades the picker to the curated
|
||||
fallback list. The request-side path in ``auxiliary_client.py``
|
||||
already extracts the same claim; this mirrors that logic here so the
|
||||
probe sees the same catalog the request path will actually use.
|
||||
|
||||
Returns ``None`` on any parse error — the probe then degrades
|
||||
gracefully to the unauthenticated fallback list instead of crashing.
|
||||
"""
|
||||
try:
|
||||
parts = access_token.split(".")
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4)
|
||||
claims = json.loads(base64.urlsafe_b64decode(payload_b64))
|
||||
acct_id = (
|
||||
claims.get("https://api.openai.com/auth", {}).get("chatgpt_account_id")
|
||||
if isinstance(claims, dict)
|
||||
else None
|
||||
)
|
||||
return acct_id if isinstance(acct_id, str) and acct_id else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_models_from_api(access_token: str) -> List[str]:
|
||||
"""Fetch available models from the Codex API. Returns visible models sorted by priority."""
|
||||
try:
|
||||
import httpx
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
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={"Authorization": f"Bearer {access_token}"},
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue