Skip to content

feat(skills): Skills Registry & Hub — register skills, browse in AI Hub, public skill hub - #25118

Merged
ishaan-berri merged 22 commits into
litellm_ishaan_april3from
worktree-magical-puzzling-tide
Apr 4, 2026
Merged

feat(skills): Skills Registry & Hub — register skills, browse in AI Hub, public skill hub#25118
ishaan-berri merged 22 commits into
litellm_ishaan_april3from
worktree-magical-puzzling-tide

Conversation

@ishaan-berri

Copy link
Copy Markdown
Contributor

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_hub endpoint (no auth) — returns all enabled skills
  • domain and namespace fields stored in manifest_json (no schema change)

UI changes:

  • Skills moved to top-level nav (was buried under Experimental)
  • Smart URL input in Add Skill form — paste any GitHub URL, source type and name auto-detected
  • Skill detail view (Guardrail Garden pattern) — Overview + How to Use tabs, install command, source link
  • "Select Skills to Make Public" button in AI Hub → 2-step checklist modal
  • Skill Hub tab in AI Hub: stats row, domain dropdown filter, browsable table
  • Skill Hub tab on public AI Hub page (no auth required)

Pre-Submission checklist

  • No breaking changes to existing /claude-code/plugins endpoints
  • /public/skill_hub added to no-auth whitelist in _types.py
  • domain/namespace stored in existing manifest_json column — no migration needed

Type

  • Bug fix
  • New feature
  • Refactor

Changes

  • litellm/types/proxy/claude_code_endpoints.py — domain/namespace fields
  • litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py — persist domain/namespace
  • litellm/proxy/public_endpoints/public_endpoints.py/public/skill_hub
  • litellm/proxy/_types.py — whitelist new public endpoint
  • ui/ — 15 UI files across nav, forms, hub table, detail view, public page

@vercel

vercel Bot commented Apr 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 4, 2026 0:13am

Request Review

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed-hq

codspeed-hq Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing worktree-magical-puzzling-tide (5579b82) with main (48d4dec)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 GET /public/skill_hub endpoint. The backend changes are minimal (no schema migration: domain/namespace are stored inside the existing manifest_json column), and the UI adds a top-level Skills nav item, a smart GitHub URL parser in the Add Skill form, a detail view, and a 2-step "Make Public" modal. The feature follows the established patterns for /public/agent_hub and /public/mcp_hub.

Key findings:

  • Inline imports in public_skill_hub() (public_endpoints.py) — _get_prisma_client, ListPluginsResponse, and PluginListItem are imported inside the handler body, violating the project's "no inline imports" rule from CLAUDE.md. Neither import creates a circular dependency, so they can move to the module-level import block.
  • SkillDetail silently drops propsisAdmin, accessToken, and onPublishClick are declared in the interface and passed by both callers, but are never destructured or used in the component. The onPublishClick={fetchPlugins} callback therefore never fires, leaving parent lists stale after any action taken from the detail view.
  • Unbounded find_many in the public endpointfind_many(where={\"enabled\": True}) has no take limit; at scale this will load all enabled skills into memory on every public page load.
  • Redundant URL parser condition in add_plugin_form.tsxparts.length === 2 || (parts.length === 2 && repoBase) always reduces to parts.length === 2.
  • No unit tests addedCLAUDE.md asks for at least one test when adding new features; no test covers the new /public/skill_hub endpoint.

Confidence Score: 4/5

Safe 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).

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "docs(skills): add loom walkthrough video..." | Re-trigger Greptile

Comment on lines +255 to +260
"""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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Comment on lines +273 to +290
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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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)

Comment on lines +14 to +17
const SkillDetail: React.FC<SkillDetailProps> = ({
skill,
onBack,
}) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@ishaan-berri
ishaan-berri changed the base branch from main to litellm_ishaan_april3 April 4, 2026 00:24
@ishaan-berri
ishaan-berri merged commit f6911b2 into litellm_ishaan_april3 Apr 4, 2026
53 of 60 checks passed
@ishaan-berri
ishaan-berri deleted the worktree-magical-puzzling-tide branch April 4, 2026 00:25
@codecov

codecov Bot commented Apr 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...s/claude_code_endpoints/claude_code_marketplace.py 50.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

ishaan-berri added a commit that referenced this pull request Apr 6, 2026
#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>
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants