mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(cli): support model validation for anthropic_messages and cloudflare-protected endpoints
- probe_api_models: add api_mode param; use x-api-key + anthropic-version headers for anthropic_messages mode (Anthropic's native Models API auth) - probe_api_models: add User-Agent header to avoid Cloudflare 403 blocks on third-party OpenAI-compatible endpoints - validate_requested_model: pass api_mode through from switch_model - validate_requested_model: for anthropic_messages mode, attempt probe with correct auth; if probe fails (many proxies don't implement /v1/models), accept the model with an informational warning instead of rejecting - fetch_api_models: propagate api_mode to probe_api_models
This commit is contained in:
parent
25465fd8d7
commit
647900e813
4 changed files with 76 additions and 8 deletions
|
|
@ -2900,11 +2900,16 @@ def _model_flow_named_custom(config, provider_info):
|
|||
|
||||
name = provider_info["name"]
|
||||
base_url = provider_info["base_url"]
|
||||
api_mode = provider_info.get("api_mode", "")
|
||||
api_key = provider_info.get("api_key", "")
|
||||
key_env = provider_info.get("key_env", "")
|
||||
saved_model = provider_info.get("model", "")
|
||||
provider_key = (provider_info.get("provider_key") or "").strip()
|
||||
|
||||
# Resolve key from env var if api_key not set directly
|
||||
if not api_key and key_env:
|
||||
api_key = os.environ.get(key_env, "")
|
||||
|
||||
print(f" Provider: {name}")
|
||||
print(f" URL: {base_url}")
|
||||
if saved_model:
|
||||
|
|
@ -2912,7 +2917,10 @@ def _model_flow_named_custom(config, provider_info):
|
|||
print()
|
||||
|
||||
print("Fetching available models...")
|
||||
models = fetch_api_models(api_key, base_url, timeout=8.0)
|
||||
models = fetch_api_models(
|
||||
api_key, base_url, timeout=8.0,
|
||||
api_mode=api_mode or None,
|
||||
)
|
||||
|
||||
if models:
|
||||
default_idx = 0
|
||||
|
|
|
|||
|
|
@ -821,6 +821,7 @@ def switch_model(
|
|||
target_provider,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
api_mode=api_mode or None,
|
||||
)
|
||||
except Exception as e:
|
||||
validation = {
|
||||
|
|
|
|||
|
|
@ -2220,8 +2220,15 @@ def probe_api_models(
|
|||
api_key: Optional[str],
|
||||
base_url: Optional[str],
|
||||
timeout: float = 5.0,
|
||||
api_mode: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Probe an OpenAI-compatible ``/models`` endpoint with light URL heuristics."""
|
||||
"""Probe a ``/models`` endpoint with light URL heuristics.
|
||||
|
||||
For ``anthropic_messages`` mode, uses ``x-api-key`` and
|
||||
``anthropic-version`` headers (Anthropic's native auth) instead of
|
||||
``Authorization: Bearer``. The response shape (``data[].id``) is
|
||||
identical, so the same parser works for both.
|
||||
"""
|
||||
normalized = (base_url or "").strip().rstrip("/")
|
||||
if not normalized:
|
||||
return {
|
||||
|
|
@ -2253,7 +2260,10 @@ def probe_api_models(
|
|||
|
||||
tried: list[str] = []
|
||||
headers: dict[str, str] = {"User-Agent": _HERMES_USER_AGENT}
|
||||
if api_key:
|
||||
if api_key and api_mode == "anthropic_messages":
|
||||
headers["x-api-key"] = api_key
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
elif api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
if normalized.startswith(COPILOT_BASE_URL):
|
||||
headers.update(copilot_default_headers())
|
||||
|
|
@ -2318,13 +2328,14 @@ def fetch_api_models(
|
|||
api_key: Optional[str],
|
||||
base_url: Optional[str],
|
||||
timeout: float = 5.0,
|
||||
api_mode: Optional[str] = None,
|
||||
) -> Optional[list[str]]:
|
||||
"""Fetch the list of available model IDs from the provider's ``/models`` endpoint.
|
||||
|
||||
Returns a list of model ID strings, or ``None`` if the endpoint could not
|
||||
be reached (network error, timeout, auth failure, etc.).
|
||||
"""
|
||||
return probe_api_models(api_key, base_url, timeout=timeout).get("models")
|
||||
return probe_api_models(api_key, base_url, timeout=timeout, api_mode=api_mode).get("models")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -2452,6 +2463,7 @@ def validate_requested_model(
|
|||
*,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
api_mode: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Validate a ``/model`` value for the active provider.
|
||||
|
|
@ -2493,7 +2505,11 @@ def validate_requested_model(
|
|||
}
|
||||
|
||||
if normalized == "custom":
|
||||
probe = probe_api_models(api_key, base_url)
|
||||
# Try probing with correct auth for the api_mode.
|
||||
if api_mode == "anthropic_messages":
|
||||
probe = probe_api_models(api_key, base_url, api_mode=api_mode)
|
||||
else:
|
||||
probe = probe_api_models(api_key, base_url)
|
||||
api_models = probe.get("models")
|
||||
if api_models is not None:
|
||||
if requested_for_lookup in set(api_models):
|
||||
|
|
@ -2542,12 +2558,17 @@ def validate_requested_model(
|
|||
f"Note: could not reach this custom endpoint's model listing at `{probe.get('probed_url')}`. "
|
||||
f"Hermes will still save `{requested}`, but the endpoint should expose `/models` for verification."
|
||||
)
|
||||
if api_mode == "anthropic_messages":
|
||||
message += (
|
||||
"\n Many Anthropic-compatible proxies do not implement the Models API "
|
||||
"(GET /v1/models). The model name has been accepted without verification."
|
||||
)
|
||||
if probe.get("suggested_base_url"):
|
||||
message += f"\n If this server expects `/v1`, try base URL: `{probe.get('suggested_base_url')}`"
|
||||
|
||||
return {
|
||||
"accepted": False,
|
||||
"persist": False,
|
||||
"accepted": api_mode == "anthropic_messages",
|
||||
"persist": True,
|
||||
"recognized": False,
|
||||
"message": message,
|
||||
}
|
||||
|
|
@ -2635,6 +2656,41 @@ def validate_requested_model(
|
|||
),
|
||||
}
|
||||
|
||||
# Anthropic Messages API: many proxies don't implement /v1/models.
|
||||
# Try probing with correct auth; if it fails, accept with a warning.
|
||||
if api_mode == "anthropic_messages":
|
||||
api_models = fetch_api_models(api_key, base_url, api_mode=api_mode)
|
||||
if api_models is not None:
|
||||
if requested_for_lookup in set(api_models):
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"message": None,
|
||||
}
|
||||
auto = get_close_matches(requested_for_lookup, api_models, n=1, cutoff=0.9)
|
||||
if auto:
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"corrected_model": auto[0],
|
||||
"message": f"Auto-corrected `{requested}` → `{auto[0]}`",
|
||||
}
|
||||
# Probe failed or model not found — accept anyway (proxy likely
|
||||
# doesn't implement the Anthropic Models API).
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": False,
|
||||
"message": (
|
||||
f"Note: could not verify `{requested}` against this endpoint's "
|
||||
f"model listing. Many Anthropic-compatible proxies do not "
|
||||
f"implement GET /v1/models. The model name has been accepted "
|
||||
f"without verification."
|
||||
),
|
||||
}
|
||||
|
||||
# Probe the live API to check if the model actually exists
|
||||
api_models = fetch_api_models(api_key, base_url)
|
||||
|
||||
|
|
|
|||
|
|
@ -566,8 +566,11 @@ class TestValidateApiFallback:
|
|||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
# Unreachable /models on a custom endpoint no longer hard-rejects —
|
||||
# the model is persisted with a warning so Cloudflare-protected /
|
||||
# proxy endpoints that don't expose /models still work. See #12950.
|
||||
assert result["accepted"] is False
|
||||
assert result["persist"] is False
|
||||
assert result["persist"] is True
|
||||
assert "http://localhost:8000/v1/models" in result["message"]
|
||||
assert "http://localhost:8000/v1" in result["message"]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue