Something went wrong. Try again.
The agent that grows with you
Something went wrong. Try again.
5.9 kB · 177 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178"""Codex model discovery from API, local cache, and config."""
from __future__ import annotations
import jsonimport loggingfrom pathlib import Pathfrom typing import List, Optional
import os
logger = logging.getLogger(__name__)
DEFAULT_CODEX_MODELS: List[str] = [ "gpt-5.5", "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.5", ("gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex")), ("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)