Skip to content
Merged
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: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ NVIDIA_INFERENCE_KEY=
# etc.); leave unset for stock api.openai.com.
OPENAI_API_KEY=
OPENAI_BASE_URL=
# Optional provider- and model-dependent reasoning-effort setting. Non-empty values
# are trimmed and passed through unchanged; unset or blank uses the provider default.
SKILLSPECTOR_REASONING_EFFORT=

# For SKILLSPECTOR_PROVIDER=anthropic.
ANTHROPIC_API_KEY=
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ Issues (2)
| `NVIDIA_INFERENCE_KEY` | Credential for the `nv_build` provider (build.nvidia.com). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=nv_build` |
| `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 |
| `SKILLSPECTOR_REASONING_EFFORT` | Optional provider- and model-dependent reasoning-effort setting. Non-empty values are trimmed and passed through unchanged; unset or blank preserves provider-default behavior. | Optional |
| `ANTHROPIC_API_KEY` | Credential for the Anthropic provider (`SKILLSPECTOR_PROVIDER=anthropic`). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=anthropic` |
| `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` |
Expand Down
1 change: 1 addition & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ Copy [.env.example](../.env.example) to `.env` in the project root and set value
| `NVIDIA_INFERENCE_KEY` | Credential for `nv_build`. | `nvapi-...` |
| `OPENAI_API_KEY` | Credential for `SKILLSPECTOR_PROVIDER=openai`. Also tier-2 fallback for non-OpenAI providers. | `sk-...` |
| `OPENAI_BASE_URL` | Override the OpenAI endpoint (e.g. point at Ollama). | `http://localhost:11434/v1` |
| `SKILLSPECTOR_REASONING_EFFORT` | Optional provider- and model-dependent reasoning-effort setting. Non-empty values are trimmed and passed through unchanged; unset or blank preserves provider-default behavior. | `high` |
| `ANTHROPIC_API_KEY` | Credential for `SKILLSPECTOR_PROVIDER=anthropic`. | `sk-ant-...` |
| `SKILLSPECTOR_MODEL` | Override the active provider's bundled default model (see [README.md](../README.md) for per-provider defaults). For `claude_cli`, this is passed as `--model` to the `claude` binary. | `gpt-5.2` |

Expand Down
21 changes: 13 additions & 8 deletions src/skillspector/providers/anthropic/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from pydantic import SecretStr

from skillspector.providers import registry
from skillspector.providers.chat_models import resolve_reasoning_effort

# Documented for completeness — ChatAnthropic defaults here when base_url=None.
ANTHROPIC_BASE_URL = "https://api.anthropic.com"
Expand Down Expand Up @@ -67,14 +68,18 @@ def create_chat_model(
return None

api_key, _ = creds
return ChatAnthropic(
model_name=model,
api_key=SecretStr(api_key),
base_url=ANTHROPIC_BASE_URL,
max_tokens_to_sample=max_tokens,
timeout=timeout,
stop=None,
)
kwargs = {
"model_name": model,
"api_key": SecretStr(api_key),
"base_url": ANTHROPIC_BASE_URL,
"max_tokens_to_sample": max_tokens,
"timeout": timeout,
"stop": None,
}
effort = resolve_reasoning_effort()
if effort is not None:
kwargs["effort"] = effort
return ChatAnthropic(**kwargs)

def get_context_length(self, model: str) -> int | None:
return registry.lookup_context_length(REGISTRY_PATH, model)
Expand Down
23 changes: 14 additions & 9 deletions src/skillspector/providers/anthropic_proxy/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from pydantic import SecretStr

from skillspector.providers import registry
from skillspector.providers.chat_models import resolve_reasoning_effort

REGISTRY_PATH = str(Path(__file__).with_name("model_registry.yaml"))

Expand Down Expand Up @@ -231,15 +232,19 @@ def create_chat_model(

bearer_token, endpoint_url = creds

return _ChatAnthropicProxy(
proxy_endpoint_url=endpoint_url,
proxy_bearer_token=bearer_token,
model_name=model,
anthropic_api_key=SecretStr("anthropic-proxy-placeholder"),
max_tokens=max_tokens,
default_request_timeout=timeout,
stop_sequences=None,
)
kwargs = {
"proxy_endpoint_url": endpoint_url,
"proxy_bearer_token": bearer_token,
"model_name": model,
"anthropic_api_key": SecretStr("anthropic-proxy-placeholder"),
"max_tokens": max_tokens,
"default_request_timeout": timeout,
"stop_sequences": None,
}
effort = resolve_reasoning_effort()
if effort is not None:
kwargs["effort"] = effort
return _ChatAnthropicProxy(**kwargs)

def get_context_length(self, model: str) -> int | None:
return registry.lookup_context_length(REGISTRY_PATH, model)
Expand Down
27 changes: 19 additions & 8 deletions src/skillspector/providers/chat_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from __future__ import annotations

import logging
import os
from urllib.parse import urlparse

from langchain_core.language_models.chat_models import BaseChatModel
Expand All @@ -27,6 +28,12 @@
logger = logging.getLogger(__name__)


def resolve_reasoning_effort() -> str | None:
"""Resolve the optional provider- and model-dependent reasoning effort."""
reasoning_effort = os.environ.get("SKILLSPECTOR_REASONING_EFFORT", "").strip()
return reasoning_effort or None


def validate_base_url(url: str | None) -> None:
"""Warn if *url* is not a well-formed http(s) URL.

Expand Down Expand Up @@ -64,11 +71,15 @@ def create_openai_compatible_chat_model(

api_key, base_url = credentials
validate_base_url(base_url)
return ChatOpenAI(
model=model,
base_url=base_url,
api_key=SecretStr(api_key),
max_completion_tokens=max_tokens,
timeout=timeout,
default_headers=default_headers,
)
kwargs = {
"model": model,
"base_url": base_url,
"api_key": SecretStr(api_key),
"max_completion_tokens": max_tokens,
"timeout": timeout,
"default_headers": default_headers,
}
reasoning_effort = resolve_reasoning_effort()
if reasoning_effort:
kwargs["reasoning_effort"] = reasoning_effort
return ChatOpenAI(**kwargs)
55 changes: 55 additions & 0 deletions tests/unit/test_anthropic_proxy_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def _clean_env(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("SKILLSPECTOR_MODEL", raising=False)
monkeypatch.delenv("SKILLSPECTOR_PROVIDER", raising=False)
monkeypatch.delenv("SKILLSPECTOR_SSL_VERIFY", raising=False)
monkeypatch.delenv("SKILLSPECTOR_REASONING_EFFORT", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("NVIDIA_INFERENCE_KEY", raising=False)
Expand Down Expand Up @@ -90,6 +91,49 @@ def test_creates_chat_anthropic_subclass(self, monkeypatch: pytest.MonkeyPatch)
assert llm.model == "claude-sonnet-4-6"
assert llm.max_tokens == 4096

@pytest.mark.parametrize("effort", ["provider-specific-value"])
def test_reasoning_effort_passthrough(
self, monkeypatch: pytest.MonkeyPatch, effort: str
) -> None:
captured: dict[str, object] = {}

def fake_proxy(**kwargs: object) -> dict[str, object]:
captured.update(kwargs)
return kwargs

monkeypatch.setattr(
"skillspector.providers.anthropic_proxy.provider._ChatAnthropicProxy", fake_proxy
)
monkeypatch.setenv("ANTHROPIC_PROXY_API_KEY", "bearer-tok")
monkeypatch.setenv("ANTHROPIC_PROXY_ENDPOINT_URL", "https://proxy.example.com/predict")
monkeypatch.setenv("SKILLSPECTOR_REASONING_EFFORT", f" {effort} ")

AnthropicProxyProvider().create_chat_model("claude-sonnet-4-6", max_tokens=4096)

assert captured["effort"] == effort

@pytest.mark.parametrize("value", [None, " "])
def test_reasoning_effort_blank_or_unset_omits_effort(
self, monkeypatch: pytest.MonkeyPatch, value: str | None
) -> None:
captured: dict[str, object] = {}

def fake_proxy(**kwargs: object) -> dict[str, object]:
captured.update(kwargs)
return kwargs

monkeypatch.setattr(
"skillspector.providers.anthropic_proxy.provider._ChatAnthropicProxy", fake_proxy
)
monkeypatch.setenv("ANTHROPIC_PROXY_API_KEY", "bearer-tok")
monkeypatch.setenv("ANTHROPIC_PROXY_ENDPOINT_URL", "https://proxy.example.com/predict")
if value is not None:
monkeypatch.setenv("SKILLSPECTOR_REASONING_EFFORT", value)

AnthropicProxyProvider().create_chat_model("claude-sonnet-4-6", max_tokens=4096)

assert "effort" not in captured


class TestAnthropicProxyProviderMetadata:
"""Token-budget metadata and model resolution tests."""
Expand Down Expand Up @@ -211,6 +255,17 @@ def test_preserves_other_body_fields(self) -> None:
assert body["max_tokens"] == 200
assert body["temperature"] == 0.5

def test_preserves_output_config_effort(self) -> None:
_, body = self._make_request(
{
"model": "claude-sonnet-4-6",
"messages": [],
"max_tokens": 200,
"output_config": {"effort": "xhigh"},
}
)
assert body["output_config"]["effort"] == "xhigh"


class TestApiVersionConfiguration:
"""Tests for ANTHROPIC_PROXY_API_VERSION env var."""
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def _clean_env(monkeypatch: pytest.MonkeyPatch):
"NVIDIA_INFERENCE_KEY",
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"SKILLSPECTOR_REASONING_EFFORT",
"ANTHROPIC_API_KEY",
):
monkeypatch.delenv(key, raising=False)
Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"NVIDIA_INFERENCE_KEY",
"SKILLSPECTOR_REASONING_EFFORT",
"SKILLSPECTOR_MODEL",
"SKILLSPECTOR_PROVIDER",
)
Expand Down
Loading
Loading