fix(anthropic): live model fetching + adaptive thinking for 4.5+ models
- 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
This commit is contained in:
parent
d51243b6d3
commit
cd4e995d54
4 changed files with 67 additions and 5 deletions
|
|
@ -363,11 +363,16 @@ def build_anthropic_kwargs(
|
||||||
kwargs["tool_choice"] = {"type": "tool", "name": tool_choice}
|
kwargs["tool_choice"] = {"type": "tool", "name": tool_choice}
|
||||||
|
|
||||||
# Map reasoning_config to Anthropic's thinking parameter
|
# 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 and isinstance(reasoning_config, dict):
|
||||||
if reasoning_config.get("enabled") is not False:
|
if reasoning_config.get("enabled") is not False:
|
||||||
effort = reasoning_config.get("effort", "medium")
|
effort = reasoning_config.get("effort", "medium")
|
||||||
budget = THINKING_BUDGET.get(effort, 8000)
|
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)
|
kwargs["max_tokens"] = max(effective_max_tokens, budget + 4096)
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,11 @@ DEFAULT_CONTEXT_LENGTHS = {
|
||||||
"anthropic/claude-sonnet-4-20250514": 200000,
|
"anthropic/claude-sonnet-4-20250514": 200000,
|
||||||
"anthropic/claude-haiku-4.5": 200000,
|
"anthropic/claude-haiku-4.5": 200000,
|
||||||
# Bare Anthropic model IDs (for native API provider)
|
# 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-opus-4-20250514": 200000,
|
||||||
"claude-sonnet-4-20250514": 200000,
|
"claude-sonnet-4-20250514": 200000,
|
||||||
"claude-haiku-4-5-20251001": 200000,
|
"claude-haiku-4-5-20251001": 200000,
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,12 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||||
"MiniMax-M2.1",
|
"MiniMax-M2.1",
|
||||||
],
|
],
|
||||||
"anthropic": [
|
"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-opus-4-20250514",
|
||||||
|
"claude-sonnet-4-20250514",
|
||||||
"claude-haiku-4-5-20251001",
|
"claude-haiku-4-5-20251001",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -242,9 +246,54 @@ def provider_model_ids(provider: Optional[str]) -> list[str]:
|
||||||
return live
|
return live
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if normalized == "anthropic":
|
||||||
|
live = _fetch_anthropic_models()
|
||||||
|
if live:
|
||||||
|
return live
|
||||||
return list(_PROVIDER_MODELS.get(normalized, []))
|
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(
|
def fetch_api_models(
|
||||||
api_key: Optional[str],
|
api_key: Optional[str],
|
||||||
base_url: Optional[str],
|
base_url: Optional[str],
|
||||||
|
|
|
||||||
|
|
@ -1229,9 +1229,12 @@ def setup_model_provider(config: dict):
|
||||||
_set_default_model(config, custom)
|
_set_default_model(config, custom)
|
||||||
# else: keep current
|
# else: keep current
|
||||||
elif selected_provider == "anthropic":
|
elif selected_provider == "anthropic":
|
||||||
anthropic_models = [
|
# Try live model list first, fall back to static
|
||||||
"claude-sonnet-4-20250514",
|
from hermes_cli.models import provider_model_ids
|
||||||
"claude-opus-4-20250514",
|
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",
|
"claude-haiku-4-5-20251001",
|
||||||
]
|
]
|
||||||
model_choices = list(anthropic_models)
|
model_choices = list(anthropic_models)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue