Commit graph

4776 commits

Author SHA1 Message Date
ryan-crabbe-berri
c7cbebb84b fix(ui): right-align money and count columns across tables
Make numeric the one alignment token for the three shared table
wrappers (DataTable, MemberTable, SimpleTable) via NUMERIC_CELL_CLASS,
and flag every money, cost, and bare-count column that was still
left-aligned. Raw ui/table usages that render spend or budgets get the
same class on their header and cell.
2026-08-21 13:51:43 -07:00
tin-berri
9821b451e3
fix(ui): drive auto-router usage from the shared cost-optimization time picker (#37871)
* fix(ui): drive auto-router usage from the shared cost-optimization time picker

* fix(ui): extend a live-ending benchmarks range to the current UTC day
2026-08-21 13:49:08 -07:00
Yassin Kortam
7da34e8aed
fix(proxy): make per-model budgets track spend, enforce, and report the same counter (#37736)
Per-model budgets were three separate things pretending to be one. The
enforcement check, the post-call increment and the info endpoints each derived
their own cache key, so a budget could refuse traffic at 429 while /key/info
reported zero usage, and a Bedrock model id never matched a budget keyed on the
bare family name. /user/new echoed a model_max_budget back and stored an empty
dict, and nothing enforced a user-scoped per-model budget at all.

One owner now builds the counter key from the configured budget model, and
enforcement, the increment and the info endpoints all read it. Bedrock ids
resolve through the model-cost map. Auth carries the user's budget onto the
token on every branch that reaches the spend hook, including JWT and
auto-registration. Native passthrough attaches the three budget metadata keys
its StandardLoggingUserAPIKeyMetadata does not carry, so /anthropic/... and
/bedrock/... traffic is counted and capped like /v1/chat/completions.

The dashboard gains the per-model budget editor it never had, on the key create,
key edit and internal-user edit forms. It is read-only without an enterprise
license, matching the write gate the proxy already enforces, and an untouched
budget is left out of an update so an unrelated edit cannot trip that gate.

The editor hydrates from either BudgetConfig spelling, since model_max_budget is
a plain dict that the proxy stores exactly as the client sent it, and it carries
through the fields it does not model. Without both, editing one model would drop
another model row entirely and silently discard its tpm_limit and rpm_limit.

/user/info refreshes its local copy of the user field by field after a save, so
model_max_budget joins that list. Left out, a saved cap read back as the old one
when the form was reopened, and clearing the row to recover would then wipe the
value that had actually persisted.

A zero-dollar cap is the strictest limit expressible, not the absence of one,
so it is enforced rather than skipped on falsiness, spend exactly at the cap is
refused the way every sibling budget check already refuses it, and a counter
that was never written reads as zero spend rather than as unknown. The usage
endpoints read every counter in one batched lookup, so a large model_max_budget
cannot fan out into one concurrent cache call per configured model.

Every auth path honours the same zero-cost skip flag, so none of them can refuse
a free request that another serves. The custom-auth helper gains the flag it
never had, which also changes its pre-existing key and end-user checks.

The compaction summary gate checks the user scope alongside the key and end-user
ones. This file propagates all three budgets into the summary subrequest, so
enforcing only two let compaction increment a counter it could not be refused by.

Custom auth attaches the user's budget to the token unconditionally, since the
post-call spend hook reads it there: gating the attach on the same condition as
enforcement left the counter uncharged whenever the request was not itself
enforceable. An entry that will not validate is skipped rather than raised on,
so one malformed scope cannot abort every other scope's increment or turn a
config typo into a 500.

The edit forms re-seed the budget editor when a different key or user is loaded.
Its rows are seeded once and cannot re-read their own value prop, so without
this a save wrote the previously loaded record's budgets onto the current one.

Only the built-in provider pass-through routes carry the budget metadata.
get_model_from_request deliberately resolves no model for a user-defined
pass-through, since its body is forwarded verbatim and names an upstream model,
so attaching there would charge a counter nothing on that route can refuse.
2026-08-21 09:47:52 -07:00
Mateo Wang
66a6a09706
Merge pull request #37743 from BerriAI/litellm_cognition_provider_identity
feat(cognition): give Cognition its own provider identity
2026-08-20 18:38:23 -07:00
devin-ai-integration[bot]
c74e9e75f9
feat(ui): support project input and output TPM limits (#37676)
The Model-Specific Limits rows now carry Input TPM and Output TPM, and a
limit the operator removes is sent as an explicitly empty map so
/project/update actually drops it instead of leaving the stored quota
enforced behind a UI that shows it gone.

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 17:43:52 -07:00
Mateo Wang
02e67cd715
Merge pull request #35181 from BerriAI/litellm_block_unpriced_models
feat(proxy): add admin toggle to block requests for models without pricing
2026-08-20 17:32:28 -07:00
mateo-berri
e00301703f feat(cognition): give Cognition its own provider identity
Cognition serves an OpenAI-compatible /v1/chat/completions endpoint, so it has been onboarded as
custom_llm_provider: openai. That books its traffic as OpenAI, which means OpenAI-specific cost
discounts and provider-level reporting apply to it.

Registers cognition through the JSON provider registry: a providers.json entry with
COGNITION_API_KEY and COGNITION_API_BASE, LlmProviders.COGNITION, the constants.py provider lists,
cost map entries for swe-1.6 and swe-1.7, the provider endpoints matrix, the dashboard provider
fields, and tests. JSON providers can now also be resolved from their base url alone, so an
api_base pointing at a known provider no longer falls through to an unresolved provider.
2026-08-20 17:16:53 -07:00
yuneng-jiang
cb89c7aa8f
fix(ui): stop the Add Model mapping table from looping the page (#37741)
Entering a custom model name on the Add Model form crashed the whole page
to "This page couldn't load" (React error #185, maximum update depth
exceeded), taking the provider credential fields down with it, so the
model could never be created.

ConditionalPublicModelName kept a `tableKey` counter and bumped it from
an effect on every run to force the mappings table to remount. That was
harmless under antd, whose useWatch handed back the stored array. React
Hook Form's useWatch returns a fresh array each render, so the effect's
dependency changed every render, the effect bumped state again, and the
render loop never settled.

The table is driven by its `data` prop, so the remount counter buys
nothing: drop it, key the effects off the selection contents rather than
the array identity, and write model_mappings only when they actually
change. The two `react-hooks/set-state-in-effect` suppressions on this
file, which were recording exactly this bug, go with it.
2026-08-20 16:42:49 -07:00
devin-ai-integration[bot]
22e8b45c68
feat(proxy): add maximum_health_check_retention_period to bound the health-check table (#37681)
* feat(proxy): add health check retention cleanup

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): drop redundant health-check assertion

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): share cleanup budget across retention groups

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(proxy): clarify cleanup group deadlines

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 16:01:16 -07:00
mateo-berri
3c44f8d926 fix(ui): surface toggle failures on the block-unpriced-models setting
The hook swallowed errors into the console, so an admin flipping the switch without
STORE_MODEL_IN_DB saw nothing happen and got no reason why. Adds the missing hook tests.
2026-08-20 15:47:13 -07:00
tin-berri
cb4eb82249
feat(ui): per-model reasoning effort in the complexity tier editor (#37673)
* feat(ui): per-model reasoning effort in the complexity tier editor

* feat(ui): gate the effort control on model group reasoning support
2026-08-20 15:10:35 -07:00
tin-berri
2dcd453860
feat(shadow_eval)!: gate the per-key budget on dollar spend instead of turns (#37555) 2026-08-20 14:55:21 -07:00
tin-berri
60e03bedcf
fix(ui): surface the paginated fallback on Cost Optimization (#37659)
* fix(ui): surface the paginated fallback on Cost Optimization

The page streamed its fallback silently: useDailyActivityRange dropped
the hook's progress and cancel fields and CacheLeakageCard only showed
a loading state while empty. Extract the Usage page's fetch banner into
a shared PaginationStatusAlerts component, render it above the tabs,
and note on the cache leakage tables when pages are still arriving.

* fix(ui): gate the cache leakage streaming note on isFetchingMore only

loading also covers a fresh aggregated request over the previous
range's rows, where pagination copy mislabels stale data. Drop the
redundant component comment flagged against the repo comment policy.
2026-08-20 14:55:00 -07:00
mateo-berri
5301872093 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_block_unpriced_models 2026-08-20 14:42:03 -07:00
github-actions[bot]
6bd21fad70 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_block_unpriced_models
# Conflicts:
#	litellm/proxy/auth/auth_checks.py
#	tests/test_litellm/proxy/auth/test_auth_checks.py
#	tests/test_litellm/proxy/management_endpoints/test_cost_tracking_settings.py
#	ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/cost_tracking_settings.tsx
#	ui/litellm-dashboard/src/lib/http/schema.d.ts
2026-08-20 13:43:12 -07:00
ryan-crabbe-berri
787edb123f
refactor(ui): mark dark as beta in the theme menu, not the toolbar (#37680)
The Experimental badge sat in the top bar next to the icon, which read as if the
whole theme control were experimental and cost toolbar width for a caveat that
only applies once. It moves into the menu as a Beta tag on the Dark entry, where
it labels exactly the choice it is about and is visible before the choice is made
rather than only after.
2026-08-20 13:29:43 -07:00
ryan-crabbe-berri
933e28d900
feat(ui): add a light/dark/system theme toggle to the top bar (#37669)
* feat(ui): add a light/dark/system theme toggle

The dashboard already carried a full `.dark` palette, dark-aware surfaces and a
dark logo variant, but nothing ever put the `dark` class on the document, so
none of it could be reached. next-themes now owns that class: it reads the
stored choice, falls back to the OS preference, and stamps the class from an
inline script before first paint so there is no light flash on load.

The toggle is a three-way System / Light / Dark control in the account menu,
in both the sidebar menu and the older navbar dropdown, so it is reachable from
the gateway dashboard, chat and the model hub alike.

useIsDarkMode watched the root element with a MutationObserver purely to answer
a question next-themes now answers directly, so it goes, and useSyntaxTheme
reads resolvedTheme instead. The toaster follows the resolved theme too.

* feat(ui): move the theme control to the top bar and default to light

The toggle now lives in the header toolbar of both shells, the gateway
dashboard's DashboardHeader and the older full-width Navbar, where it replaces
the placeholder comment that had been holding its spot. It reads better there
as a single icon button with a System / Light / Dark menu than as a segmented
row buried in the account popover, so the account menus lose their theme row.

Dark mode is still being rolled out, so an install that has never touched the
control now stays light instead of following the OS. System is still a choice,
just no longer the default. While dark is active the toolbar carries a small
Experimental badge, so nobody mistakes an unstyled surface for a bug.

* fix(ui): serve the dark logo in the legacy navbar too

The sidebar already paired its logo with a dark variant, but the full-width
navbar kept a single light-only image. That did not matter while dark mode was
unreachable; now that the toggle sits in that shell's own top bar, the white
JPEG slab lands on a dark bar. It gets the same two-image swap the sidebar uses,
and a test that pins the pairing so the two shells cannot drift apart again.
2026-08-20 12:58:29 -07:00
yuneng-jiang
122675c309
feat(ui): let admins supply a dark-mode variant of their custom logo (#37662)
* feat(ui): let admins supply a dark-mode variant of their custom logo

A deployment branded through UI_LOGO_PATH got its light artwork on the
dark sidebar, and there was nothing an admin could set to change that.

Adds UI_LOGO_PATH_DARK, exposed as the logo_url_dark theme setting and a
second field on the UI theme page. /get_image now walks an ordered list
of candidates for the requested theme and serves the first usable one:
the dark logo, then the light logo, then the bundled default.

Falling through rather than failing is the point. An admin who never
sets a dark logo keeps their own light one instead of reverting to
LiteLLM's, and a dark logo that goes missing later degrades to their
light logo rather than dropping their branding entirely.

* fix(ui): recover from a dark logo the browser cannot load

A dark logo given as an http(s) URL is loaded by the browser straight
from the sidebar, so it never passes through the proxy's fallback chain.
A URL that 404s left a broken image where the admin's light logo should
have been, while the same logo given as a local path fell back cleanly.

The sidebar now remembers the dark URL that failed and drops to the light
logo, matching how the proxy resolves an unusable dark logo and how the
provider Logo component already handles a broken image.
2026-08-20 12:48:55 -07:00
tin-berri
135f234e89
feat(proxy): add POST /auto_router/validate_complexity_router_config to dry-run the complexity-router write gate (#37409)
* feat(proxy): add POST /auto_router/validate_config to dry-run the complexity-router write gate

* refactor(proxy): scope the validation endpoint name to the complexity router

* fix(proxy): give the complexity-router validate route the same audience as /model/new

* feat(proxy): gate auto-router dry runs like the write they rehearse

* chore(proxy): dedupe the validate route's self_managed_routes entry

* test(proxy): fold the dry-run route reachability check into the model-new audience parity test

* fix(proxy): scope test_routing's configured check to models the caller can use

* chore(ui): regenerate schema.d.ts for the scoped configured-check description
2026-08-20 19:48:32 +00:00
tin-berri
79cac36564
fix(ui): keep keyword tier rules that target operator-defined tiers when hydrating the edit modal (#37413) 2026-08-20 12:34:38 -07:00
yuneng-jiang
edbb3429a3
feat(ui): serve a dark-mode variant of the LiteLLM logo (#37656)
The bundled logo is a JPEG, so it carries no alpha and its white
background renders as a bright slab against a dark sidebar. Making it
transparent alone would not be enough either: the wordmark is near-black
and would disappear on dark.

Adds logo_dark.png, derived from the light logo. The sky-blue disc and
train are kept as they are behind a circular alpha mask, and the
wordmark's antialiasing is un-flattened from white into straight alpha
and repainted in the dark theme's own foreground colour. Both files are
1000x257, so swapping between them cannot shift the sidebar header.

/get_image gains a theme query param. The default response is byte for
byte what it was, and a logo configured through UI_LOGO_PATH is served
unchanged in both themes, since custom logos have no dark variant yet.
2026-08-20 11:26:10 -07:00
devin-ai-integration[bot]
282bcdadcc
feat(complexity_router): add business classification rubric preset (#37534)
* feat(complexity_router): add business classification rubric preset

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(ui): regenerate api schema for business rubric

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(ui): suppress preexisting antd import violations in touched files

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: tin <tin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 11:20:53 -07:00
tin-berri
c164944d40
fix(ui): draw one Per Day savings bar per date on Cost Optimization (#37643)
* fix(ui): draw one Per Day savings bar per date on Cost Optimization

The page paged /user/daily/activity over raw rows, so a date spanning
pages arrived N times with partial metrics and rendered as N thin bars.
Switch to the single-shot aggregated endpoint, thread
include_current_utc_day through it to keep the live-end extension from
PR #36051, and merge the paginated fallback by date.

* fix(ui): keep aggregated call at four params and mock it in view tests

Trailing userId and includeCurrentUtcDay ride a named rest tuple so the
eslint max-params baseline stays at 23, and the CostOptimizationView
suites mock the new networking export their render now reaches.
2026-08-20 11:14:04 -07:00
yuneng-jiang
7ac95b1cee
fix(ui): make hardcoded palette surfaces theme-aware (#37650)
* fix(ui): make hardcoded palette surfaces theme-aware

Twenty-one dashboard files painted fills from the raw Tailwind palette with no
dark counterpart, so in dark mode they rendered as near-white islands carrying
dark text: unreadable. The route sweep caught them on teams, access-groups,
policies, users, skills, guardrails-monitor, logs, compliance, playground,
fallbacks and the AI hub.

Where the hue already had a semantic token, the surface moves onto it. Every one
of these lines had a token on its border and a palette class on its fill, so
this finishes a migration that had stalled halfway: bg-blue-50 next to
border-info/20 becomes bg-info, bg-gray-50 becomes bg-muted, DocLink's bg-white
becomes bg-card, and the Alert error variant drops text-red-800 and text-red-600
for the destructive token its sibling variants already use.

Purple, violet and indigo have no token in the system, which is exactly why the
maps in PluginTableColumns, GuardrailsOverview, AccessGroupsTableColumns and
teamTableColumns had migrated every other entry and left those behind. Rather
than mint a brand token here, they take the dark palette step, matching what
TeamGuardrailsTab, add_agent_form, MCPToolsetsTab and mcp_connect already do.
Gradient stops get the same treatment since bg-linear stops have no token form.

Light mode is unchanged apart from the four surfaces that moved onto a token,
and those stay inside the same colour family.

The #1e1e1e code slabs in guardrail_info and CustomCodeModal are deliberately
left alone: they are intentionally dark editors in both themes, and their
gray-200 text stays legible either way.

* fix(ui): give dark surfaces a readable foreground step

The dark fills added for the purple and indigo surfaces left three nested
foregrounds on their original light-palette step, so the text and icon sitting
on those new fills dropped below readable contrast in dark mode.

text-purple-800 on purple-950 measured 1.72:1, text-indigo-600 on indigo-950
2.54:1, and text-purple-600 on the blue-950 gradient stop 2.73:1. Each now
takes the purple-300 / indigo-300 step this PR already uses elsewhere, which
lands them at 8.48:1, 8.02:1 and 8.31:1.

The pricing calculator renders the same cost expression twice, so both copies
move together rather than leaving one half-migrated.

* fix(ui): keep the guardrail chip remove button visible on hover

The chip itself moved to the dark indigo fill, but its remove button still
darkened to indigo-900 on hover, which against indigo-950 measures 1.40:1 and
makes the X vanish under the cursor in dark mode.

Dark mode now brightens to indigo-100 on hover instead, mirroring the light
theme where hover darkens away from the resting colour.
2026-08-20 18:03:15 +00:00
yuneng-jiang
0e7e640062
fix(ui): move the policy flow builder onto theme tokens (#37654)
* fix(ui): move the policy flow builder onto theme tokens

The flow builder carried its own private palette: 126 raw literals across a
1644-line file, hardcoded into React inline style objects and SVG presentation
attributes. Inline styles beat every class, so the whole page, its version
sidebar, its step cards and its test panel stayed light no matter what the
theme said.

Each literal now resolves through the token it was already imitating. The greys
map onto card, muted, border, muted-foreground and foreground; the indigo and
blue accents onto info; the pass, fail and API-failure accents onto success,
destructive and warning; and the pale status washes become a color-mix of the
same token so they track it in both themes. Six icons carried their colour as
an SVG presentation attribute, where custom properties do not substitute, so
those switch to currentColor with the token set alongside.

Light mode is not byte-identical, and that is the point: the file stops keeping
a second palette. Of the mappings, card, muted and border land on the exact same
rgb they had, covering most of the file. The rest snap to the dashboard's
canonical shade, which mostly means slightly darker text and deeper status
colours: the gray-400 labels pick up real contrast, the soft red on the fail
icon becomes the destructive red every other failure indicator uses, and the
indigo accent becomes the blue that info resolves to.

Verified in a browser on both themes. In dark mode nothing on the page paints a
light background any more; the six that still do are shadcn's inverted primary
buttons and badges, which are meant to.

* fix(ui): token the flow builder test textarea fill

The quick-chat textarea is the one bare form control left in the file, so the
@tailwindcss/forms base layer still paints it `background-color: #fff`. The
inline style overrode the plugin's border but not its fill, which left a white
box inside the now-dark test panel, and its text inherits the near-white
foreground, so the typed message was invisible in dark mode.

Pin both halves of the pair on the element the plugin styles: the card token it
sits on, and the foreground token it was already inheriting.
2026-08-20 10:59:28 -07:00
yuneng-jiang
5cd6347c2c
fix(ui): make inline styles and code blocks follow the theme (#37651)
* fix(ui): make inline styles and code blocks follow the theme

Two families of colour that a stylesheet never gets to see, so dark mode could
not reach them.

The log details drawer paints most of its chrome through React inline style
objects holding raw hex: #f0f0f0 borders, #fafafa panels, #262626 body text,
the antd-era role accents on message cards, and a green/red guardrail summary
pill. Inline styles win over any class, so the drawer stayed light on a dark
page. Every one of those literals becomes the var(--color-*) it was already
imitating, which costs nothing in light mode and now tracks the theme. The
guardrail pill keeps its layout inline and moves its three colours onto the
success and destructive tokens the rest of the dashboard uses.

The eleven code blocks pass a prism stylesheet as a prop, so the theme has to be
picked in JavaScript. There is no dark-mode toggle in the app yet, only the
`dark` class the design system keys off, so useIsDarkMode subscribes to that
class through useSyncExternalStore and useSyntaxTheme swaps in oneDark when it
is set. Each call site keeps the light stylesheet it already had, including the
two that were relying on the prism default and now name it, so light mode is
unchanged everywhere.

Six of those call sites were casting the stylesheet to `any` or re-declaring its
type to get past the prop signature; the hook returns the right type, so the
casts are gone.

* fix(ui): let the markdown code renderer keep its own syntax theme

The three ReactMarkdown code renderers spread the remaining code element
props after style, so the incoming style attribute widened the prop type
and next build's type check rejected the hook's return value. The old
`coy as any` cast hid the same conflict. Spreading first lets the
explicit props win, which is what every one of these call sites meant.

* test(ui): cover the dark-mode hooks that pick a syntax stylesheet

useIsDarkMode carries the only real logic in this change: an external
store over the root element's class list. Cover the three things that can
regress, the class already being present at mount, the class being
toggled later, and the observer being disconnected on unmount, then cover
useSyntaxTheme handing back the caller's own stylesheet in light mode and
oneDark in dark. The assertions are on which stylesheet object comes
back, by identity, not on any colour it holds.

* refactor(ui): drop the last stylesheet cast in the chat code renderer

This was the one markdown code renderer still spreading the code element
props over its style, so an incoming style attribute would have won over
the theme, and the cast on the spread was what kept that compiling.
Spreading first lets the theme win and the cast go.
2026-08-20 10:59:23 -07:00
yuneng-jiang
c794dcb91d
fix(ui): give status colours a readable foreground and drop the muted 70% step (#37649)
* fix(ui): give status colours a readable foreground and drop the muted 70% step

The four status tokens are lightened for dark mode, which is correct when they are used as text
and wrong for the 27 places that use them as a background under `text-white`. Every one of those
passes in light and fails in dark: success 1.78:1, warning 1.72:1, info 2.64:1, destructive
2.89:1. The cause is not 27 authoring mistakes, it is that no `--success-foreground` and no
sibling ever existed, so `text-white` was the only thing available to write. Adding the four
companions and registering them in `@theme` makes the correct pairing expressible, and the call
sites then read `text-success-foreground` instead of a hardcoded colour. Dark lands at 9.98, 10.31,
6.72 and 6.15.

Light is deliberately pure white rather than the near-white the other `-foreground` tokens use, so
the four ratios stay at exactly the 4.95, 5.03, 5.25 and 4.77 they are today instead of drifting
down to 4.73, 4.81, 5.02 and 4.56.

Separately `text-muted-foreground/70` measures 2.75:1 on a light page and 4.31:1 on a dark one,
so the same 183 occurrences fail AA in light and sit under it in dark. Dropping the opacity step
takes them to 4.84:1 and 7.34:1. The identical step on the placeholder base rule goes with them,
which is what put every input's placeholder at 2.75:1 in light.

Residual, not addressed here: `text-muted-foreground` over `bg-muted` reaches 4.39:1 in light,
still short of 4.5. Closing that needs `--muted-foreground` itself to move, which changes every
secondary label in the product and is a design call rather than a defect fix.

* fix(ui): finish the status-foreground swap and repoint no-op muted hovers

Four sites still forced text-white on a status fill because the class sat on
a child element rather than on the filled container, so the earlier sweep did
not reach them. The compliance quick-test bubble was worse: it paired bg-info
with text-success-foreground and its paragraph kept text-white on top, so the
dark-theme contrast the PR set out to fix was still reachable there

Dropping the /70 step also turned 21 existing "text-muted-foreground/70
hover:text-muted-foreground" pairs into hovers that change nothing, which
local/no-noop-hover-variant flags as an error. The affordance was "brighten on
hover", so these now hover to text-foreground, matching the 74 places that
already spell it that way

The remaining churn is prettier reflowing the handful of lines whose length
changed, since the token names are longer than text-white

* fix(ui): let the approve/reject confirm button pick the token its fill uses

Both submission review dialogs put text-success-foreground on the shared
button class while the fill below it swings between bg-success for Approve and
bg-destructive for Reject, so Reject drew a success token over a destructive
fill. The two tokens resolve to the same value today, so nothing looks wrong,
but the pairing only holds by coincidence and would break the moment either
token moves. Moving the token into the branch makes it track the fill

* fix(ui): drop the last 70% placeholders, still live on the legacy utility

Four inputs spell their placeholder colour with Tailwind's older
placeholder-<colour> utility rather than placeholder:text-<colour>, so the
sweep that dropped the 70% step passed over them. Tailwind 4.3 still emits
that utility, and utilities sit after base in the layer order, so those four
kept overriding the new input::placeholder rule and kept rendering at 70% in
dark mode, which is the contrast failure this PR set out to close

They now spell it the same way as the three placeholders the PR already
converted, which both removes the step and settles on one spelling
2026-08-20 10:58:32 -07:00
yuneng-jiang
e12833e6b4
fix(ui): make dark-mode form controls visible (#37648)
* fix(ui): make dark-mode form controls visible

Two dark-mode defects left form controls without any visual boundary or fill.

`--input` and `--border` share one value in `.dark`, oklch(0.309), which resolves to
rgb(48,48,48). Against `--background` (33) that is a 15-step stroke, and against `--popover` (42)
it collapses to 6 steps out of 255, so a control inside any dialog is effectively undrawn. The
controls also use `bg-transparent`, so there is no fill cue either and only the placeholder text
renders. Measured 1.09:1 against the dialog surface where WCAG 1.4.11 asks for 3.0:1 on the
boundary of a user interface component. Splitting `--input` off at oklch(0.56) restores 3.07:1
without touching `--border`, which stays where it is because it draws decorative separators rather
than control boundaries. 91 controls across 19 routes were measured at the collapsed value, every
one with an identical stroke and surface, so a single token covers all of them.

Separately, `@tailwindcss/forms` paints a white fill on every bare control. The block above
already neutralises that for `combobox-chip-input`, but its audit covered `components/ui` only,
and hand-rolled controls elsewhere still render white on a dark page: typed text lands at 1.11:1
and native selects at 2.19:1 on `/model-hub-table`, `/playground`, `/guardrails`, `/mcp-servers`
and `/models-and-endpoints`. Tracking `--background` fixes those at 14.51:1 and 7.34:1.

Light mode is unchanged by both. The token edit is scoped to `.dark`, and `--background` in
`:root` is the same white the plugin was already painting, verified control-by-control on a dev
server: backgrounds stay rgb(255,255,255) and ratios stay 20.13:1 and 4.84:1.

* fix(ui): keep the combobox chip input transparent under the bare-control fill

The new base rule matched at (0,2,1) while the combobox chip-input override
sits at (0,1,0), so ComboboxChipsInput lost its transparent background and
painted an opaque page-colored rectangle inside the chips container, which
carries its own bg-transparent / dark:bg-input/30 fill.

Folding the exclusions into one :not() list adds the chip input and drops the
selector to (0,1,1). Every @tailwindcss/forms base selector is wrapped in
:where(), so it lands at (0,0,1); (0,1,1) still outweighs it and bare inputs,
textareas and selects keep the fill this PR gives them.
2026-08-20 10:58:27 -07:00
tin-berri
d2d158f271
feat(ui): multi-key shadow eval picker and per-key breakdown (#37389)
Stacked on the multi-key shadow eval backend. The key picker becomes a
paginated multi-select with chips, built on the base-ui combobox chips
primitives, with the pagination and debounced-search logic extracted into a
shared usePaginatedCombobox hook that PaginatedSearchSelect now also uses.
The detail view gains a per key table showing each key's own status, judged
turns against its budget, and win rates from the by_key slice, and the job
headline pluralises to "N keys" for multi-key jobs
2026-08-20 17:43:27 +00:00
yuneng-jiang
5290150a05
fix(ui): keep semantic button colours on hover after the no-op hover cleanup (#37580)
PR #37579 read `text-X hover:text-X` on a shadcn Button as dead weight and
removed the hover half. On the ghost and outline variants it was not dead: both
carry their own `hover:text-foreground`, and the duplicate in the className was
the thing displacing it through tailwind-merge. Dropping it handed the hover
back to the variant, so the Remove button in a team's logging settings, the
chat storage banner's dismiss control, and the collapsed enterprise-usage rail
all lose their colour the moment you point at them.

Each of the three now carries a distinct hover value, following the alpha-step
idiom the rest of that migration used, which restores the colour and keeps
`local/no-noop-hover-variant` satisfied.

Every other hover utility that PR dropped sits on a plain element or a variant
with no competing `hover:text-`, so those stay as they are.
2026-08-19 23:13:08 -07:00
yuneng-jiang
93c1461074
fix(ui): restore hover feedback and dark-mode variants lost in the token migration (#37579)
* fix(ui): restore hover feedback and dark-mode variants lost in the token migration

PR #37576 mapped hardcoded Tailwind palette classes onto semantic tokens. Two-tone
hover pairs collapsed onto a single token, so 116 hover utilities across 49 files
became identical to their base class and produced no visible feedback, and in seven
files a dark: variant was dropped while its hardcoded light partner survived, leaving
those elements stuck light in dark mode.

Hover states now follow the alpha-step idiom the shadcn primitives already use
(hover:bg-primary/80, hover:bg-success/20): a duplicated hover:text-X or hover:bg-X
becomes /80, hover:border-border becomes hover:border-ring, and a duplicate is
dropped where another hover utility on the element already carries the change. One
transition-colors that no longer animated anything is removed.

For the dark-mode gaps, indigo maps onto info and amber onto warning. There is no
purple token in globals.css, so the purple sites keep their palette classes and get
their dark: partner back.

* fix(ui): add an eslint rule that fails a hover: utility identical to its base

The token migration collapsed two-tone hover pairs by hand, so nothing catches
the next one. `local/no-noop-hover-variant` reads every string literal and
template chunk and errors when a `hover:X` sits alongside a bare `X`, which is
exactly the shape that renders no hover feedback. It ships at error with no
suppression baseline, so the eleven sites that already carried a dead hover
before the migration are fixed here too.

The rule reads one class string at a time, so a base class supplied by a
different ternary branch than its hover partner is left alone: a selected row
whose resting colour already matches its hover colour is deliberate, not a bug.
2026-08-19 22:52:36 -07:00
ryan-crabbe-berri
57b328ff96
refactor(ui): map hardcoded Tailwind palette classes onto semantic tokens (#37576)
* refactor(ui): map hardcoded Tailwind palette classes onto semantic tokens

The dashboard painted itself with literal palette utilities (text-gray-500,
bg-blue-50, border-red-200) that resolve to one fixed color regardless of
theme, so the shadcn token layer and its .dark block could never take effect.

A codemod (scripts/codemod-color-tokens.mjs) rewrites 3,232 of those across
254 files onto the existing token scale: neutrals become foreground /
muted-foreground / muted / border / card, and red, green, amber and blue
collapse into destructive, success, warning and info, with the pale -50 to
-300 tints expressed as opacity modifiers on the same token. Hover and focus
variants map to accent so they lift rather than recess. 210 now-redundant
dark: variants are dropped since the tokens carry both modes.

The .dark palette is retuned to a neutral gray ramp with the sidebar recessed
below the content canvas, replacing the blue-tinted shadcn default where the
sidebar read as a full-height card floating on a near-black page.

Nothing sets the .dark class yet, so light mode is unchanged and dark mode
stays inert until a theme toggle lands.

* chore(ui): drop the one-shot color-token codemod script
2026-08-19 22:11:44 -07:00
ryan-crabbe-berri
7b574b9df6
chore(ui): drop the antd dependency and its leftovers (#37574)
Some checks failed
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Unit Tests: Core Utilities / core-utils (push) Has been cancelled
Unit Tests: Enterprise, Google GenAI & Routing / enterprise-routing (push) Has been cancelled
Unit Tests: Integrations (Callbacks & Logging) / integrations (push) Has been cancelled
Unit Tests: LLM Provider Transformations / Vertex AI (push) Has been cancelled
Unit Tests: LLM Provider Transformations / All Other Providers (push) Has been cancelled
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Has been cancelled
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Has been cancelled
Unit Tests: Proxy Infrastructure / proxy-infra (push) Has been cancelled
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Has been cancelled
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Has been cancelled
Unit Tests: Proxy API Endpoints / proxy-server (push) Has been cancelled
Nothing in the dashboard renders antd any more, so the package and the
scaffolding around it can go. This removes `antd` and
`@ant-design/cssinjs` from package.json, deletes the global StyleProvider
the root layout wrapped every page in, drops the `antd` cascade layer and
the z-index override that lifted Base UI popups over an antd Modal, and
retires the lint rules that policed antd imports and antd class selectors
in tests.

Fifteen test files still carried `vi.mock("antd", ...)` factories for
components that stopped importing antd during the migration. They were
inert, and they resolve the real module, so they would have broken the
moment the package left node_modules.

The compatibility shims keep their behaviour and lose the antd name:
`antdRules`/`antdRequired` become `validatorRules`/`requiredRule`,
`isAntdUrl` becomes `isValidUrl`, and `ABOVE_ANTD_MODAL` becomes
`NESTED_DIALOG_LAYER`. Comments that explain why a contract looks the way
it does still name antd, because that history is the reason.
2026-08-19 20:44:27 -07:00
ryan-crabbe-berri
0b374541bb
refactor(ui): migrate the last antd components off antd onto shadcn (#37569)
Converts the remaining dashboard components that still imported antd: admin panel, agents, MCP toolsets, policies, prompts, bulk user edit, create user, plugin settings, teams, add model, auto router, cloudzero export, BYOK credentials, credential modal, onboarding link, create key and routing groups.

Primitives map onto the house shadcn set: Typography onto semantic tags, Select onto ui/select, SearchSelect or MultiSelect, Input onto ui/input, Tooltip onto SimpleTooltip, Card, Table, Tabs, Switch, Checkbox, Radio, Tag onto Badge, Divider onto Separator, Spin onto UiLoadingSpinner, Modal onto Dialog, message onto toast, and Space, Row, Col, Flex and Layout onto flex containers.
2026-08-20 03:01:07 +00:00
yuneng-jiang
a0f367fcd1
Merge pull request #36897 from BerriAI/litellm_standard_page_header
feat(ui): standardize the Teams page header
2026-08-19 18:52:55 -07:00
yuneng-jiang
0edd245545
fix(ui): render optional array and object MCP tool parameters as JSON inputs (#37548)
* fix(ui): render optional array and object MCP tool parameters as JSON inputs

A Python signature like `tags: list[str] | None = None` serialises to
`{"anyOf": [{"type": "array"}, {"type": "null"}]}` with no top-level
`type`, so the tool test panel's control dispatch fell through to the
generic text input. Whatever the user typed was sent verbatim, and the
server rejected it as the wrong type.

Resolve a property to its single non-null union member before choosing a
control, validating, seeding defaults, and coercing the submitted value,
so all four agree and an optional array or object gets the same JSON
textarea a required one already got.

* fix(ui): keep a null-defaulted optional MCP parameter out of the call

A parameter declared `list[str] | None = None` carries `default: null`,
which means the caller should send nothing. Seeding its editor with an
empty container made the field non-blank, so an untouched parameter was
submitted as `[]` or `{}` instead of being omitted.

Treat an explicit null default as "no value" everywhere it is read: the
editor starts blank and shows its placeholder, and the submitted
arguments leave the key out entirely.
2026-08-19 18:48:43 -07:00
Yuneng Jiang
f99eec5ecb
Merge branch 'litellm_internal_staging' into litellm_standard_page_header
Teams.tsx and Teams.test.tsx both conflicted with staging's antd -> shadcn
migration of the team create form.

Teams.tsx: took staging's rewritten import block and dropped `theme` from the
antd import, since this branch replaced `<Content style={{ padding: token... }}>`
with the Tailwind inset. Dropped both `const { Text } = Typography` (staging
removed its last use) and `const { token } = theme.useToken()` (this branch
removed its last use).

Teams.test.tsx: took this branch's PageHeader-shaped assertions over staging's
older tab-bar lookup, and restored the `within` import that staging had dropped.

Removed the `toHaveClass` snapshot of the antd tab-bar Tailwind classes and the
`.closest(".ant-tabs")` lookup: staging added local/no-antd-class-selectors as a
zero-violation error rule, and those assertions are inert in jsdom anyway. Every
behavioural assertion in that test is unchanged.
2026-08-19 18:45:04 -07:00
yuneng-jiang
2672b36dc3
fix(ui): clear pass-through header rows when the create modal is reopened (#37549)
KeyValueInput and QueryParamInput each seeded a private copy of their rows
from the value prop with a one-time useState initializer. The antd form they
were written for hid that: rc-field-form bumps an internal resetCount key on
resetFields, which remounts a Field's children, so the private copy was thrown
away on every reset. react-hook-form's reset does not remount, and the modal is
hidden rather than unmounted, so after Cancel the rows stayed on screen holding
the old values while the form value went back to empty.

The visible cost was a blocked create flow. A leaked header row made the modal
look configured, but the form value behind it was gone, so submitting a fresh
path and target was refused with "Please configure the headers" and no request
was sent. Typing one character into the leaked row put a value back and the
submit went through, which is not something a user can guess.

Both inputs are now controlled off the value prop, which is an array of pairs
rather than a record. A record cannot represent a row whose name is still empty,
which is the reason the private copy existed: two blank rows collapse into one
and a half-typed row disappears as it is typed. With pairs the field value is
the editable shape, the second source of truth is gone, and a form reset clears
the rows like every other field. add_pass_through converts to a record at submit,
so the request payload is unchanged.

Headers now require at least one row with a non-empty name. Previously that was
enforced by accident, because adding a row did not notify the form at all.
2026-08-19 18:44:01 -07:00
devin-ai-integration[bot]
f5cfa84220
feat(router): allow per-tier litellm_params in complexity autorouter config (#37064)
* feat(router): support complexity tier request params

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(router): make complexity tier params immutable

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(router): simplify complexity tier overlays

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(router): preserve plain tier config round trips

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(router): mask tier params in routing decisions

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 01:39:02 +00:00
ryan-crabbe-berri
0ab1725757
refactor(ui): migrate the model and router settings pages off antd (#37523)
* refactor(ui): migrate the model and router settings pages off antd

Converts the add model flow, credential panels, model settings and router
settings onto the shadcn primitives, moves the mapping table onto the
shared DataTable, and drops the dead uploadProps prop chain that only
existed to carry antd's UploadProps type.

* fix(ui): split comma-separated custom technical keywords into one term each
2026-08-20 01:19:09 +00:00
Mateo Wang
a6163e0146
Merge pull request #37543 from BerriAI/litellm_lit_5785_vertex_regional_pricing
fix(vertex_ai): apply regional endpoint uplift to cost tracking
2026-08-19 17:56:34 -07:00
ryan-crabbe-berri
cac4870271
refactor(ui): migrate the MCP servers pages off antd (#37522)
* refactor(ui): migrate the MCP servers pages off antd

Converts the MCP server create, edit, connect and permission screens plus
the MCP tools and selector components onto the shadcn primitives, and
rewrites the test helpers that drove antd's select and collapse DOM.

* fix(ui): finish the MCP servers antd migration so the shared field rules have one contract

mcpFieldRules and MCPPermissionManagement were already flipped to the shadcn
prop shape, but CreateMCPServer and UserEnvVarsModal were still rendering antd,
so the create modal spread onValueChange onto an antd Select that ignores it and
passed searchValue props that no longer exist. Convert both off antd, drop the
searchValue plumbing the MultiSelect now owns, and normalise tag values before
the tag list renders so a delimited or empty string cannot crash it.

Rewrite testUtils.selectOption to drive the shadcn listbox instead of
.ant-select, expand the collapsed permission panel before querying its switches,
and assert the dismiss case after a reopen now that Dialog unmounts closed
content.

* fix(ui): split multi-tag entries the MCP tag inputs commit as one value

The tag input hands back whatever the admin typed as a single custom value, so
"read,write" was stored verbatim and reached the backend as one malformed scope.
tagsControl already split delimited values on the way in; run the same
normalisation on the way out and dedupe, so both directions agree.

* fix(ui): stop splitting tag entries that are not scope lists

The previous commit split every tag field on whitespace and commas, but only a
scope list is delimited. A stdio arg or an access description item may contain
both characters as part of the value, so splitting them changed the argv the
process receives. Keep those entries verbatim and move the splitting behind
scopesControl, which the OAuth, token exchange and ID-JAG scope fields use.

* fix(ui): split tag entries on comma only, matching the antd token separator

Every tag field here was an antd Select carrying a comma token separator, so a
comma committed a tag and nothing else did. Splitting on whitespace as well
broke stdio args, and splitting neither left comma-separated extra headers and
access groups stored as one malformed value. Apply the comma rule in both
directions, trim each entry, and drop the scope-specific helper the previous
commit added, since the backend types scopes as a list rather than the
space-delimited string that helper assumed.

* fix(ui): stop rewriting stored tag values that an admin never edited

Stdio args are process argv, so a comma inside one argument and a
deliberately repeated flag both have to survive a round trip through the
edit modal. Two places were rewriting them. tagsControl split and deduped
the value it read back from the server, and MultiSelect re-split every
already-committed chip on each change rather than only the entry just
typed. Both now leave settled values alone, which keeps the antd token
separator applying to typing and nothing else.
2026-08-20 00:50:02 +00:00
ryan-crabbe-berri
fc8a6b2a8d
refactor(ui): migrate shared primitives and common components off antd (#37521)
* refactor(ui): migrate shared primitives and common components off antd

Adds the success variant to the shared Alert plus success, warning and
info variants to Badge, introduces UtcDateTimeInput to replace antd's
DatePicker, and converts the common components and key/team helpers onto
the shadcn primitives.

* fix(ui): keep MultiSelect and budget input faithful to their antd behaviour

Restore the clear-all control MultiSelect lost, split comma-separated
custom entries into one value per token, and stop rounding the budget
input on every keystroke so a fractional amount survives typing.

* test(ui): drive the access group picker through the migrated MultiSelect

AccessGroupSelector no longer renders an antd Select, so the placeholder
is an input label rather than a text node and the popup inerts the page
until it closes.
2026-08-20 00:25:09 +00:00
ryan-crabbe-berri
629d7683f2
refactor(ui): swap @ant-design/icons for lucide-react (#37553)
The dashboard drew its icons from two libraries at once: lucide-react,
which shadcn/ui ships with, and @ant-design/icons, left over from antd.
This moves the last 39 files onto lucide and drops the dependency, so
the icon set matches the component library everywhere.

antd icons sized themselves from the inherited font-size and rendered as
role="img" with an aria-label, neither of which a lucide svg does, so the
swap carries explicit size classes and gives the two icon-only plugin
buttons real accessible names.
2026-08-20 00:04:14 +00:00
Mateo Wang
0a3504c8a3
Merge pull request #37527 from BerriAI/litellm_batch_file_upload_validation
feat(proxy): fast-fail validation for batch input files at /v1/files
2026-08-19 16:24:39 -07:00
yuneng-jiang
94374beb6d
fix(ui): toggle unlimited budget when its text is clicked (#37547)
The checkbox and its visible text both sat inside the max_budget
FormField label, which renders a single <label htmlFor> pointing at the
number input, so clicking the text focused Max Budget instead of ticking
the box. The checkbox only carried an aria-label, so it had no label of
its own to catch the click.

Wrap the checkbox and its text in their own <label>, the association the
antd checkbox wrapper used to provide. The accessible name now comes
from that label, so the aria-label is dropped rather than duplicated,
and the outer field label still points at the number input.
2026-08-19 16:24:12 -07:00
yuneng-jiang
663e647bc8
refactor(ui): migrate antd Modal onto the shared shadcn Dialog (#37540)
* test(ui): cover the two modals no test would catch breaking

Both files sit in the antd Modal migration's blind spot. EditSSOSettingsModal's
test replaced antd wholesale with a stub Modal and asserted the stub's own
data-testid markup, so it proved nothing about the modal a user sees and would
have stayed green through any regression. routing_groups had no test at all.

Rewrite the first against the real antd Modal, querying by dialog role and
accessible name so the assertions hold under either library, and add an
integration test for the second that drives the row menu and the delete
confirmation end to end.

Modal width drops out of the SSO assertions: antd carries it as an inline style
and shadcn as a max-width class, so either form couples the test to the library
rather than to anything a user perceives.

* refactor(ui): move the straightforward antd Modals onto the shared Dialog

Twenty files whose Modal only used title, open, width, footer, className and
onCancel, so each one maps onto Dialog without judgement calls. Width becomes a
max-width class, the body gets the house scroll cap so tall content stays
reachable, and destroyOnHidden goes away because Base UI unmounts a closed
dialog on its own.

EditMembership needed a real fix rather than a translation. Clearing the form
after a submit resolved only ever worked by accident: the reset set every field
to undefined, which react-hook-form does not push out to a subscribed
Controller, and the fields looked cleared only because antd's Modal happened to
re-render the subtree afterwards. Dialog does not, so the stale values showed
through. emptyMemberFormValues now returns the empty value each control
actually understands, an empty string, null or an empty list, and the reset
lands whatever renders around it. Its test asserted the undefined shape while
describing the behaviour it was missing, so it now checks the values instead.

* refactor(ui): migrate the antd Modals that needed a judgement call

Sixteen more files. Most carried a prop that does not translate literally:
maskClosable becomes disablePointerDismissal, afterOpenChange becomes
onOpenChangeComplete, and closable={false} becomes showCloseButton={false}.

The styles prop went away everywhere it appeared. All but one instance set the
body to 24px and the header to 24px with no border, which is what DialogContent
already renders, so keeping it would have meant writing the default back by
hand.

Several Modals passed onOk alongside footer={null}, so antd rendered no OK
button and the handler could never fire. Each of those handlers was a
character-for-character copy of the neighbouring onCancel, so they are gone
rather than translated.

Rich titles now sit inside DialogHeader with DialogTitle carrying the heading
text, instead of the whole header block being nested inside DialogTitle. That
had put an h2 inside another h2, which is invalid and gave one dialog two
headings.

UserEnvVarsModal loses its formGeneration counter. Remounting the form when the
modal finished opening only mattered because antd kept a closed modal's
children mounted; Base UI unmounts them, so reopening is blank on its own. Its
test helper had encoded that remount as a timing assumption, so the file now
states the requirement outright and checks that reopening shows an empty field.

CreateMCPServer's cancel test read the tool list while the modal was closed,
which only worked because forceRender kept it mounted. It now asserts what a
user can actually observe: the panel is gone while closed, and reopening brings
back an empty URL and no tools.

Unmounting an open Base UI dialog leaves its scroll lock on <html> and <body>,
which survives cleanup() and makes every later test in the file see a locked
page where popups compute pointer-events: none and clicks quietly do nothing.
The shared setup now releases it.

* refactor(ui): finish the antd Modal migration onto the shared Dialog

Fifteen files whose Modal relied on antd's built-in footer. okText, cancelText,
onOk, okButtonProps, cancelButtonProps and confirmLoading collapse into two
explicit buttons in a DialogFooter, with danger becoming the destructive
variant and the various loading flags becoming disabled plus aria-busy. The one
okButtonProps that also hand-set a red background drops it, since the variant
already carries that.

add_guardrail_form keeps its own chrome, so its DialogContent turns off the
built-in close button and the padding, and its heading becomes the DialogTitle.
Under antd it passed title={null} and had no accessible name at all.

Modals that positioned themselves near the top of the viewport needed
translate-y-0 alongside top-8, because DialogContent centres itself with a
transform that top alone does not undo.

TeamGuardrailsTab's test reached its Mode select by index into every combobox on
the page. A modal dialog hides the rest of the page from assistive technology,
which antd never did, so the count changed and the index pointed at the wrong
control. It asks for the field by label now.

The mask-dismissal test drove antd's .ant-modal-wrap class directly; it uses the
overlay slot our own component exposes, and still fails if
disablePointerDismissal is dropped.

CreateUserButton stays on antd. Its Modal converts cleanly, but the colocated
test file then fails a varying handful of cases, and the cause sits in the test
file rather than the component, so it wants its own change.

Pruning suppressions for the touched files also cleared four
react-hooks/set-state-in-effect entries on CreateMCPServer that were already
stale before this branch.

* test(ui): pick Base UI select options through the shared helper

React 19's flush timing loses the race this test was relying on: the option
lands in the DOM one render before its positioner drops pointer-events: none,
so user-event refused the click. tests/test-utils already exports
chooseSelectOption for exactly this, added alongside the React 19 upgrade.
2026-08-19 23:16:42 +00:00
Mateo Wang
449bf68498
Merge pull request #36987 from BerriAI/litellm_infer_single_worker_redis_banner
feat(proxy): auto-suppress the no-Redis banner for confirmed single-worker deployments
2026-08-19 15:52:58 -07:00
ryan-crabbe-berri
5d6033f8b4
refactor(ui): migrate the remaining dashboard pages off antd (#37524)
* refactor(ui): migrate the remaining dashboard pages off antd

Converts the teams, usage, guardrails, vector stores, cost tracking,
agents, policies, login and onboarding screens onto the shadcn
primitives, including the team info tab shell and the virtual keys
hover cards.

* fix(ui): close out the antd migration's failing type checks and tests

Alert and Badge were missing the success and info variants their call
sites already used. Combobox dropped disabled because Base UI merges the
primitive's own props over the render child, so the flag never reached
the input, and the guardrails status filter had no accessible name, which
left two comboboxes indistinguishable to the tests.

The remaining test updates swap antd's title-based queries for the roles
the shadcn controls expose.
2026-08-19 22:46:55 +00:00
Mateo Wang
07c61387fc
Merge pull request #37504 from BerriAI/litellm_fix_stale_member_search_results
fix(ui): drop stale user search answers so Enter commits the current match
2026-08-19 15:46:19 -07:00