chore: initial public snapshot for github upload
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Claude Code Endpoints
|
||||
|
||||
Provides endpoints for Claude Code plugin marketplace integration.
|
||||
"""
|
||||
|
||||
from litellm.proxy.anthropic_endpoints.claude_code_endpoints.claude_code_marketplace import (
|
||||
router as claude_code_marketplace_router,
|
||||
)
|
||||
|
||||
__all__ = ["claude_code_marketplace_router"]
|
||||
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
CLAUDE CODE MARKETPLACE
|
||||
|
||||
Provides a registry/discovery layer for Claude Code plugins.
|
||||
Plugins are stored as metadata + git source references in LiteLLM database.
|
||||
Actual plugin files are hosted on GitHub/GitLab/Bitbucket.
|
||||
|
||||
Endpoints:
|
||||
/claude-code/marketplace.json - GET - List plugins for Claude Code discovery
|
||||
/claude-code/plugins - POST - Register a plugin
|
||||
/claude-code/plugins - GET - List plugins (admin)
|
||||
/claude-code/plugins/{name} - GET - Get plugin details
|
||||
/claude-code/plugins/{name}/enable - POST - Enable a plugin
|
||||
/claude-code/plugins/{name}/disable - POST - Disable a plugin
|
||||
/claude-code/plugins/{name} - DELETE - Delete a plugin
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.proxy.claude_code_endpoints import (
|
||||
ListPluginsResponse,
|
||||
PluginListItem,
|
||||
RegisterPluginRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def _get_prisma_client():
|
||||
"""Get the prisma client from proxy_server."""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
return prisma_client
|
||||
|
||||
|
||||
@router.get(
|
||||
"/claude-code/marketplace.json",
|
||||
tags=["Claude Code Marketplace"],
|
||||
)
|
||||
async def get_marketplace():
|
||||
"""
|
||||
Serve marketplace.json for Claude Code plugin discovery.
|
||||
|
||||
This endpoint is accessed by Claude Code CLI when users run:
|
||||
- claude plugin marketplace add <url>
|
||||
- claude plugin install <name>@<marketplace>
|
||||
|
||||
Returns:
|
||||
Marketplace catalog with list of available plugins and their git sources.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
claude plugin marketplace add http://localhost:4000/claude-code/marketplace.json
|
||||
claude plugin install my-plugin@litellm
|
||||
```
|
||||
"""
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
plugins = await prisma_client.db.litellm_claudecodeplugintable.find_many(
|
||||
where={"enabled": True}
|
||||
)
|
||||
|
||||
plugin_list = []
|
||||
for plugin in plugins:
|
||||
try:
|
||||
manifest = json.loads(plugin.manifest_json)
|
||||
except json.JSONDecodeError:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Plugin {plugin.name} has invalid manifest JSON, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
# Source must be specified for URL-based marketplaces
|
||||
if "source" not in manifest:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Plugin {plugin.name} has no source field, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
entry: Dict[str, Any] = {
|
||||
"name": plugin.name,
|
||||
"source": manifest["source"],
|
||||
}
|
||||
|
||||
if plugin.version:
|
||||
entry["version"] = plugin.version
|
||||
if plugin.description:
|
||||
entry["description"] = plugin.description
|
||||
if "author" in manifest:
|
||||
entry["author"] = manifest["author"]
|
||||
if "homepage" in manifest:
|
||||
entry["homepage"] = manifest["homepage"]
|
||||
if "keywords" in manifest:
|
||||
entry["keywords"] = manifest["keywords"]
|
||||
if "category" in manifest:
|
||||
entry["category"] = manifest["category"]
|
||||
|
||||
plugin_list.append(entry)
|
||||
|
||||
marketplace = {
|
||||
"name": "litellm",
|
||||
"owner": {"name": "LiteLLM", "email": "support@litellm.ai"},
|
||||
"plugins": plugin_list,
|
||||
}
|
||||
|
||||
return JSONResponse(content=marketplace)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error generating marketplace: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": f"Failed to generate marketplace: {str(e)}"},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claude-code/plugins",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def register_plugin(
|
||||
request: RegisterPluginRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Register a plugin in the LiteLLM marketplace.
|
||||
|
||||
LiteLLM acts as a registry/discovery layer. Plugins are hosted on
|
||||
GitHub/GitLab/Bitbucket. Claude Code will clone from the git source
|
||||
when users install.
|
||||
|
||||
Parameters:
|
||||
- name: Plugin name (kebab-case)
|
||||
- source: Git source reference (github or url format)
|
||||
- version: Semantic version (optional)
|
||||
- description: Plugin description (optional)
|
||||
- author: Author information (optional)
|
||||
- homepage: Plugin homepage URL (optional)
|
||||
- keywords: Search keywords (optional)
|
||||
- category: Plugin category (optional)
|
||||
|
||||
Returns:
|
||||
Registration status and plugin information.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
curl -X POST http://localhost:4000/claude-code/plugins \\
|
||||
-H "Authorization: Bearer sk-..." \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"name": "my-plugin",
|
||||
"source": {"source": "github", "repo": "org/my-plugin"},
|
||||
"version": "1.0.0",
|
||||
"description": "My awesome plugin"
|
||||
}'
|
||||
```
|
||||
"""
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
# Validate name format
|
||||
if not re.match(r"^[a-z0-9-]+$", request.name):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Plugin name must be kebab-case (lowercase letters, numbers, hyphens)"
|
||||
},
|
||||
)
|
||||
|
||||
# Validate source format
|
||||
source = request.source
|
||||
source_type = source.get("source")
|
||||
|
||||
if source_type == "github":
|
||||
if "repo" not in source:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "GitHub source must include 'repo' field (e.g., 'org/repo')"
|
||||
},
|
||||
)
|
||||
elif source_type == "url":
|
||||
if "url" not in source:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "URL source must include 'url' field (e.g., 'https://github.com/org/repo.git')"
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "source.source must be 'github' or 'url'"},
|
||||
)
|
||||
|
||||
# Build manifest for storage
|
||||
manifest: Dict[str, Any] = {
|
||||
"name": request.name,
|
||||
"source": request.source,
|
||||
}
|
||||
if request.version:
|
||||
manifest["version"] = request.version
|
||||
if request.description:
|
||||
manifest["description"] = request.description
|
||||
if request.author:
|
||||
manifest["author"] = request.author.model_dump(exclude_none=True)
|
||||
if request.homepage:
|
||||
manifest["homepage"] = request.homepage
|
||||
if request.keywords:
|
||||
manifest["keywords"] = request.keywords
|
||||
if request.category:
|
||||
manifest["category"] = request.category
|
||||
|
||||
# Check if plugin exists
|
||||
existing = await prisma_client.db.litellm_claudecodeplugintable.find_unique(
|
||||
where={"name": request.name}
|
||||
)
|
||||
|
||||
if existing:
|
||||
plugin = await prisma_client.db.litellm_claudecodeplugintable.update(
|
||||
where={"name": request.name},
|
||||
data={
|
||||
"version": request.version,
|
||||
"description": request.description,
|
||||
"manifest_json": json.dumps(manifest),
|
||||
"files_json": "{}",
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
},
|
||||
)
|
||||
action = "updated"
|
||||
else:
|
||||
plugin = await prisma_client.db.litellm_claudecodeplugintable.create(
|
||||
data={
|
||||
"name": request.name,
|
||||
"version": request.version,
|
||||
"description": request.description,
|
||||
"manifest_json": json.dumps(manifest),
|
||||
"files_json": "{}",
|
||||
"enabled": True,
|
||||
"created_at": datetime.now(timezone.utc),
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"created_by": user_api_key_dict.user_id,
|
||||
}
|
||||
)
|
||||
action = "created"
|
||||
|
||||
verbose_proxy_logger.info(f"Plugin {request.name} {action} successfully")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"action": action,
|
||||
"plugin": {
|
||||
"id": plugin.id,
|
||||
"name": plugin.name,
|
||||
"version": plugin.version,
|
||||
"description": plugin.description,
|
||||
"source": request.source,
|
||||
"enabled": plugin.enabled,
|
||||
},
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error registering plugin: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": f"Registration failed: {str(e)}"},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/claude-code/plugins",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=ListPluginsResponse,
|
||||
)
|
||||
async def list_plugins(
|
||||
enabled_only: bool = False,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
List all plugins in the marketplace.
|
||||
|
||||
Parameters:
|
||||
- enabled_only: If true, only return enabled plugins
|
||||
|
||||
Returns:
|
||||
List of plugins with their metadata.
|
||||
"""
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
where = {"enabled": True} if enabled_only else {}
|
||||
plugins = await prisma_client.db.litellm_claudecodeplugintable.find_many(
|
||||
where=where
|
||||
)
|
||||
|
||||
plugin_list = []
|
||||
for p in plugins:
|
||||
# Parse manifest to get additional fields
|
||||
manifest = json.loads(p.manifest_json) if p.manifest_json else {}
|
||||
|
||||
plugin_list.append(
|
||||
PluginListItem(
|
||||
id=p.id,
|
||||
name=p.name,
|
||||
version=p.version,
|
||||
description=p.description,
|
||||
source=manifest.get("source", {}),
|
||||
author=manifest.get("author"),
|
||||
homepage=manifest.get("homepage"),
|
||||
keywords=manifest.get("keywords"),
|
||||
category=manifest.get("category"),
|
||||
enabled=p.enabled,
|
||||
created_at=p.created_at.isoformat() if p.created_at else None,
|
||||
updated_at=p.updated_at.isoformat() if p.updated_at else None,
|
||||
)
|
||||
)
|
||||
|
||||
# Sort by created_at descending (newest first)
|
||||
plugin_list.sort(key=lambda x: x.created_at or "", reverse=True)
|
||||
|
||||
return ListPluginsResponse(
|
||||
plugins=plugin_list,
|
||||
count=len(plugin_list),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error listing plugins: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/claude-code/plugins/{plugin_name}",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_plugin(
|
||||
plugin_name: str,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Get details of a specific plugin.
|
||||
|
||||
Parameters:
|
||||
- plugin_name: The name of the plugin
|
||||
|
||||
Returns:
|
||||
Plugin details including source and metadata.
|
||||
"""
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
plugin = await prisma_client.db.litellm_claudecodeplugintable.find_unique(
|
||||
where={"name": plugin_name}
|
||||
)
|
||||
|
||||
if not plugin:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": f"Plugin '{plugin_name}' not found"},
|
||||
)
|
||||
|
||||
manifest = json.loads(plugin.manifest_json) if plugin.manifest_json else {}
|
||||
|
||||
return {
|
||||
"id": plugin.id,
|
||||
"name": plugin.name,
|
||||
"version": plugin.version,
|
||||
"description": plugin.description,
|
||||
"source": manifest.get("source"),
|
||||
"author": manifest.get("author"),
|
||||
"homepage": manifest.get("homepage"),
|
||||
"keywords": manifest.get("keywords"),
|
||||
"category": manifest.get("category"),
|
||||
"enabled": plugin.enabled,
|
||||
"created_at": plugin.created_at.isoformat() if plugin.created_at else None,
|
||||
"updated_at": plugin.updated_at.isoformat() if plugin.updated_at else None,
|
||||
"created_by": plugin.created_by,
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting plugin: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claude-code/plugins/{plugin_name}/enable",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def enable_plugin(
|
||||
plugin_name: str,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Enable a disabled plugin.
|
||||
|
||||
Parameters:
|
||||
- plugin_name: The name of the plugin to enable
|
||||
"""
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
plugin = await prisma_client.db.litellm_claudecodeplugintable.find_unique(
|
||||
where={"name": plugin_name}
|
||||
)
|
||||
if not plugin:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": f"Plugin '{plugin_name}' not found"},
|
||||
)
|
||||
|
||||
await prisma_client.db.litellm_claudecodeplugintable.update(
|
||||
where={"name": plugin_name},
|
||||
data={"enabled": True, "updated_at": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
verbose_proxy_logger.info(f"Plugin {plugin_name} enabled")
|
||||
return {"status": "success", "message": f"Plugin '{plugin_name}' enabled"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error enabling plugin: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claude-code/plugins/{plugin_name}/disable",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def disable_plugin(
|
||||
plugin_name: str,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Disable a plugin without deleting it.
|
||||
|
||||
Parameters:
|
||||
- plugin_name: The name of the plugin to disable
|
||||
"""
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
plugin = await prisma_client.db.litellm_claudecodeplugintable.find_unique(
|
||||
where={"name": plugin_name}
|
||||
)
|
||||
if not plugin:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": f"Plugin '{plugin_name}' not found"},
|
||||
)
|
||||
|
||||
await prisma_client.db.litellm_claudecodeplugintable.update(
|
||||
where={"name": plugin_name},
|
||||
data={"enabled": False, "updated_at": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
verbose_proxy_logger.info(f"Plugin {plugin_name} disabled")
|
||||
return {"status": "success", "message": f"Plugin '{plugin_name}' disabled"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error disabling plugin: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/claude-code/plugins/{plugin_name}",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def delete_plugin(
|
||||
plugin_name: str,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Delete a plugin from the marketplace.
|
||||
|
||||
Parameters:
|
||||
- plugin_name: The name of the plugin to delete
|
||||
"""
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
plugin = await prisma_client.db.litellm_claudecodeplugintable.find_unique(
|
||||
where={"name": plugin_name}
|
||||
)
|
||||
if not plugin:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": f"Plugin '{plugin_name}' not found"},
|
||||
)
|
||||
|
||||
await prisma_client.db.litellm_claudecodeplugintable.delete(
|
||||
where={"name": plugin_name}
|
||||
)
|
||||
|
||||
verbose_proxy_logger.info(f"Plugin {plugin_name} deleted")
|
||||
return {"status": "success", "message": f"Plugin '{plugin_name}' deleted"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting plugin: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": str(e)},
|
||||
)
|
||||
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Unified /v1/messages endpoint - (Anthropic Spec)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.anthropic_interface.exceptions import AnthropicExceptionMapping
|
||||
from litellm.integrations.custom_guardrail import ModifyResponseException
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_request_processing import (
|
||||
ProxyBaseLLMRequestProcessing,
|
||||
create_response,
|
||||
)
|
||||
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
|
||||
from litellm.types.utils import TokenCountResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/messages",
|
||||
tags=["[beta] Anthropic `/v1/messages`"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def anthropic_response( # noqa: PLR0915
|
||||
fastapi_response: Response,
|
||||
request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Use `{PROXY_BASE_URL}/anthropic/v1/messages` instead - [Docs](https://docs.litellm.ai/docs/pass_through/anthropic_completion).
|
||||
|
||||
This was a BETA endpoint that calls 100+ LLMs in the anthropic format.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import (
|
||||
general_settings,
|
||||
llm_router,
|
||||
proxy_config,
|
||||
proxy_logging_obj,
|
||||
user_api_base,
|
||||
user_max_tokens,
|
||||
user_model,
|
||||
user_request_timeout,
|
||||
user_temperature,
|
||||
version,
|
||||
)
|
||||
|
||||
data = await _read_request_body(request=request)
|
||||
base_llm_response_processor = ProxyBaseLLMRequestProcessing(data=data)
|
||||
try:
|
||||
result = await base_llm_response_processor.base_process_llm_request(
|
||||
request=request,
|
||||
fastapi_response=fastapi_response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
route_type="anthropic_messages",
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
llm_router=llm_router,
|
||||
general_settings=general_settings,
|
||||
proxy_config=proxy_config,
|
||||
select_data_generator=None,
|
||||
model=None,
|
||||
user_model=user_model,
|
||||
user_temperature=user_temperature,
|
||||
user_request_timeout=user_request_timeout,
|
||||
user_max_tokens=user_max_tokens,
|
||||
user_api_base=user_api_base,
|
||||
version=version,
|
||||
)
|
||||
return result
|
||||
except ModifyResponseException as e:
|
||||
# Guardrail flagged content in passthrough mode - return 200 with violation message
|
||||
_data = e.request_data
|
||||
await proxy_logging_obj.post_call_failure_hook(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
original_exception=e,
|
||||
request_data=_data,
|
||||
)
|
||||
|
||||
# Create Anthropic-formatted response with violation message
|
||||
import uuid
|
||||
|
||||
from litellm.types.utils import AnthropicMessagesResponse
|
||||
|
||||
_anthropic_response = AnthropicMessagesResponse(
|
||||
id=f"msg_{str(uuid.uuid4())}",
|
||||
type="message",
|
||||
role="assistant",
|
||||
content=[{"type": "text", "text": e.message}],
|
||||
model=e.model,
|
||||
stop_reason="end_turn",
|
||||
usage={"input_tokens": 0, "output_tokens": 0},
|
||||
)
|
||||
|
||||
if data.get("stream", None) is not None and data["stream"] is True:
|
||||
# For streaming, use the standard SSE data generator
|
||||
async def _passthrough_stream_generator():
|
||||
yield _anthropic_response
|
||||
|
||||
selected_data_generator = (
|
||||
ProxyBaseLLMRequestProcessing.async_sse_data_generator(
|
||||
response=_passthrough_stream_generator(),
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
request_data=_data,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
)
|
||||
|
||||
return await create_response(
|
||||
generator=selected_data_generator,
|
||||
media_type="text/event-stream",
|
||||
headers={},
|
||||
)
|
||||
|
||||
return _anthropic_response
|
||||
except Exception as e:
|
||||
await proxy_logging_obj.post_call_failure_hook(
|
||||
user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
|
||||
)
|
||||
verbose_proxy_logger.exception(
|
||||
"litellm.proxy.proxy_server.anthropic_response(): Exception occured - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
|
||||
# Extract model_id from request metadata (same as success path)
|
||||
litellm_metadata = data.get("litellm_metadata", {}) or {}
|
||||
model_info = litellm_metadata.get("model_info", {}) or {}
|
||||
model_id = model_info.get("id", "") or ""
|
||||
|
||||
# Get headers
|
||||
headers = ProxyBaseLLMRequestProcessing.get_custom_headers(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
call_id=data.get("litellm_call_id", ""),
|
||||
model_id=model_id,
|
||||
version=version,
|
||||
response_cost=0,
|
||||
model_region=getattr(user_api_key_dict, "allowed_model_region", ""),
|
||||
request_data=data,
|
||||
timeout=getattr(e, "timeout", None),
|
||||
litellm_logging_obj=None,
|
||||
)
|
||||
|
||||
error_msg = f"{str(e)}"
|
||||
raise ProxyException(
|
||||
message=getattr(e, "message", error_msg),
|
||||
type=getattr(e, "type", "None"),
|
||||
param=getattr(e, "param", "None"),
|
||||
code=getattr(e, "status_code", 500),
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/messages/count_tokens",
|
||||
tags=["[beta] Anthropic Messages Token Counting"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def count_tokens(
|
||||
request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # Used for auth
|
||||
):
|
||||
"""
|
||||
Count tokens for Anthropic Messages API format.
|
||||
|
||||
This endpoint follows the Anthropic Messages API token counting specification.
|
||||
It accepts the same parameters as the /v1/messages endpoint but returns
|
||||
token counts instead of generating a response.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
curl -X POST "http://localhost:4000/v1/messages/count_tokens?beta=true" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-key" \
|
||||
-d '{
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"messages": [{"role": "user", "content": "Hello Claude!"}]
|
||||
}'
|
||||
```
|
||||
|
||||
Returns: {"input_tokens": <number>}
|
||||
"""
|
||||
from litellm.proxy.proxy_server import token_counter as internal_token_counter
|
||||
|
||||
try:
|
||||
request_data = await _read_request_body(request=request)
|
||||
data: dict = {**request_data}
|
||||
|
||||
# Extract required fields
|
||||
model_name = data.get("model")
|
||||
messages = data.get("messages", [])
|
||||
|
||||
if not model_name:
|
||||
raise HTTPException(
|
||||
status_code=400, detail={"error": "model parameter is required"}
|
||||
)
|
||||
|
||||
if not messages:
|
||||
raise HTTPException(
|
||||
status_code=400, detail={"error": "messages parameter is required"}
|
||||
)
|
||||
|
||||
# Create TokenCountRequest for the internal endpoint
|
||||
from litellm.proxy._types import TokenCountRequest
|
||||
|
||||
token_request = TokenCountRequest(
|
||||
model=model_name,
|
||||
messages=messages,
|
||||
tools=data.get("tools"),
|
||||
system=data.get("system"),
|
||||
)
|
||||
|
||||
# Call the internal token counter function with direct request flag set to False
|
||||
token_response = await internal_token_counter(
|
||||
request=token_request,
|
||||
call_endpoint=True,
|
||||
)
|
||||
_token_response_dict: dict = {}
|
||||
if isinstance(token_response, TokenCountResponse):
|
||||
_token_response_dict = token_response.model_dump()
|
||||
elif isinstance(token_response, dict):
|
||||
_token_response_dict = token_response
|
||||
|
||||
# Convert the internal response to Anthropic API format
|
||||
return {"input_tokens": _token_response_dict.get("total_tokens", 0)}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ProxyException as e:
|
||||
status_code = int(e.code) if e.code and e.code.isdigit() else 500
|
||||
detail = AnthropicExceptionMapping.transform_to_anthropic_error(
|
||||
status_code=status_code,
|
||||
raw_message=e.message,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status_code,
|
||||
detail=detail,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(
|
||||
"litellm.proxy.anthropic_endpoints.count_tokens(): Exception occurred - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500, detail={"error": f"Internal server error: {str(e)}"}
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/event_logging/batch",
|
||||
tags=["[beta] Anthropic Event Logging"],
|
||||
)
|
||||
async def event_logging_batch(
|
||||
request: Request,
|
||||
):
|
||||
"""
|
||||
Stubbed endpoint for Anthropic event logging batch requests.
|
||||
|
||||
This endpoint accepts event logging requests but does nothing with them.
|
||||
It exists to prevent 404 errors from Claude Code clients that send telemetry.
|
||||
"""
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
Anthropic Skills API endpoints - /v1/skills
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
|
||||
from litellm.proxy.common_utils.http_parsing_utils import (
|
||||
convert_upload_files_to_file_data,
|
||||
get_form_data,
|
||||
)
|
||||
from litellm.types.llms.anthropic_skills import (
|
||||
DeleteSkillResponse,
|
||||
ListSkillsResponse,
|
||||
Skill,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/v1/skills",
|
||||
tags=["[beta] Anthropic Skills API"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=Skill,
|
||||
)
|
||||
async def create_skill(
|
||||
fastapi_response: Response,
|
||||
request: Request,
|
||||
custom_llm_provider: Optional[str] = "anthropic",
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Create a new skill on Anthropic.
|
||||
|
||||
Requires `?beta=true` query parameter.
|
||||
|
||||
Model-based routing (for multi-account support):
|
||||
- Pass model via header: `x-litellm-model: claude-account-1`
|
||||
- Pass model via query: `?model=claude-account-1`
|
||||
- Pass model via form field: `model=claude-account-1`
|
||||
|
||||
Example usage:
|
||||
```bash
|
||||
# Basic usage
|
||||
curl -X POST "http://localhost:4000/v1/skills?beta=true" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-H "Authorization: Bearer your-key" \
|
||||
-F "display_title=My Skill" \
|
||||
-F "files[]=@skill.zip"
|
||||
|
||||
# With model-based routing
|
||||
curl -X POST "http://localhost:4000/v1/skills?beta=true" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-H "Authorization: Bearer your-key" \
|
||||
-H "x-litellm-model: claude-account-1" \
|
||||
-F "display_title=My Skill" \
|
||||
-F "files[]=@skill.zip"
|
||||
```
|
||||
|
||||
Returns: Skill object with id, display_title, etc.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import (
|
||||
general_settings,
|
||||
llm_router,
|
||||
proxy_config,
|
||||
proxy_logging_obj,
|
||||
select_data_generator,
|
||||
user_api_base,
|
||||
user_max_tokens,
|
||||
user_model,
|
||||
user_request_timeout,
|
||||
user_temperature,
|
||||
version,
|
||||
)
|
||||
|
||||
# Read form data and convert UploadFile objects to file data tuples
|
||||
form_data = await get_form_data(request)
|
||||
data = await convert_upload_files_to_file_data(form_data)
|
||||
|
||||
# Extract model for routing (header > query > body)
|
||||
model = (
|
||||
data.get("model")
|
||||
or request.query_params.get("model")
|
||||
or request.headers.get("x-litellm-model")
|
||||
)
|
||||
if model:
|
||||
data["model"] = model
|
||||
|
||||
if "custom_llm_provider" not in data:
|
||||
data["custom_llm_provider"] = custom_llm_provider
|
||||
|
||||
# Process request using ProxyBaseLLMRequestProcessing
|
||||
processor = ProxyBaseLLMRequestProcessing(data=data)
|
||||
try:
|
||||
return await processor.base_process_llm_request(
|
||||
request=request,
|
||||
fastapi_response=fastapi_response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
route_type="acreate_skill",
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
llm_router=llm_router,
|
||||
general_settings=general_settings,
|
||||
proxy_config=proxy_config,
|
||||
select_data_generator=select_data_generator,
|
||||
model=data.get("model"),
|
||||
user_model=user_model,
|
||||
user_temperature=user_temperature,
|
||||
user_request_timeout=user_request_timeout,
|
||||
user_max_tokens=user_max_tokens,
|
||||
user_api_base=user_api_base,
|
||||
version=version,
|
||||
)
|
||||
except Exception as e:
|
||||
raise await processor._handle_llm_api_exception(
|
||||
e=e,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
version=version,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/v1/skills",
|
||||
tags=["[beta] Anthropic Skills API"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=ListSkillsResponse,
|
||||
)
|
||||
async def list_skills(
|
||||
fastapi_response: Response,
|
||||
request: Request,
|
||||
limit: Optional[int] = 10,
|
||||
after_id: Optional[str] = None,
|
||||
before_id: Optional[str] = None,
|
||||
custom_llm_provider: Optional[str] = "anthropic",
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
List skills on Anthropic.
|
||||
|
||||
Requires `?beta=true` query parameter.
|
||||
|
||||
Model-based routing (for multi-account support):
|
||||
- Pass model via header: `x-litellm-model: claude-account-1`
|
||||
- Pass model via query: `?model=claude-account-1`
|
||||
- Pass model via body: `{"model": "claude-account-1"}`
|
||||
|
||||
Example usage:
|
||||
```bash
|
||||
# Basic usage
|
||||
curl "http://localhost:4000/v1/skills?beta=true&limit=10" \
|
||||
-H "Authorization: Bearer your-key"
|
||||
|
||||
# With model-based routing
|
||||
curl "http://localhost:4000/v1/skills?beta=true&limit=10" \
|
||||
-H "Authorization: Bearer your-key" \
|
||||
-H "x-litellm-model: claude-account-1"
|
||||
```
|
||||
|
||||
Returns: ListSkillsResponse with list of skills
|
||||
"""
|
||||
from litellm.proxy.proxy_server import (
|
||||
general_settings,
|
||||
llm_router,
|
||||
proxy_config,
|
||||
proxy_logging_obj,
|
||||
select_data_generator,
|
||||
user_api_base,
|
||||
user_max_tokens,
|
||||
user_model,
|
||||
user_request_timeout,
|
||||
user_temperature,
|
||||
version,
|
||||
)
|
||||
|
||||
# Read request body
|
||||
body = await request.body()
|
||||
data = orjson.loads(body) if body else {}
|
||||
|
||||
# Use query params if not in body
|
||||
if "limit" not in data and limit is not None:
|
||||
data["limit"] = limit
|
||||
if "after_id" not in data and after_id is not None:
|
||||
data["after_id"] = after_id
|
||||
if "before_id" not in data and before_id is not None:
|
||||
data["before_id"] = before_id
|
||||
|
||||
# Extract model for routing (header > query > body)
|
||||
model = (
|
||||
data.get("model")
|
||||
or request.query_params.get("model")
|
||||
or request.headers.get("x-litellm-model")
|
||||
)
|
||||
if model:
|
||||
data["model"] = model
|
||||
|
||||
# Set custom_llm_provider: body > query param > default
|
||||
if "custom_llm_provider" not in data:
|
||||
data["custom_llm_provider"] = custom_llm_provider
|
||||
|
||||
# Process request using ProxyBaseLLMRequestProcessing
|
||||
processor = ProxyBaseLLMRequestProcessing(data=data)
|
||||
try:
|
||||
return await processor.base_process_llm_request(
|
||||
request=request,
|
||||
fastapi_response=fastapi_response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
route_type="alist_skills",
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
llm_router=llm_router,
|
||||
general_settings=general_settings,
|
||||
proxy_config=proxy_config,
|
||||
select_data_generator=select_data_generator,
|
||||
model=data.get("model"),
|
||||
user_model=user_model,
|
||||
user_temperature=user_temperature,
|
||||
user_request_timeout=user_request_timeout,
|
||||
user_max_tokens=user_max_tokens,
|
||||
user_api_base=user_api_base,
|
||||
version=version,
|
||||
)
|
||||
except Exception as e:
|
||||
raise await processor._handle_llm_api_exception(
|
||||
e=e,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
version=version,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/v1/skills/{skill_id}",
|
||||
tags=["[beta] Anthropic Skills API"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=Skill,
|
||||
)
|
||||
async def get_skill(
|
||||
skill_id: str,
|
||||
fastapi_response: Response,
|
||||
request: Request,
|
||||
custom_llm_provider: Optional[str] = "anthropic",
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Get a specific skill by ID from Anthropic.
|
||||
|
||||
Requires `?beta=true` query parameter.
|
||||
|
||||
Model-based routing (for multi-account support):
|
||||
- Pass model via header: `x-litellm-model: claude-account-1`
|
||||
- Pass model via query: `?model=claude-account-1`
|
||||
- Pass model via body: `{"model": "claude-account-1"}`
|
||||
|
||||
Example usage:
|
||||
```bash
|
||||
# Basic usage
|
||||
curl "http://localhost:4000/v1/skills/skill_123?beta=true" \
|
||||
-H "Authorization: Bearer your-key"
|
||||
|
||||
# With model-based routing
|
||||
curl "http://localhost:4000/v1/skills/skill_123?beta=true" \
|
||||
-H "Authorization: Bearer your-key" \
|
||||
-H "x-litellm-model: claude-account-1"
|
||||
```
|
||||
|
||||
Returns: Skill object
|
||||
"""
|
||||
from litellm.proxy.proxy_server import (
|
||||
general_settings,
|
||||
llm_router,
|
||||
proxy_config,
|
||||
proxy_logging_obj,
|
||||
select_data_generator,
|
||||
user_api_base,
|
||||
user_max_tokens,
|
||||
user_model,
|
||||
user_request_timeout,
|
||||
user_temperature,
|
||||
version,
|
||||
)
|
||||
|
||||
# Read request body
|
||||
body = await request.body()
|
||||
data = orjson.loads(body) if body else {}
|
||||
|
||||
# Set skill_id from path parameter
|
||||
data["skill_id"] = skill_id
|
||||
|
||||
# Extract model for routing (header > query > body)
|
||||
model = (
|
||||
data.get("model")
|
||||
or request.query_params.get("model")
|
||||
or request.headers.get("x-litellm-model")
|
||||
)
|
||||
if model:
|
||||
data["model"] = model
|
||||
|
||||
# Set custom_llm_provider: body > query param > default
|
||||
if "custom_llm_provider" not in data:
|
||||
data["custom_llm_provider"] = custom_llm_provider
|
||||
|
||||
# Process request using ProxyBaseLLMRequestProcessing
|
||||
processor = ProxyBaseLLMRequestProcessing(data=data)
|
||||
try:
|
||||
return await processor.base_process_llm_request(
|
||||
request=request,
|
||||
fastapi_response=fastapi_response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
route_type="aget_skill",
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
llm_router=llm_router,
|
||||
general_settings=general_settings,
|
||||
proxy_config=proxy_config,
|
||||
select_data_generator=select_data_generator,
|
||||
model=data.get("model"),
|
||||
user_model=user_model,
|
||||
user_temperature=user_temperature,
|
||||
user_request_timeout=user_request_timeout,
|
||||
user_max_tokens=user_max_tokens,
|
||||
user_api_base=user_api_base,
|
||||
version=version,
|
||||
)
|
||||
except Exception as e:
|
||||
raise await processor._handle_llm_api_exception(
|
||||
e=e,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
version=version,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/v1/skills/{skill_id}",
|
||||
tags=["[beta] Anthropic Skills API"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=DeleteSkillResponse,
|
||||
)
|
||||
async def delete_skill(
|
||||
skill_id: str,
|
||||
fastapi_response: Response,
|
||||
request: Request,
|
||||
custom_llm_provider: Optional[str] = "anthropic",
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Delete a skill by ID from Anthropic.
|
||||
|
||||
Requires `?beta=true` query parameter.
|
||||
|
||||
Note: Anthropic does not allow deleting skills with existing versions.
|
||||
|
||||
Model-based routing (for multi-account support):
|
||||
- Pass model via header: `x-litellm-model: claude-account-1`
|
||||
- Pass model via query: `?model=claude-account-1`
|
||||
- Pass model via body: `{"model": "claude-account-1"}`
|
||||
|
||||
Example usage:
|
||||
```bash
|
||||
# Basic usage
|
||||
curl -X DELETE "http://localhost:4000/v1/skills/skill_123?beta=true" \
|
||||
-H "Authorization: Bearer your-key"
|
||||
|
||||
# With model-based routing
|
||||
curl -X DELETE "http://localhost:4000/v1/skills/skill_123?beta=true" \
|
||||
-H "Authorization: Bearer your-key" \
|
||||
-H "x-litellm-model: claude-account-1"
|
||||
```
|
||||
|
||||
Returns: DeleteSkillResponse with type="skill_deleted"
|
||||
"""
|
||||
from litellm.proxy.proxy_server import (
|
||||
general_settings,
|
||||
llm_router,
|
||||
proxy_config,
|
||||
proxy_logging_obj,
|
||||
select_data_generator,
|
||||
user_api_base,
|
||||
user_max_tokens,
|
||||
user_model,
|
||||
user_request_timeout,
|
||||
user_temperature,
|
||||
version,
|
||||
)
|
||||
|
||||
# Read request body
|
||||
body = await request.body()
|
||||
data = orjson.loads(body) if body else {}
|
||||
|
||||
# Set skill_id from path parameter
|
||||
data["skill_id"] = skill_id
|
||||
|
||||
# Extract model for routing (header > query > body)
|
||||
model = (
|
||||
data.get("model")
|
||||
or request.query_params.get("model")
|
||||
or request.headers.get("x-litellm-model")
|
||||
)
|
||||
if model:
|
||||
data["model"] = model
|
||||
|
||||
# Set custom_llm_provider: body > query param > default
|
||||
if "custom_llm_provider" not in data:
|
||||
data["custom_llm_provider"] = custom_llm_provider
|
||||
|
||||
# Process request using ProxyBaseLLMRequestProcessing
|
||||
processor = ProxyBaseLLMRequestProcessing(data=data)
|
||||
try:
|
||||
return await processor.base_process_llm_request(
|
||||
request=request,
|
||||
fastapi_response=fastapi_response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
route_type="adelete_skill",
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
llm_router=llm_router,
|
||||
general_settings=general_settings,
|
||||
proxy_config=proxy_config,
|
||||
select_data_generator=select_data_generator,
|
||||
model=data.get("model"),
|
||||
user_model=user_model,
|
||||
user_temperature=user_temperature,
|
||||
user_request_timeout=user_request_timeout,
|
||||
user_max_tokens=user_max_tokens,
|
||||
user_api_base=user_api_base,
|
||||
version=version,
|
||||
)
|
||||
except Exception as e:
|
||||
raise await processor._handle_llm_api_exception(
|
||||
e=e,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
version=version,
|
||||
)
|
||||
Reference in New Issue
Block a user