From 7232d9ec3105d952379d702de6523d55fec5c8e3 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Wed, 24 Jun 2026 16:08:23 -0400 Subject: [PATCH 1/3] fix(provider): honor ANTHROPIC_BASE_URL in native Anthropic provider Signed-off-by: Rod Boev --- README.md | 3 ++- .../providers/anthropic/provider.py | 17 +++++++------- tests/unit/test_llm_utils.py | 4 +++- tests/unit/test_providers.py | 22 ++++++++++++++++++- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0da5bdd..c45a695 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ inference gateways. | Provider (`SKILLSPECTOR_PROVIDER`) | Credential env var | Endpoint | Default model | | ---------- | ---- | ---- | ---- | | `openai` | `OPENAI_API_KEY` (+ optional `OPENAI_BASE_URL`) | api.openai.com (or any OpenAI-compatible URL) | `gpt-5.4` | -| `anthropic` | `ANTHROPIC_API_KEY` | api.anthropic.com | `claude-opus-4-6` | +| `anthropic` | `ANTHROPIC_API_KEY` (+ optional `ANTHROPIC_BASE_URL`) | api.anthropic.com or override | `claude-opus-4-6` | | `anthropic_proxy` | `ANTHROPIC_PROXY_API_KEY` + `ANTHROPIC_PROXY_ENDPOINT_URL` | Any Vertex-style raw-predict proxy | `claude-sonnet-4-6` | | `nv_build` | `NVIDIA_INFERENCE_KEY` | build.nvidia.com | `deepseek-ai/deepseek-v4-flash` | @@ -483,6 +483,7 @@ Issues (2) | `OPENAI_API_KEY` | Credential for the OpenAI provider (`SKILLSPECTOR_PROVIDER=openai`). Also serves as the tier-2 fallback in the credential waterfall when the active provider returns no credentials. | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=openai` | | `OPENAI_BASE_URL` | Override the OpenAI endpoint (e.g. point at Ollama). | Optional | | `ANTHROPIC_API_KEY` | Credential for the Anthropic provider (`SKILLSPECTOR_PROVIDER=anthropic`). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=anthropic` | +| `ANTHROPIC_BASE_URL` | Override the Anthropic endpoint. | Optional | | `ANTHROPIC_PROXY_ENDPOINT_URL` | Full endpoint URL for the Anthropic proxy provider (Vertex-style raw-predict). | Required when `SKILLSPECTOR_PROVIDER=anthropic_proxy` | | `ANTHROPIC_PROXY_API_KEY` | Bearer token for the Anthropic proxy provider. | Required when `SKILLSPECTOR_PROVIDER=anthropic_proxy` | | `ANTHROPIC_PROXY_API_VERSION` | `anthropic_version` value sent in the request body (default: `vertex-2023-10-16`). | Optional | diff --git a/src/skillspector/providers/anthropic/provider.py b/src/skillspector/providers/anthropic/provider.py index 53c3885..0c5d59f 100644 --- a/src/skillspector/providers/anthropic/provider.py +++ b/src/skillspector/providers/anthropic/provider.py @@ -15,10 +15,10 @@ """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 +Reads ``ANTHROPIC_API_KEY`` for credentials and honors +``ANTHROPIC_BASE_URL`` as an explicit endpoint override. +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``. """ @@ -48,11 +48,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, @@ -66,11 +67,11 @@ def create_chat_model( if creds is None: return None - api_key, _ = creds + api_key, base_url = creds return ChatAnthropic( 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/unit/test_llm_utils.py b/tests/unit/test_llm_utils.py index 18a1a7f..9d3caa2 100644 --- a/tests/unit/test_llm_utils.py +++ b/tests/unit/test_llm_utils.py @@ -40,6 +40,7 @@ _LLM_ENV_VARS = ( "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", "OPENAI_API_KEY", "OPENAI_BASE_URL", "NVIDIA_INFERENCE_KEY", @@ -96,10 +97,11 @@ def test_anthropic_provider_wins_with_native_credentials( ) -> None: monkeypatch.setenv("SKILLSPECTOR_PROVIDER", "anthropic") monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") + monkeypatch.setenv("ANTHROPIC_BASE_URL", "http://anthropic.example/v1") monkeypatch.setenv("OPENAI_API_KEY", "openai-key") key, base = _resolve_llm_credentials() assert key == "sk-ant-x" - assert base is None + assert base == "http://anthropic.example/v1" def test_no_credentials_raises_with_helpful_message(self) -> None: with pytest.raises(ValueError) as exc_info: diff --git a/tests/unit/test_providers.py b/tests/unit/test_providers.py index 2886d4e..d79ac2a 100644 --- a/tests/unit/test_providers.py +++ b/tests/unit/test_providers.py @@ -248,12 +248,32 @@ def test_resolves_anthropic_api_key_without_openai_endpoint( creds = AnthropicProvider().resolve_credentials() assert creds == ("sk-ant-x", None) - def test_creates_native_chat_anthropic(self, monkeypatch: pytest.MonkeyPatch) -> None: + def test_resolves_anthropic_base_url_override(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") + monkeypatch.setenv("ANTHROPIC_BASE_URL", "http://anthropic.example/v1") + creds = AnthropicProvider().resolve_credentials() + assert creds == ("sk-ant-x", "http://anthropic.example/v1") + + def test_creates_native_chat_anthropic_with_default_base_url( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") + llm = AnthropicProvider().create_chat_model("claude-opus-4-6", max_tokens=123) + assert isinstance(llm, ChatAnthropic) + assert llm.model == "claude-opus-4-6" + assert llm.max_tokens == 123 + assert str(llm.anthropic_api_url).rstrip("/") == "https://api.anthropic.com" + + def test_creates_native_chat_anthropic_with_base_url_override( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-x") + monkeypatch.setenv("ANTHROPIC_BASE_URL", "http://anthropic.example/v1") llm = AnthropicProvider().create_chat_model("claude-opus-4-6", max_tokens=123) assert isinstance(llm, ChatAnthropic) assert llm.model == "claude-opus-4-6" assert llm.max_tokens == 123 + assert str(llm.anthropic_api_url).rstrip("/") == "http://anthropic.example/v1" def test_create_chat_model_returns_none_without_key(self) -> None: # No ANTHROPIC_API_KEY → no client, signalling the caller to fall back. From fdcf75ba297265f88d24a016db96cd3d75148a4c Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Wed, 24 Jun 2026 16:13:15 -0400 Subject: [PATCH 2/3] test(provider): keep native Anthropic live check independent of override env Signed-off-by: Rod Boev --- tests/provider/test_provider_endpoint.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/provider/test_provider_endpoint.py b/tests/provider/test_provider_endpoint.py index 47d033b..c985761 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) From 57ce9342f629cac7cce4da4f9915a6afa7eaa1e1 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Wed, 24 Jun 2026 16:17:18 -0400 Subject: [PATCH 3/3] test(provider): isolate default-url checks from ambient Anthropic override Signed-off-by: Rod Boev --- tests/unit/test_providers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_providers.py b/tests/unit/test_providers.py index d79ac2a..bf996f9 100644 --- a/tests/unit/test_providers.py +++ b/tests/unit/test_providers.py @@ -66,6 +66,7 @@ def _clean_provider_env(monkeypatch: pytest.MonkeyPatch): monkeypatch.delenv("OPENAI_BASE_URL", raising=False) monkeypatch.delenv("OPENAI_PROJECT_ID", 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)