Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/anthropic_endpoints/skills_endpoints.py: 80%
106 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2Anthropic Skills API endpoints - /v1/skills
3"""
5from types import MappingProxyType
6from typing import Annotated, Final
8import orjson
9from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
10from typing_extensions import ReadOnly, TypedDict, assert_never
12import litellm
13from litellm.llms.litellm_proxy.skills.skill_search import (
14 DEFAULT_SKILL_SEARCH_TOP_K,
15 SkillSearchEmbeddingFailed,
16 SkillSearchHits,
17 SkillSearchNotConfigured,
18 SkillSearchUnsupportedProvider,
19 global_skill_search_index,
20 search_hosted_skills,
21)
22from litellm.proxy._types import UserAPIKeyAuth
23from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
24from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
25from litellm.proxy.common_utils.http_parsing_utils import (
26 convert_upload_files_to_file_data,
27 get_form_data,
28)
29from litellm.types.llms.anthropic_skills import (
30 DeleteSkillResponse,
31 ListSkillsResponse,
32 Skill,
33)
35router: Final = APIRouter()
38class _SkillSearchErrorDetail(TypedDict):
39 error: ReadOnly[str]
40 message: ReadOnly[str]
43def _skill_search_error(status_code: int, error: str, message: str) -> HTTPException:
44 detail: Final[_SkillSearchErrorDetail] = {"error": error, "message": message}
45 return HTTPException(status_code=status_code, detail=detail)
48async def _search_skills(
49 custom_llm_provider: str | None, query: str, top_k: int, user_api_key_dict: UserAPIKeyAuth
50) -> ListSkillsResponse:
51 from litellm.llms.litellm_proxy.skills.transformation import (
52 LiteLLMSkillsTransformationHandler,
53 )
54 from litellm.proxy.proxy_server import llm_router, proxy_logging_obj
56 outcome: Final = await search_hosted_skills(
57 custom_llm_provider=custom_llm_provider,
58 query=query,
59 top_k=top_k,
60 router=llm_router,
61 embedding_model=litellm.skill_search_embedding_model,
62 index=global_skill_search_index,
63 user_api_key_dict=user_api_key_dict,
64 proxy_logging_obj=proxy_logging_obj,
65 )
66 to_response: Final = LiteLLMSkillsTransformationHandler().db_skill_to_response
67 match outcome:
68 case SkillSearchHits(hits): 68 ↛ 69line 68 didn't jump to line 69 because the pattern on line 68 never matched
69 skills: Final = [ # mutable-ok: ListSkillsResponse.data requires list[Skill]; never mutated after
70 to_response(hit.skill).model_copy(update=MappingProxyType({"search_score": hit.score})) for hit in hits
71 ]
72 return ListSkillsResponse(data=skills, has_more=False, next_page=None)
73 case SkillSearchUnsupportedProvider(reason): 73 ↛ 75line 73 didn't jump to line 75 because the pattern on line 73 always matched
74 raise _skill_search_error(400, "skill_search_unsupported_provider", reason)
75 case SkillSearchNotConfigured(reason):
76 raise _skill_search_error(400, "skill_search_not_configured", reason)
77 case SkillSearchEmbeddingFailed(reason):
78 raise _skill_search_error(503, "skill_search_unavailable", reason)
79 case _:
80 assert_never(outcome)
83@router.post(
84 "/v1/skills",
85 tags=["[beta] Anthropic Skills API"],
86 dependencies=[Depends(user_api_key_auth)],
87 response_model=Skill,
88)
89async def create_skill(
90 fastapi_response: Response,
91 request: Request,
92 custom_llm_provider: str | None = "anthropic",
93 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
94):
95 """
96 Create a new skill on Anthropic.
98 Requires `?beta=true` query parameter.
100 Model-based routing (for multi-account support):
101 - Pass model via header: `x-litellm-model: claude-account-1`
102 - Pass model via query: `?model=claude-account-1`
103 - Pass model via form field: `model=claude-account-1`
105 Example usage:
106 ```bash
107 # Basic usage
108 curl -X POST "http://localhost:4000/v1/skills?beta=true" \
109 -H "Content-Type: multipart/form-data" \
110 -H "Authorization: Bearer your-key" \
111 -F "display_title=My Skill" \
112 -F "files[]=@skill.zip"
114 # With model-based routing
115 curl -X POST "http://localhost:4000/v1/skills?beta=true" \
116 -H "Content-Type: multipart/form-data" \
117 -H "Authorization: Bearer your-key" \
118 -H "x-litellm-model: claude-account-1" \
119 -F "display_title=My Skill" \
120 -F "files[]=@skill.zip"
121 ```
123 Returns: Skill object with id, display_title, etc.
124 """
125 from litellm.proxy.proxy_server import (
126 general_settings,
127 llm_router,
128 proxy_config,
129 proxy_logging_obj,
130 select_data_generator,
131 user_api_base,
132 user_max_tokens,
133 user_model,
134 user_request_timeout,
135 user_temperature,
136 version,
137 )
139 # Read form data and convert UploadFile objects to file data tuples
140 form_data: Final = await get_form_data(request)
141 data: Final = await convert_upload_files_to_file_data(form_data)
143 # Extract model for routing (header > query > body)
144 model: Final = data.get("model") or request.query_params.get("model") or request.headers.get("x-litellm-model")
145 if model: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 data["model"] = model
148 if "custom_llm_provider" not in data: 148 ↛ 152line 148 didn't jump to line 152 because the condition on line 148 was always true
149 data["custom_llm_provider"] = custom_llm_provider
151 # Process request using ProxyBaseLLMRequestProcessing
152 processor: Final = ProxyBaseLLMRequestProcessing(data=data)
153 try:
154 return await processor.base_process_llm_request(
155 request=request,
156 fastapi_response=fastapi_response,
157 user_api_key_dict=user_api_key_dict,
158 route_type="acreate_skill",
159 proxy_logging_obj=proxy_logging_obj,
160 llm_router=llm_router,
161 general_settings=general_settings,
162 proxy_config=proxy_config,
163 select_data_generator=select_data_generator,
164 model=data.get("model"),
165 user_model=user_model,
166 user_temperature=user_temperature,
167 user_request_timeout=user_request_timeout,
168 user_max_tokens=user_max_tokens,
169 user_api_base=user_api_base,
170 version=version,
171 )
172 except Exception as e:
173 raise await processor._handle_llm_api_exception(
174 e=e,
175 user_api_key_dict=user_api_key_dict,
176 proxy_logging_obj=proxy_logging_obj,
177 version=version,
178 )
181@router.get(
182 "/v1/skills",
183 tags=["[beta] Anthropic Skills API"],
184 dependencies=[Depends(user_api_key_auth)],
185 response_model=ListSkillsResponse,
186)
187async def list_skills(
188 fastapi_response: Response,
189 request: Request,
190 limit: int | None = 10,
191 after_id: str | None = None,
192 before_id: str | None = None,
193 custom_llm_provider: str | None = "anthropic",
194 query: Annotated[
195 str | None,
196 Query(
197 min_length=1,
198 description="Describe what you need in natural language to rank the skills you can access by "
199 "semantic similarity over their title and description. Each result carries a search_score. "
200 "Only supported for custom_llm_provider=litellm_proxy. Requires "
201 "litellm_settings.skill_search_embedding_model.",
202 ),
203 ] = None,
204 top_k: Annotated[
205 int,
206 Query(ge=1, le=100, description="With query: the maximum number of ranked skills to return."),
207 ] = DEFAULT_SKILL_SEARCH_TOP_K,
208 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
209):
210 """
211 List skills on Anthropic.
213 Requires `?beta=true` query parameter.
215 Model-based routing (for multi-account support):
216 - Pass model via header: `x-litellm-model: claude-account-1`
217 - Pass model via query: `?model=claude-account-1`
218 - Pass model via body: `{"model": "claude-account-1"}`
220 Example usage:
221 ```bash
222 # Basic usage
223 curl "http://localhost:4000/v1/skills?beta=true&limit=10" \
224 -H "Authorization: Bearer your-key"
226 # With model-based routing
227 curl "http://localhost:4000/v1/skills?beta=true&limit=10" \
228 -H "Authorization: Bearer your-key" \
229 -H "x-litellm-model: claude-account-1"
230 ```
232 Pass `?custom_llm_provider=litellm_proxy&query=<task>` to rank the LiteLLM-hosted skills you can
233 access by semantic similarity instead of paging through the whole registry:
234 ```bash
235 curl "http://localhost:4000/v1/skills?custom_llm_provider=litellm_proxy&query=summarize+a+pdf&top_k=5" \
236 -H "Authorization: Bearer your-key"
237 ```
239 Returns: ListSkillsResponse with list of skills
240 """
241 if query is not None:
242 return await _search_skills(
243 custom_llm_provider=custom_llm_provider, query=query, top_k=top_k, user_api_key_dict=user_api_key_dict
244 )
246 from litellm.proxy.proxy_server import (
247 general_settings,
248 llm_router,
249 proxy_config,
250 proxy_logging_obj,
251 select_data_generator,
252 user_api_base,
253 user_max_tokens,
254 user_model,
255 user_request_timeout,
256 user_temperature,
257 version,
258 )
260 # Read request body
261 body: Final = await request.body()
262 data: Final = orjson.loads(body) if body else {}
264 # Use query params if not in body
265 if "limit" not in data and limit is not None: 265 ↛ 267line 265 didn't jump to line 267 because the condition on line 265 was always true
266 data["limit"] = limit
267 if "after_id" not in data and after_id is not None:
268 data["after_id"] = after_id
269 if "before_id" not in data and before_id is not None:
270 data["before_id"] = before_id
272 # Extract model for routing (header > query > body)
273 model: Final = data.get("model") or request.query_params.get("model") or request.headers.get("x-litellm-model")
274 if model: 274 ↛ 275line 274 didn't jump to line 275 because the condition on line 274 was never true
275 data["model"] = model
277 # Set custom_llm_provider: body > query param > default
278 if "custom_llm_provider" not in data: 278 ↛ 282line 278 didn't jump to line 282 because the condition on line 278 was always true
279 data["custom_llm_provider"] = custom_llm_provider
281 # Process request using ProxyBaseLLMRequestProcessing
282 processor: Final = ProxyBaseLLMRequestProcessing(data=data)
283 try:
284 return await processor.base_process_llm_request(
285 request=request,
286 fastapi_response=fastapi_response,
287 user_api_key_dict=user_api_key_dict,
288 route_type="alist_skills",
289 proxy_logging_obj=proxy_logging_obj,
290 llm_router=llm_router,
291 general_settings=general_settings,
292 proxy_config=proxy_config,
293 select_data_generator=select_data_generator,
294 model=data.get("model"),
295 user_model=user_model,
296 user_temperature=user_temperature,
297 user_request_timeout=user_request_timeout,
298 user_max_tokens=user_max_tokens,
299 user_api_base=user_api_base,
300 version=version,
301 )
302 except Exception as e:
303 raise await processor._handle_llm_api_exception(
304 e=e,
305 user_api_key_dict=user_api_key_dict,
306 proxy_logging_obj=proxy_logging_obj,
307 version=version,
308 )
311@router.get(
312 "/v1/skills/{skill_id}",
313 tags=["[beta] Anthropic Skills API"],
314 dependencies=[Depends(user_api_key_auth)],
315 response_model=Skill,
316)
317async def get_skill(
318 skill_id: str,
319 fastapi_response: Response,
320 request: Request,
321 custom_llm_provider: str | None = "anthropic",
322 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
323):
324 """
325 Get a specific skill by ID from Anthropic.
327 Requires `?beta=true` query parameter.
329 Model-based routing (for multi-account support):
330 - Pass model via header: `x-litellm-model: claude-account-1`
331 - Pass model via query: `?model=claude-account-1`
332 - Pass model via body: `{"model": "claude-account-1"}`
334 Example usage:
335 ```bash
336 # Basic usage
337 curl "http://localhost:4000/v1/skills/skill_123?beta=true" \
338 -H "Authorization: Bearer your-key"
340 # With model-based routing
341 curl "http://localhost:4000/v1/skills/skill_123?beta=true" \
342 -H "Authorization: Bearer your-key" \
343 -H "x-litellm-model: claude-account-1"
344 ```
346 Returns: Skill object
347 """
348 from litellm.proxy.proxy_server import (
349 general_settings,
350 llm_router,
351 proxy_config,
352 proxy_logging_obj,
353 select_data_generator,
354 user_api_base,
355 user_max_tokens,
356 user_model,
357 user_request_timeout,
358 user_temperature,
359 version,
360 )
362 # Read request body
363 body: Final = await request.body()
364 data: Final = orjson.loads(body) if body else {}
366 # Set skill_id from path parameter
367 data["skill_id"] = skill_id
369 # Extract model for routing (header > query > body)
370 model: Final = data.get("model") or request.query_params.get("model") or request.headers.get("x-litellm-model")
371 if model: 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 data["model"] = model
374 # Set custom_llm_provider: body > query param > default
375 if "custom_llm_provider" not in data: 375 ↛ 379line 375 didn't jump to line 379 because the condition on line 375 was always true
376 data["custom_llm_provider"] = custom_llm_provider
378 # Process request using ProxyBaseLLMRequestProcessing
379 processor: Final = ProxyBaseLLMRequestProcessing(data=data)
380 try:
381 return await processor.base_process_llm_request(
382 request=request,
383 fastapi_response=fastapi_response,
384 user_api_key_dict=user_api_key_dict,
385 route_type="aget_skill",
386 proxy_logging_obj=proxy_logging_obj,
387 llm_router=llm_router,
388 general_settings=general_settings,
389 proxy_config=proxy_config,
390 select_data_generator=select_data_generator,
391 model=data.get("model"),
392 user_model=user_model,
393 user_temperature=user_temperature,
394 user_request_timeout=user_request_timeout,
395 user_max_tokens=user_max_tokens,
396 user_api_base=user_api_base,
397 version=version,
398 )
399 except Exception as e:
400 raise await processor._handle_llm_api_exception(
401 e=e,
402 user_api_key_dict=user_api_key_dict,
403 proxy_logging_obj=proxy_logging_obj,
404 version=version,
405 )
408@router.delete(
409 "/v1/skills/{skill_id}",
410 tags=["[beta] Anthropic Skills API"],
411 dependencies=[Depends(user_api_key_auth)],
412 response_model=DeleteSkillResponse,
413)
414async def delete_skill(
415 skill_id: str,
416 fastapi_response: Response,
417 request: Request,
418 custom_llm_provider: str | None = "anthropic",
419 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
420):
421 """
422 Delete a skill by ID from Anthropic.
424 Requires `?beta=true` query parameter.
426 Note: Anthropic does not allow deleting skills with existing versions.
428 Model-based routing (for multi-account support):
429 - Pass model via header: `x-litellm-model: claude-account-1`
430 - Pass model via query: `?model=claude-account-1`
431 - Pass model via body: `{"model": "claude-account-1"}`
433 Example usage:
434 ```bash
435 # Basic usage
436 curl -X DELETE "http://localhost:4000/v1/skills/skill_123?beta=true" \
437 -H "Authorization: Bearer your-key"
439 # With model-based routing
440 curl -X DELETE "http://localhost:4000/v1/skills/skill_123?beta=true" \
441 -H "Authorization: Bearer your-key" \
442 -H "x-litellm-model: claude-account-1"
443 ```
445 Returns: DeleteSkillResponse with type="skill_deleted"
446 """
447 from litellm.proxy.proxy_server import (
448 general_settings,
449 llm_router,
450 proxy_config,
451 proxy_logging_obj,
452 select_data_generator,
453 user_api_base,
454 user_max_tokens,
455 user_model,
456 user_request_timeout,
457 user_temperature,
458 version,
459 )
461 # Read request body
462 body: Final = await request.body()
463 data: Final = orjson.loads(body) if body else {}
465 # Set skill_id from path parameter
466 data["skill_id"] = skill_id
468 # Extract model for routing (header > query > body)
469 model: Final = data.get("model") or request.query_params.get("model") or request.headers.get("x-litellm-model")
470 if model: 470 ↛ 471line 470 didn't jump to line 471 because the condition on line 470 was never true
471 data["model"] = model
473 # Set custom_llm_provider: body > query param > default
474 if "custom_llm_provider" not in data: 474 ↛ 478line 474 didn't jump to line 478 because the condition on line 474 was always true
475 data["custom_llm_provider"] = custom_llm_provider
477 # Process request using ProxyBaseLLMRequestProcessing
478 processor: Final = ProxyBaseLLMRequestProcessing(data=data)
479 try:
480 return await processor.base_process_llm_request(
481 request=request,
482 fastapi_response=fastapi_response,
483 user_api_key_dict=user_api_key_dict,
484 route_type="adelete_skill",
485 proxy_logging_obj=proxy_logging_obj,
486 llm_router=llm_router,
487 general_settings=general_settings,
488 proxy_config=proxy_config,
489 select_data_generator=select_data_generator,
490 model=data.get("model"),
491 user_model=user_model,
492 user_temperature=user_temperature,
493 user_request_timeout=user_request_timeout,
494 user_max_tokens=user_max_tokens,
495 user_api_base=user_api_base,
496 version=version,
497 )
498 except Exception as e:
499 raise await processor._handle_llm_api_exception(
500 e=e,
501 user_api_key_dict=user_api_key_dict,
502 proxy_logging_obj=proxy_logging_obj,
503 version=version,
504 )