feat(ui): add per-model rate limits to team edit/info views - #25144
Conversation
Exposes the backend's existing model_tpm_limit/model_rpm_limit fields (which lived in team.metadata) through a new "Model-Specific Rate Limits" form section on the team Settings tab. Limits round-trip through the team-update API and render on the Overview card and Settings view. Model picker is scoped to the team's currently-selected models (unfurls wildcards, falls back to userModels for all-proxy-models / all-team-models).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds per-model TPM/RPM rate limits to the team Settings and Overview tabs in the UI. The feature adds a dynamic The backend integration uses the existing Key observations:
Confidence Score: 4/5Safe to merge with awareness that three previously-flagged UX/validation edge cases remain open; no regressions or security issues introduced. The data flow is architecturally correct and consistent with the existing backend conventions. The three previously flagged issues (silent data loss for all-blank rows, stale sibling-row duplicate validation, and stale availableRateLimitModels when switching teams) are still present and unresolved, warranting a 4 rather than 5. No new P0/P1 bugs were found in this review pass. ui/litellm-dashboard/src/components/team/TeamInfo.tsx — specifically the Form.List validation logic and the Form.useWatch placement relative to isEditing mode.
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/components/team/TeamInfo.tsx | Adds per-model rate limit form section (Form.List), read-only displays in Overview and Settings tabs, and correct metadata exclusion during save. The data flow is sound but a few UX/validation edge cases from previous threads remain unresolved. |
Sequence Diagram
sequenceDiagram
participant UI as TeamInfo.tsx (UI)
participant API as POST /team/update
participant META as _update_metadata_fields
participant DB as LiteLLM_TeamTable
UI->>UI: Build modelTpmLimit/modelRpmLimit dicts from Form.List rows
UI->>UI: Exclude model_tpm_limit/model_rpm_limit from raw metadata textarea
UI->>API: { model_tpm_limit: {…}, model_rpm_limit: {…}, metadata: {…rest} }
API->>META: _update_metadata_fields(updated_kv)
META->>META: Pop model_tpm_limit from updated_kv
META->>META: merged_metadata["model_tpm_limit"] = value
META->>DB: UPDATE SET metadata = merged_metadata
DB-->>UI: GET /team/info → { metadata: { model_tpm_limit: {…} } }
UI->>UI: Display from info.metadata?.model_tpm_limit (Overview & Settings tabs)
UI->>UI: Init Form.List from info.metadata?.model_tpm_limit (edit mode)
Reviews (2): Last reviewed commit: "fix(ui): require TPM or RPM when adding ..." | Re-trigger Greptile
| const modelTpmLimit: Record<string, number> = {}; | ||
| const modelRpmLimit: Record<string, number> = {}; | ||
| for (const entry of (values.modelLimits ?? []) as { model?: string; tpm?: number; rpm?: number }[]) { | ||
| if (entry?.model) { | ||
| if (entry.tpm != null) modelTpmLimit[entry.model] = entry.tpm; | ||
| if (entry.rpm != null) modelRpmLimit[entry.model] = entry.rpm; | ||
| } | ||
| } | ||
|
|
||
| const updateData: any = { | ||
| team_id: teamId, | ||
| team_alias: values.team_alias, | ||
| models: values.models, | ||
| tpm_limit: sanitizeNumeric(values.tpm_limit), | ||
| rpm_limit: sanitizeNumeric(values.rpm_limit), | ||
| model_tpm_limit: modelTpmLimit, | ||
| model_rpm_limit: modelRpmLimit, |
There was a problem hiding this comment.
Silent data loss when both TPM and RPM are blank
An entry where the user selects a model but leaves both tpm and rpm blank will be silently excluded from model_tpm_limit and model_rpm_limit because both if (entry.tpm != null) checks fail. The form row passes validation (neither field is required), so the user sees no error, but the row is not persisted. On the next page refresh the row simply disappears with no indication that anything went wrong.
Either require at least one of tpm/rpm to be filled in per row, or skip blank rows at the form level with a visible warning.
| { | ||
| validator: (_, value) => { | ||
| if (!value) return Promise.resolve(); | ||
| const all = form.getFieldValue("modelLimits") ?? []; | ||
| const dupes = all.filter( | ||
| (entry: { model?: string }) => entry?.model === value, | ||
| ); | ||
| if (dupes.length > 1) { | ||
| return Promise.reject(new Error("Duplicate model")); | ||
| } | ||
| return Promise.resolve(); | ||
| }, |
There was a problem hiding this comment.
Duplicate-model validation doesn't re-validate sibling rows
The custom validator reads form.getFieldValue("modelLimits") and rejects if dupes.length > 1. However, Ant Design only re-runs the validator for the field that just changed — not for the other row that became a duplicate. This means form.submit() can succeed even when a sibling row still holds a stale "duplicate" cached validity state. A Form.List-level rules validator is needed to reliably block duplicates at submit time.
| return org?.members?.some((m: any) => m.user_id === userId && m.user_role === "org_admin") ?? false; | ||
| }, [teamData, userOrganizations, userId]); | ||
|
|
||
| // Models currently selected in the team edit form, used to scope the per-model | ||
| // rate limit dropdown to models this team actually has access to. | ||
| const selectedModelsInForm = Form.useWatch("models", form) as string[] | undefined; | ||
| const availableRateLimitModels = useMemo(() => { | ||
| const selected = selectedModelsInForm ?? teamData?.team_info?.models ?? []; | ||
| if (selected.includes("all-proxy-models") || selected.includes("all-team-models")) { | ||
| return userModels; | ||
| } | ||
| return unfurlWildcardModelsInList(selected, userModels); |
There was a problem hiding this comment.
availableRateLimitModels computed outside edit mode
Form.useWatch("models", form) is called at the top of the component — outside the isEditing guard. When the user switches to a different team while the edit form is already mounted, the stale form instance may briefly show the wrong model set in the rate-limit dropdown. Consider keying the Form on teamId to force a clean reset.
Previously, a row with a model selected but both limits blank was silently dropped on save (neither model_tpm_limit nor model_rpm_limit got the key), so the row disappeared on reload with no feedback. Now the TPM field's validator blocks submission with "Set at least one of TPM or RPM" when a row has a model but neither limit filled.
cc867f1
into
litellm_ryan-apr-4
…er-model-rate-limit-ui feat(ui): add per-model rate limits to team edit/info views
Summary
all-proxy-models/all-team-models.Screenshots
Test plan
POST /team/updaterequest body in DevTools — verifymodel_tpm_limit/model_rpm_limitare sent top-levelpsql -c "SELECT metadata->'model_tpm_limit' FROM \"LiteLLM_TeamTable\" WHERE team_id='<id>'"/chat/completionsexceeding RPM — receive 429; other models on same key still allowed