mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
348 lines
13 KiB
Python
348 lines
13 KiB
Python
"""
|
|
Unit tests for Anthropic Skills API request/response transformation.
|
|
|
|
These tests validate URL construction, header generation, request payload
|
|
building, and response parsing without requiring a live Anthropic API key
|
|
or beta access to the Skills API.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from litellm.constants import ANTHROPIC_SKILLS_API_BETA_VERSION
|
|
from litellm.llms.anthropic.skills.transformation import AnthropicSkillsConfig
|
|
from litellm.types.llms.anthropic_skills import (
|
|
CreateSkillRequest,
|
|
DeleteSkillResponse,
|
|
ListSkillsParams,
|
|
ListSkillsResponse,
|
|
Skill,
|
|
)
|
|
from litellm.types.router import GenericLiteLLMParams
|
|
|
|
|
|
FAKE_API_KEY = "sk-ant-test-key-1234"
|
|
FAKE_API_BASE = "https://api.anthropic.com"
|
|
|
|
|
|
def _make_mock_response(
|
|
json_data: dict, status_code: int = 200, method: str = "POST"
|
|
) -> httpx.Response:
|
|
return httpx.Response(
|
|
status_code=status_code,
|
|
json=json_data,
|
|
request=httpx.Request(method, "https://api.anthropic.com/v1/skills"),
|
|
)
|
|
|
|
|
|
def _make_skill_payload(**kwargs) -> dict:
|
|
defaults = {
|
|
"id": "skill_abc123",
|
|
"created_at": "2025-10-15T12:00:00Z",
|
|
"updated_at": "2025-10-15T12:00:00Z",
|
|
"source": "custom",
|
|
"type": "skill",
|
|
"display_title": "Test Skill",
|
|
"latest_version": "v1",
|
|
}
|
|
defaults.update(kwargs)
|
|
return defaults
|
|
|
|
|
|
class TestAnthropicSkillsConfigURLConstruction:
|
|
def setup_method(self):
|
|
self.config = AnthropicSkillsConfig()
|
|
|
|
def test_url_without_skill_id(self):
|
|
url = self.config.get_complete_url(
|
|
api_base=FAKE_API_BASE,
|
|
endpoint="skills",
|
|
)
|
|
assert url == f"{FAKE_API_BASE}/v1/skills"
|
|
|
|
def test_url_with_skill_id(self):
|
|
url = self.config.get_complete_url(
|
|
api_base=FAKE_API_BASE,
|
|
endpoint="skills",
|
|
skill_id="skill_abc123",
|
|
)
|
|
assert url == f"{FAKE_API_BASE}/v1/skills/skill_abc123"
|
|
|
|
def test_url_with_skill_id_encodes_path_segment(self):
|
|
url = self.config.get_complete_url(
|
|
api_base=FAKE_API_BASE,
|
|
endpoint="skills",
|
|
skill_id="../../files?x=1#frag",
|
|
)
|
|
|
|
assert url == f"{FAKE_API_BASE}/v1/skills/..%2F..%2Ffiles%3Fx%3D1%23frag"
|
|
|
|
def test_url_falls_back_to_anthropic_default(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_base",
|
|
return_value="https://api.anthropic.com",
|
|
):
|
|
url = self.config.get_complete_url(
|
|
api_base=None,
|
|
endpoint="skills",
|
|
)
|
|
assert url == "https://api.anthropic.com/v1/skills"
|
|
|
|
def test_url_with_custom_api_base(self):
|
|
custom_base = "https://my-proxy.example.com"
|
|
url = self.config.get_complete_url(
|
|
api_base=custom_base,
|
|
endpoint="skills",
|
|
)
|
|
assert url == f"{custom_base}/v1/skills"
|
|
|
|
|
|
class TestAnthropicSkillsConfigHeaderValidation:
|
|
def setup_method(self):
|
|
self.config = AnthropicSkillsConfig()
|
|
|
|
def _make_litellm_params(self, api_key=FAKE_API_KEY):
|
|
return GenericLiteLLMParams(api_key=api_key)
|
|
|
|
def test_sets_api_key_header(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_key",
|
|
return_value=FAKE_API_KEY,
|
|
):
|
|
headers = self.config.validate_environment(
|
|
headers={}, litellm_params=self._make_litellm_params()
|
|
)
|
|
assert headers["x-api-key"] == FAKE_API_KEY
|
|
|
|
def test_sets_anthropic_version_header(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_key",
|
|
return_value=FAKE_API_KEY,
|
|
):
|
|
headers = self.config.validate_environment(
|
|
headers={}, litellm_params=self._make_litellm_params()
|
|
)
|
|
assert headers["anthropic-version"] == "2023-06-01"
|
|
|
|
def test_sets_skills_beta_header(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_key",
|
|
return_value=FAKE_API_KEY,
|
|
):
|
|
headers = self.config.validate_environment(
|
|
headers={}, litellm_params=self._make_litellm_params()
|
|
)
|
|
assert headers["anthropic-beta"] == ANTHROPIC_SKILLS_API_BETA_VERSION
|
|
|
|
def test_merges_existing_beta_header_string(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_key",
|
|
return_value=FAKE_API_KEY,
|
|
):
|
|
headers = self.config.validate_environment(
|
|
headers={"anthropic-beta": "other-beta-2024-01-01"},
|
|
litellm_params=self._make_litellm_params(),
|
|
)
|
|
assert isinstance(headers["anthropic-beta"], list)
|
|
assert "other-beta-2024-01-01" in headers["anthropic-beta"]
|
|
assert ANTHROPIC_SKILLS_API_BETA_VERSION in headers["anthropic-beta"]
|
|
|
|
def test_merges_existing_beta_header_list(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_key",
|
|
return_value=FAKE_API_KEY,
|
|
):
|
|
headers = self.config.validate_environment(
|
|
headers={"anthropic-beta": ["other-beta-2024-01-01"]},
|
|
litellm_params=self._make_litellm_params(),
|
|
)
|
|
assert ANTHROPIC_SKILLS_API_BETA_VERSION in headers["anthropic-beta"]
|
|
assert "other-beta-2024-01-01" in headers["anthropic-beta"]
|
|
|
|
def test_does_not_duplicate_beta_header(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_key",
|
|
return_value=FAKE_API_KEY,
|
|
):
|
|
headers = self.config.validate_environment(
|
|
headers={"anthropic-beta": ANTHROPIC_SKILLS_API_BETA_VERSION},
|
|
litellm_params=self._make_litellm_params(),
|
|
)
|
|
beta = headers["anthropic-beta"]
|
|
if isinstance(beta, list):
|
|
assert beta.count(ANTHROPIC_SKILLS_API_BETA_VERSION) == 1
|
|
else:
|
|
assert beta == ANTHROPIC_SKILLS_API_BETA_VERSION
|
|
|
|
def test_raises_without_api_key(self):
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_key",
|
|
return_value=None,
|
|
):
|
|
with pytest.raises(ValueError, match="ANTHROPIC_API_KEY"):
|
|
self.config.validate_environment(
|
|
headers={}, litellm_params=self._make_litellm_params(api_key=None)
|
|
)
|
|
|
|
|
|
class TestAnthropicSkillsConfigCreateRequestTransformation:
|
|
def setup_method(self):
|
|
self.config = AnthropicSkillsConfig()
|
|
self.litellm_params = GenericLiteLLMParams(api_key=FAKE_API_KEY)
|
|
|
|
def test_display_title_included(self):
|
|
create_request: CreateSkillRequest = {"display_title": "My Skill"}
|
|
body = self.config.transform_create_skill_request(
|
|
create_request=create_request,
|
|
litellm_params=self.litellm_params,
|
|
headers={},
|
|
)
|
|
assert body["display_title"] == "My Skill"
|
|
|
|
def test_none_values_excluded(self):
|
|
create_request: CreateSkillRequest = {"display_title": None, "files": None}
|
|
body = self.config.transform_create_skill_request(
|
|
create_request=create_request,
|
|
litellm_params=self.litellm_params,
|
|
headers={},
|
|
)
|
|
assert "display_title" not in body
|
|
assert "files" not in body
|
|
|
|
def test_empty_request_produces_empty_body(self):
|
|
create_request: CreateSkillRequest = {}
|
|
body = self.config.transform_create_skill_request(
|
|
create_request=create_request,
|
|
litellm_params=self.litellm_params,
|
|
headers={},
|
|
)
|
|
assert body == {}
|
|
|
|
|
|
class TestAnthropicSkillsConfigListRequestTransformation:
|
|
def setup_method(self):
|
|
self.config = AnthropicSkillsConfig()
|
|
self.litellm_params = GenericLiteLLMParams(api_key=FAKE_API_KEY)
|
|
|
|
def test_limit_included_in_query_params(self):
|
|
list_params: ListSkillsParams = {"limit": 25}
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_base",
|
|
return_value=FAKE_API_BASE,
|
|
):
|
|
url, query_params = self.config.transform_list_skills_request(
|
|
list_params=list_params,
|
|
litellm_params=self.litellm_params,
|
|
headers={},
|
|
)
|
|
assert query_params["limit"] == 25
|
|
assert url == f"{FAKE_API_BASE}/v1/skills"
|
|
|
|
def test_source_filter_included(self):
|
|
list_params: ListSkillsParams = {"source": "custom"}
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_base",
|
|
return_value=FAKE_API_BASE,
|
|
):
|
|
_, query_params = self.config.transform_list_skills_request(
|
|
list_params=list_params,
|
|
litellm_params=self.litellm_params,
|
|
headers={},
|
|
)
|
|
assert query_params["source"] == "custom"
|
|
|
|
def test_empty_params_produce_empty_query(self):
|
|
list_params: ListSkillsParams = {}
|
|
with patch(
|
|
"litellm.llms.anthropic.common_utils.AnthropicModelInfo.get_api_base",
|
|
return_value=FAKE_API_BASE,
|
|
):
|
|
_, query_params = self.config.transform_list_skills_request(
|
|
list_params=list_params,
|
|
litellm_params=self.litellm_params,
|
|
headers={},
|
|
)
|
|
assert query_params == {}
|
|
|
|
|
|
class TestAnthropicSkillsConfigResponseTransformation:
|
|
def setup_method(self):
|
|
self.config = AnthropicSkillsConfig()
|
|
self.logging_obj = MagicMock()
|
|
|
|
def test_create_skill_response_parses_skill(self):
|
|
payload = _make_skill_payload()
|
|
raw = _make_mock_response(payload)
|
|
skill = self.config.transform_create_skill_response(
|
|
raw_response=raw, logging_obj=self.logging_obj
|
|
)
|
|
assert isinstance(skill, Skill)
|
|
assert skill.id == "skill_abc123"
|
|
assert skill.source == "custom"
|
|
assert skill.display_title == "Test Skill"
|
|
|
|
def test_get_skill_response_parses_skill(self):
|
|
payload = _make_skill_payload(id="skill_xyz", display_title="Another")
|
|
raw = _make_mock_response(payload, method="GET")
|
|
skill = self.config.transform_get_skill_response(
|
|
raw_response=raw, logging_obj=self.logging_obj
|
|
)
|
|
assert isinstance(skill, Skill)
|
|
assert skill.id == "skill_xyz"
|
|
assert skill.display_title == "Another"
|
|
|
|
def test_list_skills_response_parses_list(self):
|
|
payload = {
|
|
"data": [_make_skill_payload(), _make_skill_payload(id="skill_def456")],
|
|
"has_more": False,
|
|
"next_page": None,
|
|
}
|
|
raw = _make_mock_response(payload, method="GET")
|
|
result = self.config.transform_list_skills_response(
|
|
raw_response=raw, logging_obj=self.logging_obj
|
|
)
|
|
assert isinstance(result, ListSkillsResponse)
|
|
assert len(result.data) == 2
|
|
assert result.data[0].id == "skill_abc123"
|
|
assert result.data[1].id == "skill_def456"
|
|
assert result.has_more is False
|
|
|
|
def test_list_skills_response_with_pagination(self):
|
|
payload = {
|
|
"data": [_make_skill_payload()],
|
|
"has_more": True,
|
|
"next_page": "page_token_xyz",
|
|
}
|
|
raw = _make_mock_response(payload, method="GET")
|
|
result = self.config.transform_list_skills_response(
|
|
raw_response=raw, logging_obj=self.logging_obj
|
|
)
|
|
assert result.has_more is True
|
|
assert result.next_page == "page_token_xyz"
|
|
|
|
def test_delete_skill_response_parses_correctly(self):
|
|
payload = {"id": "skill_abc123", "type": "skill_deleted"}
|
|
raw = _make_mock_response(payload, method="DELETE")
|
|
result = self.config.transform_delete_skill_response(
|
|
raw_response=raw, logging_obj=self.logging_obj
|
|
)
|
|
assert isinstance(result, DeleteSkillResponse)
|
|
assert result.id == "skill_abc123"
|
|
assert result.type == "skill_deleted"
|
|
|
|
def test_skill_response_optional_fields_default(self):
|
|
payload = {
|
|
"id": "skill_minimal",
|
|
"created_at": "2025-10-15T12:00:00Z",
|
|
"updated_at": "2025-10-15T12:00:00Z",
|
|
"source": "anthropic",
|
|
"type": "skill",
|
|
}
|
|
raw = _make_mock_response(payload)
|
|
skill = self.config.transform_create_skill_response(
|
|
raw_response=raw, logging_obj=self.logging_obj
|
|
)
|
|
assert skill.display_title is None
|
|
assert skill.latest_version is None
|