Commit graph

47214 commits

Author SHA1 Message Date
moe-berri
1bcfdaf7cb fix(model management): only proxy admins may set auto router compression fields 2026-09-05 13:12:41 -07:00
cursor[bot]
8544faec91
fix(ci): grant pull_requests write for release wheel reporter (#39922)
* fix(ci): grant pull_requests write for release wheel reporter

The reporter posts a PR comment via github.rest.issues.createComment.
GitHub requires both issues=write and pull_requests=write to comment on
a PR issue, as returned in x-accepted-github-permissions. The workflow
had pull-requests: read, so the POST failed with 403 'Resource not
accessible by integration'. Bumping to pull-requests: write fixes the
create path; the read-only pulls.get call still works. Same-repo scope
is preserved by the existing head_repository.full_name check.

Co-authored-by: Krrish Dholakia <krrish-berri-2@users.noreply.github.com>

* fix(ci): scope release wheel reporter permissions to pull requests

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Krrish Dholakia <krrish-berri-2@users.noreply.github.com>
Co-authored-by: Yujong Lee <yujong@berri.ai>
2026-09-05 13:04:31 -07:00
yucheng-berri
948e5755eb
test(e2e): cover presidio post_call, tool_permission, and weave logging cells (#39279)
* test(e2e): cover presidio post_call, tool_permission, and weave logging cells

Five registry cells in Logging & Guardrails had no covering test. Each one now
has a live scenario read back from the real destination:

- guardrail.presidio.post_call.masks: an output-scoped Presidio guardrail
  anonymizes the PII the model repeats back. The prompt also asks for the
  address's local part, which Presidio does not mask, so one response proves the
  model saw the raw address (no pre-call masking) while the address itself comes
  back as <EMAIL_ADDRESS>
- guardrail.tool_permission.pre_call.blocks / .allows: an allow-list of one tool.
  A request declaring an unlisted tool is rejected 400 naming it; a request
  declaring the permitted tool is served and carries
  x-litellm-applied-guardrails, so the allow half cannot pass by the guardrail
  never running
- logging.niche_integrations.success.logs_spend / .failure.logs_spend: a
  key-scoped weave_otel callback delivers to the real Weave project, read back
  through Weave's query API. Success asserts exactly one call whose
  llm.response.cost equals the x-litellm-response-cost header; failure asserts
  one ERROR-status call naming the provider exception and carrying no cost

Logging & Guardrails coverage goes 24/59 to 29/59. No registry rows are added.

* test(e2e): make the tool-permission allow case deterministic and scope the Weave read-back

Review follow-ups on the coverage PR.

- the allow scenario forced the outcome to depend on whether the model felt like
  calling an optional tool, and checked for the tool name as a substring of the
  whole body, which a prose mention would satisfy. It now sends
  tool_choice="required" and asserts the parsed response carries exactly one tool
  call, for the permitted tool
- the Weave read-back queried the newest 200 calls of a shared project and
  filtered client-side, so busy traffic could push the target out of the window
  and read as a delivery failure. The query now scopes server-side to the
  litellm_request op and to calls started after the request, and pages through
  the window with offset
- the reader builds its results as tuples instead of accumulating into lists

Also unblocks the lint gate: `basedpyright tests/e2e` runs only on PRs that touch
tests/e2e, and it has been failing on staging for three FakeItem arguments in
test_junit_properties.py. The stand-in now goes through one typed adapter that
says why, so the gate is green without touching junit_properties.py itself.

* test(e2e): scope the presidio post_call guardrail to email and phone

Running the suite three times in a row caught a real flake: Presidio's broader
recognizers sometimes claim the email's local part as an NRP entity, so the
answer came back as `<NRP>\n<EMAIL_ADDRESS>\n<PHONE_NUMBER>` and the assertion
that the raw local part survives failed. That token is what tells output masking
apart from input masking, so it has to survive.

The post_call guardrail now registers pii_entities_config for EMAIL_ADDRESS and
PHONE_NUMBER only, which is also the narrower thing the scenario means. Verified
against the exact marker that failed, plus two others.

* test(e2e): mark weave logging cells stage red

* test(e2e): use per-test stage red skips for the weave logging cells
2026-09-05 13:03:28 -07:00
devin-ai-integration[bot]
c091dd4608
perf: lazy-load SDK symbols so import litellm stays under 60 MB RSS (#39121)
* perf: lazy-load SDK symbols so import litellm stays under 60 MB RSS

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

* fix: resolve litellm.proxy submodules lazily so litellm.proxy._types stays importable

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

* fix: correct SlackAlerting lazy mapping and keep eager encoding path importable

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

* fix: register module-valued public names as module aliases instead of symbol imports

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

* chore: justify module-alias cache write with rebind-ok

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-05 19:58:35 +00:00
Yassin Kortam
17e13126cc
feat(mcp): warn when an oauth2_id_jag server outruns the SSO provider's assertion capture (#35394)
* feat(mcp): warn when an oauth2_id_jag server outruns the SSO provider's assertion capture

Only the generic OIDC login path captures the IdP id_token that an oauth2_id_jag MCP
server spends as its RFC 8693 subject token. Under Google, Microsoft, SAML or no SSO at
all, registration succeeds and then every ID-JAG credential resolution fails for every
user, with nothing in the logs, the config or the API response to say why.

Report the mismatch from the two places it is knowable: when an oauth2_id_jag server is
created or updated through the management endpoint, and at SSO callback time when a login
hands the arm nothing while such a server is registered. Provider selection mirrors the
callback's precedence, so a generic client id sitting behind GOOGLE_CLIENT_ID does not
clear the warning.

* test(sso): update merged CLI diagnostic patch target

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

* feat(mcp): warn about the ID-JAG capture gap for config-declared servers and on the SSO debug page (#39350)

* feat(sso): surface the ID-JAG capture gap on the SSO debug page

/sso/debug/callback is where an operator lands when they are already trying to work out
why ID-JAG is failing, so the reason belongs on it. The annotation appears only when the
active SSO provider captures no identity assertion AND an oauth2_id_jag server is
registered for that gap to break; a deployment without both renders the page it rendered
before, byte for byte. Only the provider name and the remedy are rendered, never a
configured value, and an unreachable MCP table costs the page its annotation rather than
the page itself.

The payload carries the one mutable-ok in this work. Conditionally including a member of a
JSON document has to construct a mapping, and the rejected alternatives are recorded on the
helper so the next reader does not rediscover them.

Held out of the diagnosability PR deliberately: that PR is already reviewed and green, and
this surface ships with the remaining config-load warning as one follow-up.

* feat(mcp): warn at config load when an oauth2_id_jag server outruns the SSO provider's assertion capture

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

* refactor(sso): trim comments on the ID-JAG debug page diagnostic

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

* fix(sso): clean up merged imports

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

* fix(sso): satisfy type discipline for diagnostic payload

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

* style(sso): keep the optional ID-JAG payload member on one line for ruff format

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

* fix(sso): use Python 3.10-compatible assert_never

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

---------

Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(sso): keep the ID-JAG capture-gap diagnostic out of the unauthenticated debug page

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

* test(sso): inject the retention check and log via caplog so the ID-JAG tests pass the test-quality gate

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

* test(sso): keep the debug-page outage test on the capture-gap path

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

* fix(sso): annotate the retention check type alias

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>
2026-09-05 12:43:09 -07:00
Yassin Kortam
110f654f34
feat(mcp): renew the stored SSO identity assertion behind ID-JAG (#35401)
* feat(mcp): renew the stored SSO identity assertion behind ID-JAG

The oauth2_id_jag arm asserts the id_token captured at the user's last
interactive SSO login, and nothing ever renewed it, so an agent holding a
brokered LiteLLM key could act for that user only until that token's exp.
The assertion already carried the IdP refresh token beside it; this
redeems it.

RefreshingSSOAssertionStore wraps the database reader and satisfies the
same protocol, so the egress arm is unchanged. Renewal is lazy and
single-flighted per user through the same RefreshCoordinator the
authorization_code arm uses, since an IdP that rotates refresh tokens
treats two concurrent redemptions as replay. A refusal leaves the expired
assertion in place so the reader still challenges the user; an
unreachable IdP surfaces as a store outage instead.

* fix(mcp): let a cross-replica loser settle the SSO assertion renewal itself

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

* fix(mcp): satisfy type discipline lint budget

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

* chore(ci): rerun checks after docs main added the missing router setting row

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

* fix(mcp): answer a cross-replica loser retryable instead of re-electing it

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

* fix(mcp): bypass stale assertion cache during renewal

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

* chore: ratchet type-discipline budget after merge

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

---------

Co-authored-by: Yassin Kortam <yassin.kortam@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-05 12:43:02 -07:00
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
yuneng-jiang
49a1145f6e
Merge pull request #39934 from BerriAI/litellm_e2e_ui_scroll_specs_page_size
test(e2e/ui): select 50 rows per page before asserting the Tags and Model Hub tables overflow
2026-09-05 12:33:00 -07:00
yuneng-jiang
c6399b5728
Merge pull request #39917 from BerriAI/litellm_e2e_key_mgmt_route_group_coverage
test(e2e): cover key spend reset, regenerate grace period, and the llm_api_routes grant
2026-09-05 12:30:59 -07:00
yuneng-jiang
29cf4e8520
Merge pull request #39920 from BerriAI/litellm_e2e_prompt_cache_cohere_passthrough_coverage
test(e2e): cover Anthropic and OpenAI prompt caching, Cohere embeddings, and costed /openai chat passthrough
2026-09-05 12:27:21 -07:00
devin-ai-integration[bot]
b290dd410e
feat(terraform/gcp): dependencies-only mode and bring-your-own-network for GKE (#39695)
* feat(terraform/gcp): add dependencies-only mode

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

* fix(terraform/gcp): review fixes for dependencies-only mode

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-05 12:22:52 -07:00
yuneng-jiang
730d789418
Merge pull request #39932 from BerriAI/litellm_/circleci-pipeline-failure-afe848
test: repair two CI tests broken by intentional changes
2026-09-05 12:22:40 -07:00
ryan-crabbe-berri
86e54a0454
Merge pull request #39930 from BerriAI/litellm_guardrail_detail_deep_links
feat(ui): deep link guardrail detail with ?guardrail= on guardrails pages
2026-09-05 12:22:01 -07:00
yucheng-berri
877197918b
fix(cloudzero): preserve late resource tags (#39873)
* fix(cloudzero): infer daily batch schema from every row

pl.DataFrame defaults to inferring column types from the first 100 rows,
so a day whose batch starts with more than 100 rows missing team_alias,
api_key_alias or user_email typed that column as Null and then raised a
ComputeError on the first row that had a value, failing the whole export
with a 500 and sending nothing.

Pass infer_schema_length=None when rebuilding each day's DataFrame, the
same guard the usage query already uses.

* test(cloudzero): cover late tag schema inference

Exercise the CloudZero resource tag field after a long run of missing values so a finite inference window fails the regression test.

* fix(cloudzero): preserve late resource tags

* style(cloudzero): remove redundant test comment
2026-09-05 12:10:05 -07:00
yucheng-berri
73e1cfb378
fix(cloudzero): infer daily batch schema from every row (#39871)
* fix(cloudzero): infer daily batch schema from every row

pl.DataFrame defaults to inferring column types from the first 100 rows,
so a day whose batch starts with more than 100 rows missing team_alias,
api_key_alias or user_email typed that column as Null and then raised a
ComputeError on the first row that had a value, failing the whole export
with a 500 and sending nothing.

Pass infer_schema_length=None when rebuilding each day's DataFrame, the
same guard the usage query already uses.

* test(cloudzero): cover late tag schema inference

Exercise the CloudZero resource tag field after a long run of missing values so a finite inference window fails the regression test.
2026-09-05 12:09:53 -07:00
Yuneng Jiang
5298deb491
test(e2e/ui): select 50 rows per page before asserting the Tags and Model Hub tables overflow
#39680 made every admin table honor the selected page size, so the Tags and Model Hub tables now paginate at 25 by default and the two scroll specs, which seed 40 rows and expect them all on one page, fail on every litellm-e2e-ui run since (builds 206 to 208). Selecting 50 rows first, the way the Request Logs spec already does, keeps the overflow assertion meaningful
2026-09-05 12:05:52 -07:00
devin-ai-integration[bot]
0ad361a728
fix(router): coordinate async and sync failure handlers at remaining router call sites (#39887)
* fix(router): coordinate async and sync failure handlers at remaining router call sites

Five router failure paths still scheduled logging_obj.async_failure_handler
as a task while starting logging_obj.failure_handler on a raw thread, so
both handlers mutated the same logging object concurrently. Route them
through dispatch_failure_handlers like the streaming paths already do.

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

* test(router): wait on the real logging executor and justify the callbacks global patch

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

* fix(logging): submit sync failure handler even when the dispatch task is cancelled

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

* test(logging): justify the executor submit patch

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-05 19:02:55 +00:00
Yuneng Jiang
da5af0cb27
test: repair two CI tests broken by intentional changes
test_no_linear_scans_in_router: #39674 renamed heuristic_v2_router_limit_violation
to auto_router_capability_violation, so the allowlist entry stopped matching and the
same admin-only scan tripped the static check. Rename the entry to follow it.

tableScrolling.spec.ts: 9ba6cab889 (LIT-4738) gave the Tags and Model Hub tables
client-side pagination at 25 rows, so the 40 seeded rows no longer render on one
page. Select 50 rows per page before counting, as the Logs case already does.
2026-09-05 12:02:48 -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
yuneng-jiang
95a1d301b6
Merge pull request #39916 from BerriAI/litellm_e2e_anthropic_chat_completions
test(e2e): cover Anthropic /chat/completions streaming and tool calls
2026-09-05 11:56:24 -07:00
moe-berri
fc3da5e830
Merge branch 'litellm_internal_staging' into litellm_auto_router_compression_split 2026-09-05 11:55:05 -07:00
Yuneng Jiang
45cc2ed082
test(e2e): require a 200 inside the regenerate grace window and drop the helper docstrings 2026-09-05 11:53:22 -07:00
devin-ai-integration[bot]
a670a4621e
fix(proxy): make the invalid-model 403 path cheap under a burst of rejections (#39892)
* fix(proxy): make the invalid-model 403 path cheap under a burst of rejections

Keep the wildcard pattern registry in specificity order at registration time
so route() no longer re-sorts every pattern per lookup, and reuse the
standardized failure payload across the async and threaded sync failure
handlers regardless of what a callback did to log_event_type. Rejections
are still logged and observable.

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

* style(router): wrap the filtered pattern tuple the way ruff format wants

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

* test(router,logging): assert registry order and callback awaits instead of patching a class

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

* test(router): inject the pattern sorter so the lookup test observes that route() never sorts

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-05 11:53:04 -07:00
devin-ai-integration[bot]
3c0900b7c5
perf(logging): scan large base64 payloads for log truncation off the event loop (#39890)
* perf(logging): scan large base64 payloads for log truncation off the event loop

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

* perf(logging): make base64 offload threshold a plain constant

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-05 11:51:15 -07:00
yucheng-berri
a0058ed157
fix(hide-secrets): stop redacting benign identifiers (#39879)
* fix(hide-secrets): stop redacting benign identifiers and make redaction deterministic

The OpenAI key detector matched `sk-` anywhere inside a word, so `<task-notification>`
became `<ta[REDACTED]>`, and the Base64 entropy limit of 3.0 flagged ordinary quoted
identifiers such as `"application/json"` and model ids. Redaction also iterated a
hash-seeded set, so the same request produced different bytes on different workers and
broke prompt caching.

- require a standalone `sk-`/`sk_` token with a digit (still catches sk-proj-/sk-ant-)
- raise Base64HighEntropyString limit from 3.0 to the detect-secrets default 4.5
- redact overlapping matches longest-first in a stable order

Resolves LIT-7049

* fix(hide-secrets): treat separators as key boundaries and defer sk_live_ to the stripe detector

The standalone-token boundary also rejected keys glued to a preceding `_`, `-`
or percent-encoded delimiter (`openai_sk-…`, `key-sk-…`, `Bearer%20sk-…`), which
the old pattern redacted, and `sk_live_…` was counted by both the OpenAI and the
Stripe detector.

* fix(hide-secrets): keep the openai key scan linear on repeated sk separators

The digit requirement was a lookahead, so every `sk` inside a long
`[a-zA-Z0-9_-]` run re-scanned the rest of that run looking for a digit.
100 KB of `-sk-` took over 5s in the worker's event loop and the proxy
closed the connection without a response. The check now runs once per
match in `analyze_string` instead.

* chore(hide-secrets): remove redundant performance test comment

* fix(hide-secrets): consume complete openai key tokens

* chore(hide-secrets): remove redundant fixture comment

* chore(hide-secrets): remove redundant test docstrings

* fix(hide-secrets): redact whole stripe live keys

* style(hide-secrets): wrap secret sorting key
2026-09-05 11:47:36 -07:00
Yuneng Jiang
9ac893df15
style(e2e): wrap the openai passthrough content assertion under 120 columns 2026-09-05 11:47:15 -07:00
tin-berri
e3b4a82ff9
Merge pull request #39926 from BerriAI/litellm_lit6981_none_url_auth
fix(mcp): reject URL credentials for none auth
2026-09-05 18:44:34 +00: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
devin-ai-integration[bot]
cba3dd5828
fix(proxy): retry deadlocks and requeue spend logs on any DB write error (#39883)
* fix(proxy): retry deadlocks and requeue spend logs on any DB write error

update_spend_logs dequeued the batch and only retried/requeued on transport
errors. A 40P01 deadlock surfaced as a plain prisma DataError and went through
poison-row isolation, which dropped every row it hit; every other DB error was
re-raised with the batch already gone from the queue.

Treat deadlocks as transient (retry, then requeue), keep them out of poison-row
isolation, and requeue the batch at the head of the queue on any other prisma
error so it lands once the DB is healthy.

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

* refactor(proxy): drop redundant docstrings and tighten test typing for spend-log requeue

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

* test(proxy): assert deadlock retries from mock call history instead of mutable lists

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-05 11:41:56 -07:00
devin-ai-integration[bot]
e6705510f8
fix(router): hold max_parallel_requests slot until streaming response is exhausted or closed (#39859)
* fix(router): hold max_parallel_requests slot until streaming response is exhausted or closed

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

* refactor(router): normalize deployment_slot once to keep stream_with_fallbacks under the C901 ceiling

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

* fix(router): close upstream stream before releasing max_parallel_requests slot

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-05 11:41:35 -07:00
devin-ai-integration[bot]
f66b3ebe0d
feat(responses): honor supported_endpoints /v1/responses opt-in for OpenAI-compatible deployments (#39725)
* feat(responses): honor supported_endpoints /v1/responses opt-in for OpenAI-compatible deployments

custom_openai and other generic OpenAI-compatible deployments have no native
Responses API config, so every /v1/responses call is bridged through
/v1/chat/completions. When model_info.supported_endpoints lists /v1/responses,
resolve OpenAILikeResponsesConfig instead so the request is forwarded to
{api_base}/responses, for streaming, non-streaming and mode: responses
deployments alike. Providers with their own Responses config are unchanged.

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

* fix(responses): drop deployment supported_endpoints opt-in after cross-provider prompt swap

A prompt manager that moves the request to another provider leaves kwargs['model_info']
describing the original deployment; without this the swapped provider was sent an
OpenAI-like /responses request it does not serve.

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

* refactor(responses): carry prompt-swap deployment metadata as a return value instead of a kwargs marker

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-05 11:40:00 -07:00
devin-ai-integration[bot]
4df284e16d
fix(guardrails): record guardrail information for undecorated custom apply_guardrail overrides (#39727)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-05 11:39:24 -07:00
moe-berri
a0b07b4791 docs(auto-router compression): cut the explanatory comments back
The module, its routing hook and its tests carried long prose rationale where the
repository allows only concise comments for genuinely complex logic. Trimmed to the
non-obvious reasons and dropped the rest; no logic or test behaviour changes.
2026-09-05 11:33:21 -07:00
moe-berri
723bc2140f refactor(auto-router compression): resolve the policy without a loop-local rebind
The marker walk rebound a loop-local on each iteration, which is the mutation the
repository's convention exists to discourage, but a `: Final` cannot express that
inside a loop body: basedpyright rejects it outright with 'A Final variable cannot
be assigned within a loop'. A lazy generator binds the name once per item and never
rebinds it, so the first marker carrying a policy still wins and the rest are never
read.
2026-09-05 11:19:39 -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
Yuneng Jiang
88c46fb1de
test(e2e): cover Anthropic and OpenAI prompt caching, Cohere embeddings, and costed /openai chat passthrough
Four registry cells that had no e2e test. The cache_control suite gains a direct
Anthropic case (the same cache_control prefix the Bedrock and Vertex rows send)
and an OpenAI case, where caching is automatic so the prefix goes out as a plain
system string with a prompt_cache_key; both assert the second identical call
reports cache-read tokens. The shared second-call helper now takes the send
callable so the OpenAI shape fits without a second copy of the retry loop.

The embeddings suite gains a cohere/embed-v4.0 deployment that must return a
non-zero vector, and the passthrough suite gains an OpenAI-format chat through
the raw /openai/v1/chat/completions prefix that must relay a real completion and
log a costed pass_through_endpoint row whose token counts match the usage the
caller was served.
2026-09-05 10:52:46 -07:00
Yuneng Jiang
df544fcc53
test(e2e): cover key spend reset, regenerate grace period, and the llm_api_routes grant
Three deterministic proxy-only cells from the coverage registry that had no e2e
test. A key over its max_budget is reset to 0 through /key/{key}/reset_spend and
must both read back 0 on /key/info and serve traffic again. /key/regenerate with
grace_period keeps the old key valid until the period elapses and rejects it 401
afterwards. A key whose allowed_routes is the llm_api_routes group must reach
/chat/completions and /embeddings while /model/new stays 403.

KeyRegenerateBody gains grace_period and the management client gains
reset_key_spend so the tests stay on the shared typed transport.
2026-09-05 10:46:05 -07:00
Yuneng Jiang
98784360e8
test(e2e): cover Anthropic /chat/completions streaming and tool calls
Adds TestAnthropicChatCompletions to the chat completions regression suite,
registering a claude-haiku-4-5 deployment via /model/new and asserting the
streamed call delivers real content deltas and a tool-forced call returns a
well-formed get_weather tool_call on both the non-streamed and streamed paths.
Covers three P0 registry cells that had no e2e test.
2026-09-05 10:37:14 -07:00
moe-berri
1c16a5910b fix(tests): undo a stray whole-file reformat and arm a real guardrail
test_router.py is not ruff-formatted on staging and CI's format check only scopes
litellm/*.py, so running ruff format over the whole file rewrote ~900 lines of
unrelated code. That reflow split long single-line patch() calls into multi-line
form, which the test-quality gate counts individually, pushing TQ008 four over its
ceiling. The file is back to staging's formatting with only the compression test
class added.

test_common_request_processing.py armed a model-side guardrail name with no such
guardrail registered, which stopped working once both hops began requiring the name
to resolve to an active compression guardrail.
2026-09-05 10:31:56 -07:00
yuneng-jiang
7672399c26
Merge pull request #39804 from BerriAI/litellm_/e2e-wildcard-probe-and-embedding-allowlist
test(e2e): repair the wildcard readiness probe and the semantic auto-router spend assertion
2026-09-05 10:27:31 -07:00
yuneng-jiang
f753dacbbb
Merge pull request #39912 from BerriAI/litellm_/release-version-bump-d7c975
chore: bump litellm-enterprise 0.1.64 -> 0.1.65, litellm-proxy-extras 0.4.93 -> 0.4.94
2026-09-05 10:26:12 -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
f0618394c1
Merge pull request #39680 from BerriAI/litellm_lit_4738_table_pagination
fix(ui): make Admin UI table pagination honor the selected page size
2026-09-05 10:16:56 -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
ryan-crabbe-berri
a5a846dd0c
Merge pull request #39672 from BerriAI/litellm_expose_v2_organization_patch_openapi
feat(organization): expose PATCH /v2/organization/{organization_id} in the OpenAPI spec
2026-09-05 10:05:09 -07:00
moe-berri
7bfbdf39a0
Merge pull request #39817 from BerriAI/litellm_shadow_eval_judge_output_cap
fix(shadow_eval): size the judge output cap for a judge that reasons
2026-09-05 09:59:45 -07:00
Yuneng Jiang
88985d00e2
bump: litellm-enterprise 0.1.64 -> 0.1.65, litellm-proxy-extras 0.4.93 -> 0.4.94 2026-09-05 09:58:44 -07:00
moe-berri
8284208af2 fix(auto-router compression): restrict both hops to real compression guardrails
The two policy fields are operator-supplied names and nothing else constrained them.
The routing hop calls apply_guardrail directly, which hands the guardrail the
conversation and POSTs it to whatever service backs that guardrail, and the model hop
is added to metadata["guardrails"], which runs it even when it is not default_on. So
naming an ordinary guardrail turned either hop into a way to invoke it and ship prompt
content to it. Both hops now refuse a name that does not resolve to an active
compression guardrail, and say so in the log rather than failing quietly.
2026-09-05 09:51:51 -07:00
tin-berri
d0d09e5343
feat(router): meter auto-router tier and prompt customization against the auto_router license feature (#39674)
Generalizes the heuristic_v2 ceiling from #39468 into a capability table whose
records own their in-process predicate, SQL spelling and refusal wording. The
existing heuristic_v2 capability keeps its own one-router ceiling. A single
customization capability combines operator-defined tier definitions with every
operator-written part of the classifier prompt. The prompt half only applies to
classifier types that call an LLM. The shipped default prompt, classification
rubric presets, tier-label renames and tier model choices remain ungated.

Scope every enforcement point to actual complexity routers. A model-less PATCH
or legacy update now decrypts the stored model before accepting strategy-router
settings, so a regular model cannot acquire a router config or spend a license
slot. Under the existing advisory lock, the cross-pod candidate query returns
only model scalars and the count decrypts and classifies them in process; old
non-router rows carrying a capability-shaped config no longer block a real
complexity router. The signed auto_router license feature makes both ceilings
unlimited.
2026-09-05 09:51:23 -07:00