mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
feat(ui): customer-level TPM/RPM limits in the Customer Portal create and edit dialogs
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
e7ad61f89e
commit
753fe187af
3 changed files with 45 additions and 5 deletions
|
|
@ -12,6 +12,8 @@ export default function CreateCustomerDialog({ open, onOpenChange, onCreated }:
|
|||
const [userId, setUserId] = useState("");
|
||||
const [alias, setAlias] = useState("");
|
||||
const [budget, setBudget] = useState("");
|
||||
const [tpm, setTpm] = useState("");
|
||||
const [rpm, setRpm] = useState("");
|
||||
const create = $api.useMutation("post", "/customer/new", {
|
||||
onSuccess: () => {
|
||||
toast.success(`Created ${alias || userId}`);
|
||||
|
|
@ -30,6 +32,8 @@ export default function CreateCustomerDialog({ open, onOpenChange, onCreated }:
|
|||
blocked: false,
|
||||
max_budget: budget ? Number(budget) : null,
|
||||
budget_duration: budget ? "1mo" : null,
|
||||
tpm_limit: tpm ? Number(tpm) : null,
|
||||
rpm_limit: rpm ? Number(rpm) : null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -52,10 +56,23 @@ export default function CreateCustomerDialog({ open, onOpenChange, onCreated }:
|
|||
<Input id="cp-alias" value={alias} onChange={(e) => setAlias(e.target.value)} placeholder="Acme Corp" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:w-1/2">
|
||||
<Label htmlFor="cp-budget">Max Budget (USD, resets monthly)</Label>
|
||||
<Input id="cp-budget" inputMode="decimal" value={budget} onChange={(e) => setBudget(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="Leave empty for no budget" />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cp-budget">Max Budget (USD, resets monthly)</Label>
|
||||
<Input id="cp-budget" inputMode="decimal" value={budget} onChange={(e) => setBudget(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="Leave empty for no budget" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cp-tpm">TPM limit</Label>
|
||||
<Input id="cp-tpm" inputMode="numeric" value={tpm} onChange={(e) => setTpm(e.target.value.replace(/[^0-9]/g, ""))} placeholder="No limit" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cp-rpm">RPM limit</Label>
|
||||
<Input id="cp-rpm" inputMode="numeric" value={rpm} onChange={(e) => setRpm(e.target.value.replace(/[^0-9]/g, ""))} placeholder="No limit" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">TPM/RPM apply to this customer across every key it uses; the proxy returns 429 above them.</p>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button type="submit" disabled={!userId.trim() || create.isPending}>Create Customer</Button>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export default function CustomerDetail({ customerId, onBack }: { customerId: str
|
|||
const label = c?.alias || customerId;
|
||||
const status = c ? customerStatus(c, spend) : "active";
|
||||
const max = c?.litellm_budget_table?.max_budget ?? null;
|
||||
const tpmLimit = c?.litellm_budget_table?.tpm_limit ?? null;
|
||||
const rpmLimit = c?.litellm_budget_table?.rpm_limit ?? null;
|
||||
const limitsText = [tpmLimit !== null ? `${tpmLimit.toLocaleString()} TPM` : null, rpmLimit !== null ? `${rpmLimit.toLocaleString()} RPM` : null].filter(Boolean).join(" / ");
|
||||
const refresh = () => { keysQuery.refetch(); trafficQuery.refetch(); };
|
||||
const removeKey = async (k: PortalKey) => {
|
||||
if (!accessToken) return;
|
||||
|
|
@ -67,6 +70,7 @@ export default function CustomerDetail({ customerId, onBack }: { customerId: str
|
|||
<p className="text-sm text-muted-foreground">
|
||||
Spend this month <span className="font-medium text-foreground">{formatUSD(spend)}</span>
|
||||
{max !== null && max !== undefined ? <> of <span className="font-medium text-foreground">{formatUSD(max)}</span> budget</> : ", no budget"}
|
||||
{limitsText && <> · {limitsText}</>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -20,8 +20,16 @@ function EditCustomerForm({ onOpenChange, customer, onSaved }: Omit<Props, "open
|
|||
const [alias, setAlias] = useState(customer.alias ?? "");
|
||||
const currentBudget = customer.litellm_budget_table?.max_budget;
|
||||
const [budget, setBudget] = useState(currentBudget === null || currentBudget === undefined ? "" : String(currentBudget));
|
||||
const budgetId = customer.litellm_budget_table?.budget_id ?? null;
|
||||
const asText = (n: number | null | undefined) => (n === null || n === undefined ? "" : String(n));
|
||||
const [tpm, setTpm] = useState(asText(customer.litellm_budget_table?.tpm_limit));
|
||||
const [rpm, setRpm] = useState(asText(customer.litellm_budget_table?.rpm_limit));
|
||||
const updateLimits = $api.useMutation("post", "/budget/update");
|
||||
const update = $api.useMutation("post", "/customer/update", {
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
if (budgetId) {
|
||||
await updateLimits.mutateAsync({ body: { budget_id: budgetId, tpm_limit: tpm ? Number(tpm) : null, rpm_limit: rpm ? Number(rpm) : null } });
|
||||
}
|
||||
toast.success("Customer updated");
|
||||
onSaved();
|
||||
onOpenChange(false);
|
||||
|
|
@ -54,9 +62,20 @@ function EditCustomerForm({ onOpenChange, customer, onSaved }: Omit<Props, "open
|
|||
<Label htmlFor="ec-budget">Max Budget (USD)</Label>
|
||||
<Input id="ec-budget" inputMode="decimal" value={budget} onChange={(e) => setBudget(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="Leave empty for no budget" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="ec-tpm">TPM limit</Label>
|
||||
<Input id="ec-tpm" inputMode="numeric" value={tpm} onChange={(e) => setTpm(e.target.value.replace(/[^0-9]/g, ""))} placeholder="No limit" disabled={!budgetId} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="ec-rpm">RPM limit</Label>
|
||||
<Input id="ec-rpm" inputMode="numeric" value={rpm} onChange={(e) => setRpm(e.target.value.replace(/[^0-9]/g, ""))} placeholder="No limit" disabled={!budgetId} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{budgetId ? "Limits apply to this customer across every key it uses." : "Set a budget or limits when creating the customer to enable TPM/RPM here."}</p>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button type="submit" disabled={update.isPending}>Save</Button>
|
||||
<Button type="submit" disabled={update.isPending || updateLimits.isPending}>Save</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue