fix(providers): require base_url for anthropic-compatible

Copilot review on #661: empty base_url let the SDK fall back to
https://api.anthropic.com, sending compat-shaped requests to the
commercial API. The lane is local-only by definition, and the /v1-strip
edge case already established fail-loudly-over-silent-prod-retarget;
apply the same principle to the empty case. create_client raises an
actionable ValueError; the admin Detect path surfaces it as a clean
error string via probe_model_endpoint's existing handler.
This commit is contained in:
Patrick Buckley
2026-06-11 20:27:12 -07:00
parent 12bd848c68
commit 7ef04e576a
3 changed files with 24 additions and 4 deletions
+5 -3
View File
@@ -775,9 +775,11 @@ wire translation as the real Anthropic lane, but every model resolves to
the `_ANTHROPIC_COMPAT_DEFAULT` capabilities (200K context, 64K output,
`token_param=max_tokens`, `thinking_mode=none`, no native
web_search/tool_search, no vision) — the static Claude table never
applies to local checkpoints. `base_url` is the server root WITHOUT
`/v1` (the Anthropic SDK appends `/v1/messages`); a trailing `/v1`
pasted out of openai-compatible habit is stripped automatically. Set a
applies to local checkpoints. `base_url` is required — the server root
WITHOUT `/v1` (the Anthropic SDK appends `/v1/messages`); a trailing
`/v1` pasted out of openai-compatible habit is stripped automatically,
and an empty value fails at client construction rather than falling
back to the commercial endpoint. Set a
placeholder `api_key` (e.g. `"dummy"`) for unauthenticated servers. Tool calling
needs the server started with `--enable-auto-tool-choice
--tool-call-parser <family>` plus the matching reasoning parser.
+10
View File
@@ -219,6 +219,16 @@ class TestCompatFactory:
create_client("anthropic-compatible", base_url="/v1", api_key="")
mock_anthropic_cls.assert_called_once_with(base_url="/v1")
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
def test_create_client_requires_base_url(self, mock_ensure: MagicMock) -> None:
"""Empty base_url fails at construction — the local-only lane must
never fall back to the SDK's https://api.anthropic.com default."""
from turnstone.core.providers import create_client
with pytest.raises(ValueError, match="anthropic-compatible requires base_url"):
create_client("anthropic-compatible", base_url="", api_key="dummy")
mock_ensure.return_value.Anthropic.assert_not_called()
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
def test_create_client_real_lane_base_url_untouched(self, mock_ensure: MagicMock) -> None:
"""The real anthropic lane forwards base_url verbatim — the /v1
+9 -1
View File
@@ -153,7 +153,15 @@ def create_client(provider_name: str, *, base_url: str, api_key: str) -> Any:
kwargs: dict[str, str] = {}
if resolved_key is not None:
kwargs["api_key"] = resolved_key
if provider_name == "anthropic-compatible" and base_url:
if provider_name == "anthropic-compatible":
# The lane targets local /v1/messages servers; without a
# base_url the SDK would default to https://api.anthropic.com
# and send compat-shaped requests to the commercial API.
if not base_url:
raise ValueError(
"anthropic-compatible requires base_url (the server root, "
"e.g. http://your-vllm-host:8000)"
)
# The Anthropic SDK appends /v1/... to base_url, so a
# /v1-suffixed URL (the openai-compatible convention) would
# request /v1/v1/messages and 404. Tolerate the suffix.