feat(skills): Skills Registry & Hub — register skills, browse in AI Hub, public skill hub - #25118
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
Greptile SummaryThis PR introduces a Skills Registry & Hub — a way for teams to register Claude Code skills, browse them in the AI Hub dashboard, and expose a public, unauthenticated Key findings:
Confidence Score: 4/5Safe to merge after addressing the dropped SkillDetail props (which silently breaks the intended refresh callback) and moving inline imports to module level. One P1 finding (SkillDetail props not destructured, causing onPublishClick to never fire) should be fixed before merge. The remaining findings are P2 style/quality issues (inline imports, missing pagination cap, redundant condition, no tests) that don't break the primary happy path. ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx (dropped props) and litellm/proxy/public_endpoints/public_endpoints.py (inline imports, missing pagination).
|
| Filename | Overview |
|---|---|
| litellm/proxy/public_endpoints/public_endpoints.py | Adds /public/skill_hub endpoint; uses inline imports inside the handler (violates CLAUDE.md) and imports the private _get_prisma_client across module boundaries. |
| litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py | Persists domain and namespace fields in manifest_json; change is minimal and correct. list_plugins now returns domain/namespace from manifest. |
| litellm/proxy/_types.py | Adds /public/skill_hub to the no-auth whitelist; correct placement consistent with other public hub routes. |
| litellm/types/proxy/claude_code_endpoints.py | Adds optional domain/namespace fields to RegisterPluginRequest and PluginListItem; backward-compatible, no migration required. |
| ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx | Declares isAdmin, accessToken, onPublishClick in SkillDetailProps but does not destructure or use them; onPublishClick callback wired by both callers will never fire from within this view. |
| ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx | 2-step modal for selecting and confirming which skills appear in the public hub; logic is sound but uses NotificationsManager.fromBackend() for client-side validation errors (minor semantic mismatch). |
| ui/litellm-dashboard/src/components/AIHub/SkillHubDashboard.tsx | Stats row, domain dropdown filter, and skill table; client-side filtering is correct and concise. |
| ui/litellm-dashboard/src/components/claude_code_plugins/add_plugin_form.tsx | Smart GitHub URL parser added; logic is mostly correct but the condition parts.length === 2 |
| ui/litellm-dashboard/src/components/networking.tsx | Adds skillHubPublicCall() — uses proxyBaseUrl with correct fallback to relative URL; no auth header sent, consistent with other public calls. |
| ui/litellm-dashboard/src/components/public_model_hub.tsx | Adds Skill Hub tab to public AI Hub; fetches skillHubPublicCall() independently alongside model/agent/MCP fetches, correct pattern. |
Sequence Diagram
sequenceDiagram
participant User as Public User / Agent
participant UI as Public AI Hub Page
participant NET as networking.tsx
participant EP as /public/skill_hub
participant MKT as claude_code_marketplace.py
participant DB as Prisma DB
User->>UI: Load public AI Hub
UI->>NET: skillHubPublicCall()
NET->>EP: GET /public/skill_hub (no auth)
EP->>MKT: _get_prisma_client() (inline import)
MKT->>EP: prisma_client
EP->>DB: find_many(where={enabled: True})
DB->>EP: List[Plugin records]
EP->>EP: parse manifest_json for each record
EP->>NET: ListPluginsResponse{plugins, count}
NET->>UI: response.plugins
UI->>User: Render SkillHubDashboard (filter by domain, search)
Note over UI,NET: Admin flow — Make Skills Public
User->>UI: Click Select Skills to Make Public
UI->>UI: Open MakeSkillPublicForm modal (2-step)
User->>UI: Select skills and Confirm
UI->>NET: enableClaudeCodePlugin / disableClaudeCodePlugin per skill
NET->>MKT: POST /claude-code/plugins/{name}/enable or disable
MKT->>DB: update(enabled=True or False)
DB->>MKT: updated record
MKT->>NET: success
NET->>UI: Refresh skills list
Reviews (1): Last reviewed commit: "docs(skills): add loom walkthrough video..." | Re-trigger Greptile
| """Return enabled (public) Claude Code skills — no auth required.""" | ||
| from litellm.proxy.anthropic_endpoints.claude_code_endpoints.claude_code_marketplace import ( | ||
| _get_prisma_client, | ||
| ) | ||
| from litellm.types.proxy.claude_code_endpoints import ListPluginsResponse, PluginListItem | ||
|
|
There was a problem hiding this comment.
Inline imports violate project style guide
CLAUDE.md says: "Avoid imports within methods — place all imports at the top of the file (module-level). The only exception is avoiding circular imports where absolutely necessary."
Neither import here creates a circular dependency — public_endpoints.py already imports from litellm.proxy.auth.user_api_key_auth (which touches proxy_server) and litellm.types.* freely at module level, so these two can move up as well.
Additionally, _get_prisma_client carries a leading underscore that conventionally marks it as module-private. Importing a private symbol across module boundaries is a design smell; consider exposing a public helper or duplicating the two-line guard inline.
Move both lines to the top-level import block alongside the other litellm.types.* imports already present (lines 20–30).
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| name=plugin.name, | ||
| enabled=plugin.enabled, | ||
| created_at=str(plugin.created_at) if plugin.created_at else None, | ||
| updated_at=str(plugin.updated_at) if plugin.updated_at else None, | ||
| source=manifest.get("source", {}), | ||
| description=manifest.get("description"), | ||
| version=manifest.get("version"), | ||
| category=manifest.get("category"), | ||
| keywords=manifest.get("keywords"), | ||
| author=manifest.get("author"), | ||
| homepage=manifest.get("homepage"), | ||
| domain=manifest.get("domain"), | ||
| namespace=manifest.get("namespace"), | ||
| ) | ||
| ) | ||
| return ListPluginsResponse(plugins=items, count=len(items)) | ||
| except Exception as e: | ||
| raise HTTPException(status_code=500, detail=str(e)) |
There was a problem hiding this comment.
No pagination guard on unbounded
find_many
CLAUDE.md states: "Bound large result sets. Prisma materializes full results in memory."
find_many(where={"enabled": True}) has no take limit. If an organisation registers thousands of skills, every call to the public hub endpoint will load them all into memory. The other public hub endpoints (/public/agent_hub, /public/mcp_hub) have the same pattern, but that doesn't make it correct here.
Consider adding a reasonable take cap (e.g. 500) and documenting it, or at minimum noting it as a known limitation in a follow-up ticket.
Context Used: CLAUDE.md (source)
| const SkillDetail: React.FC<SkillDetailProps> = ({ | ||
| skill, | ||
| onBack, | ||
| }) => { |
There was a problem hiding this comment.
isAdmin, accessToken, and onPublishClick props are silently dropped
SkillDetailProps declares three props that both callers (claude_code_plugins.tsx lines 84-86 and SkillHubDashboard.tsx) explicitly wire up, but the component only destructures skill and onBack — the other three are silently ignored.
The most concrete consequence: onPublishClick={fetchPlugins} is passed from claude_code_plugins.tsx so the parent list refreshes after an action on the detail page. Because onPublishClick is never called inside the component, that refresh never happens — any publish/unpublish action started from the detail view will leave the parent list stale.
If admin actions on the detail page are intentionally deferred, remove the unused props from the interface and the two call sites. If they are meant to be used, destructure them and wire up the admin UI.
| const repoBase = repo.replace(/\.git$/, ""); | ||
|
|
||
| // github.com/org/repo (exactly 2 parts, or ends with .git) | ||
| if (parts.length === 2 || (parts.length === 2 && repoBase)) { |
There was a problem hiding this comment.
Redundant condition in URL parser
The condition parts.length === 2 || (parts.length === 2 && repoBase) simplifies to parts.length === 2 because the second disjunct is always a strict subset of the first. The comment says "exactly 2 parts, or ends with .git" but the .git stripping already happens earlier via repo.replace(/\.git$/, "") — so both branches produce the same result.
More importantly, a URL like https://github.com/org/repo/tree/branch (4 parts, no file path) returns null from this parser since parts.length >= 5 is not satisfied. Consider whether that case needs a fallback to the plain github source type.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
#25109) * feat: multiple concurrent budget windows per API key and team (#24883) * feat(proxy): add BudgetLimitEntry type and wire budget_limits into key/team models * feat(schema): add budget_limits Json column to VerificationToken and TeamTable * feat(migrations): add migration for budget_limits column on keys and teams * feat(keys): initialize budget_limits windows with reset_at on key create/update * feat(teams): initialize budget_limits windows with reset_at on team create/update * feat(auth): add _virtual_key_multi_budget_check and _team_multi_budget_check * feat(auth): call multi-budget checks from common_checks for keys and teams * feat(proxy): increment per-window Redis spend counters after each request * feat(budget): reset individual budget windows on schedule via reset_budget_job * feat(ui): add hourly option to BudgetDurationDropdown * feat(ui): add budget_limits field to KeyResponse type * feat(ui): add Budget Windows editor to key edit view * feat(ui): add Budget Windows editor to create key form * fix(proxy): strip budget_limits=None before Prisma upsert to fix login 500 Prisma rejects nullable JSON fields (Json? without @default) when passed as Python None — it needs the field omitted entirely so the DB stores NULL via the column's nullable constraint. This was breaking /v2/login because the UI session key creation path hit the upsert with budget_limits=None. * ui(key-edit): use antd InputNumber+Button for budget windows, add reset hints * ui(create-key): use antd InputNumber+Button for budget windows, add reset hints * docs(users): add multiple budget windows section with API + dashboard walkthrough * fix: BudgetExceededError returns HTTP 429 instead of 400 - Add status_code=429 to BudgetExceededError class - auth_exception_handler hardcoded code=400 → code=429 * fix: no-op else branch in multi-budget auth checks causes KeyError - BudgetLimitEntry objects must be coerced via model_dump() not left as-is - Move _virtual_key_multi_budget_check into common_checks (was asymmetric with _team_multi_budget_check which already lived there) * fix: len() on JSON string returns char count not window count Guard with isinstance check + json.loads() before iterating per-window Redis counters in increment_spend_counters * fix: silent except:pass hides Redis reset failures in reset_budget_windows Log Redis counter reset failures as warnings so they are observable * test: add unit tests for multi-budget window enforcement 5 tests covering: no budget_limits passes, under budget passes, over hourly window raises 429, over monthly window raises 429, BudgetLimitEntry objects coerced without KeyError * fix: key per-window counters stable across reorders (duration key, not index) * fix: team+key per-window spend increments use duration key, not index * fix: budget window reset uses duration key; log failures instead of swallowing * refactor: extract BudgetWindowsEditor to shared component * refactor: key_edit_view imports BudgetWindowsEditor from shared component * refactor: create_key_button imports BudgetWindowsEditor from shared component --------- Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com> * fix(reset_budget_job): extract _reset_expired_window helper to fix PLR0915 too many statements * feat(skills): Skills Registry & Hub — register skills, browse in AI Hub, public skill hub (#25118) * feat(skills): add domain and namespace fields to plugin types * feat(skills): store and return domain/namespace inside manifest_json * feat(skills): add /public/skill_hub endpoint for unauthenticated access * feat(skills): whitelist /public/skill_hub from auth requirements * feat(skills): add domain, namespace to Plugin and RegisterPluginRequest types * feat(skills): smart URL parser — paste github URL, auto-detect source type and name * feat(skills): replace enable toggle with Public badge, make rows clickable * feat(skills): add skill detail view with Overview and How to Use tabs * feat(skills): add MakeSkillPublicForm modal for publishing skills to the hub * feat(skills): rename panel to Skills, wire in skill detail view on row click * feat(skills): add skill hub table columns — name, description, domain, source, status * feat(skills): add SkillHubDashboard with stats row, domain dropdown filter, and table * feat(skills): add Skill Hub tab to AI Hub with Select Skills to Make Public button * feat(skills): move Skills to top-level nav item directly under MCP Servers * feat(skills): add skillHubPublicCall and NEXT_PUBLIC_BASE_URL support * feat(skills): add Skill Hub tab to public AI Hub page * feat(skills): add skills page routing in main app router * feat(skills): add /skills page route * chore: update package-lock after npm install * docs(skills): add Skills Gateway doc page with mermaid architecture diagram * docs(skills): add Skills Gateway to sidebar under Agent & MCP Gateway * docs(skills): add loom walkthrough video to Skills Gateway doc * chore: fixes --------- Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com> Co-authored-by: Yuneng Jiang <yuneng@berri.ai>
…I#24883) (BerriAI#25109) * feat: multiple concurrent budget windows per API key and team (BerriAI#24883) * feat(proxy): add BudgetLimitEntry type and wire budget_limits into key/team models * feat(schema): add budget_limits Json column to VerificationToken and TeamTable * feat(migrations): add migration for budget_limits column on keys and teams * feat(keys): initialize budget_limits windows with reset_at on key create/update * feat(teams): initialize budget_limits windows with reset_at on team create/update * feat(auth): add _virtual_key_multi_budget_check and _team_multi_budget_check * feat(auth): call multi-budget checks from common_checks for keys and teams * feat(proxy): increment per-window Redis spend counters after each request * feat(budget): reset individual budget windows on schedule via reset_budget_job * feat(ui): add hourly option to BudgetDurationDropdown * feat(ui): add budget_limits field to KeyResponse type * feat(ui): add Budget Windows editor to key edit view * feat(ui): add Budget Windows editor to create key form * fix(proxy): strip budget_limits=None before Prisma upsert to fix login 500 Prisma rejects nullable JSON fields (Json? without @default) when passed as Python None — it needs the field omitted entirely so the DB stores NULL via the column's nullable constraint. This was breaking /v2/login because the UI session key creation path hit the upsert with budget_limits=None. * ui(key-edit): use antd InputNumber+Button for budget windows, add reset hints * ui(create-key): use antd InputNumber+Button for budget windows, add reset hints * docs(users): add multiple budget windows section with API + dashboard walkthrough * fix: BudgetExceededError returns HTTP 429 instead of 400 - Add status_code=429 to BudgetExceededError class - auth_exception_handler hardcoded code=400 → code=429 * fix: no-op else branch in multi-budget auth checks causes KeyError - BudgetLimitEntry objects must be coerced via model_dump() not left as-is - Move _virtual_key_multi_budget_check into common_checks (was asymmetric with _team_multi_budget_check which already lived there) * fix: len() on JSON string returns char count not window count Guard with isinstance check + json.loads() before iterating per-window Redis counters in increment_spend_counters * fix: silent except:pass hides Redis reset failures in reset_budget_windows Log Redis counter reset failures as warnings so they are observable * test: add unit tests for multi-budget window enforcement 5 tests covering: no budget_limits passes, under budget passes, over hourly window raises 429, over monthly window raises 429, BudgetLimitEntry objects coerced without KeyError * fix: key per-window counters stable across reorders (duration key, not index) * fix: team+key per-window spend increments use duration key, not index * fix: budget window reset uses duration key; log failures instead of swallowing * refactor: extract BudgetWindowsEditor to shared component * refactor: key_edit_view imports BudgetWindowsEditor from shared component * refactor: create_key_button imports BudgetWindowsEditor from shared component --------- Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com> * fix(reset_budget_job): extract _reset_expired_window helper to fix PLR0915 too many statements * feat(skills): Skills Registry & Hub — register skills, browse in AI Hub, public skill hub (BerriAI#25118) * feat(skills): add domain and namespace fields to plugin types * feat(skills): store and return domain/namespace inside manifest_json * feat(skills): add /public/skill_hub endpoint for unauthenticated access * feat(skills): whitelist /public/skill_hub from auth requirements * feat(skills): add domain, namespace to Plugin and RegisterPluginRequest types * feat(skills): smart URL parser — paste github URL, auto-detect source type and name * feat(skills): replace enable toggle with Public badge, make rows clickable * feat(skills): add skill detail view with Overview and How to Use tabs * feat(skills): add MakeSkillPublicForm modal for publishing skills to the hub * feat(skills): rename panel to Skills, wire in skill detail view on row click * feat(skills): add skill hub table columns — name, description, domain, source, status * feat(skills): add SkillHubDashboard with stats row, domain dropdown filter, and table * feat(skills): add Skill Hub tab to AI Hub with Select Skills to Make Public button * feat(skills): move Skills to top-level nav item directly under MCP Servers * feat(skills): add skillHubPublicCall and NEXT_PUBLIC_BASE_URL support * feat(skills): add Skill Hub tab to public AI Hub page * feat(skills): add skills page routing in main app router * feat(skills): add /skills page route * chore: update package-lock after npm install * docs(skills): add Skills Gateway doc page with mermaid architecture diagram * docs(skills): add Skills Gateway to sidebar under Agent & MCP Gateway * docs(skills): add loom walkthrough video to Skills Gateway doc * chore: fixes --------- Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com> Co-authored-by: Yuneng Jiang <yuneng@berri.ai>
Relevant issues
Closes #skills-registry
What this does
Adds a Skills Registry to LiteLLM — a way for teams to register Claude Code skills, browse them in the AI Hub, and expose a public skill hub for agents and developers.
Backend:
/public/skill_hubendpoint (no auth) — returns all enabled skillsdomainandnamespacefields stored inmanifest_json(no schema change)UI changes:
Pre-Submission checklist
/claude-code/pluginsendpoints/public/skill_hubadded to no-auth whitelist in_types.pydomain/namespacestored in existingmanifest_jsoncolumn — no migration neededType
Changes
litellm/types/proxy/claude_code_endpoints.py— domain/namespace fieldslitellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py— persist domain/namespacelitellm/proxy/public_endpoints/public_endpoints.py—/public/skill_hublitellm/proxy/_types.py— whitelist new public endpointui/— 15 UI files across nav, forms, hub table, detail view, public page