Commit graph

5149 commits

Author SHA1 Message Date
ryan-crabbe-berri
f59021f0e0
Merge pull request #39807 from BerriAI/litellm_lit_6690_relax_routing_group_name
fix(ui): accept any routing group name the backend accepts
2026-09-04 14:39:37 -07:00
moe-berri
336891bbc7
Merge pull request #39696 from BerriAI/litellm_bound_classifier_timeout
fix(router): bound auto-router classifier latency
2026-09-04 14:37:52 -07:00
yuneng-jiang
2849aee57d
fix(health): probe test_connection with the credential the request names (#39801)
* fix(health): probe test_connection with the credential the request names

/health/test_connection matches the request's model string against the
configured deployments and merges the match's litellm_params underneath the
request. A request that named a stored credential but no key of its own
still satisfied the "request sets no connection fields" test, so it inherited
the matched deployment's api_key and api_base, and load_credentials_from_list
then skipped the named credential because api_key was already set.

A wildcard route covering the model is enough to match, so the Add Model
page's Test Connect probed with an unrelated deployment's key while echoing
back the credential that was selected.

Naming a credential the configuration does not name now withholds the
configuration's credential fields, the same set already withheld from a
request that supplies its own endpoint. Naming no credential still inherits
them, as documented.

* test(health): drop test docstrings that restate their own names

* test(health): assert the credential probe on the wire, not on the call args

The connection-test regressions patched litellm.ahealth_check and read the
params handed to it. Driving the endpoint through the app with respx faking
the upstream instead lets the real credential resolution run, so the tests
assert the key and host that actually go out, which is what the bug was about.

It also drops three of the five patched proxy internals; the two that are left
are proxy-global wiring with no injection seam, the same ones the image_edit
connection test already has to reach for.

* chore(ui): regenerate schema.d.ts for the test_connection docs change
2026-09-04 14:18:41 -07:00
ryan-crabbe-berri
6f18a4d81e fix(ui): accept any routing group name the backend accepts
The create form rejected names with slashes or spaces even though the
proxy stores and routes any non-empty string. Drop the client-only
character pattern and trim the name before the required check so a
whitespace-only name is still refused

Claude-Session: https://claude.ai/code/session_01HkaXiD6gssHnx3kqu1rR8C
2026-09-04 14:06:34 -07:00
tin-berri
8beca1d58d
fix(auto-router): route 1M complex tier to GPT Sol (#39797)
* feat(ui): add 1M context auto-router preset

* feat(ui): use heuristic v2 for 1M preset

* fix(ui): keep 1M preset test within lint budget

* fix(auto-router): route 1M complex tier to GPT Sol

* test(auto-router): update 1M complex tier expectation
2026-09-04 13:33:06 -07:00
ryan-crabbe-berri
3bdc5ecd0e refactor(ui): drop the per-user usage page clamp now handled by the shared DataTable
The shared DataTable clamps a server-mode page index whenever rowCount no
longer reaches it (#39776), including the empty-dataset case this table's
own clamp skipped because it required total_pages > 0. Remove the local
clamp and cover the empty case through the component so the wiring into
the shared behavior is what the tests prove
2026-09-04 12:47:37 -07:00
ryan
7fde31fe08 fix(ui): fall back to the last page when per-user usage shrinks under the current page
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-04 12:45:26 -07:00
ryan
fd42bddee6 fix(ui): reset per-user usage page in the same render as the tag filter change
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-04 12:45:26 -07:00
ryan
11f272e08b fix(ui): paginate per-user usage with the shared server-side DataTable footer
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-04 12:45:26 -07:00
ryan-crabbe-berri
62087c5d5f
Merge pull request #39776 from BerriAI/litellm_datatable_server_page_clamp
fix(ui): clamp server-paginated DataTable page index when rowCount shrinks
2026-09-04 12:43:38 -07:00
devin-ai-integration[bot]
dd01abc439
feat(team): report per-user spend within a team for JWT traffic (#39771)
* feat(team): report per-user spend within a team for JWT traffic

Add GET /team/spend/by_user, which groups raw spend logs by (team_id, user)
so JWT/SSO requests with no virtual key are attributed to the user inside
each selected team. Team admins see every member, plain members see only
their own row. The Team Usage page gets a Spend Per User Within Team card
with CSV export backed by the same endpoint.

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

* test(team): cover /team/spend/by_user in behavior suite, tf audit allowlist and EntityUsage unit test

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

* refactor(team): drop explanatory docstrings from /team/spend/by_user and regen schema.d.ts

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-04 12:00:47 -07:00
ryan-crabbe-berri
d05d2a6f05 fix(ui): clamp server-paginated DataTable page index when rowCount shrinks
Server-mode tables kept whatever page index the user was on after the
server's total dropped below it, for example after deleting the last
rows of the final page or when a refetch came back empty. The footer
then read "Page 2 of 1" and "Showing 26-25 of 25" with Previous and
First enabled over an empty body, and every one of the 13 server-mode
consumers was exposed since none of them clamped

The shared DataTable now snaps the controlled page index to the last
valid page as soon as a non-loading rowCount no longer reaches it, so
the fix applies to every consumer without per-table clamps. Loading
responses are ignored so a pending fetch never bounces the user to
page 1
2026-09-04 11:33:26 -07:00
Mateo Wang
04a198e3e3
Merge pull request #39568 from BerriAI/litellm_fix-batch-spend-key-double-hash-bcae
fix(spend-tracking): keep batch spend keys joinable after v1.99 provenance gate
2026-09-04 10:47:34 -07:00
yuneng-jiang
c8635ecc67
feat: page the public model hub table off /public/v1/model_hub, keeping every filter (#39691)
* feat(ui): page the public model hub table off /public/v1/model_hub

The public Model Hub page loaded every published model group in one call and
did all of its searching, sorting and filtering in the browser, so a proxy with
a few thousand groups sent megabytes to render one screen.

The models table now asks /public/v1/model_hub for one page at a time. Paging,
sorting, search and the provider and mode filters are query parameters on that
route, and the pagination footer counts from the response envelope's
total_count rather than the rows on screen. Column sortability is derived from
the fields the route declares sortable, so a header can no longer ask it for a
sort it answers with a 400.

The feature filter is dropped: supports_* are booleans and the route has no
boolean filter, so it could only ever have filtered the page in view.

* fix(ui): offer the model modes litellm actually prices in the hub filter

The mode filter listed 'moderations', which no model group's mode is ever set
to, so picking it could only ever return nothing; 'anthropic_messages' was
dead the same way. Four real modes the catalogue does use, search, ocr,
guardrail and vector_store, were missing entirely.

The list is now the mode vocabulary in model_prices_and_context_window.json,
and a test reads that file so an option that matches nothing, or a mode with
no option, fails instead of silently filtering to an empty table. A failed
page fetch logs the route's error detail again, as it did before the table
moved to the paginated route.

* test(ui): keep the model hub health rows out of the inline-object budget

frontend-lint's local/no-large-inline-object-arg budget went 555 to 556: the
health check rows became arguments to the row helper. They are plain literals
spreading a shared default again, which is what they were before, and the gate
reports 554 against a max of 555.

* feat: keep every model hub filter when the table pages

Moving the table onto /public/v1/model_hub cost it two controls the route
could not serve: the provider filter fell back to one substring because
providers only declared contains, and the feature filter went away entirely
because supports_* are booleans with no filter at all. Options for the
dropdowns went with them, since a page of rows only knows the values on that
page.

The route now declares providers in, a features field whose value is the
capability names a row has, and providers, rpm and tpm as sortable. Features
is one repeated field rather than a boolean per flag so selecting two of them
matches either, which is what the multi-select has always meant. Three facet
routes serve the distinct providers, modes and features across the published
groups, carrying the parent's filters, per section 12 of the list design.

All of it is additive: the route rejects unknown parameters, so no request
that worked before changes, and the design's stability policy calls new
filters and parameters safe within a version.

Health status stays unsortable. Health is read for the rows on the page, and
ordering the match set by it would mean reading it for every published group,
which is the cost the paging exists to avoid.

* fix(ui): put the model hub facet types where the generator emits them

The generated file lists paths in sorted order and operations in path order.
Both new blocks were spliced in one entry too late, after
/queue/chat/completions rather than before it, so the schema.d.ts sync check
regenerated the file and found them misplaced. Same blocks, byte for byte,
moved to the position the generator gives them.

* fix(proxy): type a facet payload as the sequence the framework hands it

The lint job's basedpyright gate flagged one new reportArgumentType: handle_facet
passes a tuple, and FacetListResponse declared data as list[str]. A list would
have traded that error for an LIT002 mutable construction, and both budgets are
already at their ceiling on the base.

Sequence[str] is what the framework actually produces and what the model always
accepted: pydantic emits the same array schema either way, verified against
model_json_schema, so the OpenAPI spec and schema.d.ts are unchanged, and the
existing list-passing caller in spend_logs still type checks.

* test(proxy): pin the facet route's rejection contract

handle_facet answers six ways before it ever reaches the executor, and
none of them was covered: a denied scope, a filter operator the spec does
not offer, a repeated parameter, a non-positive page or page_size, and the
where clause those last two feed. Every one is a 400 or 403 an
unauthenticated caller can reach, so each gets a test that fails when the
branch stops firing.
2026-09-03 22:36:16 -07:00
moe-berri
04c6ce9ac3
Merge pull request #39693 from BerriAI/litellm_auto_setup_simple
feat(ui): add one-click Auto Router setup
2026-09-03 20:28:01 -07:00
moe-berri
d5481ca037 fix(ui): respect reported reasoning efforts 2026-09-03 20:01:44 -07:00
moe-berri
510424c86c feat(router): add classifier circuit breaker 2026-09-03 19:59:14 -07:00
moe-berri
901e312b17 style(ui): position Auto Setup before templates 2026-09-03 19:47:44 -07:00
moe-berri
7e2f345a86 style(ui): place Auto Setup under templates 2026-09-03 19:46:07 -07:00
moe-berri
2cb27985d9 fix(ui): refresh Auto Setup model ladders 2026-09-03 19:43:55 -07:00
moe-berri
78c40ed6a7 style(ui): compact Auto Setup control 2026-09-03 19:30:04 -07:00
moe-berri
8acb8de997 refactor(ui): simplify Auto Setup model selection 2026-09-03 19:28:54 -07:00
moe-berri
a2e5e7e066 copy(ui): describe recommended Auto Setup models 2026-09-03 19:03:07 -07:00
moe-berri
72ddd699dd feat(ui): prefer proven models in Auto Setup fallback 2026-09-03 19:00:26 -07:00
moe-berri
a48afd2242 fix(ui): exclude existing Auto Routers from auto setup 2026-09-03 18:48:40 -07:00
devin-ai-integration[bot]
16db51e2cf
feat(caching): add semantic_cache_scope to isolate semantic cache hits per end user (#39590)
Semantic cache keys omit the prompt, so every end user behind one virtual key
shares a bucket and can be served another user's semantically similar response.
Add an opt-in cache_params.semantic_cache_scope (key | end_user) that appends the
authenticated end-user id to the tenant scope, read from metadata and
litellm_metadata so /v1/chat/completions, /v1/responses and /v1/messages are all
covered, falling back to the key scope when no end-user id is present. Expose the
setting in the cache settings API and the Admin UI cache settings form

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 18:44:54 -07:00
moe-berri
940fdfb26b feat(ui): add one-click Auto Router setup 2026-09-03 18:36:28 -07:00
devin-ai-integration[bot]
aec083cdac
feat(proxy): per-worker admission control that rejects excess requests with 503 (#39352)
* feat(proxy): reject excess per-worker requests with 503

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

* refactor(proxy): drop redundant suppressions in admission middleware

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

* fix(proxy): exempt the /metrics/ redirect target from admission control

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

* ci: allowlist live Granian saturation benchmark

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

* fix(proxy): regenerate dashboard API types

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

* fix(proxy): normalize root_path for admission exemptions, validate settings, inject state

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

* test(proxy): cover prometheus metric factory, lifespan scope, and prefix lookalike paths

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

* fix(proxy): queue behind pending waiters, cache admission settings parsing, log invalid limits

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

* refactor(proxy): simplify invalid admission settings handling

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 18:19:04 -07:00
ryan-crabbe-berri
b7d1e89667
Merge pull request #39684 from BerriAI/litellm_lit_4738_table_scrolling
fix(ui): scroll admin table rows inside the table instead of the page
2026-09-03 17:30:53 -07:00
ryan-crabbe-berri
ff97e71652 refactor(ui): type and de-mutate the table scrolling spec, drop CSS narration
DataTable loses the comment that narrated its sticky header classes. The
table scrolling e2e spec now types every management API response it
reads, seeds rows through an immutable reduce instead of pushing into
arrays, and deletes what it seeded in each test's finally block instead
of draining a shared mutable list in afterEach.

Refs LIT-4738

Claude-Session: https://claude.ai/code/session_018yW93iDaEMhoQUXcYjus7D
2026-09-03 17:25:26 -07:00
tin-berri
e26d607f5d
feat(ui): configure auto-router affinity idle TTL (#39679) 2026-09-03 17:22:47 -07:00
ryan-crabbe-berri
dc9f40c11f fix(ui): scroll admin table rows inside the table instead of the page
Virtual Keys, Teams, Request Logs and Tags now hand DataTable a bounded
flex chain and use fillHeight, so the app shell main stays the only page
scroller, the rows scroll under a pinned header and the pagination footer
sits at the bottom of the page. DataTable keeps the sticky header inside
its own scroller in maxBodyHeight mode too, which is what let the header
scroll away with the rows on Keys, Teams and Models. Model Hub, Vector
Stores and the team detail keys tab drop their 75vh boxes and flow with
the page scroller.

Adds an e2e spec that fails on the merge base for every one of those
pages and passes at this tip.

Refs LIT-4738

Claude-Session: https://claude.ai/code/session_018yW93iDaEMhoQUXcYjus7D
2026-09-03 17:13:18 -07:00
mateo-berri
f1f0294796 Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_fix-batch-spend-key-double-hash-bcae 2026-09-03 16:36:12 -07:00
ryan-crabbe-berri
a53c550951
Merge pull request #39661 from BerriAI/litellm_lit_4741_copy_id_search
feat(ui): find rows by a pasted ID on keys, agents, memory, audit, and request logs
2026-09-03 16:17:59 -07:00
ryan-crabbe-berri
9baa19c7d1 fix(proxy): stop hashing raw sk- values in list searches
The search= param on /key/list, /audit, and /spend/logs/ui, plus key_hash= on /key/list, now compare the pasted value verbatim. Only a copied key ID (the hash) matches, so a raw virtual key never needs to travel in a GET query string

Claude-Session: https://claude.ai/code/session_01Q5sbiogJzPcCRmYSbaHxZf
2026-09-03 15:54:42 -07:00
ryan-crabbe-berri
e504477a69 chore(ui): regenerate schema.d.ts for the new search params
Claude-Session: https://claude.ai/code/session_01Q5sbiogJzPcCRmYSbaHxZf
2026-09-03 15:20:55 -07:00
ryan-crabbe-berri
a6b7ef6abe feat(ui): find a row by its pasted ID on every list page
Virtual Keys and Team Virtual Keys send the search box to the new
key/list search param so a key hash matches. Agents matches agent_id
client-side. Memory sends the box as search so a memory_id matches.
Audit Logs gains a search box. Request Logs sends the box as search so
a session, team, user, key hash, or model id matches without opening
the filter drawer.

Claude-Session: https://claude.ai/code/session_01Q5sbiogJzPcCRmYSbaHxZf
2026-09-03 15:20:46 -07:00
devin-ai-integration[bot]
ab44e8d602
fix(team_endpoints): stop partial /team/update from wiping team metadata (#36328)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 14:44:09 -07:00
Mateo Wang
3a00e8020a
Merge pull request #39464 from BerriAI/litellm_containers_error_passthrough_pagination
fix(containers): pass upstream error status through and forward list pagination params
2026-09-03 14:36:44 -07:00
ryan-crabbe-berri
ed9865c945
Merge pull request #39604 from BerriAI/litellm_lit_4738_users_search_by_id
fix(ui): let the Internal Users search box match user_id as well as email
2026-09-03 14:14:22 -07:00
Mateo Wang
5a821b593c
Merge pull request #38440 from BerriAI/litellm_prompt_registry_env
fix(prompts): key the in-memory prompt registry by environment
2026-09-03 14:09:26 -07:00
ryan
792852b4f3 test(ui): name the mocked user list body to stay within the inline-object lint budget
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 21:04:55 +00:00
devin-ai-integration[bot]
aff6b7e212
fix(ui): clear agents when updating team permissions (#39600)
Always serialize object_permission.agents and agent_access_groups in
the team update payload so removing the last agent in the dashboard
sends an explicit empty array instead of omitting the key, which the
backend merge treats as no change

Resolves LIT-6861

Co-authored-by: yassin <yassin@berri.ai>
2026-09-03 13:51:04 -07:00
ryan-crabbe-berri
1c14ded0e4
Merge pull request #39598 from BerriAI/litellm_lit_4929_session_token_aggregation
fix(ui): aggregate session token usage in the logs table
2026-09-03 12:57:27 -07:00
ryan-crabbe-berri
f173a6baf1
Merge pull request #39215 from BerriAI/litellm_lit_6592_team_overview_inherited_mcp_agents
fix(ui): show MCP servers and agents inherited from access groups on team overview
2026-09-03 12:53:27 -07:00
devin-ai-integration[bot]
92122086ec
fix: stop a cleared Organization field from failing key creation (#39316)
* fix: stop a cleared Organization field from failing key creation

Clearing the Organization combobox in the Create Key modal left organization_id set to an empty string, so /key/generate looked up an organization named "" and failed with "Organization doesn't exist in db. Organization=".

OrganizationDropdown now emits null on clear, and GenerateKeyRequest normalizes an empty organization_id or project_id to None the same way it already does for team_id.

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

* test: drop customer-specific docstring from key request normalization test

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yassin <yassin@berri.ai>
2026-09-03 19:12:11 +00:00
devin-ai-integration[bot]
e046aee3d5
fix(spend_tracking): add missing_session_id: omit to leave SpendLogs.session_id null without a client session (#39458)
* fix(spend_tracking): leave SpendLogs.session_id null when no client session id was established

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

* chore(lint): ratchet basedpyright budget after session_id fix

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

* fix(spend_tracking): ignore trace ids as session ids

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

* fix(spend_tracking): gate null SpendLogs.session_id behind missing_session_id: omit

Unset, generate and reject keep the legacy trace id fallback. omit records only
metadata.session_id, the key Langfuse reads, so a trace id copied into
litellm_session_id by get_litellm_params never becomes a session.

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

* fix(spend_tracking): stamp the omit decision on the request so a config reload cannot fabricate a session

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

* fix(spend_tracking): keep omit covering requests the pre-call stamp never reaches

Router-model provider pass-through calls allm_passthrough_route directly and
skips add_litellm_data_to_request, so those requests never run the pre-call
helper and carry no omit stamp. Reading only the stamp made POST
/anthropic/v1/messages write a fabricated uuid into SpendLogs.session_id under
missing_session_id: omit while its Langfuse trace had no session, the exact
divergence the policy exists to remove.

The stamp now only pins omit on, and an unstamped request falls back to the
configured policy, so a config reload still cannot fabricate a session for a
request that was decided pre-call.

* fix(spend_tracking): make the session-omission marker proxy-owned so clients cannot forge it

* fix(spend_tracking): strip the client-sent omission marker from both metadata buckets before they merge

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

* fix(spend_tracking): strip the session-omission marker from both metadata buckets

The pre-call policy ran before litellm_metadata is merged into metadata, so a
client that planted the marker in litellm_metadata had it copied back into the
route's own bucket after the strip and still got a null SpendLogs.session_id.

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-09-03 11:57:56 -07:00
ryan
4d2ffe2e8e test(ui): hoist inherited-grant fixture out of the inline createMockTeamData arg
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 18:38:22 +00:00
ryan
3b13a5fda9 test(ui): query the tokens cell by role instead of walking the DOM
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 18:31:43 +00:00
ryan-crabbe-berri
07c5908b18 fix(ui): let the Internal Users search box match user_id as well as email
GET /user/list gains an optional search query param that ORs a case-insensitive contains match over user_id and user_email. The Users page search box now sends that param and reads "Search by email or ID…", the way the Teams page already searches by name or ID. Every existing /user/list param keeps its meaning and the Filters drawer is untouched

Claude-Session: https://claude.ai/code/session_018yW93iDaEMhoQUXcYjus7D
2026-09-03 11:27:56 -07:00