mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
add plugin on marketplace / ai hub
This commit is contained in:
parent
357287e6e3
commit
d9df5e46eb
7 changed files with 479 additions and 1 deletions
|
|
@ -120,6 +120,8 @@ const routeFor = (slug: string): string => {
|
|||
return "experimental/api-playground";
|
||||
case "tag-management":
|
||||
return "experimental/tag-management";
|
||||
case "claude-code-plugins":
|
||||
return "experimental/claude-code-plugins";
|
||||
case "usage": // "Old Usage"
|
||||
return "experimental/old-usage";
|
||||
|
||||
|
|
@ -257,6 +259,13 @@ const menuItems: MenuItemCfg[] = [
|
|||
icon: <TagsOutlined style={{ fontSize: 18 }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "27",
|
||||
page: "claude-code-plugins",
|
||||
label: "Claude Code Plugins",
|
||||
icon: <ToolOutlined style={{ fontSize: 18 }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{ key: "4", page: "usage", label: "Old Usage", icon: <BarChartOutlined style={{ fontSize: 18 }} /> },
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import AdminPanel from "@/components/admins";
|
|||
import AgentsPanel from "@/components/agents";
|
||||
import BudgetPanel from "@/components/budgets/budget_panel";
|
||||
import CacheDashboard from "@/components/cache_dashboard";
|
||||
import ClaudeCodePluginsPanel from "@/components/claude_code_plugins";
|
||||
import { fetchTeams } from "@/components/common_components/fetch_teams";
|
||||
import LoadingScreen from "@/components/common_components/LoadingScreen";
|
||||
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
|
||||
|
|
@ -530,6 +531,8 @@ export default function CreateKeyPage() {
|
|||
<SearchTools accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "tag-management" ? (
|
||||
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "claude-code-plugins" ? (
|
||||
<ClaudeCodePluginsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "vector-stores" ? (
|
||||
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
|
||||
) : page == "new_usage" ? (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Input } from "antd";
|
||||
import { Card, TabGroup, TabList, Tab, TabPanels, TabPanel, Text } from "@tremor/react";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { getClaudeCodeMarketplace } from "../networking";
|
||||
import { ModelDataTable } from "../model_dashboard/table";
|
||||
import { getMarketplaceTableColumns } from "./marketplace_table_columns";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import {
|
||||
MarketplaceResponse,
|
||||
MarketplacePluginEntry,
|
||||
} from "../claude_code_plugins/types";
|
||||
import {
|
||||
extractCategories,
|
||||
filterPluginsBySearch,
|
||||
filterPluginsByCategory,
|
||||
} from "../claude_code_plugins/helpers";
|
||||
|
||||
interface ClaudeCodeMarketplaceTabProps {
|
||||
publicPage?: boolean;
|
||||
}
|
||||
|
||||
const ClaudeCodeMarketplaceTab: React.FC<ClaudeCodeMarketplaceTabProps> = ({
|
||||
publicPage = false,
|
||||
}) => {
|
||||
const [marketplaceData, setMarketplaceData] =
|
||||
useState<MarketplaceResponse | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMarketplace();
|
||||
}, []);
|
||||
|
||||
const fetchMarketplace = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data: MarketplaceResponse = await getClaudeCodeMarketplace();
|
||||
console.log("Claude Code marketplace:", data);
|
||||
setMarketplaceData(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching marketplace:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
NotificationsManager.success("Copied to clipboard!");
|
||||
};
|
||||
|
||||
// Extract unique categories from plugins
|
||||
const categories = useMemo(() => {
|
||||
if (!marketplaceData) return ["All"];
|
||||
return extractCategories(marketplaceData.plugins);
|
||||
}, [marketplaceData]);
|
||||
|
||||
// Get selected category name
|
||||
const selectedCategory = categories[selectedCategoryIndex] || "All";
|
||||
|
||||
// Filter plugins by search and category
|
||||
const filteredPlugins = useMemo(() => {
|
||||
if (!marketplaceData) return [];
|
||||
|
||||
let plugins = marketplaceData.plugins;
|
||||
|
||||
// Apply category filter
|
||||
plugins = filterPluginsByCategory(plugins, selectedCategory);
|
||||
|
||||
// Apply search filter
|
||||
plugins = filterPluginsBySearch(plugins, searchTerm);
|
||||
|
||||
return plugins;
|
||||
}, [marketplaceData, selectedCategory, searchTerm]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => getMarketplaceTableColumns(copyToClipboard, publicPage),
|
||||
[publicPage]
|
||||
);
|
||||
|
||||
if (!marketplaceData && !isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="text-center p-12">
|
||||
<Text className="text-gray-500">
|
||||
Failed to load marketplace. Please try again later.
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search Bar */}
|
||||
<div className="max-w-md">
|
||||
<Input
|
||||
placeholder="Search plugins by name, description, or keywords..."
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
allowClear
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
<TabGroup index={selectedCategoryIndex} onIndexChange={setSelectedCategoryIndex}>
|
||||
<TabList className="mb-4">
|
||||
{categories.map((category) => {
|
||||
// Count plugins in this category
|
||||
const categoryPlugins = filterPluginsByCategory(
|
||||
marketplaceData?.plugins || [],
|
||||
category
|
||||
);
|
||||
const count = filterPluginsBySearch(
|
||||
categoryPlugins,
|
||||
searchTerm
|
||||
).length;
|
||||
|
||||
return (
|
||||
<Tab key={category}>
|
||||
{category} {count > 0 && `(${count})`}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{categories.map((category) => (
|
||||
<TabPanel key={category}>
|
||||
<Card>
|
||||
{/* Plugin Table */}
|
||||
<ModelDataTable
|
||||
columns={columns}
|
||||
data={filteredPlugins}
|
||||
isLoading={isLoading}
|
||||
defaultSorting={[{ id: "name", desc: false }]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="mt-4 text-center space-y-2">
|
||||
<Text className="text-sm text-gray-600">
|
||||
Showing {filteredPlugins.length} of{" "}
|
||||
{marketplaceData?.plugins.length || 0} plugin
|
||||
{marketplaceData?.plugins.length !== 1 ? "s" : ""}
|
||||
{searchTerm && ` matching "${searchTerm}"`}
|
||||
{selectedCategory !== "All" && ` in ${selectedCategory}`}
|
||||
</Text>
|
||||
</div>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClaudeCodeMarketplaceTab;
|
||||
|
|
@ -5,6 +5,7 @@ import MakeModelPublicForm from "@/components/AIHub/forms/MakeModelPublicForm";
|
|||
import { mcpHubColumns, MCPServerData } from "@/components/mcp_hub_table_columns";
|
||||
import { modelHubColumns } from "@/components/model_hub_table_columns";
|
||||
import UsefulLinksManagement from "@/components/AIHub/UsefulLinksManagement";
|
||||
import ClaudeCodeMarketplaceTab from "@/components/AIHub/ClaudeCodeMarketplaceTab";
|
||||
import { ModelDataTable } from "@/components/model_dashboard/table";
|
||||
import ModelFilters from "@/components/model_filters";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
|
|
@ -372,12 +373,13 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab System for Model Hub, Agent Hub, and MCP Hub */}
|
||||
{/* Tab System for Model Hub, Agent Hub, MCP Hub, and Plugin Marketplace */}
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Model Hub</Tab>
|
||||
<Tab>Agent Hub</Tab>
|
||||
<Tab>MCP Hub</Tab>
|
||||
<Tab>Claude Code Plugin Marketplace</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -462,6 +464,11 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
</Text>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
{/* Plugin Marketplace Tab */}
|
||||
<TabPanel>
|
||||
<ClaudeCodeMarketplaceTab publicPage={publicPage} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button, Badge, Text } from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
import { CopyOutlined } from "@ant-design/icons";
|
||||
import { MarketplacePluginEntry } from "@/components/claude_code_plugins/types";
|
||||
import {
|
||||
formatInstallCommand,
|
||||
getCategoryBadgeColor,
|
||||
getSourceDisplayText,
|
||||
} from "@/components/claude_code_plugins/helpers";
|
||||
|
||||
export const getMarketplaceTableColumns = (
|
||||
copyToClipboard: (text: string) => void,
|
||||
publicPage: boolean = false,
|
||||
): ColumnDef<MarketplacePluginEntry>[] => {
|
||||
const allColumns: ColumnDef<MarketplacePluginEntry>[] = [
|
||||
{
|
||||
header: "Plugin Name",
|
||||
accessorKey: "name",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const plugin = row.original;
|
||||
const installCommand = formatInstallCommand(plugin);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium text-sm">{plugin.name}</Text>
|
||||
<Tooltip title="Copy install command">
|
||||
<CopyOutlined
|
||||
onClick={() => copyToClipboard(installCommand)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* Show description on mobile */}
|
||||
<div className="md:hidden">
|
||||
<Text className="text-xs text-gray-600">
|
||||
{plugin.description || "No description"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const plugin = row.original;
|
||||
|
||||
return (
|
||||
<Text className="text-xs line-clamp-2">
|
||||
{plugin.description || "-"}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden md:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Version",
|
||||
accessorKey: "version",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const plugin = row.original;
|
||||
|
||||
return plugin.version ? (
|
||||
<Badge color="blue" size="sm">
|
||||
v{plugin.version}
|
||||
</Badge>
|
||||
) : (
|
||||
<Text className="text-xs text-gray-400">-</Text>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden lg:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Category",
|
||||
accessorKey: "category",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const plugin = row.original;
|
||||
const badgeColor = getCategoryBadgeColor(plugin.category);
|
||||
|
||||
return plugin.category ? (
|
||||
<Badge color={badgeColor} size="sm">
|
||||
{plugin.category}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="gray" size="sm">
|
||||
Uncategorized
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden lg:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Source",
|
||||
accessorKey: "source",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const plugin = row.original;
|
||||
const sourceText = getSourceDisplayText(plugin.source);
|
||||
|
||||
return <Text className="text-xs text-gray-600">{sourceText}</Text>;
|
||||
},
|
||||
meta: {
|
||||
className: "hidden xl:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Keywords",
|
||||
accessorKey: "keywords",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const plugin = row.original;
|
||||
const keywords = plugin.keywords?.slice(0, 3) || [];
|
||||
const remaining = (plugin.keywords?.length || 0) - 3;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{keywords.map((keyword, index) => (
|
||||
<Badge key={index} color="gray" size="xs">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<Badge color="gray" size="xs">
|
||||
+{remaining}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden xl:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Install Command",
|
||||
id: "install_command",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const plugin = row.original;
|
||||
const installCommand = formatInstallCommand(plugin);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono truncate max-w-[200px]">
|
||||
{installCommand}
|
||||
</code>
|
||||
<Tooltip title="Copy command">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
icon={CopyOutlined}
|
||||
onClick={() => copyToClipboard(installCommand)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return allColumns;
|
||||
};
|
||||
112
ui/litellm-dashboard/src/components/claude_code_plugins/types.ts
Normal file
112
ui/litellm-dashboard/src/components/claude_code_plugins/types.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* TypeScript types for Claude Code Marketplace
|
||||
* Matches backend API types from /litellm/types/proxy/claude_code_endpoints.py
|
||||
*/
|
||||
|
||||
export interface PluginSource {
|
||||
source: "github" | "url";
|
||||
repo?: string; // Format: "org/repo" for GitHub
|
||||
url?: string; // Full URL for other sources
|
||||
}
|
||||
|
||||
export interface PluginAuthor {
|
||||
name: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
id: string;
|
||||
name: string; // kebab-case
|
||||
version?: string; // semantic version
|
||||
description?: string;
|
||||
source: PluginSource;
|
||||
author?: PluginAuthor;
|
||||
homepage?: string;
|
||||
keywords?: string[];
|
||||
category?: string;
|
||||
enabled: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
export interface PluginListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
source: PluginSource;
|
||||
author?: PluginAuthor;
|
||||
homepage?: string;
|
||||
keywords?: string[];
|
||||
category?: string;
|
||||
enabled: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ListPluginsResponse {
|
||||
plugins: PluginListItem[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RegisterPluginRequest {
|
||||
name: string;
|
||||
source: PluginSource;
|
||||
version?: string;
|
||||
description?: string;
|
||||
author?: PluginAuthor;
|
||||
homepage?: string;
|
||||
keywords?: string[];
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface RegisterPluginResponse {
|
||||
plugin: Plugin;
|
||||
action: "created" | "updated";
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Public marketplace types
|
||||
export interface MarketplacePluginEntry {
|
||||
name: string;
|
||||
source: PluginSource;
|
||||
version?: string;
|
||||
description?: string;
|
||||
author?: PluginAuthor;
|
||||
homepage?: string;
|
||||
keywords?: string[];
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface MarketplaceOwner {
|
||||
name: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface MarketplaceResponse {
|
||||
name: string; // Marketplace name (e.g., "litellm")
|
||||
owner: MarketplaceOwner;
|
||||
plugins: MarketplacePluginEntry[];
|
||||
}
|
||||
|
||||
// UI-specific types
|
||||
export interface CategoryTab {
|
||||
key: string;
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface PluginFormData {
|
||||
name: string;
|
||||
sourceType: "github" | "url";
|
||||
repo: string;
|
||||
url: string;
|
||||
version: string;
|
||||
description: string;
|
||||
authorName: string;
|
||||
authorEmail: string;
|
||||
homepage: string;
|
||||
category: string;
|
||||
keywords: string; // Comma-separated string, will be split into array
|
||||
}
|
||||
|
|
@ -256,6 +256,13 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
icon: <TagsOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "claude-code-plugins",
|
||||
page: "claude-code-plugins",
|
||||
label: "Claude Code Plugins",
|
||||
icon: <ToolOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
page: "usage",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue