Commit graph

5061 commits

Author SHA1 Message Date
Yuneng Jiang
44b95bbfcb
fix(logs): scope the End User filter to the caller's teams and bound its scan
The End User filter listed every row of LiteLLM_EndUserTable, which is both
unscoped and the wrong source. Team admins and internal users can open the
Logs page, and their log view is already restricted to their own requests
plus the teams they administer, but the filter dropdown offered them every
end user on the proxy.

Team attribution only exists on spend logs, so /customer/aliases now reads
LiteLLM_SpendLogs and applies the same scoping /spend/logs/ui does: a proxy
admin sees the whole window, everyone else sees ("user" = caller OR team_id
IN permitted_teams), reusing _get_permitted_team_ids_for_spend_logs so the
two paths cannot drift. A caller with neither matches FALSE rather than
falling through to unscoped, and a failed team lookup degrades to
own-rows-only.

Querying spend logs safely is the other half. start_date/end_date are now
required, so the query always has the indexed startTime bound, and the
inner scan is capped at MAX_SPENDLOG_ROWS_TO_SCAN_FOR_FILTERS rows ordered
by startTime DESC. DISTINCT therefore runs over a bounded row set instead
of the whole table the way /global/all_end_users does.

Also adds /customer/aliases to spend_tracking_routes. Without it RouteChecks
rejects INTERNAL_USER and INTERNAL_USER_VIEW_ONLY before the handler runs,
which would have made the scoping above dead code; a test pins the route to
the same access tier as /spend/logs/ui.

The dropdown now shows the end users present in the window the table is
showing, so the filter list matches what it filters. formatLogsWindow is
shared with the logs query so the two windows cannot diverge.
2026-07-24 16:21:46 -07:00
tin-berri
166c443b4f
Merge pull request #34454 from BerriAI/litellm_dashboard_object_permission_generated_type
refactor(ui): derive the dashboard object_permission type from the generated schema
2026-07-24 16:18:07 -07:00
Yuneng Jiang
b9e922cee7
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/key-activity-missing-c5383e 2026-07-24 16:12:14 -07:00
ryan-crabbe-berri
79c5c169d8
feat(ui): migrate the Create Organization form to shadcn and react-hook-form (#34552)
* feat(ui): migrate the Create Organization form to shadcn and react-hook-form

* fix(ui): guard double submit, test escape-close reset, drop dead organizationCreateCall

* fix(ui): block org create dialog dismissal while a create is in flight

* fix(ui): render budget duration labels instead of raw values in the shadcn Select
2026-07-24 16:09:30 -07:00
Yuneng Jiang
3966fbf5ec
chore(ui): drop the unused antd table header sort dropdown
TableHeaderSortDropdown had no importers left; the shared DataTable's
DataTableSortHeader covers the same ascending/descending/reset menu on Base UI.
knip did not flag it because its own test file counted as a usage.
2026-07-24 16:08:35 -07:00
ryan-crabbe-berri
5f2c9a952d
fix(ui): bind key duration input to one Form.Item so pre-filled expiry submits (#34521)
* fix(ui): bind key duration input to one Form.Item so pre-filled expiry submits

The Create and Edit key forms kept the displayed expiry in a Tremor TextInput's
local state while the value actually submitted lived in a separate hidden antd
Form.Item. After the first create, form.resetFields() cleared the hidden field
but not the local state, so a second create showed a stale "1d" that was never
sent unless the user deleted and retyped it

Wrap the visible input in a real Form.Item (name="duration") inside
KeyLifecycleSettings and drop both hidden mirror fields plus the local
durationValue state, so what is displayed is always what is submitted. The
Regenerate key flow already used this pattern

* test(ui): restore custom rotation interval coverage in real-form harness

The KeyLifecycleSettings test rewrite dropped the custom interval branch:
selecting Custom interval, typing a value, propagation to the parent, and
hiding the input when switching back to a predefined interval. Cover it in
the real antd Form harness, asserting the parent-held rotationInterval state
instead of a mocked callback
2026-07-24 16:08:32 -07:00
Yuneng Jiang
9e56630347
refactor(ui): migrate routing groups table onto the shared DataTable
Rebuilds the Router Settings > Routing Groups table on the shared DataTable
and cell library, the last antd entity grid in the dashboard.

The table splits into a thin RoutingGroupsTable container plus
RoutingGroupsTableColumns, with the usage snippets moving to their own
RoutingGroupUsagePanel on ui/tabs and the shared CodeBlock instead of antd
Tabs and Paragraph copyable. Models render through the shared ModelsCell so
long lists collapse behind "+N more" rather than wrapping the row, and the
two inline icon buttons become a single overflow menu with Edit and Delete.

antd gave the snippet panel its own chevron column; under the shared pattern
a row has two click targets, the name cell and the overflow menu, so clicking
the group name now opens the panel. Column set, order, actions, and the
backend row order are otherwise unchanged.
2026-07-24 16:08:29 -07:00
Yuneng Jiang
3476240f11
fix(ui): keep entity usage tabs aligned with their panels
Tremor's TabPanels hands each child an index via React.Children.map, while
the selected index comes from HeadlessUI counting only real Tab elements.
An empty fragment, false, or null still consumes a panel index but
contributes no tab, so the team-only Agent Activity conditional made the
two lists drift for every non-team entity type: Key Activity resolved to
the empty slot and rendered nothing at all, and Endpoint Activity rendered
the key metrics

Drive both lists from a single tab array so adding or removing a
conditional tab touches one place and the indices cannot diverge
2026-07-24 16:07:23 -07:00
Tin Chi Lo
ba9f6d75d8 refactor(ui): derive the dashboard object_permission type from the generated schema
The dashboard declared the server-owned object_permission shape by hand
in five places, each with a different subset of fields and none matching
the OpenAPI schema. That is what hid LIT-4766: KeyResponse.object_permission
never declared mcp_toolsets, so a form that wrote the field without reading
it compiled cleanly and silently wiped the grant

Replace four of those copies with one alias over the generated
LiteLLM_ObjectPermissionTable. The agent shape stays separate because the
agent endpoint really does return a narrower type, so it points at its own
generated AgentObjectPermission
2026-07-24 16:05:52 -07:00
Yuneng Jiang
5246de63e7
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/litellm-logs-ui-lag-0ca4b8 2026-07-24 15:56:39 -07:00
Yuneng Jiang
745f7ad163
perf(ui): back the logs End User filter with a paginated endpoint
Opening Logs > Filters fetched the entire customer table through
/customer/list, which is an unbounded find_many that eagerly loads the
budget and object-permission relations for every row. On a proxy with
61k customers that is a 20 MB, 7.6 s response; the dropdown then built an
option per row and rendered all of them, since the combobox does not
virtualize. The result was a multi-second freeze every time the drawer
opened.

Adds GET /customer/aliases, a projection of user_id alone with page/size/
search, mirroring /key/aliases. The End User field now uses
PaginatedSearchSelect behind an infinite query, the same shape the Key
Alias and Model filters already use, so it fetches 50 rows at a time and
pushes the typed query to the server.

The response reports has_more instead of a total count. A total needs
COUNT(*) over the whole match set on every keystroke, which is the cost
this endpoint exists to avoid; ordering by the user_id primary key and
fetching one row past the page lets Postgres stop early and still tells
the client whether to request more.

LIKE metacharacters in the search term are escaped, because end-user ids
routinely contain underscores and an unescaped one silently widens the
match.

Drops the now-unused accessToken prop threaded from RequestLogsPanel
through RequestLogsTable into the filters.
2026-07-24 15:56:34 -07:00
tin-berri
57ed2ed718
Merge pull request #34452 from BerriAI/litellm_lit4766_key_edit_mcp_toolsets
fix(ui): keep a key's MCP toolsets when saving an edit
2026-07-24 15:38:33 -07:00
Tin Chi Lo
2ccdb0896d feat(mcp): send RFC 8707 resource indicators on upstream OAuth legs
The gateway acts as an MCP client toward upstream MCP servers, and the MCP
authorization spec requires an MCP client to send the RFC 8707 resource
parameter on both the authorization request and every token request. The
gateway sent it on none of its upstream OAuth legs, so an authorization server
that requires resource indicators rejected the exchange with invalid_target
with no way to configure around it.

Authorization servers disagree irreconcilably and nothing advertises which
camp they are in, so this is a per-server opt-in rather than a default: most
providers ignore the parameter, some hard-reject it and carry audience in
scopes instead, and strict or MCP-native ones refuse to mint a correctly
scoped token without it. The new upstream_resource setting is unset by
default, which keeps today's requests byte-identical.

Both outbound OAuth stacks resolve the value from the server exactly once and
carry it structurally rather than attaching it per call site. In v1 every
plain-OAuth2 token leg builds its body through one helper that resolves the
resource in the same call as the mandatory client authentication; in v2 the
adapter, the single place an MCPServer becomes an outbound config, resolves it
onto the client_credentials config that the HTTP/SSE M2M path uses, and it
joins the config's mint identity so retargeting a live server refreshes the
token rather than serving the previous audience's. A leg cannot authenticate
without also naming the resource its sibling legs named, which is what an
attach-per-call-site approach kept getting wrong.

The setting is non-secret admin config sharing a blob with real secrets, and
the backend classifies which key is which rather than nulling the blob
wholesale or gating on its truthiness: redaction returns admin config to an
admin, session inheritance ignores it when deciding whether a real credential
was supplied and carries it onto the derived server, and the edit form renders
the same shared OAuth component as create so the field exists on both, an
emptied field submitting an explicit null that the credential merge drops.
2026-07-24 15:01:38 -07:00
Yassin Kortam
35dc982692
feat(proxy): add SAML 2.0 SSO for the admin UI (#31429)
litellm already supports Google, Microsoft and generic OIDC SSO through
fastapi-sso, which has no SAML support; AuthMethod.SAML existed only as an
unused enum value. This adds real SAML 2.0 single sign-on for the admin UI.

A new SAMLAuthHandler validates signed assertions with the OneLogin
python3-saml toolkit and maps them onto a CustomOpenID, then reuses the
shared post-login path every other provider goes through, so provisioning,
role/team mapping and the UI session JWT are unchanged. Both SP-initiated
and IdP-initiated HTTP-POST flows are supported. SP-initiated logins are
bound to the browser that started them via an HttpOnly state cookie plus a
cached AuthnRequest id, and the ACS rejects any response whose InResponseTo
doesn't match; unsolicited (IdP-initiated) responses cannot be browser-bound
so they are rejected unless SAML_ALLOW_UNSOLICITED=true. Replays are rejected
by a consumed-assertion guard whose lifetime tracks each assertion's
NotOnOrAfter, and both the replay guard and the login-state binding go
through the proxy's shared in-memory + Redis cache for multi-instance
deployments. The ACS honors DISABLE_ADMIN_UI and re-applies the
free-SSO-user Enterprise gate after the assertion is validated, so an
unvalidated POST can no longer drive the billable-user count query.

SAML is configurable from the admin UI SSO settings (IdP metadata URL or
inline XML, SP entity ID, and an allow-unsolicited toggle), which persists
the SAML_* environment variables the handler reads, exactly like the Google,
Microsoft and generic OIDC providers.

python3-saml is kept as an optional saml extra; its xmlsec and lxml wheels
bundle the native libraries so no system packages are required, and the
import is guarded so the proxy still starts without the package with the
SAML routes returning a clear 501.

Resolves LIT-4016
2026-07-24 12:51:28 -07:00
yuneng-jiang
5e98e8f196
Merge pull request #34469 from BerriAI/litellm_/blissful-torvalds-5a5be3
refactor(ui): migrate mcp-servers, tag-management, tool-policies to shadcn
2026-07-24 10:32:29 -07:00
tin-berri
c742a9007f
Merge pull request #34334 from BerriAI/litellm_connect_page_standalone
feat(ui): standalone /connect route for MCP OAuth, decoupled from Chat UI flag
2026-07-24 10:27:14 -07:00
yuneng-jiang
33b9524daf
Merge pull request #34468 from BerriAI/litellm_/wonderful-northcutt-14b37d
refactor(ui): migrate logging-and-alerts, caching, policies to shadcn
2026-07-24 07:08:27 -07:00
yuneng-jiang
afa8fffd93
Merge pull request #34466 from BerriAI/litellm_/sleepy-pascal-0e7ee6
refactor(ui): migrate access-groups, vector-stores, organizations to shadcn
2026-07-24 07:07:46 -07:00
yuneng-jiang
2bd7c86291
Merge pull request #34465 from BerriAI/litellm_/dazzling-gagarin-3d4c69
refactor(ui): migrate budgets, skills, ui-theme to shadcn
2026-07-24 07:07:08 -07:00
Yuneng Jiang
f7f9dab7d0
fix(ui): make the suggested MCP network range keyboard operable
The suggested CIDR chip was a click-only span both before and after the shadcn
migration, so keyboard users could not reach or activate it. Render it as a
Button, which brings focus and Enter/Space activation with it, and cover the
keyboard path with a test that fails against the old span.
2026-07-23 23:43:24 -07:00
yuneng-jiang
bd753aecf3
Merge pull request #34366 from BerriAI/litellm_/migrate-page-memory-9b2c09
refactor(ui): migrate memory page to shadcn
2026-07-23 23:39:03 -07:00
Yuneng Jiang
bda431f8a8
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/blissful-torvalds-5a5be3 2026-07-23 23:30:25 -07:00
Yuneng Jiang
64f6f45c92
fix(ui): render each policy template parameter once
A template with no LLM enrichment rendered every parameter field twice: the
shared list already covers them, because nonEnrichmentParams is the full
parameter list when there is no enrichment, and a second no-enrichment branch
mapped the same list again.

Predates the shadcn migration and was carried forward by it. The test now
asserts exactly one field per parameter, and fails if the duplicate branch
comes back.
2026-07-23 23:30:18 -07:00
Yuneng Jiang
428d23249a
refactor(ui): migrate mcp-servers, tag-management and tool-policies to shadcn
Replaces antd and Tremor with shadcn primitives across the 18 files these three
routes exclusively own. Markup only: no behaviour, data flow or copy changed, and
no shared or form-bearing component is touched, so the blast radius stops at
these pages.

The 12 tests covering these components are unchanged from the previous commit and
still pass, which is the evidence that the rewrite preserved behaviour. Also
prunes the six antd no-restricted-imports suppressions these files no longer
need.
2026-07-23 23:30:14 -07:00
Yuneng Jiang
68bba5ac0d
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/wonderful-northcutt-14b37d 2026-07-23 23:18:02 -07:00
Yuneng Jiang
b2cf17d4a2
refactor(ui): migrate logging-and-alerts, caching and policies to shadcn
Markup-only migration of the 17 files these three routes exclusively own,
replacing antd and Tremor with the installed shadcn (base-vega) primitives and
lucide icons. No route behaviour changes; the tests written in the previous
commit are untouched here and pass against both the old and the new markup.

Colour now comes from tokens rather than from hardcoded utilities, so the
health-check button, the alerts and the badges no longer pin their own palette.
email_settings also loses an invalid DOM nesting (a table cell inside a div, and
a div inside a paragraph) that React had been warning about.

Two modals on the policies page moved from the Policies panel up to the panel
root. Base UI Tabs mounts only the active panel, unlike Tremor, and both are
opened from the Templates tab, so leaving them nested would have made "Use
Template" do nothing.

Retires 53 antd import suppressions from the eslint baseline.
2026-07-23 23:17:52 -07:00
Yuneng Jiang
59730325da
fix(ui): keep tab panel state across tab switches on the migrated routes
Greptile caught a real regression in the shadcn migration: starting to edit organization
settings and switching to another tab silently discarded the unsaved input.

antd Tabs and Tremor TabGroup mount a panel lazily and then keep it mounted, so a
half-filled form or a search history survives leaving the tab and coming back. Base UI
unmounts inactive panels instead. Its keepMounted escape hatch is not equivalent either:
it mounts every panel eagerly, which renders work the user may never ask for and, on the
organization view, put the organization name on screen twice.

useVisitedTabs reproduces the original semantics by tracking which tabs have been opened
and keeping only those mounted. It is applied to the two tab strips whose panels wrap
stateful children: organization Settings, and the vector-stores Create and Test tabs,
where an in-progress upload or a search history was equally exposed. The access-group
detail tabs render lists derived from props, so they stay lazy.

The added regression test fails without the fix and passes with it, and it also passes
against the pre-migration antd component, so it pins parity rather than the new markup.
2026-07-23 23:16:34 -07:00
Yuneng Jiang
33dc162893
test(ui): pin logging-and-alerts, caching and policies behaviour before the shadcn migration
Establishes the regression net for the upcoming markup migration of these
three routes. Every assertion here is written against the current antd and
Tremor components and passes against them, so it carries no knowledge of the
markup that replaces them and stays meaningful afterwards.

Adds characterisation tests for the seven components that had none, and
rewrites cache_dashboard's chart-card lookup to anchor on each chart's own
title instead of asserting a global count of card nodes, which would break the
moment another card appears on the page.

No component is touched in this commit.
2026-07-23 23:01:09 -07:00
Yuneng Jiang
1ab5ff2360
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/sleepy-pascal-0e7ee6 2026-07-23 22:54:06 -07:00
Yuneng Jiang
a1bacb660f
refactor(ui): migrate access-groups, vector-stores, organizations to shadcn
Moves the nine files these three routes exclusively own off antd and Tremor onto the
shadcn primitives in src/components/ui. Scope came from the migration analyzer's import
closure, so nothing reached by a second route is touched and every file carrying an antd
Form is left alone until #34195 lands.

access-groups gets the page header, search box and the whole detail view; vector-stores
gets the tab shell, the store picker and the tester panel; organizations gets the
organization detail view and the three filter controls.

Two changes are behavioural rather than cosmetic. The vector-stores tab strip moves from
Tremor, which mounts every panel at once, to Base UI, which mounts only the active panel;
that is the correct behaviour and the reworked test now opens the tab it asserts on. The
antd Select on the Test Vector Store tab becomes a combobox rather than a plain select so
its showSearch type-ahead survives.

organization_view keeps one antd import, the ColumnsType used to build the extra columns
it hands to the shared MemberTable; that is dictated by the shared component's API and
goes away when MemberTable migrates. eslint-suppressions.json ratchets down accordingly:
eight files lose their no-restricted-imports entry and organization_view drops from three
to one.

Every test passes unedited across the migration, and the visual gate reports the three
migrated routes changed with the other 32 pixel-identical
2026-07-23 22:54:00 -07:00
Yuneng Jiang
f2d531737a
test(ui): pin mcp-servers, tag-management and tool-policies behaviour before the shadcn migration
Rewrite the two markup-coupled assertions off antd class selectors and onto
role/text queries, and add characterisation tests for the nine route-owned
components that had none. Both rewritten tests and all nine new ones are green
against the current antd and Tremor components, so the migration that follows
can be judged by tests it never touched.
2026-07-23 22:50:04 -07:00
Yuneng Jiang
ab1484b83b
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/dazzling-gagarin-3d4c69 2026-07-23 22:40:31 -07:00
Yuneng Jiang
39f0b56502
refactor(ui): migrate budgets, skills and ui-theme to shadcn
Replaces antd and Tremor with the installed shadcn primitives on the three
route-exclusive panels: Tremor tabs, buttons and text on budgets; the antd
delete Modal and Tremor button on skills; the Tremor card, inputs and buttons
on ui-theme.

Markup only, no behaviour change. The characterisation tests added in the
previous commit are untouched and stay green, and the ui-theme inputs now
carry real label associations.

Shared components stay on antd; they are reached by other routes and are
migrated separately. The form-bearing files on these routes are left alone.
2026-07-23 22:40:25 -07:00
Yuneng Jiang
f231d46375
test(ui): decouple access-groups, vector-stores and organizations tests from antd markup
Prepares the shadcn migration of these three routes by removing every assertion that
depends on the current component library, so the same tests can gate the migration
without being edited.

FiltersButton and its OrganizationFilters consumer both asserted on the ".ant-badge"
wrapper class; they now assert the active-filter indicator element itself, and
FiltersButton additionally asserts that it is absent when there are no active filters.
TestVectorStoreTab drove the antd Select with fireEvent.mouseDown and picked options by
node; it now clicks through the combobox role and the option text, which works against
any listbox implementation.

The vector-stores index test relied on Tremor mounting every TabPanel at once, so it
read the Manage tab's table without ever opening that tab. It now clicks the tab
first, which is what a user does and what any tabs implementation supports.

VectorStoreTester had no test at all, so this adds a characterisation suite covering
the empty state, the blank-query guard, the search call and its rendered result,
result expansion, Enter versus Shift+Enter, the failure path and clearing history.

All of these pass against the current antd and Tremor components
2026-07-23 22:37:18 -07:00
Yuneng Jiang
212421207e
test(ui): characterise budgets, skills and ui-theme panels before migration
Adds a role/text-based characterisation test for UIThemeSettings, which had
none, and extends the skills panel test to cover the delete confirmation.
Both are green against the current antd/Tremor components so they can prove
the shadcn migration keeps behaviour identical without being edited.
2026-07-23 22:25:05 -07:00
tin-berri
2798f39f5a
Merge pull request #34434 from BerriAI/litellm_lit4748_autorouter_logs
feat(ui): show in the log drawer and session sidebar when an auto-router served a request
2026-07-23 22:09:34 -07:00
tin-berri
c93c3f7582
Merge pull request #34439 from BerriAI/litellm_cache_leakage_header_layout
fix(ui): keep cache leakage time range picker inline at narrow widths
2026-07-23 21:53:42 -07:00
Tin Chi Lo
42aba4f32a feat(ui): show in the log drawer and session sidebar when an auto-router served a request
The dashboard already receives the requested model name as model_group on
every spend-log row, but LogEntry dropped the field, so nothing distinguished
an auto-routed request from a direct one.

Surface it precisely rather than by comparing requested against resolved:
model_group differs from model for plain aliases and wildcard deployments
too, so a bare mismatch tags almost every row and identifies nothing. The
indication is driven instead by which deployments are auto-routers, resolved
from every page of /v2/model/info and shared through context.

The request drawer header names the router in a badge next to the provider;
the session sidebar swaps the entry's leading icon. Rows that no auto-router
served render exactly as before.
2026-07-23 20:43:54 -07:00
Tin Chi Lo
dce1b0d1fd fix(ui): allow null for mcp_toolsets in the dashboard key response type
The generated schema declares object_permission.mcp_toolsets as
string[] | null; the handwritten KeyResponse shape omitted the null.
ObjectPermissionsView consumes the same value, so its prop type widens
with it
2026-07-23 18:33:08 -07:00
Tin Chi Lo
a469bb7924 fix(ui): keep a key's MCP toolsets when saving an edit
The key edit form seeded mcp_servers_and_groups from the key with only
servers and accessGroups, but handleKeyUpdate writes mcp_toolsets from
that same value, so every save posted an empty list and the backend
merge applied it literally. A key granted a toolset lost the grant on
any edit, including a budget change, and then got a 403 from
/toolset/<name>/mcp

Read toolsets in both places the form initializes from keyData, declare
mcp_toolsets on KeyResponse.object_permission so a write-without-read is
a type error, and carry toolsets through the create flow, which only
looked at servers and accessGroups
2026-07-23 18:06:57 -07:00
Yuneng Jiang
caac7d8aa8
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/migrate-page-memory-9b2c09 2026-07-23 16:44:34 -07:00
Yuneng Jiang
58083b978c
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/migrate-page-memory-9b2c09 2026-07-23 16:43:15 -07:00
ryan-crabbe-berri
a507394841
fix(ui): find logs by request id across pages and dates (LIT-3981) (#31743)
* fix(spend): resolve spend logs by request_id across all dates (LIT-3981)

The /spend/logs/ui search only filtered the page already loaded, so a log id
copied from another page or from outside the active date window could not be
found. request_id is the primary key of LiteLLM_SpendLogs, so when it is
supplied on the internal UI route the mandatory date window is dropped and the
lookup resolves across all time. The date window stays required when no
request_id is given, and the public /spend/logs/v2 contract is unchanged.

A non-admin id lookup is gated by the same ownership check the detail endpoint
uses, so the relaxed window cannot be used to read another tenant's log by id

* fix(ui): send the logs request_id search to the server (LIT-3981)

The "Search by Request ID" box filtered only the rows already on the current
page, so an id from another page never matched. It now feeds the existing
server-side request_id filter via handleFilterChange, which debounces, resets
to page one, and rides the existing react-query key. The dead client-side
filter and its searchTerm state are removed; the session composition and dedup
logic is unchanged.

The box is now an exact request_id lookup, matching its label; the incidental
client-side model and user substring matching it used to do is dropped in
favor of the dedicated filters

* refactor(spend): model the request_id spend-log lookup as an explicit point lookup (LIT-3981)

The date-window relaxation for request_id lookups rode an apply_date_window flag threaded through the date validation and parsing. Model the two intents directly instead. A UI request_id query is a point lookup on the @id primary key that drops the time window and authorizes by row ownership; every other query, including the public /spend/logs/v2 route, takes the range-scan path that still requires a window

Because the ownership check fully authorizes the single row, the general user/team scoping is now skipped for id lookups rather than layered on top redundantly. The confusing `is_v2 or request_id is None` guard is gone, and moving the date requirement into the range-scan branch lets the type checker narrow the dates it parses

Behavior is preserved: the v2 contract still requires dates even when a request_id is supplied, and a non-owner is still rejected with 403. A regression test covers the non-admin owner id lookup, which resolves across all time and filters by the primary key alone
2026-07-23 16:40:19 -07:00
ryan-crabbe-berri
e906a7e796
refactor(ui): extract shared tab-routing helpers and adopt them in Models + Endpoints (#34435)
* refactor(ui): extract shared tab-routing helpers

Every per-tab-routed page copy-pastes the same URL<->slug logic and the
same active-tab/redirect engine. Extract two reusable pieces:

- createTabRoutes(baseSegment, slugs) in utils/tabRoutes.ts returns
  { baseSegment, slugs, tabHref, slugFromPathname }, the trailing-slash
  href builder (via migratedHref) and the pathname->slug reader.
- useTabRouting({ routes, baseTabKey, visibleKeys, ready }) derives the
  active tab from the pathname, redirects an unknown/forbidden slug to
  base once ready, and returns an onTabChange navigator.

visibleKeys + ready exist so a role-gated page can pass its filtered tab
set and defer the redirect until permissions resolve, rather than
bouncing a user off a still-loading valid tab. Both are pure/unit-tested.
No page consumes them yet.

* refactor(ui): migrate Models + Endpoints onto the shared tab-routing helpers

Replace the page's hand-rolled tabRoutes.ts (base segment + slug tuple +
href builder + slugFromPathname) with createTabRoutes, keeping the
existing named exports as thin re-exports so callers and tests are
unchanged. The layout drops its local activeSlug/isKnownSlug/activeKey
derivation, its redirect useEffect and its router.push onChange in favor
of useTabRouting, passing the role-filtered visibleKeys and a ready flag
(!teamsLoading && !uiSettingsLoading) so the permission-gated redirect
behavior is preserved exactly. The antd tab bar, role-gated tab set, the
refresh button and the ?model=/?team= drill-in overlay are untouched; the
file's pre-existing antd import is now recorded in the suppressions
baseline since editing it makes it a linted-as-changed file.

The existing models-and-endpoints layout.test.tsx and tabRoutes.test.ts
pass unchanged, which is the regression guarantee.
2026-07-23 16:36:25 -07:00
yuneng-jiang
07726b4f60
refactor(ui): migrate agents to shadcn (#34365)
* test(ui): make the agents route's tests markup-agnostic before migration

Rewrites the two assertions that were coupled to antd's DOM and adds the
missing characterisation test for agent_cost_view, so the suite describes
behaviour rather than antd markup and can stay untouched across the shadcn
migration.

The skill selection test reached the checkbox with a querySelector on
input[type=checkbox]; antd renders an input while Base UI renders a
span[role=checkbox], so it now queries by role and accessible name, which
both libraries derive from the wrapping label.

The delete confirmation test queried role=dialog; antd Modal is a dialog
while Base UI AlertDialog is an alertdialog, so it now anchors on the
confirmation text and accepts either role.

agent_cost_view had no test at all; it gets one covering the null render,
the dollar-prefixed values, the omitted rows, and a zero cost that must not
be mistaken for unset.

All 55 tests pass against the current antd components.

* refactor(ui): migrate agents to shadcn

Replaces antd and Tremor with shadcn (base-vega) primitives across the five
files the agents route exclusively owns. Markup only; no behaviour, data
fetching or route structure changes.

Modal becomes AlertDialog, with a plain destructive Button in the footer
rather than AlertDialogAction, because that action is AlertDialog.Close and
would dismiss the dialog before the delete request settles, losing the
in-flight state. Alert, Tag, Spin, Space, Collapse, Descriptions, Typography
and the antd icons map onto alert, badge, ui-loading-spinner, flex/grid
utilities, collapsible, a definition list, semantic headings and lucide.

The shadcn CLI emits alert.tsx importing cva from class-variance-authority,
which this project does not depend on; it uses the cva object syntax from
lib/cva.config. The generated file fails to typecheck, so the adapted copy
lives in components/shared instead, per the convention that ui/ stays
CLI-managed.

Colour comes from tokens throughout, so the info callout is now the neutral
card style rather than antd's blue, and nothing hardcodes a colour in the way
of a later theme change.

The 55 tests in the route pass unchanged from the previous commit. The visual
gate re-baselined agents and all 34 other routes stayed pixel-identical.
2026-07-23 16:35:56 -07:00
Tin Chi Lo
2b77e8c4db fix(ui): keep cache leakage time range picker inline at narrow widths
The card header used flex-wrap, so the date picker was the element that
gave way when the row ran out of room; at higher browser zoom it dropped
onto its own line under the description. Pin the picker with shrink-0 and
let the title/description block shrink instead (min-w-0), so the copy
wraps to a second line and the picker stays on the right. Below md the
header stacks, since a 300px input plus its nowrap label leaves nothing
usable beside it.
2026-07-23 15:38:42 -07:00
tin-berri
43e7b96b83
Merge pull request #33978 from BerriAI/litellm_cost_optimization_tools
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
feat(cost-optimization): add spend-by-tool and cache leakage views
2026-07-23 13:43:23 -07:00
Tin Chi Lo
090fd491d4 feat(cost-optimization): sortable cache leakage columns and clearer token column name
Makes the three metric columns on the cache leakage table sortable, each with a
sensible first-click direction: most uncached tokens and biggest potential
savings first, worst cache hit rate first. Repeat clicks toggle the direction.
Renames Uncached input to Uncached input tokens, since the column is a token
count
2026-07-23 13:22:03 -07:00
Tin Chi Lo
d6d52d95e5 feat(cost-optimization): add by-model view to cache leakage table with plain-language columns
Adds a By virtual key / By model toggle to the cache leakage table. The model
view aggregates the daily activity model breakdown and is scoped to Anthropic
(Claude) models, which support prompt caching. Renames the columns to plain
language: Uncached input, Cache hit rate, and Potential savings (replacing
Realized caching savings and Est. savings left), with a tooltip on Potential
savings that spells out how it is calculated
2026-07-23 12:24:26 -07:00
Tin Chi Lo
531854db4e fix(ui): hold the landing until the role hydrates before deciding the redirect
AuthContext sets token and clears authLoading in one effect, then a
second token-keyed effect populates userRole, so there is a render where
the user is signed in but userRole is still the initial empty string. The
positive internalUserRoles check reads that interim role as non-internal,
which let the api-keys dashboard paint for a frame before the role
arrived and the keyless redirect ran.

Treat "signed in on the post-login landing with an unhydrated role" as a
resolving state that holds the loading screen, so the dashboard never
flashes. Every login=success token carries a required user_role claim, so
the role always hydrates within a tick and this cannot hang; it is scoped
to the landing, so ordinary dashboard visits are unaffected.
2026-07-23 10:53:59 -07:00