""" 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