+ {sourcesFailed && (
+
+ Failed to load router settings. Reload the page to edit fallbacks
+
+ )}
{canModify && (
{
expect(await screen.findByText("Set in config.yaml and cannot be changed here")).toBeInTheDocument();
});
+ it("drops the previous source map while a new session is loading", async () => {
+ vi.mocked(getRouterSettingsCall).mockResolvedValueOnce({ ...mockRouterSettingsResponse, source: {} });
+ const { rerender } = renderWithProviders();
+ expect(await screen.findByRole("button", { name: /save changes/i })).toBeInTheDocument();
+
+ vi.mocked(getRouterSettingsCall).mockReturnValue(new Promise(() => {}));
+ rerender();
+
+ expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument();
+ });
+
+ it("shows an error instead of a blank page when the source request fails", async () => {
+ vi.mocked(getRouterSettingsCall).mockRejectedValue(new Error("boom"));
+ renderWithProviders();
+
+ expect(await screen.findByRole("alert")).toHaveTextContent("Failed to load router settings");
+ expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument();
+ });
+
it("holds the form until the source map has loaded so config owned fields never render editable", async () => {
let resolveSources: (
value: typeof mockRouterSettingsResponse & { source: Record },
diff --git a/ui/litellm-dashboard/src/components/router_settings/index.tsx b/ui/litellm-dashboard/src/components/router_settings/index.tsx
index 483e2726471..c611a7eed23 100644
--- a/ui/litellm-dashboard/src/components/router_settings/index.tsx
+++ b/ui/litellm-dashboard/src/components/router_settings/index.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
-import type { FieldSourceMap } from "@/components/shared/ConfigOwnedField";
+import type { SourcesState } from "@/components/shared/ConfigOwnedField";
import { toast } from "@/lib/toast";
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../networking";
import RouterSettingsForm, { RouterSettingsFormValue } from "./RouterSettingsForm";
@@ -25,7 +25,11 @@ 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(null);
+ const [sourcesState, setSourcesState] = useState(null);
+ const sessionKey = `${accessToken}:${userRole}:${userID}`;
+ const loadedSources = sourcesState?.sessionKey === sessionKey ? sourcesState : null;
+ const routerSources = loadedSources?.sources ?? null;
+ const sourcesFailed = loadedSources?.failed ?? false;
useEffect(() => {
if (!accessToken || !userRole || !userID) {
@@ -44,43 +48,45 @@ const RouterSettings: React.FC = ({ accessToken, userRole,
selectedStrategy: initialStrategy,
}));
});
- getRouterSettingsCall(accessToken).then((data) => {
- setRouterSources(data.source ?? {});
- if (data.fields) {
- // Build metadata map for easy lookup
- const fieldsMap: { [key: string]: any } = {};
- data.fields.forEach((field: any) => {
- fieldsMap[field.field_name] = {
- ui_field_name: field.ui_field_name,
- field_description: field.field_description,
- options: field.options,
- link: field.link,
- };
- });
- setRouterFieldsMetadata(fieldsMap);
+ getRouterSettingsCall(accessToken)
+ .then((data) => {
+ setSourcesState({ sessionKey, sources: data.source ?? {}, failed: false });
+ if (data.fields) {
+ // Build metadata map for easy lookup
+ const fieldsMap: { [key: string]: any } = {};
+ data.fields.forEach((field: any) => {
+ fieldsMap[field.field_name] = {
+ ui_field_name: field.ui_field_name,
+ field_description: field.field_description,
+ options: field.options,
+ link: field.link,
+ };
+ });
+ setRouterFieldsMetadata(fieldsMap);
- // Extract routing strategies from the routing_strategy field's options
- const routingStrategyField = data.fields.find((field: any) => field.field_name === "routing_strategy");
- if (routingStrategyField?.options) {
- setAvailableRoutingStrategies(routingStrategyField.options);
- }
+ // Extract routing strategies from the routing_strategy field's options
+ const routingStrategyField = data.fields.find((field: any) => field.field_name === "routing_strategy");
+ if (routingStrategyField?.options) {
+ setAvailableRoutingStrategies(routingStrategyField.options);
+ }
- // Store routing strategy descriptions
- if (data.routing_strategy_descriptions) {
- setRoutingStrategyDescriptions(data.routing_strategy_descriptions);
- }
+ // Store routing strategy descriptions
+ if (data.routing_strategy_descriptions) {
+ setRoutingStrategyDescriptions(data.routing_strategy_descriptions);
+ }
- // Set enable_tag_filtering value
- const tagFilteringField = data.fields.find((field: any) => field.field_name === "enable_tag_filtering");
- if (tagFilteringField?.field_value !== null && tagFilteringField?.field_value !== undefined) {
- setFormValue((prev) => ({
- ...prev,
- enableTagFiltering: tagFilteringField.field_value,
- }));
+ // Set enable_tag_filtering value
+ const tagFilteringField = data.fields.find((field: any) => field.field_name === "enable_tag_filtering");
+ if (tagFilteringField?.field_value !== null && tagFilteringField?.field_value !== undefined) {
+ setFormValue((prev) => ({
+ ...prev,
+ enableTagFiltering: tagFilteringField.field_value,
+ }));
+ }
}
- }
- });
- }, [accessToken, userRole, userID]);
+ })
+ .catch(() => setSourcesState({ sessionKey, sources: null, failed: true }));
+ }, [accessToken, userRole, userID, sessionKey]);
const handleSaveChanges = async () => {
if (!accessToken) {
@@ -177,7 +183,19 @@ const RouterSettings: React.FC = ({ accessToken, userRole,
}
};
- if (!accessToken || routerSources === null) {
+ if (!accessToken) {
+ return null;
+ }
+
+ if (sourcesFailed) {
+ return (
+
+ Failed to load router settings. Reload the page to try again
+
+ );
+ }
+
+ if (routerSources === null) {
return null;
}
diff --git a/ui/litellm-dashboard/src/components/shared/ConfigOwnedField.tsx b/ui/litellm-dashboard/src/components/shared/ConfigOwnedField.tsx
index 0879f459082..1ded0e5cd35 100644
--- a/ui/litellm-dashboard/src/components/shared/ConfigOwnedField.tsx
+++ b/ui/litellm-dashboard/src/components/shared/ConfigOwnedField.tsx
@@ -6,6 +6,12 @@ import type { components } from "@/lib/http/schema";
export type FieldSource = components["schemas"]["RouterSettingsResponse"]["source"][string];
export type FieldSourceMap = Partial>;
+export interface SourcesState {
+ sessionKey: string;
+ sources: FieldSourceMap | null;
+ failed: boolean;
+}
+
export const CONFIG_OWNED_MESSAGE = "Set in config.yaml and cannot be changed here";
export const isConfigOwned = (sources: FieldSourceMap | null | undefined, fieldName: string): boolean =>