mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(claude-code): create-only skill registration with a PUT update route (LIT-4110) (#31752)
* fix(claude-code): make skill registration create-only with a PUT update route
POST /claude-code/plugins upserted by name, so re-registering an existing
name silently overwrote the stored skill's source and metadata. The "Add
New Skill" UI button posts here, so a name collision clobbered a different
skill with no signal to the user.
Make POST create-only: it returns 409 if the name already exists, with a
unique-violation guard mapping the find-then-create race to the same 409.
Add an explicit PUT /claude-code/plugins/{plugin_name} for updates (404 if
the name is missing). PUT is a full replace and documents that omitted
fields reset to their defaults, so UpdatePluginRequest defaults version to
None instead of fabricating the create-time 1.0.0.
The shared mutable fields move to a PluginSpec base; RegisterPluginRequest
keeps its name and its generated schema unchanged, UpdatePluginRequest
carries no name. Regenerated the dashboard types and the lazy openapi
snapshot for the new route.
Resolves LIT-4110
* fix(ui): surface the proxy error detail so the skill 409 conflict is legible
The add-skill form rendered the raw HTTPException envelope on failure
because deriveErrorMessage did not unwrap an object-shaped detail
({"detail": {"error": ...}}), so the new create-only 409 reached the user
as a JSON blob. Unwrap object-shaped detail at the client layer, which
covers every handler that returns detail={"error": ...}, and surface the
resulting message verbatim on the form instead of burying it under a
generic prefix.
* refactor(claude-code): replace blind excepts in plugin mutations with typed handling
Narrow register_plugin's create-conflict guard from a broad 'except Exception'
+ isinstance dance to a direct 'except UniqueViolationError', using an Exception
subclass sentinel (not None) as the prisma-absent fallback so the sentinel can be
caught directly. Drop update_plugin's outer 'except Exception -> 500' wrapper so
HTTPExceptions propagate on their own and unexpected DB errors surface as FastAPI's
default 500 rather than echoing str(e). Keeps the BLE001 strict-rule budget green.
* fix(claude-code): restore structured 500 handling on update_plugin via typed PrismaError catch
Flattening update_plugin to satisfy the no-blind-except rule dropped its error
wrapper entirely, so a data-layer failure (e.g. a dropped DB connection) would
skip the intentional verbose_proxy_logger.exception call and degrade the response
from the endpoint's structured {"error": ...} body to FastAPI's default
{"detail": "Internal Server Error"}, inconsistent with every sibling route.
Wrap update_plugin in 'except PrismaError' instead of the blind 'except Exception'
the other routes use: it logs and returns the structured 500 for real DB failures
while letting genuine code bugs surface rather than masking them as 'Update failed',
and stays off the BLE001 budget. Add a regression test that a PrismaError during
the update maps to a structured 500.
* fix(claude-code): import prisma error types at function level to satisfy LIT009
* refactor(claude-code): typed plugin mutation responses and lint gate fixes
Return RegisterPluginResponse models from POST and PUT instead of ad-hoc
dicts, declare them as response_model so the OpenAPI schema and dashboard
types carry the real response shape, build the stored manifest via
model_dump, and drop update_plugin's unused auth parameter (the route
dependency already enforces auth). Keeps the LIT002/B008/UP045 budgets at
their ratcheted ceilings after merging litellm_internal_staging
This commit is contained in:
parent
24dbd2b2db
commit
0a42114847
11 changed files with 813 additions and 114 deletions
|
|
@ -4525,6 +4525,66 @@
|
|||
"title": "PluginListItem",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginResponse": {
|
||||
"description": "Plugin information in API responses.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Plugin description",
|
||||
"title": "Description"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Whether plugin is enabled",
|
||||
"title": "Enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"description": "Plugin unique ID",
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Plugin name",
|
||||
"title": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Git source reference",
|
||||
"title": "Source",
|
||||
"type": "object"
|
||||
},
|
||||
"version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Plugin version",
|
||||
"title": "Version"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"source",
|
||||
"enabled"
|
||||
],
|
||||
"title": "PluginResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"RegisterPluginRequest": {
|
||||
"description": "Request body for registering a plugin in the marketplace.\n\nLiteLLM acts as a registry/discovery layer. Plugins are hosted on\nGitHub/GitLab/Bitbucket and referenced by their git source.",
|
||||
"properties": {
|
||||
|
|
@ -4643,14 +4703,163 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"source"
|
||||
"source",
|
||||
"name"
|
||||
],
|
||||
"title": "RegisterPluginRequest",
|
||||
"type": "object"
|
||||
},
|
||||
"RegisterPluginResponse": {
|
||||
"description": "Response from plugin registration.",
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "Action taken (created/updated)",
|
||||
"title": "Action",
|
||||
"type": "string"
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/components/schemas/PluginResponse",
|
||||
"description": "Plugin information"
|
||||
},
|
||||
"status": {
|
||||
"description": "Operation status",
|
||||
"title": "Status",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"action",
|
||||
"plugin"
|
||||
],
|
||||
"title": "RegisterPluginResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"UpdatePluginRequest": {
|
||||
"description": "Request body for replacing an existing plugin.\n\nThe plugin name is the resource identity and is supplied as the path\nparameter, so it cannot be changed here. This is a full replace: omitted\nfields reset to their defaults, so version is cleared rather than\ndefaulting to the create-time \"1.0.0\".",
|
||||
"properties": {
|
||||
"author": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PluginAuthor"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Plugin author"
|
||||
},
|
||||
"category": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Plugin category",
|
||||
"title": "Category"
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Plugin description",
|
||||
"title": "Description"
|
||||
},
|
||||
"domain": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Skill domain (e.g., 'Productivity')",
|
||||
"title": "Domain"
|
||||
},
|
||||
"homepage": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Plugin homepage URL",
|
||||
"title": "Homepage"
|
||||
},
|
||||
"keywords": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Search keywords",
|
||||
"title": "Keywords"
|
||||
},
|
||||
"namespace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Skill namespace within domain (e.g., 'workflows')",
|
||||
"title": "Namespace"
|
||||
},
|
||||
"source": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"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'}\n- Git Subdir: {'source': 'git-subdir', 'url': 'https://github.com/org/repo.git', 'path': 'plugins/plugin-name'}",
|
||||
"title": "Source",
|
||||
"type": "object"
|
||||
},
|
||||
"version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Semantic version; cleared if omitted",
|
||||
"title": "Version"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"source"
|
||||
],
|
||||
"title": "UpdatePluginRequest",
|
||||
"type": "object"
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"ctx": {
|
||||
"title": "Context",
|
||||
"type": "object"
|
||||
},
|
||||
"input": {
|
||||
"title": "Input"
|
||||
},
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
|
|
@ -4754,7 +4963,7 @@
|
|||
]
|
||||
},
|
||||
"post": {
|
||||
"description": "Register a plugin in the LiteLLM marketplace.\n\nLiteLLM acts as a registry/discovery layer. Plugins are hosted on\nGitHub/GitLab/Bitbucket. Claude Code will clone from the git source\nwhen users install.\n\nParameters:\n - name: Plugin name (kebab-case)\n - source: Git source reference (github, url, or git-subdir 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 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. Claude Code will clone from the git source\nwhen 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\nParameters:\n - name: Plugin name (kebab-case)\n - source: Git source reference (github, url, or git-subdir 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 ```",
|
||||
"operationId": "register_plugin_claude_code_plugins_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
|
|
@ -4770,7 +4979,9 @@
|
|||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RegisterPluginResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
|
|
@ -4885,6 +5096,62 @@
|
|||
"tags": [
|
||||
"claude_code_marketplace"
|
||||
]
|
||||
},
|
||||
"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\nParameters:\n - plugin_name: Name of the plugin to update (path parameter)\n - source: Git source reference (github, url, or git-subdir 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 ```",
|
||||
"operationId": "update_plugin_claude_code_plugins__plugin_name__put",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "plugin_name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"title": "Plugin Name",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdatePluginRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RegisterPluginResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation Error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"APIKeyHeader": []
|
||||
}
|
||||
],
|
||||
"summary": "Update Plugin",
|
||||
"tags": [
|
||||
"claude_code_marketplace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/claude-code/plugins/{plugin_name}/disable": {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ 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 - POST - Register a new plugin (create-only)
|
||||
/claude-code/plugins - GET - List plugins (admin)
|
||||
/claude-code/plugins/{name} - GET - Get plugin details
|
||||
/claude-code/plugins/{name} - PUT - Update an existing plugin
|
||||
/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
|
||||
|
|
@ -30,7 +31,11 @@ from litellm.repositories.table_repositories import ClaudeCodePluginRepository
|
|||
from litellm.types.proxy.claude_code_endpoints import (
|
||||
ListPluginsResponse,
|
||||
PluginListItem,
|
||||
PluginResponse,
|
||||
PluginSpec,
|
||||
RegisterPluginRequest,
|
||||
RegisterPluginResponse,
|
||||
UpdatePluginRequest,
|
||||
)
|
||||
|
||||
router: Final = APIRouter()
|
||||
|
|
@ -174,22 +179,43 @@ def _validate_plugin_source(source: dict[str, Any]) -> None:
|
|||
)
|
||||
|
||||
|
||||
def _build_plugin_manifest(name: str, spec: PluginSpec) -> dict[str, Any]:
|
||||
"""Build the stored manifest dict shared by plugin create and update."""
|
||||
dumped = spec.model_dump(exclude_none=True)
|
||||
return {"name": name, **{key: value for key, value in dumped.items() if value and key != "name"}}
|
||||
|
||||
|
||||
def _error_response(status_code: int, message: str) -> HTTPException:
|
||||
return HTTPException(status_code=status_code, detail={"error": message})
|
||||
|
||||
|
||||
def _name_conflict_error(name: str) -> HTTPException:
|
||||
return _error_response(
|
||||
409, f"A skill named '{name}' already exists. Update the existing skill instead of adding it again."
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claude-code/plugins",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=RegisterPluginResponse,
|
||||
)
|
||||
async def register_plugin(
|
||||
request: RegisterPluginRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Register a plugin in the LiteLLM marketplace.
|
||||
Register a new 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.
|
||||
|
||||
This endpoint is create-only and never overwrites. If a plugin with
|
||||
the same name already exists it returns 409 Conflict; use
|
||||
PUT /claude-code/plugins/{plugin_name} to update an existing plugin.
|
||||
|
||||
Parameters:
|
||||
- name: Plugin name (kebab-case)
|
||||
- source: Git source reference (github, url, or git-subdir format)
|
||||
|
|
@ -201,7 +227,7 @@ async def register_plugin(
|
|||
- category: Plugin category (optional)
|
||||
|
||||
Returns:
|
||||
Registration status and plugin information.
|
||||
Registration status (action is always "created") and plugin information.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
|
|
@ -216,58 +242,26 @@ async def register_plugin(
|
|||
}'
|
||||
```
|
||||
"""
|
||||
from prisma.errors import UniqueViolationError
|
||||
|
||||
try:
|
||||
prisma_client: Final = 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: Final = request.source
|
||||
_validate_plugin_source(source)
|
||||
_validate_plugin_source(request.source)
|
||||
|
||||
# Build manifest for storage
|
||||
manifest: Final[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
|
||||
if request.domain:
|
||||
manifest["domain"] = request.domain
|
||||
if request.namespace:
|
||||
manifest["namespace"] = request.namespace
|
||||
|
||||
# Check if plugin exists
|
||||
existing = await ClaudeCodePluginRepository(prisma_client).table.find_unique(where={"name": request.name})
|
||||
|
||||
if existing:
|
||||
plugin = await ClaudeCodePluginRepository(prisma_client).table.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:
|
||||
raise _name_conflict_error(request.name)
|
||||
|
||||
manifest = _build_plugin_manifest(request.name, request)
|
||||
|
||||
try:
|
||||
plugin = await ClaudeCodePluginRepository(prisma_client).table.create(
|
||||
data={
|
||||
"name": request.name,
|
||||
|
|
@ -281,22 +275,23 @@ async def register_plugin(
|
|||
"created_by": user_api_key_dict.user_id,
|
||||
}
|
||||
)
|
||||
action = "created"
|
||||
except UniqueViolationError:
|
||||
raise _name_conflict_error(request.name)
|
||||
|
||||
verbose_proxy_logger.info("Plugin %s %s successfully", request.name, action)
|
||||
verbose_proxy_logger.info("Plugin %s created successfully", request.name)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"action": action,
|
||||
"plugin": {
|
||||
"id": plugin.id,
|
||||
"name": plugin.name,
|
||||
"version": plugin.version,
|
||||
"description": plugin.description,
|
||||
"source": request.source,
|
||||
"enabled": plugin.enabled,
|
||||
},
|
||||
}
|
||||
return RegisterPluginResponse(
|
||||
status="success",
|
||||
action="created",
|
||||
plugin=PluginResponse(
|
||||
id=plugin.id,
|
||||
name=plugin.name,
|
||||
version=plugin.version,
|
||||
description=plugin.description,
|
||||
source=request.source,
|
||||
enabled=plugin.enabled,
|
||||
),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -432,6 +427,101 @@ async def get_plugin(
|
|||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/claude-code/plugins/{plugin_name}",
|
||||
tags=["Claude Code Marketplace"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=RegisterPluginResponse,
|
||||
)
|
||||
async def update_plugin(
|
||||
plugin_name: str,
|
||||
request: UpdatePluginRequest,
|
||||
):
|
||||
"""
|
||||
Update an existing plugin in the LiteLLM marketplace.
|
||||
|
||||
The plugin is identified by its name in the path, which is the resource
|
||||
identity and cannot be changed here. This is a full replace, not a merge:
|
||||
the manifest is rebuilt from the request body, so any optional field left
|
||||
out is reset to its default (e.g. an omitted version is cleared, not kept).
|
||||
Send the full desired state.
|
||||
|
||||
Returns 404 if no plugin with the given name exists; use
|
||||
POST /claude-code/plugins to create a new plugin.
|
||||
|
||||
Parameters:
|
||||
- plugin_name: Name of the plugin to update (path parameter)
|
||||
- source: Git source reference (github, url, or git-subdir 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:
|
||||
Update status (action is always "updated") and plugin information.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
curl -X PUT http://localhost:4000/claude-code/plugins/my-plugin \\
|
||||
-H "Authorization: Bearer sk-..." \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"source": {"source": "github", "repo": "org/my-plugin"},
|
||||
"version": "2.0.0",
|
||||
"description": "My awesome plugin"
|
||||
}'
|
||||
```
|
||||
"""
|
||||
from prisma.errors import PrismaError
|
||||
|
||||
try:
|
||||
prisma_client = await _get_prisma_client()
|
||||
|
||||
_validate_plugin_source(request.source)
|
||||
|
||||
existing = await ClaudeCodePluginRepository(prisma_client).table.find_unique(
|
||||
where={"name": plugin_name} # mutable-ok: prisma query arguments must be plain dicts
|
||||
)
|
||||
if not existing:
|
||||
raise _error_response(404, f"Plugin '{plugin_name}' not found")
|
||||
|
||||
manifest = _build_plugin_manifest(plugin_name, request)
|
||||
|
||||
plugin = await ClaudeCodePluginRepository(prisma_client).table.update(
|
||||
where={"name": plugin_name}, # mutable-ok: prisma query arguments must be plain dicts
|
||||
data={ # mutable-ok: prisma query arguments must be plain dicts
|
||||
"version": request.version,
|
||||
"description": request.description,
|
||||
"manifest_json": json.dumps(manifest),
|
||||
"files_json": "{}",
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
},
|
||||
)
|
||||
|
||||
verbose_proxy_logger.info("Plugin %s updated successfully", plugin_name)
|
||||
|
||||
return RegisterPluginResponse(
|
||||
status="success",
|
||||
action="updated",
|
||||
plugin=PluginResponse(
|
||||
id=plugin.id,
|
||||
name=plugin.name,
|
||||
version=plugin.version,
|
||||
description=plugin.description,
|
||||
source=request.source,
|
||||
enabled=plugin.enabled,
|
||||
),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except PrismaError as e:
|
||||
verbose_proxy_logger.exception("Error updating plugin: %s", e)
|
||||
raise _error_response(500, f"Update failed: {e}")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/claude-code/plugins/{plugin_name}/enable",
|
||||
tags=["Claude Code Marketplace"],
|
||||
|
|
|
|||
|
|
@ -21,19 +21,9 @@ class PluginOwner(BaseModel):
|
|||
email: Optional[str] = Field(None, description="Owner email")
|
||||
|
||||
|
||||
class RegisterPluginRequest(BaseModel):
|
||||
"""
|
||||
Request body for registering a plugin in the marketplace.
|
||||
class PluginSpec(BaseModel):
|
||||
"""Mutable fields shared by plugin create and update requests."""
|
||||
|
||||
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=(
|
||||
|
|
@ -53,6 +43,34 @@ class RegisterPluginRequest(BaseModel):
|
|||
namespace: Optional[str] = Field(None, description="Skill namespace within domain (e.g., 'workflows')")
|
||||
|
||||
|
||||
class RegisterPluginRequest(PluginSpec):
|
||||
"""
|
||||
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-]+$",
|
||||
)
|
||||
|
||||
|
||||
class UpdatePluginRequest(PluginSpec):
|
||||
"""
|
||||
Request body for replacing an existing plugin.
|
||||
|
||||
The plugin name is the resource identity and is supplied as the path
|
||||
parameter, so it cannot be changed here. This is a full replace: omitted
|
||||
fields reset to their defaults, so version is cleared rather than
|
||||
defaulting to the create-time "1.0.0".
|
||||
"""
|
||||
|
||||
version: str | None = Field(None, description="Semantic version; cleared if omitted")
|
||||
|
||||
|
||||
class PluginResponse(BaseModel):
|
||||
"""Plugin information in API responses."""
|
||||
|
||||
|
|
|
|||
|
|
@ -168,11 +168,11 @@ async def test_register_plugin(mock_prisma_client):
|
|||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
assert response["status"] == "success"
|
||||
assert response["action"] == "created"
|
||||
assert response["plugin"]["name"] == plugin_name
|
||||
assert response["plugin"]["version"] == "1.0.0"
|
||||
assert response["plugin"]["enabled"] is True
|
||||
assert response.status == "success"
|
||||
assert response.action == "created"
|
||||
assert response.plugin.name == plugin_name
|
||||
assert response.plugin.version == "1.0.0"
|
||||
assert response.plugin.enabled is True
|
||||
|
||||
# Verify the plugin was stored in the mock
|
||||
stored_plugin = (
|
||||
|
|
@ -274,16 +274,16 @@ async def test_register_plugin_git_subdir(mock_prisma_client):
|
|||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
assert response["status"] == "success"
|
||||
assert response["action"] == "created"
|
||||
assert response["plugin"]["name"] == plugin_name
|
||||
assert response["plugin"]["source"]["source"] == "git-subdir"
|
||||
assert response.status == "success"
|
||||
assert response.action == "created"
|
||||
assert response.plugin.name == plugin_name
|
||||
assert response.plugin.source["source"] == "git-subdir"
|
||||
assert (
|
||||
response["plugin"]["source"]["url"]
|
||||
response.plugin.source["url"]
|
||||
== "https://github.com/test-org/monorepo.git"
|
||||
)
|
||||
assert response["plugin"]["source"]["path"] == "plugins/my-plugin"
|
||||
assert response["plugin"]["enabled"] is True
|
||||
assert response.plugin.source["path"] == "plugins/my-plugin"
|
||||
assert response.plugin.enabled is True
|
||||
|
||||
# Cleanup
|
||||
await mock_prisma_client.db.litellm_claudecodeplugintable.delete(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ Unit tests for claude_code_marketplace.py source validation.
|
|||
Covers the git-subdir source type added alongside the existing github and url types.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
|
@ -11,9 +13,13 @@ from unittest.mock import AsyncMock, MagicMock
|
|||
import litellm
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.proxy_server import LitellmUserRoles
|
||||
from litellm.types.proxy.claude_code_endpoints import RegisterPluginRequest
|
||||
from litellm.types.proxy.claude_code_endpoints import (
|
||||
RegisterPluginRequest,
|
||||
UpdatePluginRequest,
|
||||
)
|
||||
from litellm.proxy.anthropic_endpoints.claude_code_endpoints.claude_code_marketplace import (
|
||||
register_plugin,
|
||||
update_plugin,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -68,42 +74,141 @@ _GIT_SUBDIR_SOURCE = {
|
|||
@pytest.fixture(autouse=True)
|
||||
def _patch_proxy_globals(monkeypatch):
|
||||
"""Scope prisma_client/master_key mutations to each test via monkeypatch."""
|
||||
monkeypatch.setattr(
|
||||
litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma()
|
||||
)
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
|
||||
monkeypatch.setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_plugin_git_subdir_success():
|
||||
"""git-subdir with both url and path fields registers successfully."""
|
||||
request = RegisterPluginRequest(
|
||||
name="my-monorepo-plugin", source=_GIT_SUBDIR_SOURCE
|
||||
)
|
||||
request = RegisterPluginRequest(name="my-monorepo-plugin", source=_GIT_SUBDIR_SOURCE)
|
||||
|
||||
response = await register_plugin(request=request, user_api_key_dict=_USER)
|
||||
|
||||
assert response["status"] == "success"
|
||||
assert response["action"] == "created"
|
||||
assert response["plugin"]["source"]["source"] == "git-subdir"
|
||||
assert response["plugin"]["source"]["path"] == "plugins/my-plugin"
|
||||
assert response.status == "success"
|
||||
assert response.action == "created"
|
||||
assert response.plugin.source["source"] == "git-subdir"
|
||||
assert response.plugin.source["path"] == "plugins/my-plugin"
|
||||
|
||||
|
||||
async def _read_stored_manifest(name: str) -> dict:
|
||||
table = litellm.proxy.proxy_server.prisma_client.db.litellm_claudecodeplugintable
|
||||
record = await table.find_unique(where={"name": name})
|
||||
return json.loads(record.manifest_json)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_plugin_git_subdir_update():
|
||||
"""Registering the same git-subdir plugin twice returns action=updated."""
|
||||
request = RegisterPluginRequest(
|
||||
name="my-monorepo-plugin", source=_GIT_SUBDIR_SOURCE, version="1.0.0"
|
||||
async def test_register_plugin_duplicate_name_conflicts():
|
||||
"""A second POST with an existing name returns 409 and leaves the stored plugin untouched."""
|
||||
name = "my-monorepo-plugin"
|
||||
await register_plugin(
|
||||
request=RegisterPluginRequest(name=name, source=_GIT_SUBDIR_SOURCE, version="1.0.0"),
|
||||
user_api_key_dict=_USER,
|
||||
)
|
||||
await register_plugin(request=request, user_api_key_dict=_USER)
|
||||
|
||||
request2 = RegisterPluginRequest(
|
||||
name="my-monorepo-plugin", source=_GIT_SUBDIR_SOURCE, version="2.0.0"
|
||||
stored_before = await _read_stored_manifest(name)
|
||||
assert stored_before["version"] == "1.0.0"
|
||||
|
||||
conflicting = RegisterPluginRequest(
|
||||
name=name,
|
||||
source={
|
||||
"source": "git-subdir",
|
||||
"url": "https://github.com/org/other.git",
|
||||
"path": "plugins/other-plugin",
|
||||
},
|
||||
version="2.0.0",
|
||||
)
|
||||
response = await register_plugin(request=request2, user_api_key_dict=_USER)
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await register_plugin(request=conflicting, user_api_key_dict=_USER)
|
||||
|
||||
assert response["status"] == "success"
|
||||
assert response["action"] == "updated"
|
||||
assert exc_info.value.status_code == 409
|
||||
assert "already exists" in exc_info.value.detail["error"]
|
||||
|
||||
stored_after = await _read_stored_manifest(name)
|
||||
assert stored_after == stored_before
|
||||
assert stored_after["version"] == "1.0.0"
|
||||
assert stored_after["source"]["url"] == "https://github.com/org/monorepo.git"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_plugin_replaces_existing_source():
|
||||
"""PUT updates an existing plugin: action=updated and the stored source is replaced."""
|
||||
name = "my-monorepo-plugin"
|
||||
await register_plugin(
|
||||
request=RegisterPluginRequest(name=name, source=_GIT_SUBDIR_SOURCE, version="1.0.0"),
|
||||
user_api_key_dict=_USER,
|
||||
)
|
||||
|
||||
new_source = {"source": "github", "repo": "org/replacement"}
|
||||
response = await update_plugin(
|
||||
plugin_name=name,
|
||||
request=UpdatePluginRequest(source=new_source, version="2.0.0", description="updated"),
|
||||
)
|
||||
|
||||
assert response.status == "success"
|
||||
assert response.action == "updated"
|
||||
assert response.plugin.version == "2.0.0"
|
||||
assert response.plugin.source == new_source
|
||||
|
||||
stored = await _read_stored_manifest(name)
|
||||
assert stored["source"] == new_source
|
||||
assert stored["version"] == "2.0.0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_plugin_not_found():
|
||||
"""PUT on a name that does not exist raises HTTP 404."""
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_plugin(
|
||||
plugin_name="does-not-exist",
|
||||
request=UpdatePluginRequest(source=_GIT_SUBDIR_SOURCE),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_plugin_create_race_maps_unique_violation_to_409():
|
||||
"""A concurrent insert that slips past the find_unique pre-check (create raises
|
||||
the unique-constraint error) is mapped to 409, not surfaced as a 500."""
|
||||
from prisma.errors import UniqueViolationError
|
||||
|
||||
table = litellm.proxy.proxy_server.prisma_client.db.litellm_claudecodeplugintable
|
||||
table.create = AsyncMock(side_effect=UniqueViolationError({}, message="duplicate name"))
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await register_plugin(
|
||||
request=RegisterPluginRequest(name="racy-plugin", source=_GIT_SUBDIR_SOURCE),
|
||||
user_api_key_dict=_USER,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 409
|
||||
assert "already exists" in exc_info.value.detail["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_plugin_db_error_maps_to_structured_500():
|
||||
"""A data-layer failure during the update (e.g. a dropped DB connection) is caught and
|
||||
returned as a structured 500, not swallowed silently or leaked as an unhandled error."""
|
||||
from prisma.errors import PrismaError
|
||||
|
||||
name = "my-monorepo-plugin"
|
||||
await register_plugin(
|
||||
request=RegisterPluginRequest(name=name, source=_GIT_SUBDIR_SOURCE, version="1.0.0"),
|
||||
user_api_key_dict=_USER,
|
||||
)
|
||||
|
||||
table = litellm.proxy.proxy_server.prisma_client.db.litellm_claudecodeplugintable
|
||||
table.update = AsyncMock(side_effect=PrismaError("connection lost"))
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_plugin(
|
||||
plugin_name=name,
|
||||
request=UpdatePluginRequest(source={"source": "github", "repo": "org/replacement"}),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 500
|
||||
assert "connection lost" in exc_info.value.detail["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -270,4 +270,19 @@ describe("AddPluginForm", () => {
|
|||
expect(mockMessageError).toHaveBeenCalledWith(expect.stringContaining("Plugin 'claude-code' already exists"));
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces the 409 name-conflict reason verbatim without burying it under a generic failure prefix", async () => {
|
||||
const conflictMessage =
|
||||
"A skill named 'gitlab' already exists. Update the existing skill instead of adding it again.";
|
||||
mockRegister.mockRejectedValueOnce(new Error(conflictMessage));
|
||||
renderWithProviders(<AddPluginForm {...DEFAULT_PROPS} />);
|
||||
|
||||
await typeUrl("https://github.com/anthropics/claude-code");
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMessageError).toHaveBeenCalledWith(conflictMessage);
|
||||
});
|
||||
expect(mockMessageError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -145,8 +145,7 @@ const AddPluginForm: React.FC<AddPluginFormProps> = ({ visible, onClose, accessT
|
|||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error registering skill:", error);
|
||||
const reason = error instanceof Error && error.message ? error.message : "Failed to register skill";
|
||||
MessageManager.error(`Failed to register skill: ${reason}`);
|
||||
MessageManager.error(error instanceof Error && error.message ? error.message : "Failed to register skill");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7236,7 +7236,8 @@ export const getClaudeCodePluginDetails = async (accessToken: string, pluginName
|
|||
};
|
||||
|
||||
/**
|
||||
* Register or update a Claude Code plugin (admin only)
|
||||
* Register a new Claude Code plugin (admin only). Create-only: the proxy returns
|
||||
* 409 if a plugin with the same name already exists.
|
||||
* @param accessToken - Admin access token
|
||||
* @param pluginData - Plugin registration data
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -64,6 +64,18 @@ describe("createApiClient", () => {
|
|||
expect(onError).toHaveBeenCalledWith("no access");
|
||||
});
|
||||
|
||||
it("unwraps an object-shaped detail ({detail:{error}}) rather than dumping the JSON envelope (FastAPI HTTPException shape)", async () => {
|
||||
const conflict = "A skill named 'gitlab' already exists. Update the existing skill instead of adding it again.";
|
||||
const fetchImpl = vi.fn(async () => errorResponse(409, { detail: { error: conflict } }));
|
||||
const onError = vi.fn();
|
||||
const client = createApiClient({ getBaseUrl: () => "", onError, fetchImpl });
|
||||
|
||||
const promise = client.get("/claude-code/plugins", { accessToken: "sk" });
|
||||
|
||||
await expect(promise).rejects.toMatchObject({ message: conflict, status: 409 });
|
||||
expect(onError).toHaveBeenCalledWith(conflict);
|
||||
});
|
||||
|
||||
it("falls back to the raw text body when a non-2xx response is not JSON (e.g. an HTML 502)", async () => {
|
||||
const fetchImpl = vi.fn(async () => rawErrorResponse(502, "<html>Bad Gateway</html>"));
|
||||
const onError = vi.fn();
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const deriveDetailMessage = (detail: any): string | undefined => {
|
|||
if (Array.isArray(detail)) return detail.map((d: any) => d?.msg || JSON.stringify(d)).join("; ");
|
||||
if (typeof detail === "string") return detail;
|
||||
if (typeof detail?.error === "string") return detail.error;
|
||||
if (detail && typeof detail === "object") return detail.error?.message || detail.message;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
|
|
|||
201
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
201
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -1451,12 +1451,16 @@ export interface paths {
|
|||
put?: never;
|
||||
/**
|
||||
* Register Plugin
|
||||
* @description Register a plugin in the LiteLLM marketplace.
|
||||
* @description Register a new 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.
|
||||
*
|
||||
* This endpoint is create-only and never overwrites. If a plugin with
|
||||
* the same name already exists it returns 409 Conflict; use
|
||||
* PUT /claude-code/plugins/{plugin_name} to update an existing plugin.
|
||||
*
|
||||
* Parameters:
|
||||
* - name: Plugin name (kebab-case)
|
||||
* - source: Git source reference (github, url, or git-subdir format)
|
||||
|
|
@ -1468,7 +1472,7 @@ export interface paths {
|
|||
* - category: Plugin category (optional)
|
||||
*
|
||||
* Returns:
|
||||
* Registration status and plugin information.
|
||||
* Registration status (action is always "created") and plugin information.
|
||||
*
|
||||
* Example:
|
||||
* ```bash
|
||||
|
|
@ -1508,7 +1512,45 @@ export interface paths {
|
|||
* Plugin details including source and metadata.
|
||||
*/
|
||||
get: operations["get_plugin_claude_code_plugins__plugin_name__get"];
|
||||
put?: never;
|
||||
/**
|
||||
* Update Plugin
|
||||
* @description Update an existing plugin in the LiteLLM marketplace.
|
||||
*
|
||||
* The plugin is identified by its name in the path, which is the resource
|
||||
* identity and cannot be changed here. This is a full replace, not a merge:
|
||||
* the manifest is rebuilt from the request body, so any optional field left
|
||||
* out is reset to its default (e.g. an omitted version is cleared, not kept).
|
||||
* Send the full desired state.
|
||||
*
|
||||
* Returns 404 if no plugin with the given name exists; use
|
||||
* POST /claude-code/plugins to create a new plugin.
|
||||
*
|
||||
* Parameters:
|
||||
* - plugin_name: Name of the plugin to update (path parameter)
|
||||
* - source: Git source reference (github, url, or git-subdir 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:
|
||||
* Update status (action is always "updated") and plugin information.
|
||||
*
|
||||
* Example:
|
||||
* ```bash
|
||||
* curl -X PUT http://localhost:4000/claude-code/plugins/my-plugin \
|
||||
* -H "Authorization: Bearer sk-..." \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -d '{
|
||||
* "source": {"source": "github", "repo": "org/my-plugin"},
|
||||
* "version": "2.0.0",
|
||||
* "description": "My awesome plugin"
|
||||
* }'
|
||||
* ```
|
||||
*/
|
||||
put: operations["update_plugin_claude_code_plugins__plugin_name__put"];
|
||||
post?: never;
|
||||
/**
|
||||
* Delete Plugin
|
||||
|
|
@ -23841,7 +23883,7 @@ export interface components {
|
|||
* @description Default role assigned to new users created
|
||||
* @default internal_user_viewer
|
||||
*/
|
||||
user_role: ("proxy_admin" | "proxy_admin_viewer" | "internal_user" | "internal_user_viewer") | null;
|
||||
user_role: ("internal_user" | "internal_user_viewer" | "proxy_admin" | "proxy_admin_viewer") | null;
|
||||
};
|
||||
/**
|
||||
* DefaultTeamSSOParams
|
||||
|
|
@ -29717,6 +29759,44 @@ export interface components {
|
|||
/** Version */
|
||||
version: string | null;
|
||||
};
|
||||
/**
|
||||
* PluginResponse
|
||||
* @description Plugin information in API responses.
|
||||
*/
|
||||
PluginResponse: {
|
||||
/**
|
||||
* Description
|
||||
* @description Plugin description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Enabled
|
||||
* @description Whether plugin is enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* Id
|
||||
* @description Plugin unique ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Name
|
||||
* @description Plugin name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Source
|
||||
* @description Git source reference
|
||||
*/
|
||||
source: {
|
||||
[key: string]: string;
|
||||
};
|
||||
/**
|
||||
* Version
|
||||
* @description Plugin version
|
||||
*/
|
||||
version?: string | null;
|
||||
};
|
||||
/**
|
||||
* PolicyAttachmentCreateRequest
|
||||
* @description Request body for creating a policy attachment.
|
||||
|
|
@ -30989,6 +31069,24 @@ export interface components {
|
|||
*/
|
||||
version: string | null;
|
||||
};
|
||||
/**
|
||||
* RegisterPluginResponse
|
||||
* @description Response from plugin registration.
|
||||
*/
|
||||
RegisterPluginResponse: {
|
||||
/**
|
||||
* Action
|
||||
* @description Action taken (created/updated)
|
||||
*/
|
||||
action: string;
|
||||
/** @description Plugin information */
|
||||
plugin: components["schemas"]["PluginResponse"];
|
||||
/**
|
||||
* Status
|
||||
* @description Operation status
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
/** RejectMCPServerRequest */
|
||||
RejectMCPServerRequest: {
|
||||
/** Review Notes */
|
||||
|
|
@ -33077,6 +33175,64 @@ export interface components {
|
|||
/** Model Names */
|
||||
model_names?: string[] | null;
|
||||
};
|
||||
/**
|
||||
* UpdatePluginRequest
|
||||
* @description Request body for replacing an existing plugin.
|
||||
*
|
||||
* The plugin name is the resource identity and is supplied as the path
|
||||
* parameter, so it cannot be changed here. This is a full replace: omitted
|
||||
* fields reset to their defaults, so version is cleared rather than
|
||||
* defaulting to the create-time "1.0.0".
|
||||
*/
|
||||
UpdatePluginRequest: {
|
||||
/** @description Plugin author */
|
||||
author?: components["schemas"]["PluginAuthor"] | null;
|
||||
/**
|
||||
* Category
|
||||
* @description Plugin category
|
||||
*/
|
||||
category?: string | null;
|
||||
/**
|
||||
* Description
|
||||
* @description Plugin description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Domain
|
||||
* @description Skill domain (e.g., 'Productivity')
|
||||
*/
|
||||
domain?: string | null;
|
||||
/**
|
||||
* Homepage
|
||||
* @description Plugin homepage URL
|
||||
*/
|
||||
homepage?: string | null;
|
||||
/**
|
||||
* Keywords
|
||||
* @description Search keywords
|
||||
*/
|
||||
keywords?: string[] | null;
|
||||
/**
|
||||
* Namespace
|
||||
* @description Skill namespace within domain (e.g., 'workflows')
|
||||
*/
|
||||
namespace?: string | null;
|
||||
/**
|
||||
* Source
|
||||
* @description Git source reference. Supported formats:
|
||||
* - GitHub: {'source': 'github', 'repo': 'org/repo'}
|
||||
* - Git URL: {'source': 'url', 'url': 'https://github.com/org/repo.git'}
|
||||
* - Git Subdir: {'source': 'git-subdir', 'url': 'https://github.com/org/repo.git', 'path': 'plugins/plugin-name'}
|
||||
*/
|
||||
source: {
|
||||
[key: string]: string;
|
||||
};
|
||||
/**
|
||||
* Version
|
||||
* @description Semantic version; cleared if omitted
|
||||
*/
|
||||
version?: string | null;
|
||||
};
|
||||
/**
|
||||
* UpdateProjectRequest
|
||||
* @description Request model for POST /project/update
|
||||
|
|
@ -37053,7 +37209,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
"application/json": components["schemas"]["RegisterPluginResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
|
|
@ -37098,6 +37254,41 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
update_plugin_claude_code_plugins__plugin_name__put: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
plugin_name: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdatePluginRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RegisterPluginResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
delete_plugin_claude_code_plugins__plugin_name__delete: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue