seen.push(v)} />);
+
+ await user.clear(screen.getByRole("spinbutton", { name: "RPM limit" }));
+
+ expect(seen[seen.length - 1][0].rpm_limit).toBeNull();
+ });
+
+ it("should remove only the clicked row", async () => {
+ const user = userEvent.setup();
+ const initial: TagRateLimitEntry[] = [
+ { id: "r1", tag: "keep-me", rpm_limit: 10 },
+ { id: "r2", tag: "delete-me", rpm_limit: 20 },
+ ];
+ render();
+
+ await user.click(screen.getAllByRole("button", { name: "Remove tag limit" })[1]);
+
+ const tags = screen.getAllByRole("textbox", { name: "Tag" });
+ expect(tags).toHaveLength(1);
+ expect(tags[0]).toHaveValue("keep-me");
+ });
+
+ it("should not submit the surrounding form when a row is removed", async () => {
+ const user = userEvent.setup();
+ let submitted = false;
+ render(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: "Remove tag limit" }));
+
+ expect(submitted).toBe(false);
+ expect(screen.queryAllByRole("textbox", { name: "Tag" })).toHaveLength(0);
+ });
+});
+
+describe("tagRowsToLimits", () => {
+ it("should map named rows with numeric limits into the rpm map", () => {
+ expect(tagRowsToLimits([{ id: "a", tag: "cell-1", rpm_limit: 60 }])).toEqual({ tag_rpm_limit: { "cell-1": 60 } });
+ });
+
+ it("should drop rows with a blank tag or a null limit", () => {
+ const rows: TagRateLimitEntry[] = [
+ { id: "a", tag: " ", rpm_limit: 60 },
+ { id: "b", tag: "cell-2", rpm_limit: null },
+ ];
+ expect(tagRowsToLimits(rows)).toEqual({ tag_rpm_limit: {} });
+ });
+});
+
+describe("tagLimitsToRows", () => {
+ it("should rebuild rows from a stored rpm map", () => {
+ const rows = tagLimitsToRows({ "cell-1": 60 });
+ expect(rows).toHaveLength(1);
+ expect(rows[0]).toMatchObject({ tag: "cell-1", rpm_limit: 60 });
+ });
+
+ it("should ignore non-numeric entries", () => {
+ expect(tagLimitsToRows({ "cell-1": "sixty" })).toEqual([]);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/TagRateLimitEditor.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/TagRateLimitEditor.tsx
index ee022ee9a75..151e1593765 100644
--- a/ui/litellm-dashboard/src/components/key_team_helpers/TagRateLimitEditor.tsx
+++ b/ui/litellm-dashboard/src/components/key_team_helpers/TagRateLimitEditor.tsx
@@ -1,5 +1,6 @@
-import { Button, Input, InputNumber } from "antd";
import React from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
export interface TagRateLimitEntry {
// Stable identity for React list keys so deleting a middle row doesn't shift
@@ -72,25 +73,29 @@ export function TagRateLimitEditor({ value, onChange }: TagRateLimitEditorProps)
{value.map((row, idx) => (
updateRow(idx, "tag", e.target.value)}
placeholder="Tag (e.g. cell-1)"
style={{ width: 180 }}
/>
- updateRow(idx, "rpm_limit", v ?? null)}
+ value={row.rpm_limit ?? ""}
+ onChange={(e) => updateRow(idx, "rpm_limit", e.target.value === "" ? null : Number(e.target.value))}
placeholder="RPM"
style={{ width: 120 }}
/>
-
))}
{
e.preventDefault();
addRow();
diff --git a/ui/litellm-dashboard/src/components/router_settings/LatencyBasedConfiguration.tsx b/ui/litellm-dashboard/src/components/router_settings/LatencyBasedConfiguration.tsx
index 1d1f0c92ad1..f140e4cc371 100644
--- a/ui/litellm-dashboard/src/components/router_settings/LatencyBasedConfiguration.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/LatencyBasedConfiguration.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Input } from "antd";
+import { Input } from "@/components/ui/input";
interface routingStrategyArgs {
ttl?: number;
diff --git a/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx b/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx
index da089552b11..dc6b397827a 100644
--- a/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Input } from "antd";
+import { Input } from "@/components/ui/input";
interface ReliabilityRetriesSectionProps {
routerSettings: { [key: string]: any };
diff --git a/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.test.tsx b/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.test.tsx
index 76965afd603..2c3c7808b0f 100644
--- a/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.test.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.test.tsx
@@ -22,6 +22,11 @@ describe("TagFilteringToggle", () => {
expect(screen.getByText("Enable Tag Filtering")).toBeInTheDocument();
});
+ it("should name the switch with the metadata label so it is reachable by accessible name", () => {
+ render();
+ expect(screen.getByRole("switch", { name: "Tag Filtering" })).toBeInTheDocument();
+ });
+
it("should display the label from metadata when provided", () => {
render();
expect(screen.getByText("Tag Filtering")).toBeInTheDocument();
diff --git a/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx b/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx
index 44bac326bb0..3c3ba4e3160 100644
--- a/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx
@@ -1,5 +1,5 @@
-import React from "react";
-import { Switch } from "antd";
+import React, { useId } from "react";
+import { Switch } from "@/components/ui/switch";
interface TagFilteringToggleProps {
enabled: boolean;
@@ -8,11 +8,13 @@ interface TagFilteringToggleProps {
}
const TagFilteringToggle: React.FC = ({ enabled, routerFieldsMetadata, onToggle }) => {
+ const toggleId = useId();
+
return (
-
-
+
);
diff --git a/ui/litellm-dashboard/src/components/router_settings/index.test.tsx b/ui/litellm-dashboard/src/components/router_settings/index.test.tsx
index 94cbb94d164..788886f30c7 100644
--- a/ui/litellm-dashboard/src/components/router_settings/index.test.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/index.test.tsx
@@ -134,6 +134,33 @@ describe("RouterSettings", () => {
);
});
+ // handleSaveChanges reads each setting's value straight off the DOM via
+ // document.querySelector('input[name="..."]'), so the payload only stays correct
+ // while the rendered input keeps its name attribute and its live value.
+ it("should send the edited input value, not the loaded one, on Save Changes", async () => {
+ const user = userEvent.setup();
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByTestId("strategy-select")).toBeInTheDocument();
+ });
+
+ const numRetries = await screen.findByRole("textbox", { name: /num_retries/i });
+ await user.clear(numRetries);
+ await user.type(numRetries, "42");
+
+ await user.click(screen.getByRole("button", { name: /save changes/i }));
+
+ await waitFor(() =>
+ expect(setCallbacksCall).toHaveBeenCalledWith(
+ "test-token",
+ expect.objectContaining({
+ router_settings: expect.objectContaining({ num_retries: 42 }),
+ }),
+ ),
+ );
+ });
+
it("should show a success notification after saving", async () => {
const user = userEvent.setup();
renderWithProviders();
diff --git a/ui/litellm-dashboard/src/components/router_settings/index.tsx b/ui/litellm-dashboard/src/components/router_settings/index.tsx
index aea08425388..b2668d98d69 100644
--- a/ui/litellm-dashboard/src/components/router_settings/index.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/index.tsx
@@ -1,5 +1,5 @@
-import { Button } from "antd";
import React, { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
import NotificationsManager from "../molecules/notifications_manager";
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../networking";
import RouterSettingsForm, { RouterSettingsFormValue } from "./RouterSettingsForm";
@@ -190,10 +190,10 @@ const RouterSettings: React.FC = ({ accessToken, userRole,
{/* Actions - Sticky at bottom */}
- window.location.reload()}>Reset
-
- Save Changes
+ window.location.reload()}>
+ Reset
+ Save Changes