diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx index 3787b5954fc..54f11acf0e4 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx @@ -289,6 +289,38 @@ describe("AddAutoRouterTab", () => { expect(mockHandleAddAutoRouterSubmit).toHaveBeenCalledTimes(1); }); + // submitRecommendedRouter awaits a network round trip (the submit-time re-check) before it ever + // reaches the create call. If accessToken rotates while that await is pending, a closure-captured + // value would carry the token that was live at click time into a create call that fires after the + // replacement token is active. Read it through a ref updated every render instead. + it("uses the current access token for creation even if it rotates while the submit-time re-check is in flight", async () => { + const user = userEvent.setup(); + let resolveInitialRecheck: (models: ModelGroup[]) => void = () => undefined; + mockFetchAvailableModels + .mockResolvedValueOnce(ALL_FAMILY_MODELS) + .mockReturnValueOnce(new Promise((resolve) => (resolveInitialRecheck = resolve))) + .mockResolvedValueOnce(ALL_FAMILY_MODELS); + + const { rerender } = renderWithProviders( + , + ); + openTemplateDropdown(); + await waitFor(() => expect(isOptionDisabled(optionByLabel("Anthropic Family")!)).toBe(false)); + fireEvent.click(optionByLabel("Anthropic Family")!); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "rotated-token-router"); + await user.click(screen.getByRole("button", { name: /add auto router/i })); + await waitFor(() => expect(mockFetchAvailableModels).toHaveBeenCalledTimes(2)); + + // The token rotates while the re-check above is still in flight. + rerender(); + + resolveInitialRecheck(ALL_FAMILY_MODELS); + + await waitFor(() => expect(mockHandleAddAutoRouterSubmit).toHaveBeenCalled()); + expect(mockHandleAddAutoRouterSubmit.mock.calls.at(-1)?.[1]).toBe("fresh-token"); + }); + // The headline behavior: selecting a preset must pre-fill the tier config so the created // router carries the preset's models. Real tier validation runs here (getMissingTiersError is // not stubbed), so if selection stopped pre-filling, the empty tiers would either block the diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx index 071afecb8d0..3a78f19e8a1 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd"; import { TextInput } from "@tremor/react"; @@ -74,6 +74,13 @@ const AddAutoRouterTab: React.FC = ({ createScope = "unscoped-ok", }) => { const requiresTeamScope = createScope === "team-required"; + // submitRecommendedRouter awaits a network round trip before creating the router; reading + // accessToken through this ref instead of the closure keeps that call on whatever token is + // current when it actually fires, not whichever one was live when submit was clicked. + const accessTokenRef = useRef(accessToken); + useEffect(() => { + accessTokenRef.current = accessToken; + }, [accessToken]); const [form] = Form.useForm(); const [modelAccessGroups, setModelAccessGroups] = useState([]); @@ -300,7 +307,7 @@ const AddAutoRouterTab: React.FC = ({ model_access_group: form.getFieldValue("model_access_group"), }; - await handleAddAutoRouterSubmit(submitValues, accessToken, form, handleOk); + await handleAddAutoRouterSubmit(submitValues, accessTokenRef.current, form, handleOk); } catch (error) { console.error("Validation failed:", error); NotificationManager.fromBackend("Please fill in all required fields");