Commit graph

4669 commits

Author SHA1 Message Date
Mateo Wang
cde5465e6f
Merge pull request #37203 from BerriAI/litellm_batches_metadata_type_400
fix(proxy): return 400 for non-object metadata and litellm_metadata instead of silent drop or 500
2026-08-17 13:19:33 -07:00
Mateo Wang
3b6e56716a
Merge pull request #36978 from Scott-Wilson-ZocDoc/fix/mcp-guardrail-usage-monitor
fix(guardrails): record MCP tool guardrail evaluations and blocks in …
2026-08-17 13:18:03 -07:00
mateo-berri
f86dc8f54e test(proxy): assert non-Bedrock passthrough stream emits no content-type header 2026-08-17 13:09:17 -07:00
mateo-berri
8ced9f56a1 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_lit_4561_bedrock_passthrough_content_type 2026-08-17 13:04:18 -07:00
mateo-berri
11e2341fc9 Merge branch 'litellm_internal_staging' into feature/request-logs-user-id-filter 2026-08-17 13:01:19 -07:00
mateo-berri
c7b17b6615 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_pr36482_head 2026-08-17 12:54:14 -07:00
mateo-berri
09c8d1f1f5 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_lit5660_batches_limit_400 2026-08-17 12:46:15 -07:00
mateo-berri
a21de07b3f fix(proxy): drop every invalid metadata field before raising so failure hooks never see them 2026-08-17 12:44:46 -07:00
mateo-berri
73ae9c65dd test(proxy): give the x-litellm-model fallback test deterministic openai env creds 2026-08-17 12:42:57 -07:00
mateo-berri
f3b0cdca43 fix(proxy): keep ProxyException status codes on /v1/moderations instead of wrapping into 500 2026-08-17 12:32:30 -07:00
mateo-berri
c894697a2a fix(proxy): keep ProxyException status codes on /v1/messages instead of wrapping into 500 2026-08-17 12:21:12 -07:00
Mateo Wang
badd737526
Merge pull request #37199 from BerriAI/litellm_lit5657_batches_400_missing_fields
fix(proxy): return 400 naming the missing required param on POST /v1/batches
2026-08-17 12:20:25 -07:00
mateo-berri
6e55a21ebb fix(proxy): enforce batch list limit bounds on managed passthrough listings 2026-08-17 12:15:22 -07:00
Thijmen Stavenuiter
4e3e7d8cf6 test: cover soft budget row creation and windows without max_budget 2026-08-17 21:12:36 +02:00
mateo-berri
57e1bf41c6 fix(proxy): return 400 for non-object metadata and litellm_metadata instead of silent drop or 500 2026-08-17 12:08:56 -07:00
Thijmen Stavenuiter
c7abb519a1 Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into key-budget-window-usage 2026-08-17 21:04:01 +02:00
mateo-berri
4a7dfd75fc fix(proxy): return 404 instead of 500 for unresolvable batch and file ids on /v1/batches 2026-08-17 12:02:15 -07:00
mateo-berri
a9bb09905d test(batches): drop redundant section banner 2026-08-17 12:00:40 -07:00
mateo-berri
e4ce526900 fix(proxy): return 400 naming the missing required param on POST /v1/batches 2026-08-17 11:55:06 -07:00
mateo-berri
f55a193628 fix(proxy): reject out-of-range limit on GET /v1/batches with OpenAI-parity 400 2026-08-17 11:54:34 -07:00
ryan-crabbe-berri
ad6a3a7b9e
fix(proxy): registry caches stop per-request tag and end-user Postgres reads in auth (#36801)
* fix(proxy): cache tag-name registry so unregistered request tags skip Postgres

Request tags are free-form attribution labels, so most have no LiteLLM_TagTable
row. get_tag_objects_batch never cached that absence: every tagged request ran
a find_many that came back empty, and under Prisma pool contention those
per-request queries queued for minutes inside user_api_key_auth.

Cache the bounded set of registered tag names under one aggregate key with the
management-object TTL. Uncached request tags are filtered against it before any
per-tag DB fetch, so unregistered tags cost zero DB reads on a warm path. An
empty registry is cached as a valid answer; DB errors are not cached and fall
back to the per-tag lookup; tables past TAG_REGISTRY_MAX_SIZE cache an overflow
sentinel that disables filtering. Tag create/update/delete endpoints now evict
the registry and per-tag keys and publish cross-worker invalidation (they
previously evicted nothing). The per-tag write-back also gains the management
TTL it was missing, and the hand-built tag:{name} key strings are replaced with
a shared builder.

* fix(proxy): skip per-request end-user DB reads via restricted-id registry

Every request carrying a user id ran get_end_user_object, and with high-cardinality
auto-created end-user rows (hundreds of thousands of ids, all restriction fields
NULL) the per-pod cache missed on nearly every request, so each one paid a Postgres
find_unique that queued behind the Prisma pool during background-job bursts. True
misses were never cached, and unknown ids paid the read twice per request.

Cache the bounded set of end-user ids that carry any restriction (blocked, budget,
region, default model, or object permission) under one aggregate key with the
management-object TTL. When an id misses the per-id cache and is absent from a
usable registry, get_end_user_object returns None with zero DB reads; restricted
ids keep today's fetch-and-cache path. The skip is bypassed whenever
litellm.max_end_user_budget_id is set (default budgets make unrestricted rows
behaviorally distinct from missing rows), validate_end_user_id_in_db is on
(existence checks need the row), or the token carries end_user_max_budget from
custom auth (the row's recorded spend seeds the budget counter). Empty registries
cache as a valid answer, DB errors are never cached, and oversized tables cache an
overflow sentinel that disables filtering. Customer create/update/block/delete now
evict the registry and per-id keys and publish cross-worker invalidation (they
previously evicted nothing), and the per-id write-back gains the management TTL it
was missing so Redis entries no longer live forever.

* refactor(proxy): single generic registry loader with error sentinel and single-flight

Code review follow-ups on the two registry caches. Registry DB errors now cache
the overflow sentinel for a short REGISTRY_ERROR_NEGATIVE_CACHE_TTL window and
log at warning, so a degraded Postgres stops paying the failing registry scan on
every request on top of the per-id fallback. Cold registry loads are single-flight
per worker behind per-registry locks with a recheck after acquire, so a TTL expiry
no longer fans out one full-table scan per in-flight request. The tag and end-user
loaders collapse into one _load_bounded_registry with per-entity fetch closures,
and the triplicated evict-then-broadcast protocol becomes one evict_and_broadcast
helper beside publish_auth_cache_invalidation, shared by the tag, customer, and
project eviction paths.

* chore(lint): suppress fail-safe registry excepts and ratchet BLE001 budget

* docs(proxy): trim registry cache commentary to single-line why docstrings

* fix(lint): move tag fetch return to else block to satisfy TRY300 budget
2026-08-17 18:52:13 +00:00
Thijmen Stavenuiter
9b30e73d07 fix: cap /v2/key/info batch size to bound spend-log query fan-out 2026-08-17 20:48:12 +02:00
mateo
1e1a2b63a4 fix(ocr): validate body req_format in the proxy endpoint and run its tests in CI
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-17 18:29:35 +00:00
yucheng-berri
1139012b45
fix(guardrails): scan text on /guardrails/apply_guardrail for Azure Content Safety (#36894)
* fix(guardrails): scan text on /guardrails/apply_guardrail for Azure Content Safety

The two Azure Content Safety guardrails never implemented apply_guardrail, so the
endpoint fell through to the base no-op and answered 200 with the caller's text
echoed back, having scanned nothing.

Implementing that method also flips the proxy's unified-vs-native dispatch, which
would move request traffic off these guardrails' own hooks. Add an opt-out that
keeps every lifecycle event on the native hooks, so only the endpoint changes.

* test(guardrails): cover the remaining native-hook opt-out dispatch sites

Adds regression tests for the parallel post-call path, the MCP post-call hook, and
the policy engine step, so every read of the opt-out flag fails when removed.
2026-08-17 11:18:40 -07:00
mateo
bfc52b94db fix(ocr): return 400 for an unknown x-req-format header value
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-17 18:11:57 +00:00
mateo
57d739b433 feat(ocr): add req_format=native to return Azure Document Intelligence's own analyzeResult payload
Callers can opt into the provider's raw operation response on /v1/ocr with the x-req-format: native header (or req_format in the body) while page-based cost tracking keeps reading usage_info off the normalized response.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-17 18:03:53 +00:00
mubashir1osmani
141ada1118 feat(batches): aggregate reasoning tokens and per-line pass/fail counts
Batch retrieval already computed cost/usage on completion, but silently
dropped reasoning tokens and never counted per-line success/failure.
Adds BatchCostUsageResult (replacing bare cost/usage/models tuples) with
successful_requests/failed_requests, and threads reasoning_tokens through
the aggregated Usage. Both surface on SpendLogs the same way batch_models
already does.
2026-08-17 11:25:55 -04:00
Thijmen Stavenuiter
31130036c0 refactor: build budget window usage without in-place mutation
Replace _attach_budget_limits_usage, which rewrote the caller's key_info dict, with _budget_limits_with_usage returning a new list. Callers assign the result once. Keeps the response shape and spend-counter read path identical while following the repo's no-mutation rule.
2026-08-17 16:21:31 +02:00
mateo-berri
5965648547 fix(proxy): close websocket cleanly when OpenAI credentials are missing 2026-08-16 14:40:37 -07:00
mateo-berri
4ba9d6b136 fix(proxy): expose url join helper at module level for websocket route 2026-08-16 14:29:21 -07:00
mateo-berri
81aefe4b3c Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_pr36151_ws_passthrough
# Conflicts:
#	litellm/proxy/pass_through_endpoints/pass_through_endpoints.py
#	tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py
2026-08-16 14:21:57 -07:00
mateo-berri
862f33bbaa fix(proxy): negotiate client subprotocol on OpenAI websocket passthrough 2026-08-16 14:11:12 -07:00
mateo-berri
a258b2b130 fix(proxy): harden OpenAI websocket passthrough
- decode upstream first frame as utf-8 instead of ascii
- reject model-restricted keys at connect to match HTTP model enforcement
- log the actual request path for /openai_passthrough traffic
2026-08-16 13:51:50 -07:00
Siraj637909
785eed616f fix(proxy): strip extra_headers/headers/aws_session_token from GET /health (gh-36898)
`/health` already stripped `api_key` from each deployment row via
`ILLEGAL_DISPLAY_PARAMS`, but `extra_headers`, `headers`, and `aws_session_token`
were never added to that list, so `GET /health` leaked provider credentials
(Azure `api-key`, Google `x-goog-api-key`, Bearer tokens, AWS session tokens) in
plaintext to any caller, even without a master key.

Add those three fields to `ILLEGAL_DISPLAY_PARAMS` so `_clean_endpoint_data()`
omits them for all callers, matching how `api_key` is already handled.

Fixes #36898
2026-08-16 18:28:45 +05:30
Shivi Jain
a6dc447470 fix(proxy): stop Responses batch rows from bypassing project OTPM
Embeddings rows were identified by body shape (has `input`, no
`messages`/`prompt`), which also matches a `/v1/responses` batch row
and reserved zero output tokens for it -- letting a project caller run
large Responses generations against a quota-limited model without
consuming OTPM. Classify embeddings by the row's own `url` instead,
and read `max_output_tokens` as a Responses output cap alongside
`max_tokens`/`max_completion_tokens`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 16:26:30 +05:30
Shivi Jain
8a44c14928 Merge upstream/litellm_internal_staging and fix batch quota review comments
Resolves conflicts from the upstream merge and addresses the Veria-AI
review comment on this PR: batch rows could bypass a project's
per-model ITPM/OTPM quota when the batch's file-bound/routing model
had no quota configured. Charges each row's own model against its own
project quota instead of only the routing model's, and fixes rate
limit error messages to attribute the correct model via a new
descriptor_value field on RateLimitStatus/AtomicCounterMeta. Also
re-syncs the ruff-strict, type-discipline, and basedpyright budgets
against the correct (non-stale) merge base.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 15:47:46 +05:30
devin-ai-integration[bot]
74a1beda77
fix(panw_prisma_airs): scan tool call args as plain text, not a tool_event (#37038)
* fix(panw_prisma_airs): scan tool call args as plain text, not a tool_event

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

* refactor(panw_prisma_airs): type the tool call argument extractor

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

* test(panw_prisma_airs): cover tool call error fallback and dict masking paths

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

* fix(panw_prisma_airs): scan tool names with args and tolerate custom tool calls

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

* fix(panw_prisma_airs): scan tool call arguments that arrive already parsed

The tool call slice types arguments as a string, so a client posting parsed JSON
failed validation and the whole tool call, name included, read as unscannable and
was skipped without ever reaching AIRS. The OpenAI request path forwards
client-supplied tool_calls verbatim, so that shape is reachable.

Coerce non-string arguments instead of rejecting them, so the content is scanned.

* fix(panw_prisma_airs): route tool-block masked data by scan side, not by key name

Merging #37036 (already on staging) with this PR produces no conflict and a
silent bug. #37036 withholds prompt_masked_data on response-side tool blocks,
which was right while tool calls went out as a request-side tool_event: AIRS
reported the model's arguments under that key. This PR scans tool calls as
ordinary prompt/response text, so the side of the scan now decides which key
holds what. The model's arguments arrive under response_masked_data, already
covered by _CLIENT_HIDDEN_SCAN_FIELDS, and prompt_masked_data goes back to
being the caller's own input -- one of the audit fields LIT-5638 asks for.

Left as merged, a response-side tool block drops that field with nothing to
flag it.

- Tool-path block branch calls _build_error_detail without also_hide
- also_hide parameter removed; after this change it has no callers
- Regression test asserts both directions: model output withheld, caller
  input preserved. It fails against the auto-merged combination.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(panw_prisma_airs): a wrong-typed tool name must not suppress the scan

_ToolCallFunctionSlice types name as str, and _get_tool_call_function turns any
ValidationError into (None, None), which _scan_tool_calls_for_guardrail reads as
an unscannable tool call and skips. So a client posting "name": 123 keeps its
arguments off the wire to AIRS entirely -- no error, no log, no block. The
OpenAI request path forwards client tool_calls verbatim, so this is reachable by
any caller holding a valid key.

_coerce_arguments already existed for exactly this failure mode on the sibling
field. Widening it to cover name closes the gap:

  name='transfer_funds'   AIRS called: 1x   args scanned: True
  name=123 (int)          AIRS called: 0x   args scanned: False   <- before
  name=123 (int)          AIRS called: 1x   args scanned: True    <- after

Reported by Cursor Bugbot on fd9f6396e5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yucheng Zhu <yucheng@berri.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 23:14:43 +00:00
mateo-berri
bb1c3366cf Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_batch_cost_accounted_once
# Conflicts:
#	tests/test_litellm/proxy/openai_files_endpoint/test_files_common_utils.py
2026-08-15 15:56:11 -07:00
Mateo Wang
9a96b9327c
Merge pull request #37058 from BerriAI/litellm_passthrough_accept_encoding
fix(passthrough): stop forwarding client Accept-Encoding upstream
2026-08-15 15:49:46 -07:00
mateo-berri
90493a217f fix(passthrough): protect accept-encoding from x-pass- forwarding 2026-08-15 15:34:28 -07:00
devin-ai-integration[bot]
d7d10be063
fix(guardrails): return the full PANW AIRS scan response on blocked requests (#37036)
* fix(guardrails): return the full PANW AIRS scan response on blocked requests

The blocked-request error detail was assembled from a hardcoded allowlist, so audit fields like prompt_detection_details, prompt_masked_data, source, transaction_id and session_id never reached the client even though AIRS returned them.

Resolves LIT-5638

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

* style(guardrails): drop redundant comment in AIRS error detail

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

* fix(panw_prisma_airs): withhold response_masked_data from the blocked-response error

The full AIRS passthrough also reached the response-side block path, where
response_masked_data carries the model's own generation. That branch is only
reached when mask_response_content is False, so the operator had explicitly
declined to deliver that text, and the error body handed it back anyway.

Withhold response_masked_data from the client-visible detail. prompt_masked_data
stays: it is the caller's own input and one of the fields the ticket asks for.

Every other AIRS field, including prompt_detection_details, source,
transaction_id and session_id, is unchanged.

* fix(panw_prisma_airs): withhold generated tool args from response-side blocks

_scan_tool_calls_for_guardrail calls AIRS with is_response=False because
tool_event is request-side in the AIRS schema, so AIRS returns the scanned
tool arguments under prompt_masked_data. When the tool calls being scanned
are the model's own output, that key holds generated content, and the
_CLIENT_HIDDEN_SCAN_FIELDS default (response_masked_data, empty on this
path) does not cover it. With the default mask_response_content=False the
block branch then shipped the model's masked tool arguments in the 400 --
the same content channel this PR closed for response_masked_data.

_build_error_detail takes an extra_hidden_fields argument so the withholding
stays in one place, and the tool-call block branch passes prompt_masked_data
when is_response is True. Request-side blocks are unchanged and still carry
prompt_masked_data, which is what LIT-5638 asks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(panw_prisma_airs): apply ruff format

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yucheng Zhu <yucheng@berri.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 15:31:23 -07:00
mateo-berri
d0be6eee8a fix(passthrough): stop forwarding client Accept-Encoding upstream 2026-08-15 15:22:06 -07:00
tin-berri
4eadf92ade
feat(mcp): scope gateway session bearers to the RFC 8707 resource (#35045) 2026-08-15 14:56:34 -07:00
Mateo Wang
5e9adb9ed7
Merge pull request #37047 from BerriAI/litellm_bedrock_batch_output_bucket
fix(bedrock): resolve the managed-batch output bucket on every path that reads it
2026-08-15 14:37:25 -07:00
mateo-berri
d9e377f129 fix(batches): confirm poller batch_processed support at startup so no retrieve accounts inline before the first poll cycle
Probe the column before the scheduler registers CheckBatchCost, closing the window where a retrieve that decided the poller was inactive billed a batch the first poll cycle then billed again. Also drop narration docstrings and section banners from the new tests.
2026-08-15 12:56:53 -07:00
mateo-berri
a10669b28c Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_batch_cost_accounted_once 2026-08-15 12:17:48 -07:00
yucheng-berri
873215572a
fix(ptu): stop a PTU deployment billing for grounded search (#37043)
* fix(ptu): stop a PTU deployment billing for grounded search

A PTU deployment is billed by the flat cost of its reserved capacity, so the
model write endpoints refuse a rate the caller supplies and zero the ones already
stored. search_context_cost_per_query escaped both: it holds its rates in a table
keyed by context size, and the guard only recognised a number as a price, so a
grounded request on a PTU deployment kept billing per search on top of the flat
cost.

A table now counts as a price when it holds a non-zero rate. It is zeroed in
place rather than emptied the way tiered_pricing is, because an absent table
means the provider's own default rate rather than free, so dropping it would
start a charge instead of stopping one. For the same reason an all-zero table is
not read as a price: it is how an operator expresses free.

* fix(ptu): zero the search rate on every PTU deployment

A deployment that never stored its own search table is the normal case, and an
absent table means the provider's default rate, so the zeroing has to be written
unconditionally the way the per-token zeros already are. Writing it only where a
table was already stored left the default path billing per grounded search, which
is the charge this set out to stop.

The predicate that reads a table is split out rather than recursing, since the
repo's recursion gate rejects an unignored recursive function and one level is
all a rate table needs.
2026-08-15 12:15:46 -07:00
mateo-berri
f93098068e Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_do_36634
# Conflicts:
#	litellm/batches/batch_utils.py
2026-08-15 12:12:47 -07:00
Thijmen Stavenuiter
34bc22fffd fix: address pr comments 2026-08-15 21:10:12 +02:00
Mateo Wang
3ac2fbe1b0
Merge pull request #36876 from cu-aaii/litellm_fix_batch_spend_log_constant_request_id
fix(spend): give a batch's cost row a primary key of its own
2026-08-15 12:05:43 -07:00