From cd4e995d54dcc8907dfbe886ab64fa1db059997a Mon Sep 17 00:00:00 2001 From: teknium1 Date: Thu, 12 Mar 2026 17:04:31 -0700 Subject: [PATCH] fix(anthropic): live model fetching + adaptive thinking for 4.5+ models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _fetch_anthropic_models() to hermes_cli/models.py — hits the Anthropic /v1/models endpoint to get the live model catalog. Handles both API key and OAuth token auth headers. - Wire it into provider_model_ids() so both 'hermes model' and 'hermes setup model' show the live list instead of a stale static one. - Update static _PROVIDER_MODELS fallback with full current catalog: opus-4-6, sonnet-4-6, opus-4-5, sonnet-4-5, opus-4, sonnet-4, haiku-4-5 - Update model_metadata.py with context lengths for all current models. - Fix thinking parameter for 4.5+ models: use type='adaptive' instead of type='enabled' (Anthropic deprecated 'enabled' for newer models, warns at runtime). Detects model version from the model name string. Verified live: hermes model → Anthropic → auto-detected creds → shows 7 live models hermes chat --provider anthropic --model claude-opus-4-6 → works --- agent/anthropic_adapter.py | 7 +++++- agent/model_metadata.py | 5 ++++ hermes_cli/models.py | 51 +++++++++++++++++++++++++++++++++++++- hermes_cli/setup.py | 9 ++++--- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index 5d2620247..30de65cc0 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -363,11 +363,16 @@ def build_anthropic_kwargs( kwargs["tool_choice"] = {"type": "tool", "name": tool_choice} # Map reasoning_config to Anthropic's thinking parameter + # Newer models (4.6+) prefer "adaptive" thinking; older models use "enabled" if reasoning_config and isinstance(reasoning_config, dict): if reasoning_config.get("enabled") is not False: effort = reasoning_config.get("effort", "medium") budget = THINKING_BUDGET.get(effort, 8000) - kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget} + # Use adaptive thinking for 4.5+ models (they deprecate type=enabled) + if any(v in model for v in ("4-6", "4-5", "4.6", "4.5")): + kwargs["thinking"] = {"type": "adaptive", "budget_tokens": budget} + else: + kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget} kwargs["max_tokens"] = max(effective_max_tokens, budget + 4096) return kwargs diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 6ad741998..a609ea030 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -42,6 +42,11 @@ DEFAULT_CONTEXT_LENGTHS = { "anthropic/claude-sonnet-4-20250514": 200000, "anthropic/claude-haiku-4.5": 200000, # Bare Anthropic model IDs (for native API provider) + "claude-opus-4-6": 200000, + "claude-sonnet-4-6": 200000, + "claude-opus-4-5-20251101": 200000, + "claude-sonnet-4-5-20250929": 200000, + "claude-opus-4-1-20250805": 200000, "claude-opus-4-20250514": 200000, "claude-sonnet-4-20250514": 200000, "claude-haiku-4-5-20251001": 200000, diff --git a/hermes_cli/models.py b/hermes_cli/models.py index f67c5aa28..0353712f3 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -69,8 +69,12 @@ _PROVIDER_MODELS: dict[str, list[str]] = { "MiniMax-M2.1", ], "anthropic": [ - "claude-sonnet-4-20250514", + "claude-opus-4-6", + "claude-sonnet-4-6", + "claude-opus-4-5-20251101", + "claude-sonnet-4-5-20250929", "claude-opus-4-20250514", + "claude-sonnet-4-20250514", "claude-haiku-4-5-20251001", ], } @@ -242,9 +246,54 @@ def provider_model_ids(provider: Optional[str]) -> list[str]: return live except Exception: pass + if normalized == "anthropic": + live = _fetch_anthropic_models() + if live: + return live return list(_PROVIDER_MODELS.get(normalized, [])) +def _fetch_anthropic_models(timeout: float = 5.0) -> Optional[list[str]]: + """Fetch available models from the Anthropic /v1/models endpoint. + + Uses resolve_anthropic_token() to find credentials (env vars or + Claude Code auto-discovery). Returns sorted model IDs or None. + """ + try: + from agent.anthropic_adapter import resolve_anthropic_token, _is_oauth_token + except ImportError: + return None + + token = resolve_anthropic_token() + if not token: + return None + + headers: dict[str, str] = {"anthropic-version": "2023-06-01"} + if _is_oauth_token(token): + headers["Authorization"] = f"Bearer {token}" + headers["anthropic-beta"] = "oauth-2025-04-20" + else: + headers["x-api-key"] = token + + req = urllib.request.Request( + "https://api.anthropic.com/v1/models", + headers=headers, + ) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + data = json.loads(resp.read().decode()) + models = [m["id"] for m in data.get("data", []) if m.get("id")] + # Sort: latest/largest first (opus > sonnet > haiku, higher version first) + return sorted(models, key=lambda m: ( + "opus" not in m, # opus first + "sonnet" not in m, # then sonnet + "haiku" not in m, # then haiku + m, # alphabetical within tier + )) + except Exception: + return None + + def fetch_api_models( api_key: Optional[str], base_url: Optional[str], diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 1ea4ae0ca..49087b362 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -1229,9 +1229,12 @@ def setup_model_provider(config: dict): _set_default_model(config, custom) # else: keep current elif selected_provider == "anthropic": - anthropic_models = [ - "claude-sonnet-4-20250514", - "claude-opus-4-20250514", + # Try live model list first, fall back to static + from hermes_cli.models import provider_model_ids + live_models = provider_model_ids("anthropic") + anthropic_models = live_models if live_models else [ + "claude-opus-4-6", + "claude-sonnet-4-6", "claude-haiku-4-5-20251001", ] model_choices = list(anthropic_models)