Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions litellm/proxy/_lazy_openapi_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -5704,6 +5704,22 @@
"title": "Id",
"type": "string"
},
"installation_preference": {
"anyOf": [
{
"enum": [
"available",
"auto_install",
"required"
],
"type": "string"
},
{
"type": "null"
}
],
"title": "Installation Preference"
},
"keywords": {
"anyOf": [
{
Expand Down Expand Up @@ -5898,6 +5914,23 @@
"description": "Plugin homepage URL",
"title": "Homepage"
},
"installation_preference": {
"anyOf": [
{
"enum": [
"available",
"auto_install",
"required"
],
"type": "string"
},
{
"type": "null"
}
],
"description": "Emitted as installationPreference on this plugin's marketplace.json entry. Claude Desktop auto-installs an 'auto_install' plugin when the marketplace is served from its inference gateway origin and the plugin is an archive source with a sha256",
"title": "Installation Preference"
},
"keywords": {
"anyOf": [
{
Expand Down Expand Up @@ -6048,6 +6081,23 @@
"description": "Plugin homepage URL",
"title": "Homepage"
},
"installation_preference": {
"anyOf": [
{
"enum": [
"available",
"auto_install",
"required"
],
"type": "string"
},
{
"type": "null"
}
],
"description": "Emitted as installationPreference on this plugin's marketplace.json entry. Claude Desktop auto-installs an 'auto_install' plugin when the marketplace is served from its inference gateway origin and the plugin is an archive source with a sha256",
"title": "Installation Preference"
},
"keywords": {
"anyOf": [
{
Expand Down Expand Up @@ -6242,7 +6292,7 @@
]
},
"post": {
"description": "Register a new plugin in the LiteLLM marketplace.\n\nLiteLLM acts as a registry/discovery layer. Plugins are hosted on\nGitHub/GitLab/Bitbucket or as a zip archive on any https host (e.g. S3).\nClaude Code clones the git source or downloads the archive when users install.\n\nThis endpoint is create-only and never overwrites. If a plugin with\nthe same name already exists it returns 409 Conflict; use\nPUT /claude-code/plugins/{plugin_name} to update an existing plugin.\n\nRequires a proxy admin API key.\n\nParameters:\n - name: Plugin name (kebab-case)\n - source: Plugin source reference (github, url, git-subdir, or archive format)\n - version: Semantic version (optional)\n - description: Plugin description (optional)\n - author: Author information (optional)\n - homepage: Plugin homepage URL (optional)\n - keywords: Search keywords (optional)\n - category: Plugin category (optional)\n\nReturns:\n Registration status (action is always \"created\") and plugin information.\n\nExample:\n ```bash\n curl -X POST http://localhost:4000/claude-code/plugins \\\n -H \"Authorization: Bearer sk-...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"my-plugin\",\n \"source\": {\"source\": \"github\", \"repo\": \"org/my-plugin\"},\n \"version\": \"1.0.0\",\n \"description\": \"My awesome plugin\"\n }'\n ```",
"description": "Register a new plugin in the LiteLLM marketplace.\n\nLiteLLM acts as a registry/discovery layer. Plugins are hosted on\nGitHub/GitLab/Bitbucket or as a zip archive on any https host (e.g. S3).\nClaude Code clones the git source or downloads the archive when users install.\n\nThis endpoint is create-only and never overwrites. If a plugin with\nthe same name already exists it returns 409 Conflict; use\nPUT /claude-code/plugins/{plugin_name} to update an existing plugin.\n\nRequires a proxy admin API key.\n\nParameters:\n - name: Plugin name (kebab-case)\n - source: Plugin source reference (github, url, git-subdir, or archive format)\n - version: Semantic version (optional)\n - description: Plugin description (optional)\n - author: Author information (optional)\n - homepage: Plugin homepage URL (optional)\n - keywords: Search keywords (optional)\n - category: Plugin category (optional)\n - installation_preference: 'available', 'auto_install', or 'required' (optional)\n\nReturns:\n Registration status (action is always \"created\") and plugin information.\n\nExample:\n ```bash\n curl -X POST http://localhost:4000/claude-code/plugins \\\n -H \"Authorization: Bearer sk-...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"my-plugin\",\n \"source\": {\"source\": \"github\", \"repo\": \"org/my-plugin\"},\n \"version\": \"1.0.0\",\n \"description\": \"My awesome plugin\"\n }'\n ```",
"operationId": "register_plugin_claude_code_plugins_post",
"requestBody": {
"content": {
Expand Down Expand Up @@ -6377,7 +6427,7 @@
]
},
"put": {
"description": "Update an existing plugin in the LiteLLM marketplace.\n\nThe plugin is identified by its name in the path, which is the resource\nidentity and cannot be changed here. This is a full replace, not a merge:\nthe manifest is rebuilt from the request body, so any optional field left\nout is reset to its default (e.g. an omitted version is cleared, not kept).\nSend the full desired state.\n\nReturns 404 if no plugin with the given name exists; use\nPOST /claude-code/plugins to create a new plugin.\n\nRequires a proxy admin API key.\n\nParameters:\n - plugin_name: Name of the plugin to update (path parameter)\n - source: Plugin source reference (github, url, git-subdir, or archive format)\n - version: Semantic version (optional)\n - description: Plugin description (optional)\n - author: Author information (optional)\n - homepage: Plugin homepage URL (optional)\n - keywords: Search keywords (optional)\n - category: Plugin category (optional)\n\nReturns:\n Update status (action is always \"updated\") and plugin information.\n\nExample:\n ```bash\n curl -X PUT http://localhost:4000/claude-code/plugins/my-plugin \\\n -H \"Authorization: Bearer sk-...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"source\": {\"source\": \"github\", \"repo\": \"org/my-plugin\"},\n \"version\": \"2.0.0\",\n \"description\": \"My awesome plugin\"\n }'\n ```",
"description": "Update an existing plugin in the LiteLLM marketplace.\n\nThe plugin is identified by its name in the path, which is the resource\nidentity and cannot be changed here. This is a full replace, not a merge:\nthe manifest is rebuilt from the request body, so any optional field left\nout is reset to its default (e.g. an omitted version is cleared, not kept).\nSend the full desired state.\n\nReturns 404 if no plugin with the given name exists; use\nPOST /claude-code/plugins to create a new plugin.\n\nRequires a proxy admin API key.\n\nParameters:\n - plugin_name: Name of the plugin to update (path parameter)\n - source: Plugin source reference (github, url, git-subdir, or archive format)\n - version: Semantic version (optional)\n - description: Plugin description (optional)\n - author: Author information (optional)\n - homepage: Plugin homepage URL (optional)\n - keywords: Search keywords (optional)\n - category: Plugin category (optional)\n - installation_preference: 'available', 'auto_install', or 'required' (optional)\n\nReturns:\n Update status (action is always \"updated\") and plugin information.\n\nExample:\n ```bash\n curl -X PUT http://localhost:4000/claude-code/plugins/my-plugin \\\n -H \"Authorization: Bearer sk-...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"source\": {\"source\": \"github\", \"repo\": \"org/my-plugin\"},\n \"version\": \"2.0.0\",\n \"description\": \"My awesome plugin\"\n }'\n ```",
"operationId": "update_plugin_claude_code_plugins__plugin_name__put",
"parameters": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
import re
from collections.abc import Mapping, Sequence
from datetime import datetime, timezone
from typing import Annotated, Final, Protocol, TypedDict
from typing import Annotated, Final, Protocol, TypedDict, get_args
from urllib.parse import urlsplit

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from typing_extensions import ReadOnly

from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import CommonProxyErrors, ProxyException, UserAPIKeyAuth
Expand All @@ -37,6 +38,7 @@
from litellm.proxy.common_utils.resource_ownership import is_proxy_admin
from litellm.repositories.table_repositories import ClaudeCodePluginRepository
from litellm.types.proxy.claude_code_endpoints import (
InstallationPreference,
ListPluginsResponse,
PluginListItem,
PluginResponse,
Expand Down Expand Up @@ -70,6 +72,15 @@ class _MarketplaceEntry(TypedDict, total=False):
homepage: object
keywords: object
category: object
installationPreference: ReadOnly[InstallationPreference]


_INSTALLATION_PREFERENCES: Final[tuple[InstallationPreference, ...]] = get_args(InstallationPreference)


def _get_installation_preference(manifest: Mapping[str, object]) -> InstallationPreference | None:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
value: Final = manifest.get("installation_preference")
return value if value in _INSTALLATION_PREFERENCES else None


async def _get_prisma_client() -> object:
Expand Down Expand Up @@ -136,10 +147,16 @@ async def get_marketplace(request: Request, key: str | None = None):
verbose_proxy_logger.warning("Plugin %s has no source field, skipping", plugin.name)
continue

entry: _MarketplaceEntry = {
"name": plugin.name,
"source": manifest["source"],
}
installation_preference = _get_installation_preference(manifest)
entry: _MarketplaceEntry = (
{"name": plugin.name, "source": manifest["source"]}
if installation_preference is None
else {
"name": plugin.name,
"source": manifest["source"],
"installationPreference": installation_preference,
}
)

if plugin.version:
entry["version"] = plugin.version
Expand Down Expand Up @@ -306,6 +323,7 @@ async def register_plugin(
- homepage: Plugin homepage URL (optional)
- keywords: Search keywords (optional)
- category: Plugin category (optional)
- installation_preference: 'available', 'auto_install', or 'required' (optional)

Returns:
Registration status (action is always "created") and plugin information.
Expand Down Expand Up @@ -435,6 +453,7 @@ async def list_plugins(
category=manifest.get("category"),
domain=manifest.get("domain"),
namespace=manifest.get("namespace"),
installation_preference=_get_installation_preference(manifest),
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,
Expand Down Expand Up @@ -508,6 +527,7 @@ async def get_plugin(
"homepage": manifest.get("homepage"),
"keywords": manifest.get("keywords"),
"category": manifest.get("category"),
"installation_preference": _get_installation_preference(manifest),
"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,
Expand Down Expand Up @@ -558,6 +578,7 @@ async def update_plugin(
- homepage: Plugin homepage URL (optional)
- keywords: Search keywords (optional)
- category: Plugin category (optional)
- installation_preference: 'available', 'auto_install', or 'required' (optional)

Returns:
Update status (action is always "updated") and plugin information.
Expand Down
2 changes: 2 additions & 0 deletions litellm/proxy/public_endpoints/public_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ async def get_mcp_servers():
async def public_skill_hub():
"""Return enabled (public) Claude Code skills — no auth required."""
from litellm.proxy.anthropic_endpoints.claude_code_endpoints.claude_code_marketplace import (
_get_installation_preference,
_get_prisma_client,
)
from litellm.types.proxy.claude_code_endpoints import (
Expand Down Expand Up @@ -352,6 +353,7 @@ async def public_skill_hub():
homepage=manifest.get("homepage"),
domain=manifest.get("domain"),
namespace=manifest.get("namespace"),
installation_preference=_get_installation_preference(manifest),
)
)
return ListPluginsResponse(plugins=items, count=len(items))
Expand Down
13 changes: 13 additions & 0 deletions litellm/types/proxy/claude_code_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
Claude Code Marketplace endpoint types for LiteLLM Proxy
"""

from typing import Literal, TypeAlias

from pydantic import BaseModel, Field

InstallationPreference: TypeAlias = Literal["available", "auto_install", "required"]


class PluginAuthor(BaseModel):
"""Plugin author information."""
Expand Down Expand Up @@ -41,6 +45,14 @@ class PluginSpec(BaseModel):
category: str | None = Field(None, description="Plugin category")
domain: str | None = Field(None, description="Skill domain (e.g., 'Productivity')")
namespace: str | None = Field(None, description="Skill namespace within domain (e.g., 'workflows')")
installation_preference: InstallationPreference | None = Field(
None,
description=(
"Emitted as installationPreference on this plugin's marketplace.json entry. Claude Desktop auto-installs "
"an 'auto_install' plugin when the marketplace is served from its inference gateway origin and the "
"plugin is an archive source with a sha256"
),
)


class RegisterPluginRequest(PluginSpec):
Expand Down Expand Up @@ -104,6 +116,7 @@ class PluginListItem(BaseModel):
category: str | None = None
domain: str | None = None
namespace: str | None = None
installation_preference: InstallationPreference | None = None
enabled: bool
created_at: str | None
updated_at: str | None
Expand Down
Loading
Loading