Replace broken Anthropic Skills API integration test with unit tests (#23325)

* Replace broken Anthropic Skills API integration test with unit tests

* Update tests/test_litellm/test_anthropic_skills_transformation.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: Aarish Alam <arishalam121@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Joe Reyna 2026-03-11 09:30:08 -07:00 • committed by GitHub
parent 3f18cd2fdc
commit ca910a8fa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 345 additions and 13 deletions

View file

@ -274,17 +274,11 @@ class BaseSkillsAPITest(ABC):
print(f"Deleted skill response: {response}")
class TestAnthropicSkillsAPI(BaseSkillsAPITest):
"""
Test Anthropic Skills API implementation.
"""
def get_custom_llm_provider(self) -> str:
return "anthropic"
def get_api_key(self) -> Optional[str]:
return os.environ.get("ANTHROPIC_API_KEY")
def get_api_base(self) -> Optional[str]:
return os.environ.get("ANTHROPIC_API_BASE")
# Live integration tests for the Anthropic Skills API are not run in CI because
# the Skills API requires beta access (anthropic-beta: skills-2025-10-02) that
# is not available on the standard API key used in CI.
#
# Transformation logic (URL construction, headers, request/response parsing) is
# covered by unit tests in:
# tests/test_litellm/test_anthropic_skills_transformation.py

View file

@ -0,0 +1,338 @@
"""
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 json
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) -> httpx.Response:
return httpx.Response(
status_code=status_code,
json=json_data,
request=httpx.Request("GET", "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_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)
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)
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)
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)
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