diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.tsx
index cf449f0e869..0730342c2d5 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/PageVisibilitySettings.tsx
@@ -4,6 +4,7 @@ import { ChevronDown } from "lucide-react";
import { useMemo, useState } from "react";
import { getAvailablePages } from "@/components/page_utils";
+import { ConfigOwnedField } from "@/components/shared/ConfigOwnedField";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
@@ -13,6 +14,7 @@ interface PageVisibilitySettingsProps {
enabledPagesInternalUsers: string[] | null | undefined;
enabledPagesPropertyDescription?: string;
isUpdating: boolean;
+ configOwned?: boolean;
onUpdate: (settings: { enabled_ui_pages_internal_users: string[] | null }) => void;
}
@@ -20,6 +22,7 @@ export default function PageVisibilitySettings({
enabledPagesInternalUsers,
enabledPagesPropertyDescription,
isUpdating,
+ configOwned = false,
onUpdate,
}: PageVisibilitySettingsProps) {
const isPageVisibilitySet = enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined;
@@ -93,11 +96,14 @@ export default function PageVisibilitySettings({
const checkboxId = `page-visibility-${page.page}`;
return (
-
+
-
+
);
};
diff --git a/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx b/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx
index d0395979e50..39d452685b1 100644
--- a/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/TagFilteringToggle.tsx
@@ -1,13 +1,20 @@
import React, { useId } from "react";
+import { ConfigOwnedField } from "@/components/shared/ConfigOwnedField";
import { Switch } from "@/components/ui/switch";
interface TagFilteringToggleProps {
enabled: boolean;
routerFieldsMetadata: { [key: string]: any };
+ disabled?: boolean;
onToggle: (enabled: boolean) => void;
}
-const TagFilteringToggle: React.FC = ({ enabled, routerFieldsMetadata, onToggle }) => {
+const TagFilteringToggle: React.FC = ({
+ enabled,
+ routerFieldsMetadata,
+ disabled = false,
+ onToggle,
+}) => {
const toggleId = useId();
return (
@@ -34,7 +41,9 @@ const TagFilteringToggle: React.FC = ({ enabled, router
)}
-
+
+
+
);
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 1875085231a..e1c864a0247 100644
--- a/ui/litellm-dashboard/src/components/router_settings/index.test.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/index.test.tsx
@@ -183,4 +183,50 @@ describe("RouterSettings", () => {
});
expect(toast.success).not.toHaveBeenCalled();
});
+
+ describe("config.yaml owned fields", () => {
+ it("freezes routing strategy, tag filtering and reliability inputs whose source is config", async () => {
+ const user = userEvent.setup();
+ vi.mocked(getRouterSettingsCall).mockResolvedValue({
+ ...mockRouterSettingsResponse,
+ source: { routing_strategy: "config", enable_tag_filtering: "config", num_retries: "config", timeout: "db" },
+ });
+ renderWithProviders();
+
+ const strategySelect = await findStrategySelect();
+ expect(strategySelect).toHaveAttribute("data-disabled");
+ expect(screen.getByRole("switch")).toHaveAttribute("data-disabled");
+ expect(await screen.findByRole("textbox", { name: /num_retries/i })).toBeDisabled();
+ expect(screen.getByRole("textbox", { name: /timeout/i })).toBeEnabled();
+
+ await user.hover(strategySelect);
+ expect(await screen.findByText("Set in config.yaml and cannot be changed here")).toBeInTheDocument();
+ });
+
+ it.each(["env", "default", "db"])("keeps fields editable when source is %s", async (source) => {
+ const user = userEvent.setup();
+ vi.mocked(getRouterSettingsCall).mockResolvedValue({
+ ...mockRouterSettingsResponse,
+ source: { routing_strategy: source, enable_tag_filtering: source, num_retries: source },
+ });
+ renderWithProviders();
+
+ const strategySelect = await findStrategySelect();
+ expect(strategySelect).not.toHaveAttribute("data-disabled");
+ expect(screen.getByRole("switch")).not.toHaveAttribute("data-disabled");
+ const numRetries = await screen.findByRole("textbox", { name: /num_retries/i });
+ expect(numRetries).toBeEnabled();
+ expect(screen.queryByText("Set in config.yaml and cannot be changed here")).not.toBeInTheDocument();
+
+ fireEvent.change(numRetries, { target: { value: "7" } });
+ 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: 7 }) }),
+ ),
+ );
+ });
+ });
});
diff --git a/ui/litellm-dashboard/src/components/router_settings/index.tsx b/ui/litellm-dashboard/src/components/router_settings/index.tsx
index 53d35b81cec..4bd09bade9e 100644
--- a/ui/litellm-dashboard/src/components/router_settings/index.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/index.tsx
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
+import type { FieldSourceMap } from "@/components/shared/ConfigOwnedField";
import { toast } from "@/lib/toast";
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../networking";
import RouterSettingsForm, { RouterSettingsFormValue } from "./RouterSettingsForm";
@@ -24,6 +25,7 @@ const RouterSettings: React.FC = ({ accessToken, userRole,
const [availableRoutingStrategies, setAvailableRoutingStrategies] = useState([]);
const [routerFieldsMetadata, setRouterFieldsMetadata] = useState<{ [key: string]: any }>({});
const [routingStrategyDescriptions, setRoutingStrategyDescriptions] = useState<{ [key: string]: string }>({});
+ const [routerSources, setRouterSources] = useState({});
useEffect(() => {
if (!accessToken || !userRole || !userID) {
@@ -43,6 +45,7 @@ const RouterSettings: React.FC = ({ accessToken, userRole,
}));
});
getRouterSettingsCall(accessToken).then((data) => {
+ setRouterSources(data.source ?? {});
if (data.fields) {
// Build metadata map for easy lookup
const fieldsMap: { [key: string]: any } = {};
@@ -184,6 +187,7 @@ const RouterSettings: React.FC = ({ accessToken, userRole,
value={formValue}
onChange={setFormValue}
routerFieldsMetadata={routerFieldsMetadata}
+ routerSources={routerSources}
availableRoutingStrategies={availableRoutingStrategies}
routingStrategyDescriptions={routingStrategyDescriptions}
/>
diff --git a/ui/litellm-dashboard/src/components/shared/ConfigOwnedField.tsx b/ui/litellm-dashboard/src/components/shared/ConfigOwnedField.tsx
new file mode 100644
index 00000000000..0879f459082
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/shared/ConfigOwnedField.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import type { components } from "@/lib/http/schema";
+
+export type FieldSource = components["schemas"]["RouterSettingsResponse"]["source"][string];
+export type FieldSourceMap = Partial>;
+
+export const CONFIG_OWNED_MESSAGE = "Set in config.yaml and cannot be changed here";
+
+export const isConfigOwned = (sources: FieldSourceMap | null | undefined, fieldName: string): boolean =>
+ sources?.[fieldName] === "config";
+
+interface ConfigOwnedFieldProps {
+ frozen: boolean;
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function ConfigOwnedField({ frozen, children, className = "inline-flex w-full" }: ConfigOwnedFieldProps) {
+ if (!frozen) {
+ return {children};
+ }
+ return (
+
+
+ }>{children}
+ {CONFIG_OWNED_MESSAGE}
+
+
+ );
+}