From c33597ba9752af667e1b0e7f1e0130340b3e7e72 Mon Sep 17 00:00:00 2001 From: Papastavros Vaggelis Date: Thu, 23 Jul 2026 14:57:04 +0300 Subject: [PATCH] fix(anthropic): honor ANTHROPIC_BASE_URL env override Re-applies #282, reverted on main by the 2.4.3 OSS snapshot sync (a54947c). resolve_credentials() now reads ANTHROPIC_BASE_URL and create_chat_model() uses base_url or the default endpoint, so proxy/gateway deployments route through the configured host again. Regression coverage for the override and default paths restored. Signed-off-by: Papastavros Vaggelis --- .../providers/anthropic/provider.py | 21 +++++++++--------- tests/provider/test_provider_endpoint.py | 6 ++++- tests/unit/test_llm_utils.py | 1 + tests/unit/test_providers.py | 22 +++++++++++++++++-- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/skillspector/providers/anthropic/provider.py b/src/skillspector/providers/anthropic/provider.py index fc1ea6da..3c56d840 100644 --- a/src/skillspector/providers/anthropic/provider.py +++ b/src/skillspector/providers/anthropic/provider.py @@ -15,11 +15,11 @@ """Anthropic provider — Claude models via api.anthropic.com. -Reads ``ANTHROPIC_API_KEY`` for credentials and constructs -``langchain_anthropic.ChatAnthropic`` directly. Defaults to Opus 4.6 for -analyzers and Sonnet 4.6 for ``meta_analyzer`` (cheaper for the -high-volume filter pass), mirroring the policy used by -``NvInferenceProvider``. +Reads ``ANTHROPIC_API_KEY`` for credentials and honors ``ANTHROPIC_BASE_URL`` +as an explicit endpoint override (e.g. a local proxy); when unset, requests +go to api.anthropic.com. Constructs ``langchain_anthropic.ChatAnthropic`` +directly. It defaults to Opus 4.6 for analyzers and Sonnet 4.6 for +``meta_analyzer`` (cheaper for the high-volume filter pass). """ from __future__ import annotations @@ -34,7 +34,7 @@ from skillspector.providers import registry from skillspector.providers.chat_models import resolve_reasoning_effort -# Documented for completeness — ChatAnthropic defaults here when base_url=None. +# Default endpoint; overridden by ``ANTHROPIC_BASE_URL`` when set. ANTHROPIC_BASE_URL = "https://api.anthropic.com" REGISTRY_PATH = str(Path(__file__).with_name("model_registry.yaml")) @@ -49,11 +49,12 @@ class AnthropicProvider: } def resolve_credentials(self) -> tuple[str, str | None] | None: - """Return ``(api_key, base_url)`` from ``ANTHROPIC_API_KEY``.""" + """Return ``(api_key, base_url)`` from ``ANTHROPIC_API_KEY`` / ``ANTHROPIC_BASE_URL``.""" api_key = os.environ.get("ANTHROPIC_API_KEY", "").strip() if not api_key: return None - return api_key, None + base_url = os.environ.get("ANTHROPIC_BASE_URL", "").strip() or None + return api_key, base_url def create_chat_model( self, @@ -67,11 +68,11 @@ def create_chat_model( if creds is None: return None - api_key, _ = creds + api_key, base_url = creds kwargs = { "model_name": model, "api_key": SecretStr(api_key), - "base_url": ANTHROPIC_BASE_URL, + "base_url": base_url or ANTHROPIC_BASE_URL, "max_tokens_to_sample": max_tokens, "timeout": timeout, "stop": None, diff --git a/tests/provider/test_provider_endpoint.py b/tests/provider/test_provider_endpoint.py index 47d033bc..c985761b 100644 --- a/tests/provider/test_provider_endpoint.py +++ b/tests/provider/test_provider_endpoint.py @@ -71,11 +71,15 @@ def test_openai_provider_makes_live_structured_request( assert result == ProviderResult(ok=True) -def test_anthropic_provider_makes_live_structured_request() -> None: +def test_anthropic_provider_makes_live_structured_request( + monkeypatch: pytest.MonkeyPatch, +) -> None: """Anthropic provider reaches its default endpoint and returns structured output.""" from skillspector.providers.anthropic import ANTHROPIC_BASE_URL, AnthropicProvider _skip_without_env("ANTHROPIC_API_KEY") + # This live provider check must hit Anthropic's default base URL, not a proxy. + monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) model = _model_from_env("SKILLSPECTOR_ANTHROPIC_TEST_MODEL", AnthropicProvider.DEFAULT_MODEL) llm = AnthropicProvider().create_chat_model(model, max_tokens=32, timeout=60) diff --git a/tests/unit/test_llm_utils.py b/tests/unit/test_llm_utils.py index fb0c57da..92609337 100644 --- a/tests/unit/test_llm_utils.py +++ b/tests/unit/test_llm_utils.py @@ -53,6 +53,7 @@ _LLM_ENV_VARS = ( "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", "OPENAI_API_KEY", "OPENAI_BASE_URL", "NVIDIA_INFERENCE_KEY", diff --git a/tests/unit/test_providers.py b/tests/unit/test_providers.py index bd297248..f964a7f4 100644 --- a/tests/unit/test_providers.py +++ b/tests/unit/test_providers.py @@ -45,7 +45,7 @@ resolve_provider_credentials, use_provider, ) -from skillspector.providers.anthropic import AnthropicProvider +from skillspector.providers.anthropic import ANTHROPIC_BASE_URL, AnthropicProvider from skillspector.providers.antigravity_cli import AntigravityCLIProvider from skillspector.providers.chat_models import create_openai_compatible_chat_model from skillspector.providers.claude_cli import ClaudeCLIProvider @@ -118,6 +118,7 @@ def _clean_provider_env(monkeypatch: pytest.MonkeyPatch): monkeypatch.delenv("OPENAI_PROJECT_ID", raising=False) monkeypatch.delenv("SKILLSPECTOR_REASONING_EFFORT", raising=False) monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) monkeypatch.delenv("SKILLSPECTOR_MODEL", raising=False) monkeypatch.delenv("SKILLSPECTOR_MODEL_REGISTRY", raising=False) monkeypatch.delenv("SKILLSPECTOR_PROVIDER", raising=False) @@ -300,7 +301,13 @@ def test_resolves_anthropic_api_key_without_openai_endpoint( ) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") creds = AnthropicProvider().resolve_credentials() - assert creds == ("sk-ant-x", None) + assert creds == ("sk-ant-x", None) # None → ChatAnthropic uses api.anthropic.com + + def test_honors_anthropic_base_url_override(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") + monkeypatch.setenv("ANTHROPIC_BASE_URL", "http://localhost:8787") + creds = AnthropicProvider().resolve_credentials() + assert creds == ("sk-ant-x", "http://localhost:8787") def test_creates_native_chat_anthropic(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") @@ -308,6 +315,17 @@ def test_creates_native_chat_anthropic(self, monkeypatch: pytest.MonkeyPatch) -> assert isinstance(llm, ChatAnthropic) assert llm.model == "claude-opus-4-6" assert llm.max_tokens == 123 + # No override → ChatAnthropic points at the default Anthropic endpoint. + assert str(llm.anthropic_api_url).rstrip("/") == ANTHROPIC_BASE_URL.rstrip("/") + + def test_create_chat_model_honors_base_url_override( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") + monkeypatch.setenv("ANTHROPIC_BASE_URL", "http://localhost:8787") + llm = AnthropicProvider().create_chat_model("claude-opus-4-6", max_tokens=123) + assert isinstance(llm, ChatAnthropic) + assert str(llm.anthropic_api_url).rstrip("/") == "http://localhost:8787" @pytest.mark.parametrize("effort", ["provider-specific-value"]) def test_reasoning_effort_passthrough(