Skip to content

feat(ui): add per-model rate limits to team edit/info views - #25144

Merged
ryan-crabbe-berri merged 2 commits into
litellm_ryan-apr-4from
litellm_feat-per-team-per-model-rate-limit-ui
Apr 4, 2026
Merged

feat(ui): add per-model rate limits to team edit/info views#25144
ryan-crabbe-berri merged 2 commits into
litellm_ryan-apr-4from
litellm_feat-per-team-per-model-rate-limit-ui

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a new Model-Specific Rate Limits form section to the team Settings tab, allowing admins to set per-model TPM/RPM limits on a team (alongside the existing team-level TPM/RPM).
  • Displays per-model limits in the Overview tab Rate Limits card and in the Settings tab view mode, so admins can see configured limits without entering edit mode.
  • Model picker is scoped to the team's currently-selected models, with wildcard unfurling and a fallback to userModels when the team uses all-proxy-models / all-team-models.

Screenshots

Screenshot 2026-04-04 at 12 38 55 PM Screenshot 2026-04-04 at 1 38 16 PM Screenshot 2026-04-04 at 1 38 29 PM

Test plan

  • Edit a team in the UI, click Add Model Limit, pick a model, set TPM=1000 / RPM=100, Save
  • Inspect the POST /team/update request body in DevTools — verify model_tpm_limit / model_rpm_limit are sent top-level
  • Verify persistence: psql -c "SELECT metadata->'model_tpm_limit' FROM \"LiteLLM_TeamTable\" WHERE team_id='<id>'"
  • Refresh the page, re-open edit mode — verify saved rows reappear
  • Overview tab Rate Limits card shows the per-model limits
  • Settings tab (view mode) shows the per-model limits
  • Delete a row, Save, refresh — verify removal persists
  • Duplicate-model validator blocks submission
  • Create a key under the team, hit /chat/completions exceeding RPM — receive 429; other models on same key still allowed

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).
@vercel

vercel Bot commented Apr 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 4, 2026 8:51pm

Request Review

@codspeed-hq

codspeed-hq Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing litellm_feat-per-team-per-model-rate-limit-ui (f0bbd41) with main (4c06e43)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds per-model TPM/RPM rate limits to the team Settings and Overview tabs in the UI. The feature adds a dynamic Form.List for model-rate-limit rows in edit mode, displays configured limits in both the Overview Rate Limits card and the Settings view tab, and correctly scopes the model picker to the team's selected models (with wildcard/all-proxy-models fallback to userModels).

The backend integration uses the existing _update_metadata_fields pipeline — the UI sends model_tpm_limit/model_rpm_limit as top-level fields in the POST /team/update body, and _update_metadata_field then moves them into the metadata JSON column, which is where the UI reads them back via info.metadata?.model_tpm_limit. This is consistent with how the backend stores and enforces these limits.

Key observations:

  • The data flow (write via top-level → store in metadata → display from metadata) is correct and consistent with the existing backend conventions.
  • The metadata textarea correctly excludes model_tpm_limit/model_rpm_limit via the destructure-rest pattern on line 861, preventing double-write or conflict with the separate top-level fields.
  • The previous threads identified three UX/validation concerns (silent data loss for all-blank rows, stale duplicate-model validity state across sibling rows, and the stale availableRateLimitModels when switching teams) — those remain open.
  • No new security or data-integrity regressions are introduced by this change.

Confidence Score: 4/5

Safe 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.

Important Files Changed

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)
Loading

Reviews (2): Last reviewed commit: "fix(ui): require TPM or RPM when adding ..." | Re-trigger Greptile

Comment on lines +481 to +497
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +1004 to +1015
{
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();
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines 195 to +206
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.
@ryan-crabbe-berri
ryan-crabbe-berri had a problem deploying to integration-redis-postgres April 4, 2026 20:50 — with GitHub Actions Failure
@ryan-crabbe-berri
ryan-crabbe-berri changed the base branch from main to litellm_ryan-apr-4 April 4, 2026 23:14
@ryan-crabbe-berri
ryan-crabbe-berri merged commit cc867f1 into litellm_ryan-apr-4 Apr 4, 2026
102 of 116 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_feat-per-team-per-model-rate-limit-ui branch April 4, 2026 23:14
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…er-model-rate-limit-ui

feat(ui): add per-model rate limits to team edit/info views
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant