[Feature] UI - Teams: Allow Editing Router Settings After Team Creation#25398
[Feature] UI - Teams: Allow Editing Router Settings After Team Creation#25398yuneng-berri merged 11 commits intomainfrom
Conversation
Add RouterSettingsAccordion to the team edit form and a read-only display of router settings (routing strategy, retries, fallbacks, cooldown, timeout, tag filtering) in the Settings tab.
The RouterSettingsAccordion uses uncontrolled DOM inputs for numeric fields. The onChange effect only fires when formValue or fallbacks change, not when the user types into uncontrolled inputs. Using the ref's getValue() method at save time ensures we read fresh values from the DOM.
… old values When a user clears a numeric field in the RouterSettingsAccordion, the empty input should result in null, not fall through to the previous value from state.
The uncontrolled TabGroup reset to the first tab whenever the component re-rendered due to onChange state updates. Using controlled index/onIndexChange keeps the user on the current tab during fallback selection.
…tab reset" This reverts commit 9ff4bf66b03912f2c1ab893d2bdb31fba0ea9488.
When a fallback model was selected, the onChange effect fired, parent re-rendered with new value prop, and the useEffect re-initialized the groups with new IDs from fallbacksToGroups. This caused the active fallback group tab to reset to Group 1. Now internal updates always skip re-initialization regardless of value key changes.
…hange loop Remove the onChange/value feedback loop between TeamInfo and RouterSettingsAccordion. The parent passes initial value once and reads current values at save time via ref.getValue(). This eliminates the circular data flow that caused stale state and fallback tab resets.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds the ability to edit Confidence Score: 5/5Safe to merge — previous P1 concerns are addressed by the isMeaningfulValue + hadExistingSettings gate; the one remaining finding is a minor future-proofing suggestion. All previously raised blocking concerns have been resolved. The only remaining finding is P2: the valueKey stability issue in RouterSettingsAccordion, which is harmless in the current implementation due to unmount-on-save. RouterSettingsAccordion.tsx — the valueKey stability gap is worth tracking if the component is reused in a mounted-across-update context.
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx | Bug fix: empty DOM inputs now return null instead of falling through to stale state; minor: valueKey stability only tracks 3 of the ~8 router settings fields. |
| ui/litellm-dashboard/src/components/team/TeamInfo.tsx | Adds RouterSettingsAccordion to team edit form and read-only router settings summary; save logic correctly gates updates via isMeaningfulValue + hadExistingSettings. |
| ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx | Adds getRouterSettingsCall mock to prevent real network calls in tests; mock correctly returns { fields: [] } to satisfy the component's data contract. |
Sequence Diagram
sequenceDiagram
participant User
participant TeamInfo as TeamInfo (edit form)
participant RouterAccordion as RouterSettingsAccordion
participant DOM as DOM inputs
participant Backend as teamUpdateCall
User->>TeamInfo: Click Edit
TeamInfo->>RouterAccordion: mount(ref, value=info.router_settings)
RouterAccordion->>RouterAccordion: initialize state from value prop
RouterAccordion->>DOM: render inputs (Loadbalancing tab)
User->>TeamInfo: Click Save
TeamInfo->>RouterAccordion: routerSettingsRef.current.getValue()
RouterAccordion->>DOM: querySelector each input[name]
DOM-->>RouterAccordion: current DOM values (or null if cleared)
RouterAccordion-->>TeamInfo: { router_settings: { ... } }
TeamInfo->>TeamInfo: isMeaningfulValue check (hasNewValues)
TeamInfo->>TeamInfo: hadExistingSettings check (info.router_settings)
alt hasNewValues OR hadExistingSettings
TeamInfo->>Backend: teamUpdateCall(accessToken, { router_settings })
else neither
TeamInfo->>Backend: teamUpdateCall(accessToken, {})
end
Backend-->>TeamInfo: success
TeamInfo->>RouterAccordion: unmount (setIsEditing false)
Reviews (3): Last reviewed commit: "fix: address PR review - allow clearing ..." | Re-trigger Greptile
| if (currentRouterSettings?.router_settings) { | ||
| const hasValues = Object.values(currentRouterSettings.router_settings).some( | ||
| (value) => value !== null && value !== undefined && value !== "", | ||
| ); | ||
| if (hasValues) { | ||
| updateData.router_settings = currentRouterSettings.router_settings; | ||
| } | ||
| } |
There was a problem hiding this comment.
hasValues guard always passes, sending router_settings on every save
buildRouterSettings() always includes enable_tag_filtering: formValue.enableTagFiltering, which is false by default. Since false !== null && false !== undefined && false !== "" evaluates to true, hasValues is always truthy — even when the user never touched Router Settings. Every team save will include router_settings: { routing_strategy: null, allowed_fails: null, ..., enable_tag_filtering: false } in the update payload, which can silently overwrite existing backend state (e.g. a team with enable_tag_filtering: true set via the API would have it flipped to false on the next unrelated settings save).
| if (currentRouterSettings?.router_settings) { | |
| const hasValues = Object.values(currentRouterSettings.router_settings).some( | |
| (value) => value !== null && value !== undefined && value !== "", | |
| ); | |
| if (hasValues) { | |
| updateData.router_settings = currentRouterSettings.router_settings; | |
| } | |
| } | |
| const currentRouterSettings = routerSettingsRef.current?.getValue(); | |
| if (currentRouterSettings?.router_settings) { | |
| const hasValues = Object.values(currentRouterSettings.router_settings).some( | |
| (value) => value !== null && value !== undefined && value !== "" && value !== false, | |
| ); | |
| if (hasValues) { | |
| updateData.router_settings = currentRouterSettings.router_settings; | |
| } | |
| } |
| ref={routerSettingsRef} | ||
| accessToken={accessToken || ""} | ||
| value={info.router_settings ? { router_settings: info.router_settings } : undefined} | ||
| modelData={userModels.length > 0 ? { data: userModels.map((model) => ({ model_name: model })) } : undefined} |
There was a problem hiding this comment.
modelData prop passed but unused by the component
RouterSettingsAccordion declares modelData?: any in its props interface and destructures it, but never uses it — the component calls fetchAvailableModels(accessToken) internally to populate available models. Passing the prop here creates a misleading impression that it influences the fallback model list. The prop can be safely omitted.
…r settings enable_tag_filtering: false (the default) passed the hasValues guard, causing every team save to send router_settings even for teams that never had them configured. This could silently overwrite backend-set values. Now exclude false and empty arrays from the check.
| const currentRouterSettings = routerSettingsRef.current?.getValue(); | ||
| if (currentRouterSettings?.router_settings) { | ||
| const hasValues = Object.values(currentRouterSettings.router_settings).some( | ||
| (value) => | ||
| value !== null && | ||
| value !== undefined && | ||
| value !== "" && | ||
| value !== false && | ||
| !(Array.isArray(value) && value.length === 0), | ||
| ); | ||
| if (hasValues) { | ||
| updateData.router_settings = currentRouterSettings.router_settings; | ||
| } | ||
| } |
There was a problem hiding this comment.
hasValues guard silently drops clears of all router settings
Because the guard skips false and null values, a user who clears every router setting (e.g., changes routing_strategy from "latency-based-routing" to no selection, with enable_tag_filtering unchecked) gets hasValues = false — and router_settings is never sent in the update payload. The backend retains the old values, with no error shown to the user.
A concrete failure path: team has { routing_strategy: "latency-based-routing" }. User opens edit, clears the strategy dropdown, saves. buildRouterSettings() returns all-null/false, hasValues = false, update is skipped silently.
A simple fix is to always send router_settings when the team already had them:
| const currentRouterSettings = routerSettingsRef.current?.getValue(); | |
| if (currentRouterSettings?.router_settings) { | |
| const hasValues = Object.values(currentRouterSettings.router_settings).some( | |
| (value) => | |
| value !== null && | |
| value !== undefined && | |
| value !== "" && | |
| value !== false && | |
| !(Array.isArray(value) && value.length === 0), | |
| ); | |
| if (hasValues) { | |
| updateData.router_settings = currentRouterSettings.router_settings; | |
| } | |
| } | |
| const currentRouterSettings = routerSettingsRef.current?.getValue(); | |
| if (currentRouterSettings?.router_settings) { | |
| const existingRouterSettings = info.router_settings; | |
| const hasExistingSettings = existingRouterSettings && Object.values(existingRouterSettings).some( | |
| (value) => | |
| value !== null && | |
| value !== undefined && | |
| value !== "" && | |
| value !== false && | |
| !(Array.isArray(value) && value.length === 0), | |
| ); | |
| const hasValues = Object.values(currentRouterSettings.router_settings).some( | |
| (value) => | |
| value !== null && | |
| value !== undefined && | |
| value !== "" && | |
| value !== false && | |
| !(Array.isArray(value) && value.length === 0), | |
| ); | |
| // Send router_settings if there are new values OR if the user is clearing existing ones | |
| if (hasValues || hasExistingSettings) { | |
| updateData.router_settings = currentRouterSettings.router_settings; | |
| } | |
| } |
…d modelData prop - Send router_settings when clearing existing settings (so backend updates instead of silently retaining old values) - Remove unused modelData prop from RouterSettingsAccordion (it fetches models internally via fetchAvailableModels)
Relevant issues
Summary
Problem
Users can set router settings when creating a team, but cannot edit them afterward in the UI. The backend endpoints fully support
router_settings, so the gap was purely in the frontend.Fix
Reuse the existing
RouterSettingsAccordioncomponent (already used in the create flow) in the team Settings tab. The component is rendered inline with its Loadbalancing/Fallbacks tabs visible directly — no extra accordion wrapper.Also fixes two pre-existing bugs in
RouterSettingsAccordion:onChangeonly fired on strategy/fallback changes. Fixed by reading values viaref.getValue()at save time.null. Fixed by treating empty DOM inputs asnull.Changes
router_settingstoTeamDatainterface and wiredRouterSettingsAccordioninto the team edit form with a ref-based approach (pass initial value once, read at save time)RouterSettingsAccordionto returnnullfor empty inputs instead of stale state valuesgetRouterSettingsCallmock to TeamInfo testsTesting
Type
🆕 New Feature
🐛 Bug Fix
✅ Test