Commit graph

5196 commits

Author SHA1 Message Date
moe-berri
b3f28a77d8
Merge pull request #39823 from BerriAI/litellm_auto_router_compression_split
feat(auto-router): decouple compression between the routing decision and the model call
2026-09-05 12:35:37 -07:00
ryan-crabbe-berri
ee5d66d030 fix(ui): keep Back to Guardrails reachable when a ?guardrail= link is stale
With the selection in the URL, a mistyped or deleted guardrail id lands
on the info view's not-found branch, which rendered only the message and
left no way back to the table short of editing the address bar. The
not-found branch now shares the Back to Guardrails button with the
loaded view
2026-09-05 11:56:56 -07:00
ryan-crabbe-berri
86038318ce feat(ui): deep link guardrail detail with ?guardrail= on guardrails pages
The Guardrails and Guardrails Monitor pages kept the selected guardrail
in local React state, so the detail view could not be shared, reloaded,
or reached with the browser back button. Both pages now read and write
the selection through the nuqs `guardrail` query param, matching how the
keys, teams, orgs, projects, users, models and logs pages deep link their
detail views. Opening a guardrail pushes a history entry and closing it
replaces the entry so back returns to the page the user came from
2026-09-05 11:43:52 -07:00
moe-berri
ff942c3a74 fix(auto-router compression): surface a stored model-only policy in the edit form
The backend treats either compression key on its own as an authoritative policy, but
hydrate returned the untouched inherit state whenever the routing key was absent. A
config carrying only auto_router_model_compression was therefore invisible in the
form, and picking a routing value then overwrote the stored model hop. Only neither
key set now reads as untouched, and an absent key on either hop hydrates as no
compression for that hop rather than same-as-the-other.
2026-09-05 11:05:50 -07:00
moe-berri
d143b1954e Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_auto_router_compression_split 2026-09-05 10:19:48 -07:00
ryan-crabbe-berri
d885104125 chore: merge litellm_internal_staging into litellm_lit_4738_table_pagination
per_user_usage.tsx conflicted with the server pagination that already landed on
staging (default 50 rows, stale-response guard, tag and page-size resets). Took
the staging version and dropped this PR's now-redundant 25-row test for it

Claude-Session: https://claude.ai/code/session_01HkaXiD6gssHnx3kqu1rR8C
2026-09-05 10:08:11 -07:00
moe-berri
f4329d5491 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_auto_router_compression_split
# Conflicts:
#	ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx
2026-09-05 10:06:01 -07:00
Mateo Wang
275484fb09
Merge pull request #39856 from BerriAI/litellm_ui_lint_inline_object_budget
fix(ui): bring the inline-object lint budget back under its ceiling
2026-09-05 09:42:30 -07:00
moe-berri
00b49ccc8b fix(auto-router compression): honor the policy on the SDK path and on re-save
The router reused the model hop's compression for routing whenever both hops named
the same guardrail, on the premise that arm_pre_call had already run it. Only the
proxy calls arm_pre_call, so through the SDK nothing armed the guardrail and nothing
had compressed anything: the shortcut skipped routing compression too and served the
request with no compression on either hop. The reuse is now conditional on the model
hop actually having been armed.

The Admin UI hydrated an absent auto_router_model_compression as same-as-routing,
while the backend reads it as no model-hop compression. Opening a router configured
with only auto_router_routing_compression and saving any unrelated edit wrote the
routing guardrail onto the model hop, silently starting to compress the model call.

Both carry a regression test that fails when the fix is reverted.
2026-09-05 09:31:18 -07:00
mateo-berri
5c80e308cd Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_gate_openai_ws_passthrough 2026-09-05 01:26:47 -07:00
mateo-berri
05c4e16b48 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_ui_lint_inline_object_budget
# Conflicts:
#	ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
2026-09-05 00:44:04 -07:00
tin-berri
f3cf557898
feat(dashboard): configure classifier vision input (#39840) 2026-09-04 23:57:53 -07:00
moe-berri
f03f82381e merge origin/litellm_internal_staging, keep the reportPrivateUsage suppression 2026-09-04 21:03:28 -07:00
tin-berri
8b6ea72845
feat(shadow_eval): scope a job to model groups, ANDed with its key, team, and user targets (#39828)
A shadow eval job could only be scoped by identity, so "this user's traffic on model X
across every key they own" was not expressible and a models field on the start body was
silently dropped. The job now carries a models list that every target is narrowed to,
matched on the requested model group with model_group_alias resolved on both sides. An
unresolvable name is a 400 at start. Empty means every model, which is what every existing
row reads as. The dashboard start form gains an "Only on models" picker and the job
headline shows the scope.
2026-09-04 20:50:46 -07:00
mateo-berri
c27f1e348d fix(ui): name the object arguments at two new call sites to bring the inline-object lint budget back under its ceiling 2026-09-04 19:47:31 -07:00
ryan-crabbe-berri
2151dcbd73
Merge pull request #39822 from BerriAI/litellm_lit_6594_access_group_resource_names
feat(access-groups): resolve resource names on access group responses
2026-09-04 19:03:20 -07:00
tin-berri
d8ca43a800
feat(complexity_router): let the LLM classifier see request images (#39825)
The classifier scores extracted text, so a turn whose complexity lives in
its image is invisible to it: a screenshot of a stack trace classifies on
its caption, and an image-only turn flattens to empty text and never
reaches the classifier at all.

classifier_llm_config.vision opts in, off by default, with max_images
bounding what one turn can add. Images are still dropped when the
classifier model is declared supports_vision false. Anthropic and
Responses image parts are rewritten into chat-completions dialect before
they reach the classifier call, since /v1/messages hands the pre-routing
hook its own dialect untranslated.

The local scorer no longer short-circuits heuristic_first or hybrid on a
turn carrying forwarded images, because it reads text alone and its
confidence describes a request it has only partly seen.
2026-09-04 18:59:50 -07:00
moe-berri
639b3f4f62
Merge pull request #39809 from BerriAI/litellm_stall_escalation
feat(router): auto-escalate stalled complexity-router tasks
2026-09-04 18:44:25 -07:00
yuneng-jiang
e733ca1065
Merge pull request #39811 from BerriAI/litellm_/mongodb-vector-store-e4ff63
feat(vector_stores): add a MongoDB vector store provider for Atlas and self-managed deployments
2026-09-04 18:19:03 -07:00
mateo-berri
a2d5215a4f fix(proxy): gate the OpenAI websocket passthrough behind an explicit opt-in 2026-09-04 18:14:44 -07:00
ryan-crabbe-berri
be76dfad9c
Merge pull request #35853 from BerriAI/litellm_retry_policy_503
fix(router): resolve retry_policy by exception hierarchy, add ServiceUnavailableErrorRetries and DefaultRetries
2026-09-04 17:34:00 -07:00
moe-berri
e273cf301f fix(ci): satisfy ruff format, prettier, and eslint max-lines gates
- ruff format on auto_router_compression.py (a long comprehension wrapped
  across three lines instead of one)
- prettier on buildAutoRouterCompression.ts and the two test files it touched
- ComplexityRouterConfig.tsx crossed the 800-line eslint max-lines ceiling
  once the compression accordion entry landed. Extracted TierRowSelect into
  its own file (already self-contained, used only within this file and
  PlanModeOverrideControls) and simplified CompressionControls' props to a
  single state/onChange pair instead of six individual callbacks, moving the
  per-field derivation into the component that already owns this state shape
2026-09-04 17:21:04 -07:00
tin-berri
b3c867c7b2
fix(auto_router): derive tier definitions in prompt editor (#39688) 2026-09-04 16:48:33 -07:00
moe-berri
9e286fe94b fix(auto-router): close review findings on per-hop compression
- Suppression markers now carry the per-process token `_pre_call_marker`
  already uses, so a caller cannot switch off an always-on PII, content-filter
  or compression guardrail by naming it in its own request metadata.
- Routing set to "none" with the model side compressed now classifies on the
  pre-compression snapshot instead of the model-side guardrail's output.
- Both the proxy's pre-call arming and the router's routing hook resolve the
  policy through one tag-aware `policy_for_model`, so an alias with several
  tag-scoped markers can no longer suppress one marker's guardrail and then
  route under another marker's policy.
- The pre-compression snapshot moved from request metadata to a ContextVar:
  `refresh_proxy_server_request_body_snapshot` copies metadata into
  `proxy_server_request.body`, which deployments persist, and the snapshot
  holds the prompt as it was before any masking guardrail rewrote it.
- The compression selector lists Compresr guardrails too, not just Headroom.
2026-09-04 16:42:11 -07:00
moe-berri
dd60b7e40f feat(auto-router): decouple compression between the routing decision and the model call
An auto router marker deployment can now set auto_router_routing_compression
and auto_router_model_compression in its litellm_params, naming the
compression guardrail each hop should use (or "none" for no compression on
that hop). Neither key set means the request's own compression guardrails
keep applying to both hops unchanged.

Backend: Router.async_pre_routing_hook resolves the marker's policy and
compresses a copy of the messages for the routing decision only when the
policy differs from what the model call already got; when both hops share
the same compression, it reuses what the ordinary pre-call guardrail
pipeline already produced instead of compressing twice. The proxy layer
suppresses every other compression guardrail once a policy is engaged and
arms the model-side guardrail even when it is not default_on.

UI: the auto router's Detailed Configuration gains an Advanced: Compression
section with a routing-decision selector and a same/different toggle for
the model call, matching the same/different address pattern.
2026-09-04 16:16:38 -07:00
moe-berri
544822b1a8 fix: escalate a stalled keyword-forced tier, and let a blocked toggle clear
Two issues Bugbot found on #39809.

A keyword_tier_rule forces its tier and returns before any classification
runs, so stall escalation never reached that path even though keyword
escalation did. That left the one path that can pin a weak model to a
whole conversation as the one path a stall could not lift. Stall
detection now resolves before the override branch and both paths bump.

The dashboard switch disabled itself whenever session pinning or
user-turn classification was on, including for a router that already had
stall escalation enabled. The conflicting keys stayed set, the backend
rejected the save, and the disabled switch was the only way to clear
them. It now disables only the off-to-on direction.
2026-09-04 16:14:28 -07:00
ryan-crabbe-berri
b29f9a94bc refactor(router): resolve retry policy by exception MRO and add DefaultRetries
Replace the hand-ordered isinstance ladder in get_num_retries_from_retry_policy
with a class-to-field mapping walked along the exception's MRO, most specific
class first. A RetryPolicy field can no longer go silently dead the way
InternalServerErrorRetries did, and subclasses such as
ContentPolicyViolationError or MidStreamFallbackError pick up their parent's
field when they have none of their own.

Add a DefaultRetries catch-all so errors without a dedicated field
(BadGatewayError, APIConnectionError, NotFoundError, ...) can be governed by the
policy too. Specific fields still win over DefaultRetries.

Wiring the previously dead InternalServerErrorRetries changes one test
expectation: a policy of 2 now overrides a per-deployment num_retries of 5, so
the amplification test sees 3 upstream requests instead of 6.

Expose DefaultRetries as "All other errors" in the Admin UI retry settings tab
and ratchet the lint budgets down by the violations this branch fixed.
2026-09-04 16:09:01 -07:00
ryan-crabbe-berri
c5c10bc91f feat(access-groups): resolve resource names on access group responses
The access group detail page rendered MCP servers, agents, attached teams and keys as bare ids, so an admin had to look each one up elsewhere to audit a group

Every access group response now also carries access_mcp_servers, access_agents, assigned_teams and assigned_keys as {id, name} pairs. Names come from the DB rows first and fall back to config-declared MCP servers and agents (including legacy agent ids), resolved with one query per table across all groups in a list call. The existing *_ids columns are unchanged

The UI renders the name with the id in a tooltip, links teams and keys to their detail pages, and shows the raw id only when nothing resolves
2026-09-04 16:08:50 -07:00
moe-berri
939039f492 fix(router): anchor stall detection on the newest tool call
Counting whichever pattern was most common across the window escalated a
task that had already recovered: three identical failures stay in the
window for a few turns after the model breaks out of them, and on their
own they met the threshold.

Both tests now anchor on the newest call. The repeat test counts calls
matching the newest one, and the error test only runs while the newest
call is itself an error, so a window whose recent calls are healthy no
longer escalates. The matches still do not have to be adjacent, so a
retry loop broken up by an unrelated lookup keeps counting.

Found by Greptile on #39809.
2026-09-04 16:03:20 -07:00
ryan-crabbe-berri
2de21d0695 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_retry_policy_503
# Conflicts:
#	litellm/types/router.py
#	tests/test_litellm/test_router.py
2026-09-04 15:58:04 -07:00
moe-berri
01b55daee9 feat(ui): add stalled task escalation controls to the auto-router form
Adds an "Advanced: Stalled Task Escalation" section to the complexity
router config: a toggle plus the repeat threshold and the window of recent
tool calls to examine. Both knobs are seeded on enable and cleared on
disable, so an off router sends none of the three keys, which is what the
backend requires next to session pinning and a custom tier set.

The toggle locks out with an explanation when "How often to classify" is
set to once-per-session or new-user-message, since both replay a held
routing decision instead of classifying and a stall would never reach the
classifier. The keys join the custom-tier restriction registry, which both
strips them from a custom-tier save and marks the section restricted.

ResponseFormatControls moves into its own file to keep
ComplexityRouterConfig.tsx under the 800-line lint ceiling, matching the
one-file-per-control layout its siblings already use.
2026-09-04 15:31:54 -07:00
ryan-crabbe-berri
d23bec84c4
Merge pull request #39196 from BerriAI/litellm_guardrail_usage_cost_rollup
feat(guardrails): roll up Bedrock guardrail cost per usage counter
2026-09-04 15:21:16 -07:00
tin-berri
6dff3a5f72
fix(complexity_router): fall back to a live peer when the decided tier model is fully cooled down (#39675)
A complexity tier can name several model groups, but the pool pick and the session-pin
replay both returned a group without consulting deployment health, so a group whose every
deployment was in cooldown was still routed to and the request died at the router's
zero-deployment check while a healthy peer sat in the same tier.

Gate the decided response at the pre-routing hook's exits, the seam the modality gate
already occupies, so every arm that can place a request is covered by one owner: a fresh
classification, a replayed or escalated pin, a plan-mode floor, a context-window
escalation, an adaptive pick, and whatever arm is added next.

Peers come from the decided tier only. Climbing to a higher tier costs more than the
classifier asked for and is left to a follow-up. The gate fails open on every uncertainty:
an unreadable cooldown view, a decision carrying no tier, a group the router knows no
deployments for, or a tier whose peers are all cooling.
2026-09-04 22:11:50 +00:00
ryan-crabbe-berri
9bd34adb6d Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_guardrail_usage_cost_rollup
# Conflicts:
#	type-discipline-budget.json
2026-09-04 14:41:55 -07:00
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
ryan-crabbe-berri
6c81a5c423 feat(guardrails): store untracked units on the rollup row instead of nulling cost
A row that received both priced and unpriced increments used to collapse
to cost NULL, throwing away the priced subtotal and making every unit on
it read as untracked. The rollup now carries a second column,
untracked_units, that the aggregator increments for units with no known
price while cost keeps accruing for the rest, so cost covers exactly
units - untracked_units. Rows written before the migration keep cost
NULL and still read as untracked in full

The endpoints read untracked units off the column (or the whole row for
a legacy NULL) rather than from a NULL filter, and the policies overview
now fills totalUntrackedUsageUnits, which the previous commit missed

Claude-Session: https://claude.ai/code/session_01EX13mWex6RaBo9PYnkAtFW
2026-09-04 14:38:08 -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
38cd1bff7b
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/mongodb-vector-store-e4ff63 2026-09-04 14:20:31 -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
1548be8235 feat(guardrails): report the usage units a guardrail's cost leaves out
A row's cost sums only the daily rows that carry a tracked cost, so it
silently under-reports whenever some rows are NULL (pre-migration days,
old pods mid-rollout, an unpriced counter). Both usage endpoints now
return the per-counter units behind those NULL rows next to the cost
(untrackedUsageUnits / totalUntrackedUsageUnits on the overview,
untracked_usage_units on the detail), so a partial cost is never mistaken
for a complete one and the reader can see exactly what it excludes

Claude-Session: https://claude.ai/code/session_01EX13mWex6RaBo9PYnkAtFW
2026-09-04 14:09:59 -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
Yuneng Jiang
431579dc16
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/mongodb-vector-store-e4ff63
# Conflicts:
#	.github/workflows/_test-unit-base.yml
#	litellm/litellm_core_utils/sensitive_data_masker.py
#	tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py
#	uv.lock
2026-09-04 14:00:10 -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