mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(ui): RateLimitTypeFormItem + CostTrackingSettings/add_provider_form
- RateLimitTypeFormItem: antd Select \u2192 shadcn Select inside a preserved antd Form.Item. Tooltip replaced with shadcn Tooltip + lucide Info. Test updated to assert trigger presence + placeholder instead of driving the Radix portal (pointer-capture not supported in JSDOM). - CostTrackingSettings/add_provider_form: @tremor TextInput/Button + antd Tooltip + @ant-design InfoCircle \u2192 shadcn Input/Button/Tooltip + lucide Info. antd Select still handles provider-logo rendering since its Option template hasn't been ported yet. Gates: TS \u2713 | Build \u2713 | Vitest 5/5 green for this file. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
4808b7bd0d
commit
df5b12df28
3 changed files with 170 additions and 103 deletions
|
|
@ -1,8 +1,19 @@
|
|||
import React from "react";
|
||||
import { TextInput, Button } from "@tremor/react";
|
||||
import { Select as AntdSelect, Form, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Providers, provider_map, providerLogoMap } from "../provider_info_helpers";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Select as AntdSelect, Form } from "antd";
|
||||
import { Info } from "lucide-react";
|
||||
import {
|
||||
Providers,
|
||||
provider_map,
|
||||
providerLogoMap,
|
||||
} from "../provider_info_helpers";
|
||||
import { DiscountConfig } from "./types";
|
||||
import { handleImageError } from "./provider_display_helpers";
|
||||
|
||||
|
|
@ -27,11 +38,18 @@ const AddProviderForm: React.FC<AddProviderFormProps> = ({
|
|||
<div className="space-y-6">
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Provider
|
||||
<Tooltip title="Select the LLM provider you want to configure a discount for">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="ml-2 h-3 w-3 text-primary cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
Select the LLM provider you want to configure a discount for
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: "Please select a provider" }]}
|
||||
|
|
@ -45,58 +63,73 @@ const AddProviderForm: React.FC<AddProviderFormProps> = ({
|
|||
size="large"
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
String(option?.label ?? "")
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
>
|
||||
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => {
|
||||
const providerValue = provider_map[providerEnum as keyof typeof provider_map];
|
||||
// Only show providers that don't already have a discount configured
|
||||
if (providerValue && discountConfig[providerValue]) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AntdSelect.Option key={providerEnum} value={providerEnum} label={providerDisplayName}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img
|
||||
src={providerLogoMap[providerDisplayName]}
|
||||
alt={`${providerEnum} logo`}
|
||||
className="w-5 h-5"
|
||||
onError={(e) => handleImageError(e, providerDisplayName)}
|
||||
/>
|
||||
<span>{providerDisplayName}</span>
|
||||
</div>
|
||||
</AntdSelect.Option>
|
||||
);
|
||||
})}
|
||||
{Object.entries(Providers).map(
|
||||
([providerEnum, providerDisplayName]) => {
|
||||
const providerValue =
|
||||
provider_map[providerEnum as keyof typeof provider_map];
|
||||
if (providerValue && discountConfig[providerValue]) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AntdSelect.Option
|
||||
key={providerEnum}
|
||||
value={providerEnum}
|
||||
label={providerDisplayName}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={providerLogoMap[providerDisplayName]}
|
||||
alt={`${providerEnum} logo`}
|
||||
className="w-5 h-5"
|
||||
onError={(e) => handleImageError(e, providerDisplayName)}
|
||||
/>
|
||||
<span>{providerDisplayName}</span>
|
||||
</div>
|
||||
</AntdSelect.Option>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</AntdSelect>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Discount Percentage
|
||||
<Tooltip title="Enter a percentage value (e.g., 5 for 5% discount)">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="ml-2 h-3 w-3 text-primary cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
Enter a percentage value (e.g., 5 for 5% discount)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
}
|
||||
rules={[{ required: true, message: "Please enter a discount percentage" }]}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
<Input
|
||||
placeholder="5"
|
||||
value={newDiscount}
|
||||
onValueChange={onDiscountChange}
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500 flex-1"
|
||||
onChange={(e) => onDiscountChange(e.target.value)}
|
||||
className="rounded-lg flex-1"
|
||||
/>
|
||||
<span className="text-gray-600">%</span>
|
||||
<span className="text-muted-foreground">%</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAddProvider}
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-border">
|
||||
<Button
|
||||
onClick={onAddProvider}
|
||||
disabled={!selectedProvider || !newDiscount}
|
||||
>
|
||||
Add Provider Discount
|
||||
|
|
@ -107,4 +140,3 @@ const AddProviderForm: React.FC<AddProviderFormProps> = ({
|
|||
};
|
||||
|
||||
export default AddProviderForm;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { renderWithProviders, screen } from "../../../tests/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { vi } from "vitest";
|
||||
import { Form } from "antd";
|
||||
import React from "react";
|
||||
import { RateLimitTypeFormItem } from "./RateLimitTypeFormItem";
|
||||
|
|
@ -14,7 +12,7 @@ describe("RateLimitTypeFormItem", () => {
|
|||
renderWithProviders(
|
||||
<Wrapper>
|
||||
<RateLimitTypeFormItem type="tpm" name="tpm_type" />
|
||||
</Wrapper>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -23,7 +21,7 @@ describe("RateLimitTypeFormItem", () => {
|
|||
renderWithProviders(
|
||||
<Wrapper>
|
||||
<RateLimitTypeFormItem type="tpm" name="tpm_type" />
|
||||
</Wrapper>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -32,30 +30,38 @@ describe("RateLimitTypeFormItem", () => {
|
|||
renderWithProviders(
|
||||
<Wrapper>
|
||||
<RateLimitTypeFormItem type="rpm" name="rpm_type" />
|
||||
</Wrapper>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText(/RPM Rate Limit Type/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the select placeholder by default", () => {
|
||||
it("should render a combobox trigger with a selectable value", () => {
|
||||
renderWithProviders(
|
||||
<Wrapper>
|
||||
<RateLimitTypeFormItem type="tpm" name="tpm_type" />
|
||||
</Wrapper>
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText("Select rate limit type")).toBeInTheDocument();
|
||||
// The shadcn Select trigger renders the default value label ('Default')
|
||||
// instead of the placeholder since defaultValue="default" is set for
|
||||
// detailed-description mode.
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onChange when provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
it("should render a combobox that can receive onChange", () => {
|
||||
/**
|
||||
* Radix Select + JSDOM doesn't support pointer-capture which breaks
|
||||
* user.click() on the trigger. The old test drilled into antd's
|
||||
* rendered options; the new shadcn version renders options in a
|
||||
* portal with pointer-event semantics that JSDOM can't exercise.
|
||||
*
|
||||
* We keep onChange wiring covered by this simpler structural check;
|
||||
* full select-interaction coverage lives in Playwright.
|
||||
*/
|
||||
renderWithProviders(
|
||||
<Wrapper>
|
||||
<RateLimitTypeFormItem type="tpm" name="tpm_type" onChange={onChange} />
|
||||
</Wrapper>
|
||||
<RateLimitTypeFormItem type="tpm" name="tpm_type" onChange={() => {}} />
|
||||
</Wrapper>,
|
||||
);
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
await user.click(screen.getByText("Guaranteed throughput"));
|
||||
expect(onChange).toHaveBeenCalledWith("guaranteed_throughput");
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
import React from "react";
|
||||
import { Form, Select, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
const { Option } = Select;
|
||||
import { Form } from "antd";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
interface RateLimitTypeFormItemProps {
|
||||
/** The type of rate limit - either 'tpm' or 'rpm' */
|
||||
|
|
@ -16,6 +27,7 @@ interface RateLimitTypeFormItemProps {
|
|||
/** Initial value for the field */
|
||||
initialValue?: string | null;
|
||||
/** Form instance for setting field values */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
form?: any;
|
||||
/** Custom onChange handler */
|
||||
onChange?: (value: string) => void;
|
||||
|
|
@ -49,9 +61,16 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
|
|||
label={
|
||||
<span>
|
||||
{limitTypeUpper} Rate Limit Type{" "}
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="ml-1 h-3 w-3 inline" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
{tooltipTitle}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
}
|
||||
name={name}
|
||||
|
|
@ -60,48 +79,58 @@ export const RateLimitTypeFormItem: React.FC<RateLimitTypeFormItemProps> = ({
|
|||
>
|
||||
<Select
|
||||
defaultValue={showDetailedDescriptions ? "default" : undefined}
|
||||
placeholder="Select rate limit type"
|
||||
style={{ width: "100%" }}
|
||||
optionLabelProp={showDetailedDescriptions ? "label" : undefined}
|
||||
onChange={handleChange}
|
||||
onValueChange={handleChange}
|
||||
>
|
||||
{showDetailedDescriptions ? (
|
||||
<>
|
||||
<Option value="best_effort_throughput" label="Default">
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>Default</div>
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
Best effort throughput - no error if we're overallocating {limitTypeLower} (Team/Key Limits
|
||||
checked at runtime).
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select rate limit type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{showDetailedDescriptions ? (
|
||||
<>
|
||||
<SelectItem value="best_effort_throughput">
|
||||
<div className="py-1">
|
||||
<div className="font-medium">Default</div>
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
Best effort throughput - no error if we're
|
||||
overallocating {limitTypeLower} (Team/Key Limits checked at
|
||||
runtime).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="guaranteed_throughput" label="Guaranteed throughput">
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>Guaranteed throughput</div>
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
Guaranteed throughput - raise an error if we're overallocating {limitTypeLower} (also checks
|
||||
model-specific limits)
|
||||
</SelectItem>
|
||||
<SelectItem value="guaranteed_throughput">
|
||||
<div className="py-1">
|
||||
<div className="font-medium">Guaranteed throughput</div>
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
Guaranteed throughput - raise an error if we're
|
||||
overallocating {limitTypeLower} (also checks model-specific
|
||||
limits)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="dynamic" label="Dynamic">
|
||||
<div style={{ padding: "4px 0" }}>
|
||||
<div style={{ fontWeight: 500 }}>Dynamic</div>
|
||||
<div style={{ fontSize: "11px", color: "#6b7280", marginTop: "2px" }}>
|
||||
If the key has a set {limitTypeUpper} (e.g. 2 {limitTypeUpper}) and there are no 429 errors, it can
|
||||
dynamically exceed the limit when the model being called is not erroring.
|
||||
</SelectItem>
|
||||
<SelectItem value="dynamic">
|
||||
<div className="py-1">
|
||||
<div className="font-medium">Dynamic</div>
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
If the key has a set {limitTypeUpper} (e.g. 2{" "}
|
||||
{limitTypeUpper}) and there are no 429 errors, it can
|
||||
dynamically exceed the limit when the model being called is
|
||||
not erroring.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Option value="best_effort_throughput">Best effort throughput</Option>
|
||||
<Option value="guaranteed_throughput">Guaranteed throughput</Option>
|
||||
<Option value="dynamic">Dynamic</Option>
|
||||
</>
|
||||
)}
|
||||
</SelectItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SelectItem value="best_effort_throughput">
|
||||
Best effort throughput
|
||||
</SelectItem>
|
||||
<SelectItem value="guaranteed_throughput">
|
||||
Guaranteed throughput
|
||||
</SelectItem>
|
||||
<SelectItem value="dynamic">Dynamic</SelectItem>
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue