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:
ryan-crabbe-berri 2026-06-30 11:28:51 -07:00
parent 0965a4d1f4
commit 9968499aab
3 changed files with 49 additions and 9 deletions

View file

@ -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">

View file

@ -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();
});
});

View file

@ -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) {