diff --git a/litellm/proxy/public_endpoints/public_endpoints.py b/litellm/proxy/public_endpoints/public_endpoints.py index bba5ef681d0..5f56f2ef4ab 100644 --- a/litellm/proxy/public_endpoints/public_endpoints.py +++ b/litellm/proxy/public_endpoints/public_endpoints.py @@ -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 ( @@ -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)) diff --git a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py index 18839a65d62..b05a9948bcf 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -1,6 +1,8 @@ +import asyncio import json import re from datetime import datetime, timezone +from types import SimpleNamespace from typing import Final from unittest.mock import AsyncMock, MagicMock, patch @@ -1469,3 +1471,43 @@ async def test_fetch_remote_autorouter_presets_parses_and_rejects_empty(monkeypa response.json = MagicMock(return_value={}) with pytest.raises(ValueError, match="empty"): await _fetch_remote_autorouter_presets("https://example.test/presets.json") + + +@pytest.mark.parametrize("preference", ["auto_install", None]) +def test_public_skill_hub_echoes_registered_installation_preference(preference): + from litellm.proxy.anthropic_endpoints.claude_code_endpoints.claude_code_marketplace import register_plugin + from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + from litellm.types.proxy.claude_code_endpoints import RegisterPluginRequest + + table: Final = MagicMock() + table.find_unique = AsyncMock(return_value=None) + table.create = AsyncMock(side_effect=lambda data: SimpleNamespace(id="plugin-id", **data)) + table.find_many = AsyncMock( + side_effect=lambda where: [SimpleNamespace(id="plugin-id", **table.create.call_args.kwargs["data"])] + ) + prisma: Final = MagicMock() + prisma.db.litellm_claudecodeplugintable = table + app: Final = FastAPI() + app.include_router(router) + + with patch("litellm.proxy.proxy_server.prisma_client", prisma): + asyncio.run( + register_plugin( + request=RegisterPluginRequest( + name="security-review", + source={ + "source": "archive", + "url": "https://plugins.example.com/security-review-1.0.0.zip", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + installation_preference=preference, + ), + user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin"), + ) + ) + response: Final = TestClient(app).get("/public/skill_hub") + + assert response.status_code == 200 + assert [(p["name"], p["installation_preference"]) for p in response.json()["plugins"]] == [ + ("security-review", preference) + ]