Commit graph

46529 commits

Author SHA1 Message Date
Yujong Lee
b7453a25d7 perf(rust): make mimalloc opt-in behind a cargo feature
pydantic-core shipped mimalloc from Jun 2022 to Aug 2023 and removed it
(pydantic-core#900) after: a TLS-linkage fix (pydantic-core#363), manylinux
cross-compile breakage (pydantic-core#893), 1GiB of reserved virtual memory
at import breaking RLIMIT_AS-limited processes (pydantic#7167), and
container memory-footprint complaints (pydantic#6620). litellm wheels land
in exactly those memory-limited container deployments, so the allocator
override must be an explicit build-time choice, not a default imposed on
every wheel. The feature stays available for self-built deployments that
want it; wheels build without it.
2026-09-03 10:15:03 -07:00
Yujong Lee
17bcc04800 perf(rust): bound provider connection pools and adopt mimalloc in the bridge
The four shared reqwest clients (core messages/chat_completions/
audio_transcription, ai-gateway) now set pool_max_idle_per_host(64) and
tcp_keepalive(60s):

- reqwest defaults pool_max_idle_per_host to usize::MAX, so a burst of
  N concurrent calls to one provider host leaves N idle TLS connections
  in the pool after completion, each held until its idle timeout; under
  sustained bursts this grows file descriptors without bound. 64 keeps
  a warm set large enough for handshake-free reuse.
- tcp_keepalive lets silent half-open peers (NAT/LB reaping, vanished
  upstreams) be detected during long streaming responses instead of
  blocking until the 600s full-request timeout.

The python-bridge cdylib now uses mimalloc as its Rust global allocator
(the pydantic-core/orjson pattern). It only governs allocations made by
Rust code in the extension - CPython's pymalloc heap is untouched - but
Rust-side request/response buffering in a long-lived interpreter
process gets mimalloc's sharded free lists and eager page purging
instead of glibc malloc, which favors RSS growth under churn.

No behavior change beyond the socket/allocator characteristics;
timeouts and request paths are untouched.
2026-09-03 10:05:35 -07:00
moe-berri
4990f06acc
feat(auto-router): support classifier reasoning effort (#39372)
* feat(auto-router): support classifier reasoning effort

* fix(auto-router): harden classifier reasoning effort

* fix(ui): satisfy classifier config lint limits

* refactor(auto-router): simplify classifier effort support

* fix(auto-router): clear frontend-lint and type-discipline gates, trim LOC

---------

Co-authored-by: Tin Chi Lo <tin@berri.ai>
2026-09-03 08:59:04 -07:00
Mateo Wang
34d4f7f8ae
fix: 1.99.0-rc2 UI bug batch (empty org on key create, session pagination, access group rename/delete) (#39436)
* fix(ui): clearing the organization picker no longer sends organization_id="" on key create

* fix(proxy): paginate Request Logs by conversation and aggregate session type counts and models server-side

* fix(proxy): keep access groups in sync when a model is renamed or deleted

* fix(proxy): cap the Request Logs conversation total like the row total

* fix(proxy): judge access group backing by the database for db models

A worker whose router has not polled the database yet still lists a sibling under its old
name, so a delete or rename handled there kept the stale name in every access group. Only
config-sourced deployments count as router backing now; db models are counted in the table.

* fix(ui): keep the conversation badge when an MCP call represents a conversation

A conversation that straddles the bounded page window can be represented by one of its MCP
rows, which showed a plain MCP badge and hid the session counts. The badge now reads the
server aggregates whenever the conversation has more than one call.

* fix(proxy): list every model of a conversation in Request Logs and keep the conversation badge for MCP representatives

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

* fix(proxy): type session spend aggregates

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

* fix(ui): satisfy request logs lint budget

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

* fix(proxy): cap per-session model aggregation in request logs

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

* chore: ratchet type-discipline budget after staging merge

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

* fix(ui): send an explicit null when the key edit form clears the organization

Clearing the Organization picker in the key edit form wrote undefined into
the form value, and JSON.stringify drops undefined-valued keys, so
/key/update never saw the field and the key kept its old organization.
Writing null instead survives serialization, and the backend's
model_dump(exclude_unset=True) preserves it, so the column is set to NULL.

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 08:58:48 -07:00
Rakesh
1de960bce7
fix(docker): bump nginx runtime to 1.31.5-alpine3.24 and pin digest to resolve critical CVEs (#39561) 2026-09-03 08:46:07 -07:00
devin-ai-integration[bot]
e80e78d3ef
feat(cli): enable Claude Code gateway model discovery by default in lite claude (#39445)
* feat(cli): enable Claude Code gateway model discovery by default in lite claude

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

* refactor(cli): build agent env declaratively and document discovery key for lite up

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

* fix(cli): keep build_agent_env within LIT002 type-discipline budget

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 08:38:25 -07:00
Mateo Wang
45495e1ab5
Merge pull request #39506 from BerriAI/litellm_fix_v2_migration_resolver_attempt_accounting
fix(proxy-extras): only spend a migrate-deploy attempt when a pass made no progress
2026-09-03 08:36:31 -07:00
Mateo Wang
11a02b9581
Merge pull request #36260 from eeshsaxena/fix-credential-endpoints-raise-not-return
fix(proxy): 404 a credential delete that matched nothing, and raise instead of return
2026-09-03 06:17:56 -07:00
mateo-berri
f83b6029e3 Merge branch 'litellm_internal_staging' into fix-credential-endpoints-raise-not-return 2026-09-03 05:36:50 -07:00
mateo-berri
418d0e79ba fix(proxy): answer 404 when deleting a credential that was never stored
prisma's `delete` returns None when the `where` clause matched no row
instead of raising, and the handler never looked at the return value. It
went straight on to filter an in-memory list that never held the name and
answered 200 "Credential deleted successfully", so an operator scripting
credential cleanup could not tell a real deletion from a typo.

Look at what the repository returned and answer 404 with the name, the
same rejection PATCH /credentials/{credential_name} already gives. A
credential that only exists in the config yaml is never written to the
table, so it now answers 404 too, which is honest: reporting success for
it is the same lie, since it comes back on the next proxy boot.

Adds regression tests for the delete 404, the still-working delete, the
config-yaml-only credential, and for the raise-not-return fix on both
DELETE /credentials/{credential_name} and GET /credentials, which
serialized a rejection as the 200 response body.
2026-09-03 05:30:28 -07:00
yuneng-jiang
658f50663d
fix(ui): keep Virtual Keys list state in the URL so it survives leaving the page (#39481)
* fix(ui): keep Virtual Keys list state in the URL so it survives leaving the page

The search term, sort, pagination and drawer filters lived in component
state, so navigating away from Virtual Keys and back reset the table to an
unfiltered first page. Move them into query state alongside the existing
?key= deep link, which also makes a filtered view shareable.

* fix(ui): namespace the Virtual Keys filter params and bound page inputs

The unprefixed team_id filter hijacked the /api-keys create-key deep link,
which already takes team_id as a prefill, so ?create=true&team_id=X silently
filtered the list underneath the modal. Prefix the four drawer filters.

Now that page and page_size come from the address bar, clamp them to what
/key/list accepts instead of forwarding 0, negatives or an int64-overflowing
page straight through, and trim filter values arriving from a URL the same
way the drawer already trims them.

* fix(ui): fall back to a sortable column when the URL names an unknown one

A hand-edited or stale sort_by reached /key/list, which 400s it, leaving the
Virtual Keys page on its loading skeleton with no error. Validate it against
the fields the table's own headers can produce, and clear sort_by rather than
blanking it when a sort is reset so the URL stays clean.

Also replaces a default-state URL assertion that ran before any query-state
write could land, so it could not fail for the regression it named.

* fix(ui): use TanStack's functionalUpdate instead of a hand-rolled updater resolver

The local helper narrowed typeof updater === "function" against an
unconstrained T, which TypeScript cannot do because T itself may be a function
type, so next build failed to type check. table-core already exports the same
helper.
2026-09-03 00:35:32 -07:00
Mateo Wang
066d5f0694
Merge pull request #39502 from BerriAI/litellm_/triage-slack-message-4bd4e6
fix(test): drop the duplicate embedding_executor arg in the Bedrock KB fake handler
2026-09-03 00:26:08 -07:00
mateo-berri
8572544b44 refactor(proxy-extras): pull the migrate deploy recovery branches into a budget helper
_setup_database_v2 decided the next attempt budget inline in eight
branches, each rebinding budget before continuing. The branches now live
in _budget_after_deploy_failure, which returns the budget the next pass
runs under, and the two identical idempotent-recovery blocks share
_mark_migration_applied. The loop backs off whenever a pass spent an
attempt, which is the same set of paths that slept before.
2026-09-03 00:20:46 -07:00
yucheng-berri
ecabfbd5af
fix(guardrail): hide-secrets playground redaction and guardrail telemetry (#39398)
* Fix hide-secrets guardrail: playground redaction, UI dropdown entry, spend-log telemetry

The hide-secrets guardrail never implemented apply_guardrail, so the UI test
playground echoed secrets verbatim; it was missing from the Add Guardrail
dropdown; and it recorded no guardrail_information, so Spend Logs could not
distinguish a redacted request from a clean one.

- implement apply_guardrail (unified interface) with use_native_lifecycle_hooks
  so proxied traffic stays on async_pre_call_hook (per-key opt-out and
  data["prompt"] handling live only there)
- record standard_logging_guardrail_information (allow/mask + masked_entity_count)
  via _process_response/_process_error; opted-out keys and legacy nameless
  callback instances record nothing
- advertise hide-secrets in /guardrails/ui/add_guardrail_settings (pre_call only)
  and /guardrails/ui/provider_specific_params with a config model

Resolves LIT-3548

* Fix hide-secrets passthrough telemetry and JSON config input

* fix(guardrails): validate hide-secrets object config before submit

- apply_guardrail treats empty-string-only texts as no input, so no
  false allow is recorded
- the UI object field keeps raw text while editing and blocks submission
  until it parses to a JSON object, instead of posting a string to an
  object-only API
- supported_modes_by_provider keeps its dict[str, list[str]] value type

* fix(guardrails): record no hide-secrets telemetry when nothing was inspected

walk_user_text and the prompt redaction now report how many non-empty
strings they visited; when neither inspected anything (image-only
content, empty strings), the run records no guardrail entry instead of
an 'allow' row that counts a check which never saw any text.
2026-09-03 00:01:03 -07:00
mateo-berri
7b8cc0319e fix(proxy-extras): only spend a migrate-deploy attempt when a pass made no progress
The v2 migration resolver gave `prisma migrate deploy` four attempts, and
every recovery path ended in a bare `continue`, so each one burned an attempt.
A database first brought up with `--use_prisma_db_push` has a full schema and
no migrations ledger, so the baseline spent attempt one and the first three
migrations whose objects already existed spent the rest. The proxy then exited
before binding its port, and that database could never be moved onto the
resolver.

The retry budget now counts only attempts that got nowhere. Creating the
baseline, and each migration newly marked applied, leaves the budget alone, so
a push-created database works through its pre-existing objects one pass at a
time. Timeouts, deadlock rollbacks, advisory-lock waits, and a repeat of a
recovery that already ran still spend an attempt, so a run that stops making
progress gives up exactly as before.
2026-09-02 23:56:09 -07:00
Yuneng Jiang
47611fa207
fix(test): drop the duplicate embedding_executor arg in the Bedrock KB fake handler
Two branches independently added embedding_executor to the same fake
search handler in this file, #39472 in the middle of the signature and
#39474 at the end. Neither conflicted with the other, so both edits
merged and the function ended up declaring the parameter twice.

Python rejects that at compile time, so the whole module fails to
import and every test in the file is uncollectable, taking the
logging_testing job down on staging.

Keep the earlier of the two, which sits where the real handler declares
the parameter.
2026-09-02 23:22:40 -07:00
yuneng-jiang
3cac5e5cd4
fix(sso): resolve multi-valued role claims to the highest privilege role (#39480)
* fix(sso): resolve multi-valued role claims to the highest privilege role

A role claim carrying several roles used to resolve to whichever one the IdP
listed first, so a user holding both proxy_admin_viewer and internal_user lost
org-level spend visibility depending on claim ordering alone.

get_litellm_user_role now picks the highest privilege role out of a list-valued
claim, and the Entra app_roles path shares that same resolution instead of
keeping its own copy of the hierarchy. SAML assertions carrying several role
values go through the same path rather than taking the first value.

* test(sso): lock ranked-over-unranked role resolution for mixed claims

org_admin, team and customer sit outside the privilege ladder. Pin the
resolution for a claim that mixes one of them with a ranked role so the
asymmetry is covered rather than implicit.

* fix(sso): label the claim-sequence cast for the type-discipline gate

* fix(sso): resolve claim entries without recursing

The repo's recursive-function gate rejects self-recursion here, and a role
claim is flat anyway. Pull the single-value lookup into its own helper so the
list branch maps over it instead of calling back into itself.
2026-09-02 22:44:17 -07:00
Mateo Wang
62ed7e1942
Merge pull request #39478 from BerriAI/litellm_ui_presets_mock_build_ctx
fix(ui): read the preset catalog at runtime in the dashboard tests
2026-09-02 22:38:59 -07:00
yuneng-jiang
c841a56e9a
fix(ui): stop the create team form resetting organization and models (#39476)
* fix(ui): stop the create team form resetting organization and models

The organization preselect ran in an effect keyed on the organizations
query, so any refetch of that list while the Create Team modal was open
overwrote the user's organization pick, which in turn cleared their
models pick. The models field was also cleared whenever the available
models fetch resolved.

Preselect the organization when the modal opens instead, and clear the
models only when the user picks a different organization. An org admin
whose admin orgs narrow to one while the form is open can still pick,
rather than facing a locked empty field.

* fix(ui): block team create when the picked organization is no longer available

An organization picked in the Create Team form now survives a refetch of
the organization list, so it can outlive the admin's access to it. Refuse
the create with a message on the field rather than letting the request
fail authorization at the proxy.

* fix(ui): keep the team create organization field usable when the pick goes stale

Locking the field on a single admin organization also locked it while it
held a rejected organization, so an admin who lost access could not pick
the one organization left. Lock it only while it holds that organization.

* test(ui): hoist the created team fixture out of the mock call

The inline object pushed the repo past its no-large-inline-object-arg
lint budget, which has no headroom.
2026-09-02 22:34:15 -07:00
tin-berri
534003da03
feat(ui): add 1M context auto-router preset (#39490)
* 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
2026-09-02 22:33:14 -07:00
Mateo Wang
99da04a1b0
Merge pull request #39441 from BerriAI/litellm_internal_copy_36281
fix(xai): bill from the cost xAI reports instead of recomputing it (internal copy of #36281)
2026-09-02 22:23:50 -07:00
Mateo Wang
66a3d24b3f
Merge pull request #39474 from BerriAI/litellm_s3_vectors_query_embedding_executor
refactor(s3_vectors): embed search queries through the shared vector store executor
2026-09-02 22:23:44 -07:00
mateo-berri
f62e87d28c fix(ui): read the preset catalog through the shared mock in the lib test 2026-09-02 21:42:47 -07:00
mateo-berri
7f7e0d5517 fix(vector-store): embed through the SDK when the Router does not serve the query embedding model
The Router executor only routed a query embedding when the vector store
carried extra embedding configuration, so a store registered with no
embedding model at all always went to the Router and 500'd on the
s3_vectors default text-embedding-3-small when no deployment served it.
Route on whether the Router serves the model, which is the rule the
executor had before, and keep the request metadata on the SDK fallback so
the embedding stays attributed either way.
2026-09-02 21:32:00 -07:00
Mateo Wang
ff17e8b987
Merge pull request #39472 from BerriAI/litellm_fix_kb_hook_test_embedding_executor
test(vector-store): accept embedding_executor in the Bedrock KB hook fake handler
2026-09-02 21:11:24 -07:00
ishaan-berri
e058aa68c4
test: add mistral ocr transformation parity coverage (#39482)
* test: cover mistral ocr transformation parity

Co-Authored-By: Claude Code <noreply@anthropic.com>

* test: map mistral ocr parity contracts

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-02 20:40:00 -07:00
devin-ai-integration[bot]
291d02f8aa
fix(mcp): never exchange the LiteLLM virtual key as the upstream subject token (#39446) 2026-09-02 20:32:43 -07:00
Tin Chi Lo
fcc9b813af fix(ui): resolve the preset catalog relative to the mock, not cwd
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HqEPCNLDrssxsuezhAaL4j
2026-09-02 20:01:35 -07:00
Tin Chi Lo
099d26204f fix(ui): read the preset catalog at runtime in the vitest mock
The autoRouterPresets mock imported litellm/proxy/public_endpoints/autorouter_presets.json
as a module. That path sits outside ui/litellm-dashboard, the only directory the UI
Dockerfile copies, so `next build` type-checking inside the image failed with
"Cannot find module" and the ui-image job went red on every PR that touched an
image-scan path. Read the file with fs at runtime instead; vitest still derives
expectations from the real bundled catalog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HqEPCNLDrssxsuezhAaL4j
2026-09-02 19:54:09 -07:00
ishaan-berri
bcd3e2d94d
feat(rust-python-harness): wire existing e2e SDK tests into the matrix (#39463)
Adds chat_completions and transcription as SDK function columns, backed
by the existing rust_bridge test files. Adds a fourth strategy folder,
existing_e2e_test_sdk, that points at already-existing live-API SDK
tests (tests/ocr_tests/ as a whole folder, plus chat completion and
Whisper transcription tests) instead of writing new parity tests.
Extends selector_matches_node with trailing-slash folder selectors so
a whole test folder can back one matrix cell.
2026-09-03 02:50:08 +00:00
yucheng-berri
291e84e565
feat(datadog_llm_obs): cost tag dimensions, router decision fields, reasoning token metric, redaction gating (#39402)
* feat(datadog_llm_obs): cost tag dimensions, router decision fields, reasoning token metric, redaction gating

* test(datadog_llm_obs): satisfy test quality gate

* fix: forward integer parent_id as its string form

* fix(datadog): sanitize redacted message roles

* fix(datadog): keep the A2A agent role on redacted spans

* fix(datadog): merge current staging budget

* style(datadog): format redaction tests

* fix(datadog): handle malformed redacted roles

* test(datadog): put the test quality suppression on the reported line

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-02 19:46:09 -07:00
tin-berri
64e45a069d
feat(complexity_router): opt-in modality override of a kept session-affinity pin (#39454)
A session pinned to a text-only model failed every image turn with a provider 400, because the modality gate exempts a kept pin by cause. Add modality_pin_override so that exemption is conditional: the image turn is re-placed on a capable model for that request only, reported as cause modality_pin_override, and the stored pin is left untouched so the next text turn replays it.

The pin write on the replay path already happens upstream of the gate and stores the session's own model, so pin survival is structural rather than bookkeeping. The new cause joins the non-pinnable set. Default off at every layer.
2026-09-02 19:36:09 -07:00
mateo-berri
f77b3b2b52 refactor(s3_vectors): embed search queries through the shared vector store executor
S3 Vectors now subclasses BaseQueryEmbeddingVectorStoreConfig, so its query
embedding runs through the Router executor with the request metadata instead
of a private router lookup. embedding_model stays accepted as an alias of
litellm_embedding_model. The router kwarg is gone from the search handler and
every provider transform now that nothing but the executor fallback read it.
2026-09-02 19:26:31 -07:00
ishaan-berri
a5639b8e2a
test: add OCR python-to-rust test parity ledger (WIP) (#39434)
* test: add OCR python-to-rust test parity ledger

* feat: add ledger loader for OCR test parity data

* feat: add drift audit for OCR test parity ledger (WIP, untested)

* fix: correct drift in OCR test-parity ledger

Two entries referenced a typo'd Python test name, four duplicated entries already tracked under TestProxySecurityGuard, five real Python tests in test_rust_bridge.py were untracked, and three real Rust custom_logger tests were missing from rust_only_tests. Found by running validate_ledger.py's audit against the live repo.

* test: add regression coverage for the OCR ledger and audit script

Covers schema validation, AST/regex test enumeration, drift detection on both the Python and Rust sides, LedgerDriftError content, and a live-repo clean-audit guard against future drift.

* test: simplify ledger test to one drift-guard assertion

Replace the ledger-internals unit tests with a single test that runs the real audit against the live repo and asserts every OCR test is accounted for (mapped, unmapped-with-reason, or rust_only), printing the exact diff on failure.

* chore: move OCR test-parity ledger to core/ocr

validate_sub_methods/ mixes strategy-catalog metadata with the ledger. Ledger data belongs under a per-function core/<function>/ path instead.

* fix: point LEDGER_PATH at the new core/ocr location
2026-09-02 19:22:52 -07:00
mateo-berri
f81928f7ae test(vector-store): accept embedding_executor in the Bedrock KB hook fake handler 2026-09-02 19:22:12 -07:00
yucheng-berri
7a81ae98e6
fix(model_armor): handle Anthropic Messages and Responses streams in post_call (#39181)
* fix(model_armor): handle Anthropic Messages and Responses streams in post_call

The post_call streaming hook buffered every chunk and fed it to
stream_chunk_builder, which only understands chat-completion deltas.
/v1/messages streams raw Anthropic SSE bytes and /v1/responses streams
typed Responses events, so both raised litellm.APIError and surfaced to
the client as a 500 on every streamed request.

Assemble each surface with its own reader, frame guardrail failures as
terminal items in that surface's wire format, and pass the stream
through unscanned when it cannot be assembled instead of raising.

* fix(model_armor): classify the stream surface and fail closed when it cannot be assembled

Decide the wire format explicitly instead of inferring it from a boolean pair, so an
opaque raw SSE stream (the Google :streamGenerateContent route) is never refused in
Anthropic framing, and a stream that cannot be assembled is blocked rather than
released unscanned unless fail_on_error is disabled.

Also scan Responses tool-call arguments, read the body only off a terminal Responses
event, and record the applied guardrail on the fail-closed path.

* test(model_armor): pin the error-only stream predicate against content-carrying streams

is_sse_error_stream decides whether a buffered stream is forwarded to the client
untouched, so a stream that still carries content must not qualify: the frames-only
join drops typed chunks, an empty stream is not a refusal, and a content event may
carry an empty error field.

* fix(model_armor): let a streamed de-identify match mask instead of blocking

A de-identify template reports MATCH_FOUND for every redaction it makes. The
streaming block check omitted allow_sanitization, so with mask_response_content
enabled that match read as a refusal and the client got a 400 where the
non-streaming sibling returned the redacted text. Pass the flag through, as the
non-streaming hook already does, and stamp the logged status from the same
decision so the spend row agrees with what the client received.

Also drop Any from the chat-completion assembler's parameter; stream_chunk_builder
takes a bare list, so list[object] carries the mutability requirement without
erasing the element type.

* fix(model_armor): fail closed when a streamed de-identify match cannot be applied

Allowing sanitization past the streaming block check is a promise to apply the
redaction Model Armor asked for. Two paths broke that promise and released the
buffered original instead: a match that comes back with no sanitized text, and a
surface with no assembled body to rewrite.

The outcome is now resolved once, before it is recorded, so the status stamped on
request metadata agrees with what the client receives rather than reporting the
success the block check alone would have implied.

* fix: scan the deltas when a Responses stream ends without a body

response.failed and response.incomplete are terminal events like
response.completed, but a turn that broke mid-generation reports an empty
output while the deltas ahead of it already spelled the answer out to the
client. Reading only the terminal body found nothing to scan there, and the
empty-content shortcut then forwarded every buffered delta past the guardrail.

Fall back to the text the delta events carry whenever a Responses stream
assembles to nothing.

* fix: read the Responses delta event types off the event enum

The hand-listed set left out response.mcp_call_arguments.delta, so a turn that
streamed only MCP tool arguments and then reported an empty body still took the
no-content shortcut and forwarded those chunks unscanned.

Deriving the set from ResponsesAPIStreamEvents keeps it complete as the enum
grows, and the str guard in the reader already covers any event whose delta is
not text.

* fix(model_armor): scan responses deltas alongside the terminal body

A /v1/responses stream spells out reasoning summaries and tool-call arguments in
delta events that its terminal body never repeats, so scanning the body alone
handed every summary delta to the client unscanned whenever the body carried text.

* fix(model_armor): scan responses delta fields apart from each other

A Responses turn spells out its reasoning summary, its visible answer and its tool-call
arguments in separate delta events. Joining every delta into one string let a finding form
across the boundary between two fields that each carry nothing to find, so a safe stream
could be blocked. Group the deltas by the field they belong to, join a field's own deltas
as they streamed, and keep the fields apart.

* fix(model_armor): scan each responses field once, not twice

Separating delta fields stopped the terminal body from matching the delta text, so a turn
with two visible fields sent Model Armor both copies. Only the delta fields the body does not
already carry are appended now.

---------

Co-authored-by: yassin <yassin@berri.ai>
2026-09-02 19:15:14 -07:00
devin-ai-integration[bot]
8065ede40b
test(guardrails): expect the deduped end-of-stream scan in crowdstrike cadence test (#39467)
Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 02:11:08 +00:00
yucheng-berri
e0e249225b
feat(azure): support credential chain for storage (#39229)
* feat(azure): support credential chain for storage

* test(azure): clarify credential seam suppressions

* fix(azure): read chain tokens in a worker thread

The credential chain walk (IMDS probe, CLI subprocess) is blocking I/O,
so reading the provider inline in async set_valid_azure_ad_token stalls
every request on the worker's event loop
2026-09-02 18:55:22 -07:00
tin-berri
78ff5ac9cd
feat(router): arm safeguard-refusal fallback on generic chains when no content-policy list exists (#39274) 2026-09-02 18:31:25 -07:00
devin-ai-integration[bot]
8441dd6e8c
fix(proxy): keep SpendLogs and callback session ids in sync when the request has none (#39450)
* fix(proxy): keep SpendLogs and callback session ids in sync when the request has none

Add general_settings.missing_session_id (generate | reject). In generate mode one id is
stamped into litellm_session_id, litellm_trace_id and metadata.session_id before callbacks
run, so LiteLLM_SpendLogs.session_id and the Langfuse session id match. In reject mode such
requests get a 400. Unset keeps the legacy behavior. MCP routes are not affected

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

* chore(proxy): regenerate schema.d.ts and shorten mutable-ok comment for ruff format

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

* fix(proxy): mark generated session ids so affinity consumers do not pin on them

Fireworks x-session-affinity, the router session_affinity pre-call check and the
complexity router session pin all read metadata.session_id as a caller-chosen
stable key. A missing_session_id: generate id is fresh per request, so it now
carries metadata.litellm_session_id_generated and those consumers skip it

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-02 18:28:06 -07:00
devin-ai-integration[bot]
92edcb90db
fix: keep litellm importable on Python 3.10 and guard 3.11-only typing imports in CI (#39448)
* ci: guard against Python 3.10-incompatible typing imports

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

* ci: address Python 3.10 typing guard review

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

* fix(ci): honor version-guard direction and scan litellm-proxy-extras in py310 typing check

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-02 18:27:19 -07:00
Mateo Wang
4f7b20ec10
fix(guardrails): skip streaming guardrail rounds that re-scan cleared output (#39386)
* fix(guardrails): skip streaming guardrail rounds that re-scan cleared output

Streaming guardrails scanned the finished answer twice at end of stream
whenever the chunk count landed on a multiple of the sampling rate, ran
sampled rounds whose payload was identical to the previous one, and on
/v1/messages could scan an empty text before the first content chunk.
Every redundant round is a paid guardrail provider call.

Each endpoint handler now exposes a scan key describing what a round
would hand to apply_guardrail (the text so far, plus tool calls once the
stream has ended), and the unified streaming hook skips a sampled or
end-of-stream round whose key equals the last scanned one or carries
nothing to scan yet. Rounds that carry tool calls are never skipped.

* test(guardrails): expect one end-of-stream scan when the terminal chunk is sampled

Update sampled cadence expectations and use tuple-backed scan state

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-02 18:25:28 -07:00
Mateo Wang
3b937465c6
Merge pull request #39457 from BerriAI/litellm_e2e_openai_model_not_found_404
test(responses): expect the 404 OpenAI now returns for an unknown model
2026-09-02 18:24:58 -07:00
Mateo Wang
9e8e486f1c
Merge pull request #38877 from BerriAI/litellm_fix_guardrail_put_inmemory_sync
fix(guardrails): apply PUT /guardrails/{id} to the serving worker immediately and reject invalid configs with 422
2026-09-02 18:13:15 -07:00
yujonglee
87618b7da8
doc (#39456) 2026-09-02 18:12:03 -07:00
mateo-berri
d33fe95d19 test(responses): expect the 404 OpenAI now returns for an unknown model 2026-09-02 18:08:45 -07:00
tin-berri
993766be0e
feat(proxy): serve the auto-router preset catalog at runtime (#39412)
The dashboard's template picker imported autorouter_presets.json at build time, so every
catalog change needed a dashboard rebuild and artifacts refresh. The catalog now lives in
litellm/proxy/public_endpoints/ and GET /public/autorouter_presets serves it, fetching
litellm.autorouter_presets_url (GitHub raw on main, 1h in-process cache, bundled fallback)
so a merged catalog change propagates to running proxies like the model cost map does.
The dashboard fetches it at runtime via useAutoRouterPresets and keeps no local copy.

Resolves LIT-6764
2026-09-02 18:03:22 -07:00
tin-berri
ff1f21aea9
fix(ui): paginate request logs by session groups server-side (#39257)
* fix(ui): paginate request logs by session groups server-side

The logs table server-paginated raw spend logs and then collapsed
multi-call sessions client-side, so a page could render 3 rows while
the footer claimed 25 and sessions straddled pages. Adds an opt-in
group_by_session param to /spend/logs/ui that pages and counts one
representative row per session (DISTINCT ON, newest non-MCP call),
keeps the bounded count contract, enriches whole-session llm/agent
composition counts, and deletes the client-side collapse pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxT89fiygmzz2ALcjpu7Ve

* feat(ui): add a 10 rows-per-page option and default request logs to it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxT89fiygmzz2ALcjpu7Ve

* fix(ui): key session aggregates per api key in the logs enrichment

Grouped pagination splits a reused session id into one row per api key,
but the enrichment still aggregated by session_id alone, so both rows
showed combined spend and counts. The aggregate query now groups by
(session_id, api_key), the count folds into it (the separate group_by
query is deleted), and each row reads its own key's totals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxT89fiygmzz2ALcjpu7Ve

* fix(ui): treat an empty api_key as a real session group value

The spend-log schema defaults api_key to an empty string; truthiness
guards in the enrichment treated it as missing, so keyless multi-call
sessions lost their count and spend. Only None means missing now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxT89fiygmzz2ALcjpu7Ve

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 17:54:17 -07:00
Mateo Wang
edbd4d1f98
Merge pull request #39428 from BerriAI/litellm_lite_login_prefill_code
feat(cli): pre-fill the SSO verification code in the browser when the proxy allows it
2026-09-02 17:48:39 -07:00
Yassin Kortam
44a6c659fb
fix(mcp): fence an outbound-token write against an overlapping invalidation (#35398)
get_or_compute single-flights concurrent misses under a per-key lock, but
invalidate() deletes outside it. A mint already awaiting the IdP when an
invalidation ran wrote its result into the slot afterwards, so a bearer minted
before the invalidation was served after it for its full TTL; the upstream-401
retry then re-presented the bearer the server had just rejected.

The cache now keeps a per-key generation beside the single-flight lock.
invalidate() bumps it and get_or_compute snapshots it inside the lock right
before minting, skipping the cache write when it no longer matches. The token
is still returned to the caller it was minted for, so only the caching is
fenced. Generation and lock share one object that a running mint binds to a
local, which pins the weak map's entry for exactly as long as a write can still
land; a generation held weakly on its own would be collected across the await
and the re-check would silently pass.
2026-09-02 17:45:58 -07:00