mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
Close the remaining end-to-end gaps so the full gpt-5.6 family (sol/ terra/luna + their -pro high-effort modes, 6 slugs) works on every surface a user can reach them through: - agent/auxiliary_client.py: the Codex OAuth backend hard-caps context at 272K for gpt-5.6 exactly as it does for 5.4/5.5, but the default 50% compaction trigger would summarize at ~136K and waste half the usable window. Extend the existing _is_codex_gpt54_or_gpt55 chokepoint (single enforced predicate feeding _compression_threshold_for_model) to match gpt-5.6* on the openai-codex route so those sessions get the same 0.85 auto-raise. Direct-API/OpenRouter routes (full 1.05M window) are unaffected; the historical codex_gpt55_autoraise opt-out still applies. The one-time notice banner is model-dynamic and already renders the correct slug/cap. - hermes_cli/config.py, agent/agent_init.py: refresh the autoraise comments/notice to mention the 5.6 family. - hermes_cli/codex_models.py: add the -pro variants to DEFAULT_CODEX_MODELS + forward-compat so ChatGPT-OAuth (openai-codex) Pro users see the full family in /model, not just the base tiers. Supersedes the earlier commit's note that 5.6 was intentionally kept out of the codex catalog: the slugs are confirmed routable (OpenRouter live + codex backend), so they belong there like every other codex-capable gpt-5.x slug. E2E verified across all 6 slugs: direct-API ctx 1.05M, codex ctx 272K, pricing reachable from openai + openai-api routes, codex compaction override 0.85 (and None on direct-API + when opted out), present in openai-api picker + codex catalog, /model gpt resolves to sol on both native routes. Guard tests added for the compaction route matrix.
220 lines
8.5 KiB
Python
220 lines
8.5 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.6 series (Sol/Terra/Luna + -pro high-effort modes) — GA 2026-07-09
|
|
# (previewed 2026-06-26).
|
|
"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",
|
|
"gpt-5.3-codex",
|
|
# gpt-5.3-codex-spark is in research preview and is exposed *only* via
|
|
# the Codex CLI / OAuth backend (chatgpt.com/backend-api/codex/models)
|
|
# for ChatGPT Pro subscribers. It is NOT available in the public OpenAI
|
|
# API, so it intentionally stays out of the "openai" provider catalog
|
|
# in hermes_cli/models.py — only the openai-codex (OAuth) provider
|
|
# surfaces it. The Codex backend reports ``supported_in_api: false`` for
|
|
# this slug; that flag describes API availability, not Codex backend
|
|
# availability, so the fetch/cache code paths below intentionally do
|
|
# not filter on it. PR #12994 removed this entry on the assumption it
|
|
# was unsupported — that was wrong; restored here. Keep it in the
|
|
# curated fallback so Pro users still see Spark in `/model` when live
|
|
# discovery is unavailable (offline first run, transient API failure).
|
|
"gpt-5.3-codex-spark",
|
|
# NOTE: gpt-5.2-codex / gpt-5.1-codex-max / gpt-5.1-codex-mini were
|
|
# previously listed here but the chatgpt.com Codex backend returns
|
|
# HTTP 400 "The '<model>' model is not supported when using Codex with
|
|
# a ChatGPT account." for all three on every ChatGPT Pro account we've
|
|
# tested (verified live 2026-05-27). Keeping them in the fallback list
|
|
# leaked dead slugs into /model when live discovery was unavailable
|
|
# (transient API failure, first-run before refresh) and surfaced HTTP 400
|
|
# crashes on selection. The Codex CLI public catalog still references
|
|
# these slugs, which is why they survived previously — but those entries
|
|
# describe the public OpenAI API, not the OAuth-backed Codex backend
|
|
# Hermes uses. Removed here. If OpenAI re-enables them on Codex backend,
|
|
# live discovery will pick them up automatically via _fetch_models_from_api.
|
|
]
|
|
|
|
_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",)),
|
|
# Surface Spark whenever any compatible Codex template is present so
|
|
# accounts hitting the live endpoint with an older lineup still see
|
|
# Spark in the picker. Backend gates real availability by ChatGPT Pro
|
|
# entitlement; Hermes does not.
|
|
("gpt-5.3-codex-spark", ("gpt-5.3-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()
|
|
# Codex CLI's catalog uses ``supported_in_api`` for the public OpenAI
|
|
# API, not for the OAuth-backed Codex backend that this provider uses.
|
|
# Some valid Codex CLI models (for example gpt-5.3-codex-spark) are
|
|
# marked false here but are still accepted by the Codex route.
|
|
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()
|
|
# Do not filter on ``supported_in_api`` here. It describes the
|
|
# public OpenAI API, while Hermes openai-codex talks to the same
|
|
# OAuth-backed Codex backend as Codex CLI.
|
|
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)
|