Files
hermes-agent/tests/hermes_cli/test_doctor_azure_foundry_probe.py
Bartok9 9bd66000a7 fix: hermes doctor probes Azure Foundry Anthropic endpoints like the runtime
The generic API-key connectivity check sent `GET <base>/models` with Bearer
auth to every provider. Azure Foundry's `/anthropic` route has no models
listing, so a fully working Claude deployment showed `Azure Foundry (HTTP
404)`; when the base URL lived only in `model.base_url` (not the env var)
the probe had no URL at all and printed an httpx type error instead.

Now an Anthropic-only base (still `/anthropic` after the dual-surface
rewrite) is probed with a one-token `POST /v1/messages` built from the
adapter's own helpers — `_base_client_kwargs` for the normalized base and
the Azure `api-version` default_query, `_requires_bearer_auth` for the auth
header family — so doctor exercises exactly the request the runtime sends.
200/400 (Messages-API-shaped) = reachable, 401/403 = auth failure, 404 still
reported. OpenAI-style Foundry bases keep the existing `GET /models` probe.
The Azure row also falls back to `model.base_url` when
`AZURE_FOUNDRY_BASE_URL` is unset, matching runtime resolution.

Hand-reapplied from PR #66798 (reformat/bloat stripped: no dedicated
probe function, no models-then-messages double request, no `api-key`
header, one shared status mapping).

Fixes #66756
Salvages #66798
2026-09-19 09:29:18 -07:00

62 lines
2.9 KiB
Python

"""``hermes doctor`` connectivity probe for Azure Foundry Anthropic-style endpoints (#66756).
The ``/anthropic`` route on Foundry has no ``GET /models``; a working deployment answered the generic
Bearer ``/models`` probe with HTTP 404. The probe must follow the runtime protocol instead:
``POST <base>/v1/messages`` with Bearer auth and the ``api-version`` query the Anthropic adapter sends.
"""
from __future__ import annotations
import httpx
from hermes_cli import doctor_connectivity as dc
_AZURE_BASE = "https://res.services.ai.azure.com/anthropic"
def _run_probe(monkeypatch, status: int, base_url_in_env: bool):
calls: list = []
def _post(url, headers=None, params=None, json=None, timeout=None):
calls.append(("POST", url, headers, params, json))
return httpx.Response(status, json={"type": "error", "error": {"type": "invalid_request_error", "message": "x"}}
if status == 400 else {"id": "msg_1"})
def _get(url, headers=None, timeout=None):
calls.append(("GET", url, headers, None, None))
return httpx.Response(404)
monkeypatch.setattr(httpx, "post", _post)
monkeypatch.setattr(httpx, "get", _get)
monkeypatch.setenv("AZURE_FOUNDRY_API_KEY", "k")
if base_url_in_env:
monkeypatch.setenv("AZURE_FOUNDRY_BASE_URL", _AZURE_BASE)
else:
monkeypatch.delenv("AZURE_FOUNDRY_BASE_URL", raising=False)
monkeypatch.setattr(dc, "_model_cfg", lambda: {"provider": "azure-foundry", "base_url": _AZURE_BASE, "default": "claude-sonnet-5"})
res = dc._probe_apikey_provider("Azure Foundry", ("AZURE_FOUNDRY_API_KEY", "AZURE_FOUNDRY_BASE_URL"), None,
"AZURE_FOUNDRY_BASE_URL", True)
return res, calls
def test_anthropic_style_foundry_probe_posts_messages_like_the_runtime(monkeypatch):
"""Healthy row, one POST /v1/messages with Bearer auth + api-version + max_tokens=1 — never GET /models."""
for status in (200, 400):
res, calls = _run_probe(monkeypatch, status, base_url_in_env=True)
assert res.issues == [] and "\u2713" in res.lines[0][0], (status, res)
assert [c[0] for c in calls] == ["POST"]
_, url, headers, params, body = calls[0]
assert url == _AZURE_BASE + "/v1/messages"
assert headers["Authorization"] == "Bearer k" and "x-api-key" not in headers
assert headers["anthropic-version"] == "2023-06-01"
assert params == {"api-version": "2025-04-15"}
assert body["max_tokens"] == 1 and body["model"] == "claude-sonnet-5"
def test_foundry_base_url_falls_back_to_config_and_404_stays_visible(monkeypatch):
"""Base URL only in ``model.base_url`` is still probed; a real 404 on /v1/messages is still reported."""
res, calls = _run_probe(monkeypatch, 200, base_url_in_env=False)
assert calls[0][1] == _AZURE_BASE + "/v1/messages" and res.issues == []
res, _ = _run_probe(monkeypatch, 404, base_url_in_env=True)
assert "HTTP 404" in res.lines[0][2]