Commit graph

4786 commits

Author SHA1 Message Date
yassin
88904b2875 fix(ui): surface why a playground virtual key can't load models
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-22 19:26:30 +00:00
yuneng-jiang
1c421f3578
fix(ui): keep completion-mode models in the playground chat dropdown (#37954)
PR #36130 added a KNOWN_MODEL_MODES guard to isModelCompatibleWithEndpoint
that hides any model whose mode isn't in the ModelMode enum, to keep
rerank/ocr/batch/etc. models out of chat-style endpoints. mode: completion
(legacy text-completion models) wasn't in that enum, so it got caught by
the same guard and disappeared from every endpoint, including chat, where
it routes fine.

Add ModelMode.COMPLETION and map it to EndpointType.CHAT like the other
chat-compatible modes.
2026-08-22 11:36:24 -07:00
mateo-berri
138b0da21f chore(dashboard): regenerate api types for the files list params 2026-08-21 17:33:30 -07:00
Mateo Wang
3029f7eb84
Merge pull request #34752 from SouthernCrossAI/litellm_scx_ai_provider
feat(providers): add SCX.ai as a JSON-configured OpenAI-compatible provider
2026-08-21 17:21:46 -07:00
tin-berri
0c50286a55
feat(ui): add per-key Savings tab to key detail page (#37693)
* feat(ui): add per-key Savings tab to key detail page

Adds a "Savings" tab to the key detail view, showing the same four metrics
and time-series chart as the proxy-wide Cost Optimization view, but scoped
to a single API key.

For org admins, the tab shows the key's full savings across all requests.
Non-admins see only their own requests on the key, with a scope note
explaining the limitation.

Root cause: userDailyActivityCall and userDailyActivityAggregatedCall
never forwarded an api_key query parameter to the backend, even though
both handlers already accept and filter by it.

Changes:

- networking.tsx: Add optional apiKey param to both daily activity call
  wrappers (appended to variadic options tuple for backward compatibility).

- costOptimizationUtils.ts: Extract shared metrics helpers (compressionOf,
  cachingOf, autorouterOf, savedTokensOf, cacheHitRatio) and shortDate
  so both UsageTab and KeySavingsTab use the same formulas and prevent
  divergence.

- useDailyActivityRange.ts: Refactor into useScopedDailyActivityRange(
  accessToken, scope: {userId, apiKey?}) for reuse-by-parameter unbundling.
  Role resolution stays at the entry point (useDailyActivityRange), not in
  a scoped caller. Update test expectations for new 6-arg tuple.

- UsageTab.tsx: Simplify by importing extracted helpers and SummaryCard
  component instead of defining them inline. No behavioral change.

- key_info_view.tsx: Insert "Savings" tab trigger between "Overview" and
  "Settings"; wire TabsContent to new KeySavingsTab component with lazy
  mounting (no keepMounted) to defer daily-activity fetch until tab opened.

- NEW: components/shared/SummaryCard.tsx — Shared presenter for four-tile
  summary row (label + value + hint + optional info popover). Extracted
  from UsageTab so both surfaces show identical tile layout without CSS
  divergence.

- NEW: components/templates/KeySavingsTab.tsx — Per-key view with admin/
  non-admin scope branching, empty-state messaging, same chart toggles
  and info popovers as UsageTab.

- NEW: components/templates/KeySavingsTab.test.tsx — 7 tests covering mount,
  loading state, empty state, scoping, and scope-note visibility.

Authorization: No new permission check. Both backends gate api_key filter
by the same user role check that governs the request itself. Non-admins
must send their own user_id and can only see their own keys.

Tests: 6121 pass (1 pre-existing failure unrelated to this change).

Prior art / collision note:
- PR #37570 (budgets tab) lands in same TabsList hunks as "Savings" tab,
  but different tab names so conflict trivial if both merge.
- PR #37659 (my own) adds progress/cancelled/cancel to DailyActivityRange,
  but this PR uses stable three-field interface from staging.

* fix(ui): scope spend view by the backend's admin-view contract, not all_admin_roles

Greptile flagged org admin handling on the key savings tab. The live bug it
described does not fire today: useAuthorized supplies session-role labels and
all_admin_roles only carries the raw org_admin spelling, so an org admin was
already scoped. That safety was accidental, so replace the predicate with
spendScopeUserId / hasProxyWideSpendView in utils/roles.ts, mirroring the
backend's user_api_key_has_admin_view (proxy admin and admin viewer only, org
admin excluded in both spellings), and use it in both useDailyActivityRange
and KeySavingsTab

Reclassify the KeySavingsTab render test as an integration test per the
repo's unit/integration split, move scope-resolution coverage to roles.test.ts
as a full role matrix, use real session-role values instead of raw ones, and
assert tile totals against non-empty metrics. Replace the nested ternary in
the chart body (frontend-lint error) with flat conditional rendering

* fix(ui): show auto-router savings as the fourth key-savings tile

Cache hit rate had displaced auto-router savings from the fourth slot,
diverging from the org-wide Cost Optimization page's tile order. Match
it: Total / Compression / Prompt caching / Auto-router, with cache hit
rate as a fifth tile.

* fix(ui): drop cache hit rate from the key savings tiles

Keep the four tiles this page is meant to show: total, compression,
prompt caching, and auto-router savings.

* fix(ui): stop an empty api_key from widening a key-scoped activity read

The paginated and aggregated daily-activity wrappers disagreed on an
empty filter value: the paginated one appended it, the aggregated one
coerced it to undefined with || and dropped it. Since the aggregated
call is the one tried first, an empty key hash would have silently
turned a key-scoped read into a proxy-wide one and reported every
key's savings as this key's. Use ?? so both send the filter through
and it matches nothing instead.

* style(ui): satisfy prettier and the inline-object lint rule in key savings tests

* refactor(ui): drop the cacheHitRatio extraction left over from the removed tile

* fix(ui): pass daily-activity filters raw so both transports agree at the null boundary

* refactor(ui): share the savings tiles and totals between both surfaces

The per-key Savings tab and the proxy-wide Cost Optimization tab carried a byte-identical
four-tile block, three long metric-definition strings included, and five identical useMemo
totals. Both now render SavingsTiles and total through useSavingsTotals, so the donut cannot
slice numbers the tile above it disagrees with.

* docs(ui): say request, not mount, in the savings tab comment

The comment claimed mounting eagerly would fire the rollup sweep, which reads as a claim about
the bundle. Only the request is deferred; the module ships with the key page either way.

* test(ui): pin the daily-activity args array against the real caller signatures

The sibling unit test mocks networking, so it checks the positional array against itself and
stays green when the array and a networking signature drift apart. Swapping user_id and api_key
in the aggregated signature alone passes there and fails here on user_id=hash-abc.

* style(ui): hoist the daily-activity query options out of the call argument

The four-property object literal tripped local/no-large-inline-object-arg. The violation predates
this branch, which only moved the line into the annotated range, and the rule count drops 550 to 549.
2026-08-21 14:50:02 -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
bhuvan2134686
aef09aca18 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_scx_ai_provider
Resolves the three conflicts against the JSON provider registry refactor. The
hardcoded api.scx.ai base-url branch in get_llm_provider_logic.py is dropped in
favour of the generic JSONProviderRegistry.get_by_base_url lookup, which reads
the same base_url and api_key_env from providers.json and additionally honours
an explicitly passed api_key. constants.py and types/utils.py keep both the
cognition and scx-ai entries added on either side.
2026-08-21 17:27:49 +10: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