mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(ui): fix Router Settings Loadbalancing tab save (LIT-4057)
The Loadbalancing tab rendered routing_groups as a generic text input and sent its array value back as the JSON string "[]", which fails Pydantic list validation on POST /config/update and returns 422. routing_groups has its own dedicated Routing Groups tab, so this tab must neither render nor write it; exclude it the same way retry_policy and model_group_retry_policy are excluded for the Model Retry Settings tab. The save was also fire-and-forget: setCallbacksCall was not awaited, so the rejected promise escaped the try/catch and the success toast fired unconditionally, showing success even when the backend rejected the change. Await the call, gate the success toast on resolution, and surface the error.
This commit is contained in:
parent
0965a4d1f4
commit
9968499aab
3 changed files with 49 additions and 9 deletions
|
|
@ -20,14 +20,15 @@ const ReliabilityRetriesSection: React.FC<ReliabilityRetriesSectionProps> = ({
|
|||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{Object.entries(routerSettings)
|
||||
.filter(
|
||||
([param, value]) =>
|
||||
([param]) =>
|
||||
param != "fallbacks" &&
|
||||
param != "context_window_fallbacks" &&
|
||||
param != "routing_strategy_args" &&
|
||||
param != "routing_strategy" &&
|
||||
param != "enable_tag_filtering" &&
|
||||
param != "retry_policy" &&
|
||||
param != "model_group_retry_policy",
|
||||
param != "model_group_retry_policy" &&
|
||||
param != "routing_groups",
|
||||
)
|
||||
.map(([param, value]) => (
|
||||
<div key={param} className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -146,4 +146,44 @@ describe("RouterSettings", () => {
|
|||
|
||||
expect(NotificationsManager.success).toHaveBeenCalledWith("router settings updated successfully");
|
||||
});
|
||||
|
||||
it("should not render or save routing_groups (owned by the Routing Groups tab) (LIT-4057)", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getCallbacksCall).mockResolvedValue({
|
||||
router_settings: {
|
||||
routing_strategy: "simple-shuffle",
|
||||
num_retries: 3,
|
||||
routing_groups: [{ group_name: "g1", models: ["gpt-4"], routing_strategy: "simple-shuffle" }],
|
||||
},
|
||||
});
|
||||
renderWithProviders(<RouterSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("strategy-select")).toBeInTheDocument();
|
||||
});
|
||||
expect(document.querySelector('input[name="routing_groups"]')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
const payload = vi.mocked(setCallbacksCall).mock.calls[0][1] as {
|
||||
router_settings: Record<string, unknown>;
|
||||
};
|
||||
expect(payload.router_settings).not.toHaveProperty("routing_groups");
|
||||
});
|
||||
|
||||
it("should surface an error and not claim success when saving fails (LIT-4057)", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(setCallbacksCall).mockRejectedValue(new Error("422 Unprocessable Entity"));
|
||||
renderWithProviders(<RouterSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("strategy-select")).toBeInTheDocument();
|
||||
});
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalled();
|
||||
});
|
||||
expect(NotificationsManager.success).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
|
|||
});
|
||||
}, [accessToken, userRole, userID]);
|
||||
|
||||
const handleSaveChanges = () => {
|
||||
const handleSaveChanges = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -91,9 +91,9 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
|
|||
|
||||
const numberKeys = new Set(["allowed_fails", "cooldown_time", "num_retries", "timeout", "retry_after"]);
|
||||
const jsonKeys = new Set(["model_group_alias"]);
|
||||
// retry_policy and model_group_retry_policy are owned exclusively by the
|
||||
// Model Retry Settings tab; this page must not read or write them.
|
||||
const tabOwnedKeys = new Set(["retry_policy", "model_group_retry_policy"]);
|
||||
// retry_policy and model_group_retry_policy are owned by the Model Retry Settings tab;
|
||||
// routing_groups is owned by the Routing Groups tab. This page must not read or write them.
|
||||
const tabOwnedKeys = new Set(["retry_policy", "model_group_retry_policy", "routing_groups"]);
|
||||
|
||||
const parseInputValue = (key: string, raw: string | undefined, fallback: unknown) => {
|
||||
if (raw === undefined) return fallback;
|
||||
|
|
@ -172,12 +172,11 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
|
|||
};
|
||||
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
NotificationsManager.success("router settings updated successfully");
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
}
|
||||
|
||||
NotificationsManager.success("router settings updated successfully");
|
||||
};
|
||||
|
||||
if (!accessToken) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue