This commit is contained in:
Ishaan Jaffer 2026-01-19 13:02:35 -08:00
parent 45942dfcb4
commit 84904551f7
4 changed files with 660 additions and 440 deletions

View file

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

View file

@ -0,0 +1,533 @@
"""
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,
order_by={"created_at": "desc"},
)
return ListPluginsResponse(
plugins=[
PluginListItem(
id=p.id,
name=p.name,
version=p.version,
description=p.description,
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,
)
for p in plugins
],
count=len(plugins),
)
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)},
)

View file

@ -1,440 +0,0 @@
"""
Claude Code Marketplace Endpoints
Provides a marketplace for Claude Code plugins stored in LiteLLM database.
Enables multi-instance compatible plugin distribution.
Endpoints:
- GET /claude-code/marketplace.json - List all plugins for Claude Code discovery
- GET /claude-code/plugins/{name}/{file} - Serve plugin files
- POST /claude-code/plugins/upload - Upload a plugin (ZIP)
- GET /claude-code/plugins - List plugins (admin)
- POST /claude-code/plugins/{name}/enable - Enable a plugin
- POST /claude-code/plugins/{name}/disable - Disable a plugin
- DELETE /claude-code/plugins/{name} - Delete a plugin
"""
import io
import json
import zipfile
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import JSONResponse, Response
from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
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="Database not connected. Please set DATABASE_URL."
)
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 marketplace add <name> <url>
- claude plugin install <name>@<marketplace>
Example:
claude marketplace add litellm http://localhost:4000
claude plugin install hello-world@litellm
"""
try:
prisma_client = await get_prisma_client()
# Get all enabled plugins from database
plugins = await prisma_client.db.litellm_claudecodeplugintable.find_many(
where={"enabled": True}
)
marketplace = []
for plugin in plugins:
# Parse manifest
try:
manifest = json.loads(plugin.manifest_json)
except json.JSONDecodeError:
continue
entry = {
"name": plugin.name,
"source": f"./plugins/{plugin.name}",
}
# Add fields from manifest/plugin
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"]
marketplace.append(entry)
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=str(e))
@router.get(
"/claude-code/plugins/{plugin_name}/{file_path:path}",
tags=["Claude Code Marketplace"],
)
async def get_plugin_file(plugin_name: str, file_path: str):
"""
Serve plugin files from database.
Claude Code will request files like:
- /claude-code/plugins/my-plugin/.claude-plugin/plugin.json
- /claude-code/plugins/my-plugin/commands/hello.md
"""
try:
prisma_client = await get_prisma_client()
# Get plugin from database
plugin = await prisma_client.db.litellm_claudecodeplugintable.find_unique(
where={"name": plugin_name}
)
if not plugin or not plugin.enabled:
raise HTTPException(status_code=404, detail="Plugin not found")
# Parse files JSON
try:
files = json.loads(plugin.files_json)
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="Invalid plugin files data")
# Get requested file
if file_path not in files:
raise HTTPException(status_code=404, detail=f"File not found: {file_path}")
content = files[file_path]
# Determine media type
media_type = "text/plain"
if file_path.endswith(".json"):
media_type = "application/json"
elif file_path.endswith(".md"):
media_type = "text/markdown"
elif file_path.endswith(".sh"):
media_type = "application/x-sh"
elif file_path.endswith(".py"):
media_type = "text/x-python"
elif file_path.endswith(".js"):
media_type = "text/javascript"
return Response(
content=content,
media_type=media_type,
headers={
"Content-Disposition": f'inline; filename="{file_path.split("/")[-1]}"'
}
)
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error serving plugin file: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/claude-code/plugins/upload",
tags=["Claude Code Marketplace"],
dependencies=[Depends(user_api_key_auth)],
)
async def upload_plugin(
file: UploadFile = File(...),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Upload a Claude Code plugin as a ZIP file.
The ZIP should contain:
- .claude-plugin/plugin.json (required)
- commands/*.md (optional)
- agents/*.md (optional)
- skills/*/SKILL.md (optional)
- hooks/hooks.json (optional)
Example:
curl -X POST \\
-H "Authorization: Bearer sk-..." \\
-F "file=@my-plugin.zip" \\
http://localhost:4000/claude-code/plugins/upload
"""
if not file.filename or not file.filename.endswith('.zip'):
raise HTTPException(status_code=400, detail="File must be a ZIP archive")
try:
prisma_client = await get_prisma_client()
# Read ZIP
content = await file.read()
zip_buffer = io.BytesIO(content)
files = {}
with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
for file_info in zip_ref.filelist:
if not file_info.is_dir():
file_path = file_info.filename
# Security: block path traversal
if ".." in file_path or file_path.startswith("/"):
raise HTTPException(
status_code=400,
detail=f"Invalid path in ZIP: {file_path}"
)
file_content = zip_ref.read(file_info).decode('utf-8', errors='ignore')
files[file_path] = file_content
# Validate manifest exists
manifest_path = ".claude-plugin/plugin.json"
if manifest_path not in files:
raise HTTPException(
status_code=400,
detail="ZIP must contain .claude-plugin/plugin.json"
)
# Parse manifest
try:
manifest = json.loads(files[manifest_path])
except json.JSONDecodeError as e:
raise HTTPException(
status_code=400,
detail=f"Invalid plugin.json: {e}"
)
# Validate required fields
if "name" not in manifest:
raise HTTPException(
status_code=400,
detail="plugin.json must contain 'name' field"
)
plugin_name = manifest["name"]
plugin_version = manifest.get("version", "1.0.0")
plugin_description = manifest.get("description")
# Validate name format (kebab-case)
import re
if not re.match(r"^[a-z0-9-]+$", plugin_name):
raise HTTPException(
status_code=400,
detail="Plugin name must be kebab-case (lowercase letters, numbers, hyphens)"
)
# Check if plugin exists
existing = await prisma_client.db.litellm_claudecodeplugintable.find_unique(
where={"name": plugin_name}
)
if existing:
# Update existing plugin
plugin = await prisma_client.db.litellm_claudecodeplugintable.update(
where={"name": plugin_name},
data={
"version": plugin_version,
"description": plugin_description,
"manifest_json": json.dumps(manifest),
"files_json": json.dumps(files),
"updated_at": datetime.now(timezone.utc)
}
)
action = "updated"
else:
# Create new plugin
plugin = await prisma_client.db.litellm_claudecodeplugintable.create(
data={
"name": plugin_name,
"version": plugin_version,
"description": plugin_description,
"manifest_json": json.dumps(manifest),
"files_json": json.dumps(files),
"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 {plugin_name} {action} successfully")
return {
"status": "success",
"action": action,
"plugin_id": plugin.id,
"plugin_name": plugin.name,
"version": plugin.version,
"files_count": len(files)
}
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error uploading plugin: {e}")
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
@router.get(
"/claude-code/plugins",
tags=["Claude Code Marketplace"],
dependencies=[Depends(user_api_key_auth)],
)
async def list_plugins(
enabled_only: bool = False,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""List all plugins in the marketplace"""
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,
order_by={"created_at": "desc"}
)
return {
"plugins": [
{
"id": p.id,
"name": p.name,
"version": p.version,
"description": p.description,
"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,
}
for p in plugins
],
"count": len(plugins)
}
except HTTPException:
raise
except Exception as e:
verbose_proxy_logger.exception(f"Error listing plugins: {e}")
raise HTTPException(status_code=500, detail=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"""
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="Plugin 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=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"""
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="Plugin 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=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"""
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="Plugin 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=str(e))

View file

@ -0,0 +1,116 @@
"""
Claude Code Marketplace endpoint types for LiteLLM Proxy
"""
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
class PluginAuthor(BaseModel):
"""Plugin author information."""
name: str = Field(..., description="Author name")
email: Optional[str] = Field(None, description="Author email")
class PluginOwner(BaseModel):
"""Marketplace owner information."""
name: str = Field(..., description="Owner name")
email: Optional[str] = Field(None, description="Owner email")
class RegisterPluginRequest(BaseModel):
"""
Request body for registering a plugin in the marketplace.
LiteLLM acts as a registry/discovery layer. Plugins are hosted on
GitHub/GitLab/Bitbucket and referenced by their git source.
"""
name: str = Field(
...,
description="Plugin name (kebab-case, e.g., 'my-plugin')",
pattern=r"^[a-z0-9-]+$",
)
source: Dict[str, str] = Field(
...,
description=(
"Git source reference. Supported formats:\n"
"- GitHub: {'source': 'github', 'repo': 'org/repo'}\n"
"- Git URL: {'source': 'url', 'url': 'https://github.com/org/repo.git'}"
),
)
version: Optional[str] = Field("1.0.0", description="Semantic version")
description: Optional[str] = Field(None, description="Plugin description")
author: Optional[PluginAuthor] = Field(None, description="Plugin author")
homepage: Optional[str] = Field(None, description="Plugin homepage URL")
keywords: Optional[List[str]] = Field(None, description="Search keywords")
category: Optional[str] = Field(None, description="Plugin category")
class PluginResponse(BaseModel):
"""Plugin information in API responses."""
id: str = Field(..., description="Plugin unique ID")
name: str = Field(..., description="Plugin name")
version: Optional[str] = Field(None, description="Plugin version")
description: Optional[str] = Field(None, description="Plugin description")
source: Dict[str, str] = Field(..., description="Git source reference")
enabled: bool = Field(..., description="Whether plugin is enabled")
class RegisterPluginResponse(BaseModel):
"""Response from plugin registration."""
status: str = Field(..., description="Operation status")
action: str = Field(..., description="Action taken (created/updated)")
plugin: PluginResponse = Field(..., description="Plugin information")
class PluginListItem(BaseModel):
"""Plugin item in list responses."""
id: str
name: str
version: Optional[str]
description: Optional[str]
enabled: bool
created_at: Optional[str]
updated_at: Optional[str]
class ListPluginsResponse(BaseModel):
"""Response from listing plugins."""
plugins: List[PluginListItem]
count: int
class MarketplacePluginEntry(BaseModel):
"""Plugin entry in marketplace.json."""
name: str
source: Dict[str, str]
version: Optional[str] = None
description: Optional[str] = None
author: Optional[PluginAuthor] = None
homepage: Optional[str] = None
keywords: Optional[List[str]] = None
category: Optional[str] = None
class MarketplaceResponse(BaseModel):
"""
Marketplace catalog response.
This format is consumed by Claude Code CLI.
See: https://docs.anthropic.com/en/docs/claude-code/plugins
"""
name: str = Field(..., description="Marketplace identifier")
owner: PluginOwner = Field(..., description="Marketplace owner")
plugins: List[MarketplacePluginEntry] = Field(
default_factory=list, description="Available plugins"
)