Commit graph

4874 commits

Author SHA1 Message Date
ryan-crabbe-berri
23873f8447
fix(policies): reject non-existent team/key/model scope entries on attachment create (#32131)
* fix(policies): reject non-existent team/key/model scope entries on attachment create

Creating a policy attachment accepted arbitrary team, key, and model values with
no validation, so a typo'd or non-existent team was silently persisted (LIT-4199).
The create endpoint now rejects a concrete (non-wildcard) team, key, or model that
does not resolve to a real entity, wiring the previously-dead PolicyValidator
existence checks and reusing RouteChecks._is_wildcard_pattern so validation agrees
with request-time matching, where only a trailing "*" is a wildcard. Wildcard
patterns are still allowed through since they may match zero entities today and
more later, and tags stay free-form. The Admin UI's Teams field validates the same
rule for immediate feedback when its team list has loaded, deferring to the backend
otherwise.

* style(policies): use builtin list generics and | None in scope validator

Keeps the new find_invalid_scope_entries signature off the UP006/UP045 strict
ruff budgets instead of copying the surrounding legacy typing.List/Optional idiom.

* fix(policies): separate multiple attachment scope errors with ' | '

Addresses Greptile review: joining per-entry validation messages with a bare
space read as one run-on sentence; ' | ' makes the multi-error 400 detail easier
to parse for users and programmatically.
2026-07-04 11:58:29 -07:00
yuneng-jiang
47f493a952
Merge pull request #32074 from BerriAI/litellm_chat-keys-usage
feat(ui): migrate chat UI from antd to shadcn/ui + add key management and usage panels
2026-07-04 10:01:30 -07:00
Krrish Dholakia
08c009cce5 fix(ui): forward ref on shadcn Input so rename auto-focus works on React 18
Input didn't wrap its function component in React.forwardRef, so the ref
ConversationList passes for rename auto-focus/select silently never attached
under React 18 (function components need forwardRef to receive a ref; that
requirement is dropped in React 19, but this app is on 18.3.1).
2026-07-03 21:46:55 -07:00
ryan-crabbe-berri
aca2428d3c
chore(ui): remove debug console.log statements from dashboard (#32087)
* chore(ui): remove debug console.log statements from dashboard

Delete 463 leftover console.log/console.debug calls across 87 files in the
Admin dashboard. These logged form payloads, API responses, and render
traces into every user's browser console.

The ESLint policy already encodes the intent (no-console allows only warn
and error), so those are kept, along with the console.log = function(){}
suppression reassignments and the console.log calls that live inside
string/template literals rendered as example code snippets.

Removal used an AST codemod so only standalone console.log/console.debug
expression statements were dropped; non-statement uses (no-op chart
onValueChange props, a placeholder onClick, and a sequence-expression in
TopKeyView) were handled by hand. Ratchets the no-console lint metric from
484 to 15.

* chore(ui): drop empty blocks left after console.log removal

Greptile flagged three empty control-flow blocks (an if and an else in
chat_completion.tsx, an else in networking.tsx) left behind when their
only content was a deleted console.log. Removes those plus one more empty
if in chat_completion.tsx's catch that the review missed.

* test(ui): drop provider_info_helpers test asserting debug log

The getProviderModels debug console.log calls were removed in this PR, so
the test asserting they fire no longer applies. Remove that test and its
now-unused console.log spy; the remaining 57 tests still cover the
function's actual return-value behavior.
2026-07-03 18:10:47 -07:00
Krrish Dholakia
856367763e fix(ui): design-system audit, single-model picker, scroll fix
Establishes a real design.md/AGENTS.md for the chat UI (tokens,
component patterns, decision trees) after several rounds of hand-rolled
Tailwind shipping invisible or broken states, then audits every
component in the directory against it: raw <button>s replaced with
shadcn Button throughout, spinners replaced with Skeleton for list/table
loading states, dark-mode contrast bugs fixed (MCPAppsPanel cards were
bg-background instead of bg-card, identical to the page background in
dark mode), Badge variants and status colors aligned with the documented
semantics, and the sidebar's active-nav-item styling switched to the
purpose-built sidebar-* tokens instead of the generic accent/secondary
tokens that collapse to the same value in this theme.

Also: disables model comparison mode and multi-select in favor of a
single active model, moves the model picker from a standalone top bar
into the composer, removes the sidebar collapse toggle and the
non-functional "Search chats" entry, and renames the conversation list's
"Today" group to "Recents".

Fixes a real scroll bug: the model picker's dropdown list was
unscrollable because its container used max-height instead of an
explicit height, which doesn't count as a definite size for the
percentage-height Radix ScrollArea viewport to resolve against — so the
viewport silently expanded to full content height instead of clipping,
and scroll events fell through to the page behind it. Same latent bug
fixed in the sidebar's conversation list.
2026-07-03 17:56:23 -07:00
Krrish Dholakia
7109b2f61c fix(ui): view-switcher navigation from chat route, add beta banner
"AI Gateway" in the topnav view switcher only called setMode(), which
is meaningful inside the dashboard SPA shell but a no-op on /chat,
which lives outside it (only "Chat" had a real navigation). Now
switching modes from the chat route does a real navigation back to
the dashboard root.

Also adds a persistent banner across all chat routes flagging it as a
pre-v0 feature not for production use, with a feedback link.
2026-07-03 15:54:44 -07:00
Krrish Dholakia
a58930e94e Merge remote-tracking branch 'origin/litellm_chat-keys-usage' into litellm_chat-keys-usage 2026-07-03 15:36:49 -07:00
Krrish Dholakia
afa739b423 fix(ui): design polish and per-tab routing for chat UI
Moves Chats/Integrations/Credentials/API Keys/Usage from client-side
tab state to real nested routes (/chat, /chat/integrations,
/chat/credentials, /chat/api-keys, /chat/usage) so each is bookmarkable
and survives a hard reload. Extracts the chat sidebar into ChatShell
and shared state (MCP server selection, conversation history) into
ChatShellContext, both consumed via the new app/chat/layout.tsx.

Along the way: fixes conversation URLs pointing at the wrong path
(/ui/chat instead of /chat in dev, which 404'd after sending the first
message) by reusing the existing migratedHref helper instead of a
one-off uiConfig-based path; fixes the topnav view-switcher always
showing "AI Gateway" as selected even while on the chat route; and
cleans up several shadcn/tailwind styling bugs introduced by the antd
migration (boxed tab outline instead of underline, model-selector
dropdown overflowing its popover, sidebar nav labels centered instead
of left-aligned, duplicate logo, dead non-interactive controls).
2026-07-03 15:35:52 -07:00
yuneng-jiang
c58e2266a2
Merge pull request #32072 from BerriAI/litellm_budget_fallbacks_ui
feat(ui): add budget fallbacks configuration to key create/edit forms
2026-07-03 15:32:10 -07:00
Krrish Dholakia
15d6a29c61 merge: resolve eslint-metrics.json conflict with litellm_internal_staging
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-03 22:08:32 +00:00
Yuneng Jiang
68d52ac251
chore(ui): preserve console.warn in prod builds to match lint allow-list
The lint rule allows console.warn (allow: [warn, error]) but removeConsole
only excluded error, so approved console.warn calls were silently dropped
from production bundles. Add warn to the exclude list so the prod strip
and the lint allow-list agree; only console.log/debug/info are stripped
now, warn and error both survive (verified: warn 85 to 85, error 906 to
906, log 675 to 14).
2026-07-03 14:51:14 -07:00
Yuneng Jiang
5b7c73a573
chore(ui): sync no-console budget to 484 after staging merge
Merging litellm_internal_staging dropped 2 console.log calls (the
currentUser logs removed in #32079), so the no-console budget max and
metric move from 486 to 484 to match the current count.
2026-07-03 14:37:41 -07:00
Yuneng Jiang
2e44691f56
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/eloquent-swanson-d634ca 2026-07-03 14:35:30 -07:00
Yuneng Jiang
76a9f7b5f3
chore(ui): add no-console lint ratchet and strip console from prod builds
Introduce a gradual ratchet to remove raw console.* calls from the
dashboard, mirroring the existing no-explicit-any budget.

The no-console eslint rule is set to warn with allow: [warn, error] so
the 486 console.log/debug/info calls are tracked without force-deleting
the legitimate console.error/warn error reporting in catch blocks. The
count is grandfathered via eslint-budgets.json (max 486, target 0) and
eslint-metrics.json, so any newly added console.log fails the budget
check and follow-up PRs grind the max down toward zero.

Independently, next.config strips console output from production builds
via SWC removeConsole (exclude: [error]), gated on NODE_ENV=production so
dev keeps full console output. This gives an immediate prod-hygiene net
regardless of how long the source cleanup takes. Verified against a real
production build: app-code console.log dropped from 675 to 14 in the
bundle (remainder is node_modules, which the transform leaves alone),
console.warn app calls stripped, console.error preserved 906 to 906.
2026-07-03 14:35:23 -07:00
Krrish Dholakia
b42cd37a30 style: format key_edit_view.tsx with prettier
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-03 21:21:20 +00:00
Yuneng Jiang
fc17ea3409
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/dreamy-lovelace-4a90c8
# Conflicts:
#	ui/litellm-dashboard/vitest.config.ts
2026-07-03 14:19:48 -07:00
Yuneng Jiang
0115bfa523
test(ui): quiet vitest CI logs by silencing passing-test console output
The ui_unit_tests CircleCI job logged ~45k lines for a single run, most of
it React act() warnings, antd deprecation notices and component stack traces
emitted as console output by passing tests, which buried real failures.

Set silent: "passed-only" (Vitest 3.2+) gated on process.env.CI so console
output from passing tests is suppressed while a failing test still prints its
logs and full stack trace. Also drop two stray console.log calls in
UsagePageView that dumped the whole currentUser object on every render in
production, not just tests.

Verified by running the suite the way CI does
(CI=true npm run test -- --run --pool forks --poolOptions.forks.maxForks=6):
45,075 lines before, 981 after, all 4075 tests still passing. A throwaway
failing test confirms its console.log and assertion diff remain visible.
2026-07-03 14:19:30 -07:00
Krrish Dholakia
a55cc3aab3 fix: allow clearing budget_fallbacks in edit view when key had existing fallbacks
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-03 21:17:37 +00:00
Yuneng Jiang
682d55a37d
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/distracted-banach-e3481f 2026-07-03 14:03:27 -07:00
Yuneng Jiang
fff6a5396c
fix(ci): stop ui_unit_tests vitest onTaskUpdate RPC timeout flake
The ui_unit_tests job runs vitest with maxForks=8 on an 8-vCPU xlarge
container, leaving no headroom for the main vitest process that services
worker RPCs. Under full CPU saturation the coordinator misses the
onTaskUpdate ack, vitest raises "Timeout calling onTaskUpdate" as an
unhandled error, and the job exits 1 even though every test passes.

Lower maxForks to 6 so the coordinator, jsdom, and OS keep two cores, and
raise teardownTimeout to 60s for extra slack on heavy runs.
2026-07-03 14:00:43 -07:00
ryan-crabbe-berri
57ca48a863
feat(mcp): add all-proxy-mcpservers sentinel to grant teams every MCP server (#32012)
* feat(mcp): add all-proxy-mcpservers sentinel to grant every MCP server

Teams can now be scoped to the all-proxy-mcpservers sentinel so they gain
access to every MCP server on the proxy without listing each id. The
sentinel expands to the live registry at request time, so a server added
later is picked up with no change to the team's stored permission. The team
ceiling that validates a key's MCP scope expands the sentinel too, so a key
can be scoped to any server (including one registered after the team) and
still pass subset validation

Expose the option in the team create and edit forms via a new exclusive
"All Proxy MCP Servers" choice in MCPServerSelector, mirroring the existing
"No MCP Servers" sentinel

* Update litellm/proxy/management_helpers/object_permission_utils.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(mcp): honor all-proxy-mcpservers only on the team path, never per-key

The sentinel was expanded inside the shared expand_permission_list, which
also feeds the key, org, end_user and agent resolvers. A key whose stored
object_permission ever held all-proxy-mcpservers (a stale write, a
configured default, or a bug) would silently resolve to every MCP server at
runtime, and a teamless key had nothing to cap it, so all servers got
injected. Only write-time validation stripping the value stood between that
value and a full grant

Move the expansion out of expand_permission_list and into
_get_allowed_mcp_servers_for_team so the sentinel is honored only where it is
settable (a team). Anywhere else it now passes through as an inert literal
that matches no registered server and is denied downstream. Reserved-id
protection already blocks a real server from taking that id

* fix(mcp): require proxy admin to grant a team the all-proxy MCP sentinel

Granting a team every MCP server on the proxy is a proxy-wide authorization
decision, but team create/update let any caller who can manage a team set
object_permission.mcp_servers, with no ceiling check. Org admins reach
/team/update by default (org_admin_allowed_routes) and _verify_team_access
also admits team admins, so a non-proxy-admin could set all-proxy-mcpservers
and self-grant their team access to every MCP server on the proxy, including
servers never assigned to that team

Gate the grant in new_team and update_team: a non-proxy-admin cannot add the
all-proxy-mcpservers sentinel. The check is scoped to newly adding it, so a
team a proxy admin already scoped to all-proxy can still be edited by a team
admin without being forced to strip the sentinel. The UI only offers the
"All Proxy MCP Servers" option to proxy admins in the team create and edit
forms

* fix(ui): render friendly all-proxy MCP label for non-admins editing an all-proxy team

A team scoped to the all-proxy-mcpservers sentinel could be opened in the team
edit form by a team admin or org admin (canEditTeam admits them), but the
"All Proxy MCP Servers" option in MCPServerSelector was rendered only behind the
proxy-admin-gated allowAllProxyMcpServers flag. For a non-proxy-admin the stored
sentinel was hydrated into the selected value with no matching Select.Option, so
antd showed the raw all-proxy-mcpservers literal as a chip, and adding another
server could persist a mixed [all-proxy-mcpservers, <id>] value.

Render the option whenever the sentinel is present in the value, not only when
the caller may grant it, and drive the real-option disabling off presence too so
the selection stays exclusive. A non-proxy-admin now sees the friendly label
read-only and cannot build a mixed state; only a proxy admin can newly add it,
which the backend already enforces.

Adds regression tests: the selector shows the friendly option (not the raw
literal) when the sentinel is stored but the grant flag is off, plus exclusive
emit and disabled-real-options coverage, and MCPServerPermissions renders the
green "All" state instead of the raw sentinel string.

* fix(ui): drop redundant "All servers" hint from the all-proxy MCP chip

antd renders a Select option's children inside the selected tag, so the
all-proxy option showed both "All Proxy MCP Servers" and the green "All servers"
type-hint in the chip, which say the same thing. Collapse the option to a single
green "All Proxy MCP Servers" label so the dropdown row and the chip read cleanly
without the duplication.

* fix(ui): color the all-proxy MCP label blue to match server chips

Use the same blue (#1890ff) as regular MCP server entries for the
"All Proxy MCP Servers" option/chip instead of green.

* fix(ui): make the all-proxy MCP permissions display blue, not green

Match the blue used by the selector chip and regular server entries so the
"All Proxy MCP Servers" badge and row in MCPServerPermissions are consistent
across the team/key/org detail views. The red "Blocked" state for
no-mcp-servers is unchanged.

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-03 13:59:28 -07:00
Krrish Dholakia
640ee9384b fix: prevent stale budget fallback entries after form reset and guard empty payload in edit view
Address Greptile P1 (stale state after reset): use key prop to force
BudgetFallbacksEditor remount when parent resets budgetFallbacks to {},
matching the existing routerSettingsKey pattern.

Address Greptile P2 (inconsistent empty payload): guard budget_fallbacks
in edit view to only include when non-empty, matching create form behavior.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-03 20:52:25 +00:00
Krrish Dholakia
0b3e327d02 fix(ui): use project cva config instead of class-variance-authority in badge and tabs
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-03 20:23:11 +00:00
Krrish Dholakia
11314f4bae feat(ui): migrate chat UI from antd to shadcn/ui
Replace all Ant Design components (Table, Modal, Popover, Tooltip, Skeleton,
Select, Spin, Popconfirm, Switch) with shadcn/ui primitives and Lucide React
icons across all chat components:

- ChatPage: sidebar, model selector, input bar, comparison mode
- ConversationList: search dialog, delete confirmation, scroll area
- ChatMessages: message bubbles, tool cards, copy button
- MCPAppsPanel: list/detail views, OAuth2 flow, tabs
- MCPConnectPicker: server toggle switches
- MCPCredentialsTab: credentials table with delete
- KeysPanel: API key management with rotation dialog (enterprise)
- UsagePanel: spend/request stats with sparkline charts

Add design.md as the design specification guiding the migration.
Install 15 shadcn/ui components (dialog, popover, tooltip, table, etc.).
All existing functionality preserved; no backend changes.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-03 20:15:33 +00:00
Krrish Dholakia
ed51c96d3f feat(ui): add budget fallbacks configuration to key create/edit forms
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-03 19:50:36 +00:00
Krrish Dholakia
e06adb5588
feat(ui): re-add chat UI, allow simple UI for MCP OBO auth (#31893)
Some checks are pending
GitHub Actions Security Analysis / zizmor (push) Waiting to run
2026-07-03 12:36:36 -07:00
Krrish Dholakia
28ddad271e
feat(proxy): add key-level budget_fallbacks to reroute requests when a per-model budget is exceeded (#31783) 2026-07-03 12:20:12 -07:00
tin-berri
3235f4a499
fix(mcp): persist DCR client_id from on-create MCP OAuth Authorize & Fetch (#31920)
* fix(mcp): persist DCR client_id so interactive OAuth token refresh works

Interactive authorization_code MCP servers register an OAuth client via Dynamic
Client Registration (RFC 7591) during the authorize flow, but the minted
client_id and the discovered token_url were returned to the caller and never
written to the server row. The autonomous refresh_token grant reads client_id,
client_secret and token_url off the server, so an expired access token could not
be refreshed; the user was bounced back to re-authorize and tools/list returned
zero tools

Persist the DCR client_id (plus client_secret and token_endpoint_auth_method when
the registration returns them) and the discovered token_url onto the server row,
reusing the encrypt_credentials write that client_credentials and token exchange
already use, then refresh the in-memory registry so the value is live at refresh
time. Both the v1 refresher and the v2 AuthorizationCodeRefresher read those same
fields, so egress needs no change

* fix: reuse persisted MCP DCR clients

* fix(ui): persist DCR client_id from on-create MCP OAuth "Authorize & Fetch"

The interactive "Authorize & Fetch" flow on the create form registers an OAuth
client (RFC 7591) against a temporary server that has no DB row, then creates the
real server afterward. useMcpOAuthFlow captured the DCR client_id and client_secret
but passed only the token to onTokenReceived, so the create request dropped the
client identity and the created server could not refresh its access token; its row
had credentials={} and the refresh_token grant 401d at the upstream token endpoint

Forward the registered client to onTokenReceived and write client_id (and
client_secret when present) into the create form credentials, so the create request
carries them and the backend persists them through its existing encrypt_credentials
path. token_url is omitted because it is re-discovered on load (RFC 9728 then 8414);
token_endpoint_auth_method is unused because this flow only ever registers as
client_secret_post or none, never client_secret_basic

* fix(ui): prevent stale MCP OAuth credentials

* fix(ui): reset MCP OAuth authorization state

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-03 12:15:33 -07:00
tin-berri
b59ad212f4
feat(ui): add token endpoint auth method selector to MCP OAuth forms (#31739)
PR #31635 added a per-server token_endpoint_auth_method (client_secret_basic
or client_secret_post) for upstream OAuth token endpoints, but it could only be
set by editing the stored credentials JSON. This surfaces it in the dashboard as
an optional selector directly under the Token URL field, in both the create form
(OAuthFormFields, M2M and interactive flows) and the edit form. The field binds
to credentials.token_endpoint_auth_method, which the backend already reads; the
value is sent only when chosen, so leaving it blank keeps the existing setting
and preserves the client_secret_post default.
2026-07-03 10:25:58 -07:00
Mateo Wang
2633e8f8a8
fix(ui): include cache token columns in usage export (#32015) 2026-07-02 20:04:07 -07:00
ryan-crabbe-berri
27069bd74f
feat(ui): shadcn migration foundation: Tailwind v4, shadcn init, antd cascade fix (#31995)
* feat(ui): shadcn migration foundation: Tailwind v4, shadcn init, antd cascade fix

Upgrade the dashboard from Tailwind v3 to v4 with CSS-first config: the
official upgrade codemod renamed utilities across 151 files, and
tailwind.config.js (plus the dead tailwind.config.ts) is replaced by
@theme tokens, @source globs, and @plugin directives in globals.css. The
Tremor safelist becomes @source inline patterns and the legacy tremor
theme tokens carry over verbatim. ui_colors.json was build-time only and
fed the dying Tremor palette, so its brand values are inlined and the
file removed; runtime theming replaces that path next.

shadcn is initialized with a hand-authored components.json (rsc,
cssVariables, baseColor gray) pointing utils at the existing
lib/cva.config.ts, which now exports cn (cva beta cx + twMerge) instead
of adding class-variance-authority as a second variant library. The two
ad-hoc cn helpers fold into it. Button lands as the canary primitive,
adapted to cva beta and React 18 forwardRef, with tests covering the
variant, twMerge, asChild, and ref seams. --radius is 0.5rem so the
shadcn radius scale reproduces Tailwind defaults and legacy rounded-*
classes render unchanged.

antd v5 emits unlayered CSS-in-JS that would beat every layered v4
utility, so AntdGlobalProvider now wraps the app in StyleProvider layer
and ConfigProvider cssVar, and globals.css declares
@layer theme, base, antd, components, utilities. antd wins over
preflight but yields to utilities, which is what lets migrated shadcn
pages coexist with legacy antd pages. Preflight stays global with the
three v3 behaviors pinned (default border color, button cursor,
placeholder color).

* fix(ui): restore tremor opacity tints removed by tailwind v4

Tailwind v4 removed the *-opacity-* utilities, but the precompiled
@tremor/react dist still composes them with shade-500 palette classes
(bg-opacity-10 over bg-<color>-500 etc.), so Badge, BadgeDelta, Callout,
light Icon and Button, BarList, and ProgressBar lost their tints and
rendered solid 500-shade fills. Adversarial review caught it; the
original smoke pages only exercised antd Tags.

tremor-v3-compat.css restores exactly the pairs tremor emits: for each
of the 22 safelisted colors, bg-opacity-{10,20,40}, hover/group-hover
bg-opacity-{20,30}, and ring-opacity-{20,40} against the -500 shade,
via color-mix into the utilities layer. Tremor's colorPalette maps both
background and iconRing to 500, so the -500 pairing covers every
composition in the dist; dark: variants are inert until dark mode ships.
The shim dies with @tremor/react at the end of the migration.

The upgrade codemod also missed two hand-rolled modal scrims using
bg-black bg-opacity-{30,50} (solid black under v4); now bg-black/30 and
bg-black/50. Removed the docker/build_admin_ui.sh copy of
enterprise_colors.json into the deleted ui_colors.json; that build-time
rebrand path is retired and its runtime replacement lands with the
theming phase.

* fix(ui): pair ring-opacity-40 with shade 300 in tremor compat shim

Tremor's colorPalette maps ring to shade 300, and the only consumer of
ring-opacity-40 (Icon variant outlined) composes it with that shade,
so the shade-500 rows were dead and outlined icon rings would render
at full opacity. Latent today (no dashboard usage of the outlined
variant); caught by adversarial review. ring-opacity-20 stays at 500
(iconRing), matching Badge and BadgeDelta.
2026-07-02 19:02:27 -07:00
tin-berri
b9df7fa705
fix(mcp): surface tools/list 401 auth failures as a challenge on single-server routes (#31921)
A 401 while listing tools (a missing or expired per-user OAuth token, or an
upstream 401 for any auth_type) was swallowed to an empty tool list, so a
single-server client got a 200 with no tools and no WWW-Authenticate challenge
instead of a 401 it could re-authenticate against. Only oauth pass-through and
delegate-to-upstream oauth2 servers surfaced it; every other auth_type, and the
missing-token case for all of them, masked it.

The surface-vs-absorb decision now keys on the route, not the auth_type. An
upstream 401 in _fetch_tools_with_timeout becomes an MCPUpstreamAuthError
regardless of auth_type, and the per-user OAuth challenge raised during client
creation (a bare HTTPException 401 carrying a WWW-Authenticate header) is
converted to the same type in _get_tools_from_server. The challenge is scoped
to 401: a 403 (authenticated but forbidden, e.g. insufficient scope) is not a
re-auth signal and degrades to an empty list like any other non-auth error, and
the stdio-allowlist 403 (no challenge header) stays absorbed. The existing
routing then does the right thing: single-server routes turn the error into a
401 + WWW-Authenticate, while the multi-server aggregator absorbs it to an empty
list so one unauthenticated server does not fail the whole listing.

On the UI tools page, an OBO (per-user authorization_code) server now shows the
Authorize gate when the list call returns 401, not only when no credential row
exists. The backend already refreshes a still-refreshable token on the list
call, so a 401 means there is no valid token and none could be minted (expired
with no usable refresh token), which is exactly when the user must reauthorize.
2026-07-02 18:05:32 -07:00
yuneng-jiang
bea8c9380b
refactor(ui): drive cache settings form from a typed frontend schema (#31939)
* refactor(ui): drive cache settings form from a typed frontend schema

The Cache Settings form was dynamically generated from field metadata
shipped by the backend, and read its values back out of the DOM with
document.querySelector. That loses type safety and makes client-side
validation awkward, which is a poor fit for a form whose shape only
changes when a developer edits code.

Move the field definitions (name, label, type, default, help text, which
redis type they apply to, section, and validation rules) into a typed
frontend module and render them through antd Form with controlled state.
The GET /cache/settings endpoint is still used to populate current values,
and the save/test payload shape sent to POST /cache/settings and
/cache/settings/test is unchanged. Per-field validation now lives on each
field's antd rules, so an inline error can surface before and on submit;
this is where the upcoming Redis URL validation will slot in.

The backend's fields output in GET /cache/settings is no longer consumed
by the UI, but is left in place since removing it is a separate backend
change.

* refactor(ui): validate list-field JSON inline so bad input blocks save

sentinel_nodes and redis_startup_nodes had no validation rule, so
malformed JSON passed validateFields, was caught while building the save
payload, and the field was silently omitted; the user's cluster/sentinel
config was discarded with no feedback. Add a jsonListRule (same shape as
portRule) to both list fields so an invalid value surfaces inline and
blocks save.

* fix(ui): show valid-JSON examples for cache list fields and clarify the error

The Startup Nodes and Sentinel Nodes help text showed Python-style
single-quoted examples (e.g. [{'host': '127.0.0.1', 'port': '7001'}]),
which the JSON validator correctly rejects, so pasting the example we
display failed. Switch both examples to valid JSON with double quotes and
change the parse-error message to "Must be a valid JSON array (use double
quotes)" so the hint points at the fix. Also add a regression test
asserting a numeric field (Database Index) is included in the save payload.

* fix(ui): validate numeric cache fields as text so bad input blocks save

Numeric fields (Database Index, TTL, Max Connections, Similarity
Threshold) rendered as antd InputNumber, which silently coerces
non-numeric input to empty. Because the fields are optional, an invalid
entry like a full connection URL pasted into Database Index passed
validation and was silently dropped from the save payload.

Render numeric fields as text inputs with a validation rule (non-negative
integer for Database Index and Max Connections, number for TTL and
Similarity Threshold), mirroring how Port already works, so invalid input
is preserved, flagged inline, and blocks submit instead of vanishing. The
save payload still coerces these to real numbers. Adds a regression test
for a non-numeric value entered into a numeric field.
2026-07-02 14:09:39 -07:00
Sameer Kankute
b96f1aa686
fix(mcp): byom visibility, preview UX, and admin settings gating (#31809)
* fix(ui): show info message when MCP tool preview returns 403

Internal users submitting MCP servers hit an admin-only preview endpoint; replace the red connection error with a clear review notice while leaving other failures unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): let BYOM submitters see their approved servers

Approved user-submitted MCP servers defaulted to no access groups and allow_all_keys=false, so submitters could not see them after admin approval. Grant creator visibility for active submissions in get_allowed_mcp_servers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Improve dialogue box

* fix(security): restrict MCP semantic filter settings to proxy admins

Add an explicit PROXY_ADMIN check on PATCH /update/mcp_semantic_filter_settings
and hide Semantic Filter and Network Settings tabs from non-admin users in
the MCP Servers UI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(lint): use list[str] instead of List[str] to satisfy UP006 budget

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(mcp): cache BYOM submitter server lookup with 60s TTL

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: fix ruff format and prettier formatting

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: preserve approved BYOM server visibility

* fix(mcp): keep no-mcp-servers opt-out absolute and gate BYOM union by key scope

The autofix in 94fd2bf made the no-mcp-servers sentinel return the caller's
submitted BYOM servers, which weakened an explicit key-level opt-out into a
soft preference. Restore the absolute opt-out and additionally skip the BYOM
union for keys with an explicit object_permission.mcp_servers list and for
toolset-scoped requests, mirroring how allow_all_keys servers are handled.
Add unit tests for the sentinel, explicit scoping, toolset scope, the cache
invalidation helper, the cache-miss DB path, and the db.py query helper.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-02 01:04:22 -07:00
ryan-crabbe-berri
3d644e1f9d
refactor(ui): colocate users page into route-level _components (#31897)
Moves the user-management component tree (view_users plus BulkEditUsers, edit_user, DefaultUserSettings, user_edit_view, and the view_users table/columns/info-view) out of the shared src/components dump into the users route segment under _components, now that the app router owns the route. The page imports from a trimmed ./_components barrel

UserInfo moves into networking.tsx beside UserListResponse, its real owner: networking defines the user API response shapes that embed it, and previously reached up into a view folder (components/view_users/types) to import the type. Defining it in networking removes that backwards data-layer-to-view dependency and drains the view_users/ folder entirely. CreateUserButton and onboarding_link stay in components/ since the create-key flow also consumes them

Relative imports in the moved files are rewritten to @/components/* absolute paths, and the eight pre-existing eslint-suppressions entries are re-keyed to the new paths so the move stays behavior and lint neutral

Verified: the moved suites pass with the same 75 assertions as before the move, tsc and eslint are clean, and next build compiles the /users route
2026-07-01 20:14:07 -07:00
ryan-crabbe-berri
2a9dbc4c0d
chore(ui): remove unused dep, delete dead file, and unblock knip (#31933)
Knip flagged remark-gfm as unused and date-fns as imported-but-undeclared, so drop remark-gfm (which prunes its transitive markdown subtree from the lockfile) and declare date-fns, which keyExpiryUtils.ts imports but only received transitively. Also delete the dead memory/components/index.tsx barrel, since nothing imports it once the page pulls MemoryView from its module directly

Knip itself could not run: its Playwright plugin imports every config referenced by a --config flag in package.json scripts, and migration.serverRootPath.config.ts threw at import time when SERVER_ROOT_PATH was unset. Move that guard into a config-specific globalSetup so importing the config is side-effect-free; the check still fires loudly before any test runs when the prefix is missing
2026-07-01 20:13:58 -07:00
Yuneng Jiang
1fe76dcedb
Revert "chore: remove _experimental/out (#31546)"
This reverts commit 72bcb748b9.
2026-07-01 13:25:47 -07:00
ryan-crabbe-berri
3e0bd71ee9
feat(ui): disclaim that the Update API Key modal only rotates api_key (#31805)
Some checks are pending
GitHub Actions Security Analysis / zizmor (push) Waiting to run
* feat(ui): disclaim that the Update API Key modal only rotates api_key

An adversarial review of the credential-rotation work noted the modal always
writes litellm_params.api_key, so models that authenticate with an Azure AD
token, AWS credentials, or a Vertex service-account JSON are not rotated by it.
Adds a warning Alert to the modal so users are not misled into thinking those
secrets were rotated; broadening the modal to those providers is a follow-up

* Update ui/litellm-dashboard/src/components/update_model_credentials_modal.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* style(ui): prettier-format the credential modal

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-01 10:25:32 -07:00
Krrish Dholakia
cca71a07c2
feat(mcp): add mcp_tool_search virtual tools for large tool catalogs (#31777)
* feat(mcp): add tool search virtual tools for large catalogs

When mcp_tool_search_enabled is set on a key's object_permission,
tools/list returns only mcp_tool_search and mcp_tool_call instead of
the full catalog. The LLM searches by keyword then calls discovered
tools by name, avoiding context bloat with 100+ tool deployments.

* fix(mcp): persist mcp_tool_search_enabled and route tool_call by name

The mcp_tool_search_enabled flag existed on the Pydantic models but the
Prisma schema lacked the column, so keys generated with the flag never
persisted it and tools/list kept returning the full catalog. Add the
column across all three schema.prisma copies plus a migration.

handle_mcp_tool_call passed server_name="" into call_tool, which built a
malformed prefixed name ("-<tool>") and failed to resolve the server.
Resolve the caller's allowed servers and dispatch through execute_mcp_tool
instead, matching how the normal /tools/call path routes.

* fix(mcp): filter list_tools to virtual tools on the protocol path

The REST surface (/mcp-rest/tools/list) returned only the two virtual
tools when mcp_tool_search_enabled was set, but the MCP protocol handler
(handle_list_tools, used by real MCP clients over streamable-http/SSE)
still returned the full catalog. Apply the same early return there so an
actual MCP client sees mcp_tool_search and mcp_tool_call instead of every
tool. call_tool was already intercepted on this path.

* fix(mcp): enforce IP + server filtering on virtual tool search/call

Review flagged that the virtual mcp_tool_search/mcp_tool_call path skipped
access controls the normal MCP flow applies. mcp_tool_call resolved allowed
servers from key permissions only, never applying IP filtering, so a caller
on a public IP could invoke a tool on a server marked
available_on_public_internet: false. mcp_tool_search listed the raw catalog
via global_mcp_server_manager.list_tools, exposing tool names/schemas that
/tools/list would hide and ignoring per-key/per-server tool filters.

Route both virtual handlers through the same filtered paths used by the
normal MCP flow: search now calls _list_mcp_tools and call resolves servers
via _get_allowed_mcp_servers, both threaded with the request client IP so
filter_server_ids_by_ip applies. execute_mcp_tool then enforces the server
allowlist and per-key tool permissions. Thread client_ip through
_list_mcp_tools/_get_tools_from_mcp_servers and pass it from the REST and
SSE call sites.

* fix(ci): ruff format server.py and sync dashboard API types

ruff format normalizes the list_tools client_ip changes in server.py, and
schema.d.ts gains the mcp_tool_search_enabled object-permission field so the
generated dashboard types match the proxy OpenAPI spec.

* style(mcp): drop quoted annotations and sort imports

Clears UP037 on the virtual tool handler signatures (redundant with
from __future__ import annotations) and I001 on the list_tools import block.

* refactor(mcp): extract virtual-tool dispatch and host progress capture

Pulls the mcp_tool_search/mcp_tool_call interception and the host
progress-callback setup out of mcp_server_tool_call into helpers, keeping
that handler under the strict cyclomatic-complexity ceiling after the
client_ip threading. No behavior change.

* test(mcp): cover SSE virtual-tool dispatch and host progress helpers

Adds unit tests for _dispatch_virtual_mcp_tool (non-virtual passthrough,
flag-disabled rejection, search/call routing with client_ip),
_capture_host_progress_callback, and the protocol list_tools virtual
early-return, covering the new server.py paths.

* fix(mcp): forward per-request auth headers through virtual tool handlers

The virtual mcp_tool_search/mcp_tool_call path intercepted the request
before the normal header extraction ran, so client-supplied per-request
auth (Authorization for upstream pass-through, x-mcp-auth-<alias>) was
dropped and execute_mcp_tool/_list_mcp_tools received None. Thread
mcp_auth_header, mcp_server_auth_headers, oauth2_headers, and raw_headers
from both the REST and SSE call sites through the handlers so upstream MCP
servers that require pass-through auth can be listed and called.

* fix(mcp): preserve requested server scope in virtual tool calls

A scoped MCP session (/mcp/<server>/ or header-scoped) carries an
mcp_servers scope that the normal call path passes into routing so the
session can only reach that server. The virtual-tool branch dropped it and
resolved with mcp_servers=None, letting a scoped session call mcp_tool_call
for any server the key can access. Thread the context mcp_servers scope
through _dispatch_virtual_mcp_tool into both handlers so search and call
resolve against the same scoped server set.

* fix(mcp): convert virtual tool errors to isError on the protocol path

The virtual-tool dispatch ran before the protocol handler's HTTPException
and guardrail handling, so a rejected virtual call (e.g. an out-of-scope
403 from execute_mcp_tool) raised out of mcp_server_tool_call and broke the
MCP JSON-RPC stream instead of returning an isError CallToolResult. Move
the dispatch inside the same try that wraps call_mcp_tool so virtual-tool
errors get the same isError conversion as normal tool calls.

* fix(mcp): spend-log virtual tool calls on the REST path

The REST virtual-tool branch returned before common_processing_pre_call_logic,
so execute_mcp_tool ran without a litellm_logging_obj and virtual mcp_tool_call
invocations were not spend-logged or guardrail-checked like normal calls. Run
the same pre-call pipeline in the call branch and thread the resulting
litellm_logging_obj through handle_mcp_tool_call into execute_mcp_tool.

* fix(mcp): reject virtual tool call when key has no accessible servers

handle_mcp_tool_call passed an empty allowed_mcp_servers list into
execute_mcp_tool; an unprefixed local tool name then fell through to the
local registry, which has no server permission check, so a key with only
mcp_tool_search_enabled and no server grants could run operator-configured
local tools by name. Reject with 403 before dispatch when no servers are
accessible, matching call_mcp_tool.

* docs(mcp): document virtual tool_search module and parity rule in AGENTS.md

* style(mcp): apply ruff format at repo line-length (120)

* fix(mcp): add mcp_tool_search_enabled to ObjectPermissionDict and customer test fixture

* chore: trigger CI

* fix(mcp): mirror pre-call pipeline, guard imports, coerce top_k, honor include_disabled_tools

- SSE mcp_tool_call now runs common_processing_pre_call_logic so it spend-logs and runs guardrails like the REST path (P1)
- coerce_top_k avoids ValueError on non-integer top_k from clients (both REST and SSE)
- guard mcp.types import in tool_search behind runtime/TYPE_CHECKING per package convention
- admin list with include_disabled_tools returns the real catalog even when mcp_tool_search_enabled is set
2026-06-30 20:03:59 -07:00
ryan-crabbe-berri
833406a711
fix(ui): rotate model credentials in a dedicated modal so a normal save can't overwrite secrets (#28089)
* feat(ui): add provider auth editing to the model edit view

Provider API keys / auth could previously only be changed by hand-editing
the raw litellm_params JSON, so there was no first-class way to rotate a
model's key. Adds an Authentication section that renders the correct
provider-specific fields (reusing ProviderSpecificFields) keyed off the
model's custom_llm_provider; fields are blank ("leave blank to keep
current") so untouched secrets are preserved and only entered values are
PATCHed and encrypted at rest.

Resolves LIT-3169

* refactor(ui): simplify model auth editing; fix stale credential branch

Drop the onFieldsResolved/authFieldKeys round trip: the parent now resolves
provider auth field keys itself via the new useProviderAuthFieldKeys hook
(same metadata ProviderSpecificFields renders), removing the report-up effect
and its stable-reference footgun. ProviderSpecificFields keeps only
excludeKeys (real need: suppress duplicate visible inputs).

Fix the stale Authentication branch: derive it from the live
litellm_credential_name form value (Form.useWatch) instead of the server
snapshot, so clearing/adding a credential mid-edit shows the right UI. Also
skip inline auth updates entirely when a named credential is selected, so we
never submit a credential name and raw inline auth together.

* fix(ui): don't leak freshly-entered model auth secrets to display/console

The auth values a user types are still sent in the PATCH request, but:
- strip them from the locally-stored litellm_params after save so the
  read-only LiteLLM Params JSON doesn't render the plaintext key
- remove the debug console.log in modelPatchUpdateCall that dumped the
  full update payload (incl. api_key / vertex_credentials) to the browser
  console on every model update

Backend stores these encrypted and returns them masked on refetch.

* fix(ui): don't require blank auth fields in model edit context

Auth fields render blank ('leave blank to keep'), but required metadata
(e.g. OpenAI api_key) added a required validation rule that blocked
onFinish entirely — making it impossible to save any unrelated edit
without re-entering the secret. Add a disableRequired prop to
ProviderSpecificFields and set it in the model edit Authentication section.

* fix(ui): rotate model credentials in a dedicated modal so a normal save can't overwrite secrets

The model edit form seeded the read-only LiteLLM Params textarea with the whole
litellm_params blob and re-sent all of it on every save. Because /model/info
redacts secrets by masking them ("azur****BBCC") rather than removing them, any
save re-encrypted the asterisk mask over the real value and silently destroyed
credentials such as azure_ad_token, aws_session_token, watsonx token/zen_api_key
and the OCI key fields. api_key, client_secret, vertex_credentials and the AWS
access/secret keys were safe only because the backend strips those entirely

Credential rotation now lives in a dedicated UpdateModelCredentialsModal that
PATCHes only the fields the user types, decoupled from the params blob; the
backend already merges partial litellm_params, so the rest of the deployment is
left untouched. The general edit form drops masked values from both the textarea
seed and the outbound payload, so a normal save can never carry a redacted secret

Also removes the now-unused inline auth section and its excludeKeys and
useProviderAuthFieldKeys plumbing, strips secret-leaking console.logs from the
provider upload handler and the model-update response, and fixes a
react-hooks/use-memo error that was failing the frontend-lint CI job

* chore(ui): ratchet no-explicit-any lint metric to 2013

Removing the credential-echoing console.log (and its info: any param) from the
provider upload handler dropped the tracked count by one; update the committed
baseline so the Check lint budgets CI step is not stale

* refactor(ui): scope the model credential modal to api-key rotation only

Narrows UpdateModelCredentialsModal to a single API Key field. On submit it
PATCHes only { api_key }, so the backend merge leaves every other deployment
param untouched; a model authed via azure_ad_token, AWS keys, or a Vertex JSON
won't have anything to rotate here yet, which is the intended scope for now.

Drops the multi-field provider rendering this added earlier, which also removes
the now-unused disableRequired prop from ProviderSpecificFields and reverts that
shared component to its prior shape. The "Update API Key" trigger button is now
an antd Button rather than a TremorButton, so the feature introduces no tremor.

* refactor(ui): convert the model detail toolbar buttons from tremor to antd

Switches Test Connection, Re-use Credentials and Delete Model to antd Button so
the toolbar matches the Update API Key button and no longer mixes libraries;
Delete Model uses antd's danger styling instead of hand-rolled red classes

* style(ui): make the api-key modal submit button primary and drop the Need Help link
2026-06-30 17:20:24 -07:00
yuneng-jiang
776b272689
Merge pull request #31735 from BerriAI/litellm_lit_4057_router_settings_routing_groups_save
fix(ui): fix Router Settings Loadbalancing tab save (LIT-4057)
2026-06-30 15:39:56 -07:00
ryan-crabbe-berri
4f41a9e140 test(ui): drop the e2e typecheck CI gate, keep the typed import for the editor
The e2e runs against the real proxy, so a contract drift already fails the test at
runtime; tsc only checks the spec against schema.d.ts, a generated snapshot, so a
backend change with a stale snapshot would pass tsc while the live test still
catches it. The dedicated tsconfig + script + CI step were circular ceremony for
that. Keep the zero-runtime-cost type-only import, which still catches mistakes in
the editor, and make its comment honest about what enforces the contract.
2026-06-30 12:50:21 -07:00
ryan-crabbe-berri
3971469b71 test(ui): harden Router Settings e2e and make its typing a real CI gate
Address an adversarial review of the Loadbalancing e2e:

- The "typed against the backend schema" claim was hollow: nothing type-checked
  e2e_tests (the root tsconfig excludes it and no CI step runs tsc), so a
  contract drift would compile and run unchanged. Add e2e_tests/tsconfig.json, a
  typecheck:e2e script, and a CircleCI step so the schema typing actually gates.
- The two describe blocks both mutate the proxy's shared router_settings, and the
  Loadbalancing save echoes the whole settings object, so they could clobber each
  other under local fullyParallel. Run the file serially.
- patchRouterSettings swallowed a failed seed, which surfaced later as a
  misleading UI timeout. Assert the write succeeded, and rely on the server-side
  merge instead of echoing the whole settings object back (drops a cast and a GET).
- Empty routing_groups already reproduces the bug, so drop the non-empty seed and
  its model coupling.
2026-06-30 12:08:15 -07:00
ryan-crabbe-berri
540c860a97 test(ui): add typed e2e for Router Settings Loadbalancing save (LIT-4057)
Drives the real save flow against a live proxy: seeds a present routing_groups
array (the LIT-4057 trigger) via the typed /config/update contract, changes
num_retries on the Loadbalancing tab, and asserts the POST returns 200 instead
of 422, the success toast appears, and the value still shows after a reload (the
ticket's "refresh shows old values" symptom). The round-trip is typed against the
OpenAPI-generated backend schema (ConfigYAML write, RouterSettingsResponse read)
through a type-only import, so a backend contract drift fails the type check.
2026-06-30 11:47:35 -07:00
ryan-crabbe-berri
30141f86f8 test(ui): make router settings save tests resilient to async timing
Address Greptile P2: the routing_groups test read setCallbacksCall.mock.calls[0][1]
immediately after the now-async save handler, so any latency in the mock would throw
an opaque TypeError instead of a clean assertion failure. Assert through
toHaveBeenCalledWith inside waitFor with expect.not.objectContaining, dropping the
index access and the cast. Also drop the ticket id from the test names.
2026-06-30 11:40:09 -07:00
ryan-crabbe-berri
9968499aab fix(ui): fix Router Settings Loadbalancing tab save (LIT-4057)
The Loadbalancing tab rendered routing_groups as a generic text input and
sent its array value back as the JSON string "[]", which fails Pydantic
list validation on POST /config/update and returns 422. routing_groups has
its own dedicated Routing Groups tab, so this tab must neither render nor
write it; exclude it the same way retry_policy and model_group_retry_policy
are excluded for the Model Retry Settings tab.

The save was also fire-and-forget: setCallbacksCall was not awaited, so the
rejected promise escaped the try/catch and the success toast fired
unconditionally, showing success even when the backend rejected the change.
Await the call, gate the success toast on resolution, and surface the error.
2026-06-30 11:28:51 -07:00
ryan-crabbe-berri
7ed25de120
fix(ui): allow any git host on the skills add form (LIT-4053) (#31652)
* fix(ui): allow any git host on the skills add form (LIT-4053)

The skills add form only accepted GitHub URLs: its URL parser bailed on
any host that did not start with github.com, so GitLab, Bitbucket, and
self-hosted repos (and any repo subfolder on them) were rejected before a
request was ever sent. The backend already accepts arbitrary git hosts
via its url and git-subdir sources, with no host allowlist, so this was a
client-side restriction only.

Generalize the parser into an exported, host-agnostic parseSkillSource:
GitHub URLs keep their github / git-subdir shorthand, every other host is
treated as a raw repo url, and an optional Subfolder path field turns any
repo into a git-subdir source (url + path). When a pasted GitHub
tree/blob URL already encodes a subfolder, the field is cleared and
disabled so a contradictory source can never be submitted.

The parser is hardened to match the backend contract: query strings and
fragments are stripped, the host match is case-insensitive and drops a
leading www., the extracted and field-entered subfolder paths are both
validated against the same regex the server uses, a real file-extension
allowlist (not "any dot") decides whether a trailing blob segment is a
file, a branch-only tree URL falls back to the repo, non-GitHub URLs
require at least an org/repo, and the suggested skill name is kebab-cased
so it satisfies the name field's own rule.

The git-subdir source is now handled in the display helpers
(getSourceDisplayText, getSourceLink, formatInstallCommand), which
previously showed it as "Unknown source" with no link. The submit path
is fully typed (RegisterPluginRequest plus an AddPluginFormValues
interface), removing the two prior any usages; as a result an
author with an email but no name is dropped rather than sent, since the
backend requires the author name.

No backend changes. Tests cover the full host/subfolder matrix at the
parser level plus form-submit assertions on the exact source payload.

* refactor(ui): sync skill register types to the generated OpenAPI schema, surface backend errors

Replace the hand-maintained, already-drifted API types for the skills add
flow with the generated ones from schema.d.ts: PluginAuthor now aliases
components["schemas"]["PluginAuthor"], the registration payload is a new
SkillRegisterRequest (the generated RegisterPluginRequest envelope with
source narrowed to our PluginSource union, since the backend types source
as a loose string map, and version kept optional since the backend
defaults it), and the dead, mismatched RegisterPluginResponse is deleted.
registerClaudeCodePlugin's inline payload type (which was missing the
git-subdir path field entirely) is replaced with SkillRegisterRequest, so
the networking layer and the form can no longer drift from the backend.

Error handling: the add-skill form swallowed the real failure and always
showed "Failed to register skill". registerClaudeCodePlugin already
derives the backend message and throws it, so the form now surfaces it
("Failed to register skill: <reason>"), and the networking helper falls
back to the raw body / status when the error response is not JSON instead
of throwing a JSON parse error. A regression test asserts the backend
message reaches the user.

* fix(ui): reject credentialed git URLs on the skills form

A repo URL with embedded user-info (user:token@host) passed the raw-host
parser and was stored verbatim as the skill source, which is served on
the unauthenticated /public/skill_hub and marketplace.json feeds, leaking
the credentials. Reject any host segment containing '@'.

* fix(ui): validate skill repo URLs through one WHATWG URL gate

Replace the ad-hoc string parsing (stripScheme / splitHost / manual
scheme, @, ?# checks) with a single parseRepoUrl gate built on the URL
parser, so every malformed/unsafe class is handled in one place and the
URL stored on the public skill feeds is always canonical. It enforces
https (rejecting http/ssh/git/file/javascript/data and protocol-relative
//host), rejects embedded credentials (user:token@host, including
userinfo-confusion like github.com@evil.com), rejects IP-literal hosts
(loopback/private/metadata and obfuscated/IPv6 forms), and rebuilds the
stored url from origin+pathname so query strings, fragments, and trailing
slashes can never be published. The GitHub org/repo shorthand is now
charset-validated like the other paths, so junk can't reach the stored
repo. Closes both Veria findings (credentialed and http sources) plus the
adversarial-review follow-ups, with regression tests for each class.
2026-06-30 10:29:49 -07:00
yuneng-jiang
f8a2ea7378
Merge pull request #31426 from BerriAI/litellm_/cranky-hamilton-21b5d0
fix(ui): stop Request Logs page from overflowing horizontally and size its columns
2026-06-30 10:23:38 -07:00
ryan-crabbe-berri
3dce3daff6
feat(proxy): type Customer Management response_model for OpenAPI coverage (#31043)
* feat(proxy): type Customer Management response_model for OpenAPI coverage

Add response_model to the five remaining untyped /customer operations
(block, unblock, new, update, delete) so the generated OpenAPI schema
documents a concrete response body. new/update reuse the canonical
LiteLLM_EndUserTable (matching info/list); block, unblock, and delete
get small dedicated models in
litellm/types/proxy/management_endpoints/customer_endpoints.py.

Together with the already-typed info/list/daily-activity routes this
brings the Customer Management group to full response_model coverage.
Regression tests assert each public /customer/* route declares the
expected response_model and that /customer/new surfaces a typed schema
in app.openapi(), so dropping a response_model fails CI.

* fix(proxy): keep budget_id in typed customer responses

Address review feedback on the Customer Management response_model typing.
Greptile flagged that response_model=LiteLLM_EndUserTable on /customer/new
and /customer/update silently drops fields the raw Prisma model_dump()
echoed. Checking the schema, budget_id is the only such scalar column that
was missing from the Pydantic model (created_at/updated_at/tpm_limit do not
exist on litellm_endusertable), so add budget_id to LiteLLM_EndUserTable.

This restores budget_id on new/update and also fixes the pre-existing gap
where /customer/info and /customer/list (already typed) dropped it, which
the UI Customer type expects. A regression test pins budget_id surviving the
response_model filter on /customer/update.

Also document UnblockUsersResponse.blocked_users via a Field description: it
holds the users that remain blocked after the call. The key name predates
this PR and is kept to avoid a backwards-incompatible rename on a beta route.

* fix(proxy): keep nested budget fields in customer responses

response_model=LiteLLM_EndUserTable nests the budget as the narrow write
allowlist LiteLLM_BudgetTable, which silently drops the server-managed
fields the customer endpoints used to return (budget_reset_at, created_at).

Introduce CustomerResponse, a thin response model that nests
LiteLLM_BudgetTableFull (the repo's budget response model), and apply it on
/customer/new, /customer/update, /customer/info and /customer/list. list
also builds CustomerResponse so its budget isn't narrowed at construction
time. created_by/updated_at/updated_by remain omitted, matching how budgets
are returned elsewhere.

The shared LiteLLM_EndUserTable is left untouched: it's constructed in many
places that pass narrow budget instances, and pydantic v2 won't coerce a
budget instance into a wider nested model. Typing only at the response
boundary (where the handler hands FastAPI a dict) sidesteps that. A
regression test pins budget_reset_at + created_at through the filter and
asserts the internal audit fields stay out.

* test(proxy): add golden-master characterization tests for customer responses

Lock the exact JSON body each customer-object endpoint (info/list/new/update)
and delete return today, so the upcoming type-safety refactor of the handlers
is only allowed to land if it reproduces these byte for byte. Pins null-field
inclusion, the nested budget shape (server fields kept, audit fields dropped),
and object_permission reverse-relation stripping. Green against current code.

* refactor(proxy): make the customer response flow type-safe

Replace the untyped dict + bolt-on response_model pattern on the customer
object endpoints with explicit typed construction. A single mapper,
_to_customer_response, validates a DB row into CustomerResponse at one
Any -> typed seam; new/update/info/list now return it (or a list of it) and
carry real -> CustomerResponse / -> List[CustomerResponse] return
annotations, and delete returns DeleteCustomersResponse. basedpyright now
verifies the handlers' return shapes instead of a runtime filter doing it
silently.

This also deletes the four copy-pasted object_permission reverse-relation
cleanup loops: pydantic's extra=ignore drops those undeclared fields during
validation, so the loops were dead code (proven by the golden-master tests,
which stay byte-for-byte green). basedpyright errors on the file drop from
140 to 116, all from removed dict plumbing.

CustomerResponse stays a thin subclass of LiteLLM_EndUserTable so it inherits
the existing validators/config unchanged (behavior preservation); only the
nested budget type is widened.

* refactor(proxy): annotate customer response mapper param as BaseModel

Address review nit: the mapper's untyped `record` added an ANN001 violation.
The incoming rows are pydantic v2 models, so type the param as BaseModel
rather than object (object has no model_dump, which would just move the
problem to basedpyright). This clears the ANN001 and also drops three
basedpyright unknown-type violations the untyped param was adding.

* style(test): ruff format customer endpoint tests

* test(proxy): give customer budget test update mocks a valid model_dump

The type-safe response refactor validates the update result via
_to_customer_response (CustomerResponse.model_validate(record.model_dump())).
These budget tests mocked the end-user update to return a bare MagicMock,
so model_dump() yielded a MagicMock that fails validation. Give each update
mock a minimal valid dict; the tests assert on the prisma calls, not the body.

* chore(ui): regenerate API types from proxy OpenAPI spec

* fix(ui): make generated API types stable across Python versions

Python 3.13 strips a docstring's common leading indentation at compile
time while 3.12 keeps it, so app.openapi() emits differently-indented
description strings depending on the interpreter. The dashboard type
generator ran locally on 3.13 and in CI on 3.12, so schema.d.ts drifted
and the "Verify schema.d.ts matches the proxy OpenAPI spec" check failed

Normalize every description through inspect.cleandoc in the spec dump so
the output is identical regardless of interpreter, then regenerate
2026-06-30 09:58:01 -07:00
Mateo Wang
72bcb748b9
chore: remove _experimental/out (#31546)
* chore: remove _experimental/out

* fix(ci): recreate _experimental/out before copying UI build output

The build scripts cp the Next.js output into litellm/proxy/_experimental/out,
which was removed from git. cp failed because the target directory no longer
existed; mkdir -p recreates it before the copy.

* fix(proxy): make UI serving resilient to a missing _experimental/out

Removing the committed UI export means the source/test tree no longer
ships litellm/proxy/_experimental/out. Three things assumed it was always
present and broke once it was gone:

- get_favicon hard-coded the built favicon path and 404'd without it; it
  now falls back to the bundled swagger/favicon.ico
- the /_next and /ui static mounts raised at construction when the export
  was absent, so the whole UI-setup block was swallowed and no mounts
  registered; they now use check_dir=False
- _restructure_ui_html_files was a nested function only exposed as a
  module attribute when that block happened to succeed; it is now a real
  module-level function

test_admin_ui_export_serves_nested_extensionless_routes validated the
committed artifact, whose premise this PR removes; it now drives the same
MCP OAuth callback restructure guarantee through a synthetic export.

* chore(greptile): ignore generated _experimental/out so review fits the file limit

* Revert "chore(greptile): ignore generated _experimental/out so review fits the file limit"

ignorePatterns is applied after Greptile counts the files changed, so it
does not bring the diff under the file limit; the config had no effect.
2026-06-29 21:42:58 -07:00