mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Drop gpt-5.3-codex-spark from Codex forward-compat synthesis, provider catalogs, and context metadata now that the API no longer supports it.
175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
"""Codex model discovery from API, local cache, and config."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
import os
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_CODEX_MODELS: List[str] = [
|
|
"gpt-5.4-mini",
|
|
"gpt-5.4",
|
|
"gpt-5.3-codex",
|
|
"gpt-5.2-codex",
|
|
"gpt-5.1-codex-max",
|
|
"gpt-5.1-codex-mini",
|
|
]
|
|
|
|
_FORWARD_COMPAT_TEMPLATE_MODELS: List[tuple[str, tuple[str, ...]]] = [
|
|
("gpt-5.4-mini", ("gpt-5.3-codex", "gpt-5.2-codex")),
|
|
("gpt-5.4", ("gpt-5.3-codex", "gpt-5.2-codex")),
|
|
("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."""
|
|
try:
|
|
import httpx
|
|
resp = httpx.get(
|
|
"https://chatgpt.com/backend-api/codex/models?client_version=1.0.0",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
timeout=10,
|
|
)
|
|
if resp.status_code != 200:
|
|
return []
|
|
data = resp.json()
|
|
entries = data.get("models", []) if isinstance(data, dict) else []
|
|
except Exception as exc:
|
|
logger.debug("Failed to fetch Codex models from API: %s", exc)
|
|
return []
|
|
|
|
sortable = []
|
|
for item in entries:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
slug = item.get("slug")
|
|
if not isinstance(slug, str) or not slug.strip():
|
|
continue
|
|
slug = slug.strip()
|
|
if item.get("supported_in_api") is False:
|
|
continue
|
|
visibility = item.get("visibility", "")
|
|
if isinstance(visibility, str) and visibility.strip().lower() in ("hide", "hidden"):
|
|
continue
|
|
priority = item.get("priority")
|
|
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
|
sortable.append((rank, slug))
|
|
|
|
sortable.sort(key=lambda x: (x[0], x[1]))
|
|
return _add_forward_compat_models([slug for _, slug in sortable])
|
|
|
|
|
|
def _read_default_model(codex_home: Path) -> Optional[str]:
|
|
config_path = codex_home / "config.toml"
|
|
if not config_path.exists():
|
|
return None
|
|
try:
|
|
import tomllib
|
|
except Exception:
|
|
return None
|
|
try:
|
|
payload = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return None
|
|
model = payload.get("model") if isinstance(payload, dict) else None
|
|
if isinstance(model, str) and model.strip():
|
|
return model.strip()
|
|
return None
|
|
|
|
|
|
def _read_cache_models(codex_home: Path) -> List[str]:
|
|
cache_path = codex_home / "models_cache.json"
|
|
if not cache_path.exists():
|
|
return []
|
|
try:
|
|
raw = json.loads(cache_path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return []
|
|
|
|
entries = raw.get("models") if isinstance(raw, dict) else None
|
|
sortable = []
|
|
if isinstance(entries, list):
|
|
for item in entries:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
slug = item.get("slug")
|
|
if not isinstance(slug, str) or not slug.strip():
|
|
continue
|
|
slug = slug.strip()
|
|
if item.get("supported_in_api") is False:
|
|
continue
|
|
visibility = item.get("visibility")
|
|
if isinstance(visibility, str) and visibility.strip().lower() in ("hide", "hidden"):
|
|
continue
|
|
priority = item.get("priority")
|
|
rank = int(priority) if isinstance(priority, (int, float)) else 10_000
|
|
sortable.append((rank, slug))
|
|
|
|
sortable.sort(key=lambda item: (item[0], item[1]))
|
|
deduped: List[str] = []
|
|
for _, slug in sortable:
|
|
if slug not in deduped:
|
|
deduped.append(slug)
|
|
return deduped
|
|
|
|
|
|
def get_codex_model_ids(access_token: Optional[str] = None) -> List[str]:
|
|
"""Return available Codex model IDs, trying API first, then local sources.
|
|
|
|
Resolution order: API (live, if token provided) > config.toml default >
|
|
local cache > hardcoded defaults.
|
|
"""
|
|
codex_home_str = os.getenv("CODEX_HOME", "").strip() or str(Path.home() / ".codex")
|
|
codex_home = Path(codex_home_str).expanduser()
|
|
ordered: List[str] = []
|
|
|
|
# Try live API if we have a token
|
|
if access_token:
|
|
api_models = _fetch_models_from_api(access_token)
|
|
if api_models:
|
|
return _add_forward_compat_models(api_models)
|
|
|
|
# Fall back to local sources
|
|
default_model = _read_default_model(codex_home)
|
|
if default_model:
|
|
ordered.append(default_model)
|
|
|
|
for model_id in _read_cache_models(codex_home):
|
|
if model_id not in ordered:
|
|
ordered.append(model_id)
|
|
|
|
for model_id in DEFAULT_CODEX_MODELS:
|
|
if model_id not in ordered:
|
|
ordered.append(model_id)
|
|
|
|
return _add_forward_compat_models(ordered)
|