Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand Down Expand Up @@ -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 |
Expand Down
17 changes: 9 additions & 8 deletions src/skillspector/providers/anthropic/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
"""

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion tests/provider/test_provider_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/test_llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

_LLM_ENV_VARS = (
"ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"NVIDIA_INFERENCE_KEY",
Expand Down Expand Up @@ -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:
Expand Down
23 changes: 22 additions & 1 deletion tests/unit/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -248,12 +249,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.
Expand Down