mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
* 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>
146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Button } from "@tremor/react";
|
|
import { Modal } from "antd";
|
|
import {
|
|
getClaudeCodePluginsList,
|
|
deleteClaudeCodePlugin,
|
|
} from "./networking";
|
|
import AddPluginForm from "./claude_code_plugins/add_plugin_form";
|
|
import PluginTable from "./claude_code_plugins/plugin_table";
|
|
import SkillDetail from "./claude_code_plugins/skill_detail";
|
|
import { isAdminRole } from "@/utils/roles";
|
|
import NotificationsManager from "./molecules/notifications_manager";
|
|
import { Plugin, ListPluginsResponse } from "./claude_code_plugins/types";
|
|
|
|
interface ClaudeCodePluginsPanelProps {
|
|
accessToken: string | null;
|
|
userRole?: string;
|
|
}
|
|
|
|
const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({
|
|
accessToken,
|
|
userRole,
|
|
}) => {
|
|
const [pluginsList, setPluginsList] = useState<Plugin[]>([]);
|
|
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [pluginToDelete, setPluginToDelete] = useState<{
|
|
name: string;
|
|
displayName: string;
|
|
} | null>(null);
|
|
const [selectedSkill, setSelectedSkill] = useState<Plugin | null>(null);
|
|
|
|
const isAdmin = userRole ? isAdminRole(userRole) : false;
|
|
|
|
const fetchPlugins = async () => {
|
|
if (!accessToken) return;
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const response: ListPluginsResponse = await getClaudeCodePluginsList(
|
|
accessToken,
|
|
false
|
|
);
|
|
setPluginsList(response.plugins);
|
|
} catch (error) {
|
|
console.error("Error fetching skills:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchPlugins();
|
|
}, [accessToken]);
|
|
|
|
const handleDeleteClick = (pluginName: string, displayName: string) => {
|
|
setPluginToDelete({ name: pluginName, displayName });
|
|
};
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
if (!pluginToDelete || !accessToken) return;
|
|
|
|
setIsDeleting(true);
|
|
try {
|
|
await deleteClaudeCodePlugin(accessToken, pluginToDelete.name);
|
|
NotificationsManager.success(`Skill "${pluginToDelete.displayName}" deleted successfully`);
|
|
fetchPlugins();
|
|
} catch (error) {
|
|
console.error("Error deleting skill:", error);
|
|
NotificationsManager.error("Failed to delete skill");
|
|
} finally {
|
|
setIsDeleting(false);
|
|
setPluginToDelete(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
|
{selectedSkill ? (
|
|
<SkillDetail
|
|
skill={selectedSkill}
|
|
onBack={() => setSelectedSkill(null)}
|
|
isAdmin={isAdmin}
|
|
accessToken={accessToken}
|
|
onPublishClick={fetchPlugins}
|
|
/>
|
|
) : (
|
|
<>
|
|
<div className="flex flex-col gap-2 mb-4">
|
|
<h1 className="text-2xl font-bold">Skills</h1>
|
|
<p className="text-sm text-gray-600">
|
|
Register Claude Code skills. Published skills appear in the Skill Hub for all users and
|
|
are served via{" "}
|
|
<code className="bg-gray-100 px-1 rounded">/claude-code/marketplace.json</code>.
|
|
</p>
|
|
<div className="mt-2 flex gap-2">
|
|
<Button onClick={() => setIsAddModalVisible(true)} disabled={!accessToken || !isAdmin}>
|
|
+ Add Skill
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<PluginTable
|
|
pluginsList={pluginsList}
|
|
isLoading={isLoading}
|
|
onDeleteClick={handleDeleteClick}
|
|
accessToken={accessToken}
|
|
isAdmin={isAdmin}
|
|
onPluginClick={(id) => {
|
|
const skill = pluginsList.find((p) => p.id === id);
|
|
if (skill) setSelectedSkill(skill);
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<AddPluginForm
|
|
visible={isAddModalVisible}
|
|
onClose={() => setIsAddModalVisible(false)}
|
|
accessToken={accessToken}
|
|
onSuccess={fetchPlugins}
|
|
/>
|
|
|
|
{pluginToDelete && (
|
|
<Modal
|
|
title="Delete Skill"
|
|
open={pluginToDelete !== null}
|
|
onOk={handleDeleteConfirm}
|
|
onCancel={() => setPluginToDelete(null)}
|
|
confirmLoading={isDeleting}
|
|
okText="Delete"
|
|
okButtonProps={{ danger: true }}
|
|
>
|
|
<p>
|
|
Are you sure you want to delete skill:{" "}
|
|
<strong>{pluginToDelete.displayName}</strong>?
|
|
</p>
|
|
<p>This action cannot be undone.</p>
|
|
</Modal>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ClaudeCodePluginsPanel;
|