fix(providers): support anthropic proxy v1 endpoints
This commit is contained in:
parent
81e42335a1
commit
85e6232a07
8 changed files with 133 additions and 14 deletions
|
|
@ -9,6 +9,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import time
|
||||
|
|
@ -1690,15 +1691,36 @@ def parse_model_input(raw: str, current_provider: str) -> tuple[str, str]:
|
|||
|
||||
def _get_custom_base_url() -> str:
|
||||
"""Get the custom endpoint base_url from config.yaml."""
|
||||
model_cfg = _get_model_config_dict()
|
||||
return str(model_cfg.get("base_url", "")).strip()
|
||||
|
||||
|
||||
def _get_model_config_dict() -> dict[str, Any]:
|
||||
"""Return the main model config mapping, or an empty dict."""
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
config = load_config()
|
||||
model_cfg = config.get("model", {})
|
||||
if isinstance(model_cfg, dict):
|
||||
return str(model_cfg.get("base_url", "")).strip()
|
||||
return model_cfg
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
return {}
|
||||
|
||||
|
||||
def _base_url_looks_like_anthropic_messages(base_url: str) -> bool:
|
||||
normalized = str(base_url or "").strip().lower().rstrip("/")
|
||||
if not normalized:
|
||||
return False
|
||||
path = urllib.parse.urlparse(normalized).path.rstrip("/")
|
||||
return path.endswith("/anthropic") or path.endswith("/anthropic/v1")
|
||||
|
||||
|
||||
def _anthropic_models_url(base_url: Optional[str] = None) -> str:
|
||||
endpoint = str(base_url or "https://api.anthropic.com").strip().rstrip("/")
|
||||
if endpoint.endswith("/v1"):
|
||||
return endpoint + "/models"
|
||||
return endpoint + "/v1/models"
|
||||
|
||||
|
||||
def curated_models_for_provider(
|
||||
|
|
@ -2218,8 +2240,21 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
|
|||
except Exception:
|
||||
pass
|
||||
if normalized == "anthropic":
|
||||
live = _fetch_anthropic_models()
|
||||
model_cfg = _get_model_config_dict()
|
||||
cfg_provider = normalize_provider(str(model_cfg.get("provider", "") or ""))
|
||||
if cfg_provider == "anthropic":
|
||||
cfg_base_url = str(model_cfg.get("base_url", "") or "").strip()
|
||||
cfg_api_key = str(model_cfg.get("api_key", "") or "").strip()
|
||||
else:
|
||||
cfg_base_url = ""
|
||||
cfg_api_key = ""
|
||||
live = _fetch_anthropic_models(
|
||||
base_url=cfg_base_url or None,
|
||||
api_key=cfg_api_key or None,
|
||||
)
|
||||
if live:
|
||||
if cfg_base_url:
|
||||
return live
|
||||
# The live /v1/models dump lags newly-routed curated aliases
|
||||
# (e.g. claude-fable-5, which is reachable on Anthropic before it
|
||||
# is enumerated by the models endpoint). Surface curated entries
|
||||
|
|
@ -2288,13 +2323,16 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
|
|||
if normalized == "custom":
|
||||
base_url = _get_custom_base_url()
|
||||
if base_url:
|
||||
model_cfg = _get_model_config_dict()
|
||||
# Try common API key env vars for custom endpoints
|
||||
api_key = (
|
||||
os.getenv("CUSTOM_API_KEY", "")
|
||||
str(model_cfg.get("api_key", "") or "").strip()
|
||||
or os.getenv("CUSTOM_API_KEY", "")
|
||||
or os.getenv("OPENAI_API_KEY", "")
|
||||
or os.getenv("OPENROUTER_API_KEY", "")
|
||||
)
|
||||
live = fetch_api_models(api_key, base_url)
|
||||
api_mode = "anthropic_messages" if _base_url_looks_like_anthropic_messages(base_url) else None
|
||||
live = fetch_api_models(api_key, base_url, api_mode=api_mode)
|
||||
if live:
|
||||
return live
|
||||
# Bedrock uses live discovery keyed by the resolved AWS region so that
|
||||
|
|
@ -2543,18 +2581,24 @@ def clear_provider_models_cache(provider: Optional[str] = None) -> None:
|
|||
pass
|
||||
|
||||
|
||||
def _fetch_anthropic_models(timeout: float = 5.0) -> Optional[list[str]]:
|
||||
def _fetch_anthropic_models(
|
||||
timeout: float = 5.0,
|
||||
*,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
) -> 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.
|
||||
Claude Code auto-discovery) unless api_key is provided explicitly.
|
||||
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()
|
||||
token = (api_key or "").strip() or resolve_anthropic_token()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
|
|
@ -2569,7 +2613,7 @@ def _fetch_anthropic_models(timeout: float = 5.0) -> Optional[list[str]]:
|
|||
|
||||
def _do_request(h: dict[str, str]):
|
||||
req = urllib.request.Request(
|
||||
"https://api.anthropic.com/v1/models",
|
||||
_anthropic_models_url(base_url),
|
||||
headers=h,
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
|
|
@ -3759,7 +3803,10 @@ def validate_requested_model(
|
|||
# tokens. (The api_mode=="anthropic_messages" branch below handles the
|
||||
# Messages-API transport case separately.)
|
||||
if normalized == "anthropic":
|
||||
anthropic_models = _fetch_anthropic_models()
|
||||
anthropic_models = _fetch_anthropic_models(
|
||||
base_url=base_url or None,
|
||||
api_key=api_key or None,
|
||||
)
|
||||
if anthropic_models is not None:
|
||||
if requested_for_lookup in set(anthropic_models):
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -93,7 +94,8 @@ def _detect_api_mode_for_url(base_url: str) -> Optional[str]:
|
|||
return "codex_responses"
|
||||
if hostname == "api.openai.com":
|
||||
return "codex_responses"
|
||||
if normalized.endswith("/anthropic"):
|
||||
path = urlparse(normalized).path.rstrip("/")
|
||||
if path.endswith("/anthropic") or path.endswith("/anthropic/v1"):
|
||||
return "anthropic_messages"
|
||||
if hostname == "api.kimi.com" and "/coding" in normalized:
|
||||
return "anthropic_messages"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue