Commit graph

4286 commits

Author SHA1 Message Date
tin-berri
2306816d40
fix(shadow_eval): refuse a judge model that also serves one of the arms it grades (#38589)
A shadow eval whose judge_model is one of the router's tier models, the router's
default model, or a reverse job's baseline_model was accepted with no warning. An
LLM judge scores its own output higher than a rival's, so that tier's win rate
measures the judge instead of the models, and the job's whole budget buys a result
that has to be thrown away.

start_shadow_eval now rejects it with a 400 naming the colliding arm.

`judge_target` is the single answer to "where does a call to this name go for this
caller, and what answers it", and the resolvability gate, the collision gate and
the judge dispatch all read it. It has three outcomes and no others: the router
serves the name, the SDK serves it, or nothing does. Splitting that question is
what every bug here came from, so `router_resolves_model` and `answering_models`
are gone rather than joined by a third.

Two spellings of one model are one identity. A name is compared by what would
answer it, resolved through every channel `get_model_list` composes and then put
in the provider-qualified form litellm itself uses, so a judge given as `gpt-4o`
collides with a tier deployment serving `openai/gpt-4o`, and a judge given as
`openai/gpt-4o` collides with a deployment configured as bare `gpt-4o`. Both ends
are normalised because an admin writes them at different times.

Answering is also per-caller. The shadow and judge calls carry the shadowed key's
`user_api_key_team_id`, which is what the router selects deployments with, so the
endpoint derives the job's teams once from the keys it already looks up and every
check runs under them, and the judge dispatch picks its arm under the same team.
A team's public model name resolves to nothing for everyone else and a team's own
deployment resolves for nobody else, so a check that omits the team answers for a
caller who does not exist. A collision under any one team fails the job, because
every key's verdicts land in the same win rates.

Three sites were separately re-deriving "the provider models this name resolves
to", with unexplained divergence in whether they fell back to the literal name.
`Router.resolved_litellm_models` is now the one owner; the routing-plugin
candidate list and the stream-options check both delegate to it, and
`_deployment_litellm_model` is gone.

The router's arms come from `strategy_router_dependencies`, the same enumeration
the health check reads. Only the roles that serve are arms: a classifier or
embedding model picks the tier and never produces a response anyone judges. A
semantic auto-router keeps its routes in an opaque config blob, so only its
default model is enumerable and the guard is incomplete there by design, able to
miss a collision but never to invent one

The two regenerated artifacts carry `presidio_analyze_chunk_size_bytes` from
alters the spec; the sync gate runs on any PR touching litellm/proxy, so this one
has to carry the base's drift to go green
2026-08-27 18:44:44 -07:00
tin-berri
09b23742e7
feat(proxy): dry-run a real request body on /auto_router/test_routing (#38590)
The endpoint built messages=[{"role": "user", "content": prompt}], so a dry run
could not carry prior turns, the caller's system prompt, or the tool definitions
a request advertises. A real agentic turn reduced to its last sentence classified
as trivial, which is why a config sweep reported savings for every configuration.

Accept messages, system and tools, and forward them to the same pre-routing hook
untranslated, with the raw-body snapshot built by the serving path's own owner,
refresh_proxy_server_request_body_snapshot. Loose types are deliberate: the hook
reads whatever dialect the surface produced, so validating against one surface's
schema would reject the others.

prompt stays as the single-ask shorthand, normalized into one user turn inside the
request model so the handler carries no mode branch.
2026-08-28 01:35:10 +00:00
ryan-crabbe-berri
32b8edb4d5
Merge pull request #38572 from BerriAI/litellm_fallback_access_group_check
feat(proxy): opt-in enforce_fallback_model_access authorizes router fallbacks against the calling key
2026-08-27 18:03:34 -07:00
yucheng-berri
74050e03c5
fix(guardrails): add fail-open mode to CrowdStrike AIDR guardrail (#38568)
* fix(guardrails): add fail-open mode to CrowdStrike AIDR guardrail

Add a fail_on_error param (default True, preserving existing behaviour) to
the CrowdStrike AIDR guardrail, mirroring model_armor and generic_guardrail_api.

When fail_on_error=False the guard fails open only on server errors (5xx) and
connectivity failures, so the request proceeds unmodified. Caller-controlled
4xx responses and result.blocked policy blocks always fail closed. The
applied-guardrails header is recorded even on the fail-open path.

* fix(guardrails): fail open AIDR 4xx

* refactor(guardrails): isolate AIDR fail-open

* style(guardrails): format AIDR fail-open

* ci: satisfy unit workflow timeout invariant

* refactor(guardrails): accept AIDR mappings

* test(guardrails): inject AIDR HTTP client

* fix(guardrails): harden AIDR fail-open against delivered verdicts and record fail-open status

Reads the blocked verdict from the raw body before guard_output validation so schema drift or a changed verdict type cannot fail open past a delivered block. A transformed response that cannot be parsed fails closed so delivered redactions are never dropped. Fail-open runs record guardrail_status guardrail_failed_to_respond with timings instead of success. Restores the fail-open behavior tests dropped mid-PR and reverts the payload Mapping widening

* test(guardrails): cover fail_on_error wiring and fail-closed default for CrowdStrike AIDR

* chore(guardrails): annotate the transformed-drift detail payload for the LIT002 budget

---------

Co-authored-by: abrekhov <abrekhov@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-08-27 17:57:35 -07:00
yucheng-berri
239ec955dc
fix(presidio): chunk oversized text before /analyze so large content blocks do not fail (#38483)
* fix(presidio): chunk oversized text before /analyze so large content blocks do not fail

The Presidio PII guardrail sent each content block to the analyzer as a
single /analyze call with no size check. Analyzer deployments commonly cap
the request body (the reporting deployment rejects bodies over 1,000,000
bytes with HTTP 413), so large blocks failed closed, and analyzer latency
grew linearly with payload size.

analyze_text now splits texts larger than presidio_analyze_chunk_size_bytes
(default 500,000 UTF-8 bytes, configurable per guardrail) into overlapping
chunks, analyzes them concurrently, remaps each detection's start/end onto
the original text, and deduplicates detections from the overlap regions.
Anonymization, blocked-entity checks, score filtering, numbered-token
unmasking, telemetry, and the dashboard entity positions all consume the
remapped global offsets unchanged.

Resolves LIT-4785

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

* fix(presidio): review-round hardening for chunked analyze

- measure the chunk budget on the JSON-serialized text (non-ASCII escapes
  expand beyond raw UTF-8, so a raw-byte budget could still exceed the
  analyzer body limit)
- share the chunk fan-out semaphore per event loop and instance instead of
  per call, so many oversized blocks cannot multiply concurrent analyzer
  calls
- apply configured score thresholds and deny list per chunk BEFORE overlap
  resolution, so a below-threshold span cannot displace a detection the
  thresholds keep

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 17:41:55 -07:00
ryan-crabbe-berri
76e7bd41f4 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fallback_access_group_check 2026-08-27 17:29:08 -07:00
devin-ai-integration[bot]
eb0e3f8c18
feat(ui): session-level cache observability in request logs (#38442)
* feat(ui): session-level cache observability in request logs

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

* fix: guard cache_hit filter against non-string defaults in direct calls

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

* refactor(ui): drop redundant cache_hit field comment

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-08-27 17:10:01 -07:00
tin-berri
ec94a1f82a
fix(router): reject complexity-router settings written outside complexity_router_config (#38570)
A complexity-router setting placed beside complexity_router_config, or inside a
tier entry's litellm_params, is read by nobody: the router loads its settings only
from litellm_params.complexity_router_config. It does not stay inert. The
alias-marker forwarding and the per-tier param spread carry every unrecognized key
onto the outbound request, and all_litellm_params only knows the outer names, so
the key reaches the provider as an unknown body field and every call through that
model group fails with an error naming an internal config key.

Guard the whole set, derived from ComplexityRouterConfig.model_fields so a field
added later is covered, and scoped to complexity-router deployments because the
names only mean this there (embedding_model is a legitimate flat param on an
s3_vectors vector store). Scope is read from the same merged field view the naming
check is judged on, so a router named only by its default model is in scope and a
field added to the required-field table is covered without another edit. The write
endpoints reject with a 400 naming the keys and where they belong, config.yaml
refuses to start for the same reason max_agentic_loops does, and a tier entry is
judged by the config model itself.

An already-stored deployment keeps loading, so an upgrade cannot take a running
gateway down over a row that was written before the gate existed.
2026-08-27 17:04:49 -07:00
devin-ai-integration[bot]
d392e7faae
feat(alerting): add native Microsoft Teams alerting destination (#38367)
* feat(alerting): add native Microsoft Teams alerting destination

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

* fix(alerting): preserve active destinations on MS Teams save and confirm health test delivery

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

* fix(ui): read persisted alerting destinations at MS Teams save time

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-08-27 16:19:22 -07:00
ryan-crabbe-berri
d4c3b3e7c1 feat(proxy): gate fallback model access enforcement behind enforce_fallback_model_access 2026-08-27 15:45:10 -07:00
Mateo Wang
4ef1c28877
Merge pull request #38431 from BerriAI/litellm_fix_messages_native_tools
fix(anthropic-adapter): pass provider-native and OpenAI-format tools through on /v1/messages
2026-08-27 15:24:07 -07:00
Mateo Wang
55c1537497
Merge pull request #38376 from BerriAI/devin_ai_bedrock_guardrail_external_id
fix(guardrails): forward aws_external_id when the bedrock guardrail assumes a role
2026-08-27 15:22:11 -07:00
ryan-crabbe-berri
d18bfe176e fix(proxy): fail closed when fallback authorization lookup errors
A non-ProxyException from the team, project or access-group lookup used to
escape the fallback loop and replace the provider's error. Treat it as a
denial and log it. Also drop the unrelated reformatting of test_router.py
and test_fallback_event_handlers.py so both diffs are additions only.
2026-08-27 15:13:24 -07:00
tin-berri
40ff01b987
feat(mcp): let a resolved OAuth token target a custom upstream header (#38456)
An MCP server behind an API gateway needs two credentials on one request: the
gateway's own token on a private header, and a separate bearer on Authorization
for the server behind it. Every arm that minted or held a token hardcoded
Authorization, and the conflict rule then dropped the operator's static
Authorization to make room, so the second credential never arrived.

ApiKeyConfig already modelled this as header_name plus value_prefix behind a
header() method. Extend that carrier to the four minted-token configs, have each
resolver arm ask its config which header to use instead of naming one, and drop
only the header the resolved credential is about to occupy.

Operators set it per server via upstream_token_header, plumbed through
config.yaml, the credentials blob, the management API and the admin form, on the
M2M, token-exchange, authorization-code and ID-JAG arms. It is non-secret so it
stays plaintext and round-trips on admin reads. Unset keeps today's behaviour.

Moving a credential off Authorization means it stops inheriting what Authorization
gets for free, so the slot now carries those protections itself. httpx drops
Authorization when a redirect crosses origin and keeps every other header, so a
custom slot is dropped by the client on the same condition, mirroring httpx's own
scheme/host/port rule with an agreement test that fails if the two ever diverge.
The v1 path also mirrors the v2 conflict rule, so an injected header cannot shadow
the credential the gateway resolved for that slot.

Which header a credential occupies, and what counts as being that header, was
answered independently in nine places by four hand-rolled comparisons. same_header,
has_header and without_header in litellm/types/mcp.py are now the one owner, shared
by both MCP stacks, and the client derives its slot once instead of three times.

The header name reaches egress verbatim, so the RFC 7230 grammar lives in one
place and is checked where servers are built: a bad value fails the config load
and the management API returns 400, rather than raising while a spec is built
and emptying the aggregate tool list for every other server. A blank means unset,
matching what the endpoint already accepts.
2026-08-27 14:32:01 -07:00
ryan-crabbe-berri
3ea501430b fix(router): authorize config-level fallback targets against the calling key
Router fallbacks configured in router_settings were attempted without
re-checking whether the calling key could use the fallback model, so a key
limited to one access group was served by any model listed as a fallback
for something it could call. Auth only validated the requested model and
fallbacks sent in the request body.

Add a fallback_access_check predicate to Router, consulted before every
cross-model-group fallback attempt; rejected targets are skipped and the
primary's own error is raised when none remain. The proxy injects a check
that runs the same key, team and project model access checks the requested
model goes through.
2026-08-27 14:08:51 -07:00
Mateo Wang
0441faadca
Merge pull request #37833 from BerriAI/litellm_deflake_20260821
fix: roll up the open deflake fixes for the MCP logging queue, PTU rollup, license gate, and pricing test isolation
2026-08-27 13:42:26 -07:00
devin-ai-integration[bot]
fe87b187c6
fix: keep schema reconciliation from fighting a partitioned LiteLLM_SpendLogs (#38452)
* fix: keep schema reconciliation from fighting a partitioned LiteLLM_SpendLogs

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

* fix: scope partitioned SpendLogs detection to Prisma's target schema

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

* fix: default partition detection to Prisma's public schema, not current_schema()

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-08-27 12:52:52 -07:00
devin-ai-integration[bot]
6b1844442c
fix(key_management): allow /key/update to keep or shrink MCP server grants the key already holds (#38463)
* fix(key_management): allow /key/update to keep or shrink MCP server grants the key already holds

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

* fix(key_management): reuse key row's included object_permission instead of a second lookup

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-08-27 12:51:17 -07:00
devin-ai-integration[bot]
390595c626
fix(auth): skip guaranteed-miss team lookup for the litellm-dashboard sentinel (#38471)
* fix(auth): skip guaranteed-miss team lookup for the litellm-dashboard sentinel

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

* style: ruff format

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

* test: assert builder result instead of swallowing exceptions; drop redundant comment

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-08-27 12:48:11 -07:00
devin-ai-integration[bot]
de53283356
feat(proxy): opt-in budget rollover carrying overage into the next window (#38514)
* feat(proxy): opt-in budget rollover carrying overage into the next window

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

* fix(proxy): zero under-cap rows before decrementing over-cap rows in cascade resets

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-08-27 12:46:09 -07:00
devin-ai-integration[bot]
e16aa9f512
fix(mcp): keep upstream OAuth Authorization when jwt signer hook injects one on tools/call (#38555)
* fix(mcp): keep upstream OAuth Authorization when jwt signer hook injects one on tools/call

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

* fix(mcp): only treat server credential as occupying Authorization when it maps to that header

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-08-27 12:44:36 -07:00
Mateo Wang
452254963e
feat(health): opt-in model-group allowlist for background health checks and health-check routing (#38539)
* feat(health): opt-in model-group allowlist for background health checks and health-check routing

* fix(health): merge shared health states per writer scope instead of replacing

* refactor(health): drop restating comment and parameterize test scope annotations

* chore: remove stray generated prisma migration file

* fix(health): merge health states against the Redis snapshot, not the pod-local copy

* fix(health): fall back to the pod-local snapshot when the Redis read returns nothing
2026-08-27 12:25:56 -07:00
devin-ai-integration[bot]
2d0c9eed4d
feat(otel): support per-team/per-key service.name for OTel v2 destinations (#38532)
* feat(otel): support per-team/per-key service.name for OTel v2 destinations

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

* test(otel): pin key-level otel_service_name_override surviving team metadata merge

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

* fix(otel): key-level otel_service_name outranks team's after metadata merge

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-08-27 11:50:41 -07:00
devin-ai-integration[bot]
a7da7928fa
feat(ui): add cache hit/miss filter to Request Logs (#38432)
* feat(ui): add cache hit/miss filter to Request Logs

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

* fix: guard cache_hit_filter validation for direct handler calls

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

* chore(ui): drop redundant cache filter comment

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-08-27 17:57:07 +00:00
Mateo Wang
62341e96ae
Merge pull request #38410 from BerriAI/litellm_regenerate_lazy_openapi_snapshot
fix(proxy): regenerate lazy OpenAPI snapshot and guard it in CI
2026-08-27 10:55:38 -07:00
Imran Ismail
02dcc4d347
fix(ui_sso): resolve highest privilege Entra app role, not first in claim (#36728)
* fix(ui_sso): resolve highest privilege Entra app role, not first in claim

A user assigned more than one Entra app role — commonly by belonging to
several assigned groups — arrives at the Microsoft SSO callback with every
role in the id_token `roles` claim. LiteLLM stores a single role per user,
and get_microsoft_callback_response collapsed the list by taking the first
value that resolved to a LitellmUserRoles and breaking.

Entra does not guarantee the ordering of the `roles` claim, so which role
won was effectively arbitrary: a user in one group mapped to internal_user
and another mapped to proxy_admin_viewer could be silently demoted to
internal_user, and proxy_admin could lose to either.

The generic/Okta path already resolves this correctly via
determine_role_from_groups, which walks a documented privilege hierarchy.
Hoist that hierarchy into LITELLM_USER_ROLE_HIERARCHY and reuse it, so
app-role logins and group-mapping logins agree.

Extract the selection into MicrosoftSSOHandler.get_user_role_from_app_roles
so it is directly testable — the existing tests re-implemented the loop
inline, which is why the ordering bug was not caught.

Behaviour is unchanged for single-role claims, unrecognised values, and
empty claims. Roles the hierarchy does not rank (org_admin, team, customer)
are resolved deterministically rather than by claim order.

* refactor(ui_sso): trim role selection prose and use immutable annotations

Addresses review feedback on the app role selection helper.

Drop the explanatory comments and the Args/Returns docstring boilerplate that
restated the control flow, keeping only the part a reader cannot infer from the
code: that Entra does not guarantee claim ordering, and how unranked roles
resolve.

Type the parameter as Sequence[str] rather than list[str] and build the resolved
set as a frozenset, so the helper stops adding an LIT001 mutable-collection
annotation. Make LITELLM_USER_ROLE_HIERARCHY a tuple for the same reason.

No behaviour change: the ordering regression tests still fail against the
previous first-match-wins logic and pass here.
2026-08-27 10:26:45 -07:00
Mateo Wang
98d231c09b
Merge pull request #38398 from daniel-meismer-zocdoc/litellm_mcp_bearer_scheme_refresh
fix(mcp): canonicalize bearer scheme on bridge egress
2026-08-27 09:41:33 -07:00
Mateo Wang
86ef1fb08b
Merge pull request #38391 from BerriAI/litellm_toggle_internal_health_check_logs
feat(ui): toggle internal health check visibility in request logs
2026-08-27 09:29:45 -07:00
Devin AI
09ff5bf6cd Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_deflake_20260821 2026-08-27 09:17:37 +00:00
Mateo Wang
aedaf4d0b0
Merge pull request #38434 from BerriAI/litellm_propagate_prompt_deletes
fix(prompts): propagate prompt deletes to every worker and pod
2026-08-26 21:03:00 -07:00
devin-ai-integration[bot]
2e2c8200ae
fix(scim): apply default_team_params (incl. models) to SCIM-created teams (#38433)
* fix(scim): apply default_team_params (incl. models) to SCIM-created teams

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

* test(scim): annotate default_team_params regression test parameters

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-08-26 20:49:21 -07:00
devin-ai-integration[bot]
172e3aceaf
fix: bound row count on GET /spend/logs to stop unbounded LiteLLM_SpendLogs scans (#38420)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-26 20:48:10 -07:00
devin-ai-integration[bot]
ee76c9a6f4
fix(mcp): accept raw x-litellm-api-key on streamable HTTP admission (#38364)
* fix(mcp): accept raw x-litellm-api-key on streamable HTTP admission

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

* chore(mcp): drop comments restating parser behavior

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-08-26 20:45:51 -07:00
mateo-berri
6d7fafa347 fix(anthropic): close hybrid tool-name allowlist gap and keep native tools through guardrails 2026-08-26 20:37:11 -07:00
yuneng-jiang
3eba0b332a
Merge pull request #38430 from BerriAI/litellm_/budget-update-e2e-skip-81b8fa
fix(budget): serialize model_max_budget before the /budget/update write
2026-08-26 18:51:17 -07:00
devin-ai-integration[bot]
4bf40c4e8d
fix(logging): stop billing and logging response reads as LLM calls (#36890)
* fix(logging): stop billing and logging response reads as LLM calls

Retrieving, deleting or cancelling a stored response, and vector store management calls, run through the same logging lifecycle as inference. A retrieved response replays the usage of the call that created it, so every read priced it again and wrote a second spend log row for the same tokens. Non-inference calls now cost 0, report no usage, log no placeholder chat message, and get a litellm.responses_management operation name instead of reading as chat.

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

* fix(responses): keep billing background response jobs after the poll

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

* fix(logging): use an empty list for read-call messages

A tuple matches no branch in the loggers that walk this value, so lunary's
parse_messages falls through to clean_message and raises AttributeError on the
success hook. An empty list reads as no messages everywhere: it satisfies the
isinstance(list) checks in newrelic, mlflow and datadog, iterates zero times in
traceloop and helicone, and is what StandardLoggingPayload.messages is typed to
hold. None would be type-legal too but is not iterable, so it trades one crash
for another in mlflow and traceloop.

* fix(otel): stop the legacy emitter reporting replayed tokens on response reads

The zeroing so far lands in the standard logging payload, which the legacy
OpenTelemetry emitter does not read for usage: it takes prompt, completion and
total tokens straight off the response object, so a retrieval span still carried
the token counts of the call that produced the response, and the token usage
histogram still recorded them. That emitter is the default, so the spend row said
zero while the trace said otherwise. The background cost poller keeps its counts,
the same exemption the pricing path already makes.

* fix(logging): keep billing a background response when its retrieval is read

A response created with background=true comes back queued and carries no usage, so
its create bills nothing. The retrieval that first sees the finished job is the only
place that job's tokens are ever visible, and pricing every read at zero therefore
loses the spend outright rather than deduplicating it. On a proxy without the
enterprise cost poller a background job ended up costing $0 end to end.

is_unbilled_non_inference_call now takes the response it is deciding about and treats
a background response the same way it already treats the poller's own read, which is
the same exemption seen from the other side. The legacy OpenTelemetry emitter's time
per output token metric picks up the read gate it was missing, so it stops dividing a
read's latency by the replayed completion token count.

* test(proxy): pass the read response to the non-inference predicate

The poller test called is_unbilled_non_inference_call with the pre-background signature, so it broke when the predicate gained the response it classifies. It now hands the predicate a foreground read, and asserts that the same read is free without the origin stamp, so the stamp is what the test proves.

* fix(otel): stop the v2 metrics recorder reporting replayed tokens on response reads

The v2 span builder sources usage from the standard logging payload, so the
earlier fix already zeroes it there. The metrics recorder reads response_obj
directly, so a responses-management read still recorded the original
generation's tokens into gen_ai.client.token.usage and divided generation time
by them for gen_ai.server.time_per_output_token.

The read still records operation and response duration, under the
litellm.responses_management operation, so it stays observable.

* fix(proxy): keep the response-cost headers on calls priced at zero

Pricing responses reads and vector-store management routes at zero dropped the whole
x-litellm-response-cost family off those replies. The header build reads a falsy zero as
a cost this response never recorded and filters it out, and a call that returns before
pricing stores no cost breakdown for the component headers to read, so a client parsing
the cost off a read got a KeyError where it had previously been handed a number.

Those calls now advertise the family at zero. Retrieving a background response, and the
cost poller's read of one, still report their real cost.

The params-taking form of the predicate moves from opentelemetry into
internal_call_metadata so the proxy header build and the OTEL recorders share one copy.

* fix(proxy): report a zero cost split only under a zero cost total

The component headers were filled from call-type membership alone, while the
total they sit beside keeps its real value when the read priced normally, so a
breakdown that had not landed by the time headers were built could advertise a
real total next to an all-zero split. The split is now reported as zero only
when the total agrees with it, and is otherwise left absent.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yucheng Zhu <yucheng@berri.ai>
2026-08-26 18:34:17 -07:00
mateo-berri
4fd7b9946f fix(prompts): keep prompts created mid-sync out of the deleted-row sweep 2026-08-26 18:22:32 -07:00
mateo-berri
ba7c268a6a fix(proxy): enforce tool allowlist on OpenAI-format tools sent to /v1/messages 2026-08-26 18:16:12 -07:00
Mateo Wang
77765fd302
Merge pull request #36055 from BerriAI/devin_ai_fix_gemini_stream_billing_36042
fix(google_genai): price streamed generateContent with the provider that served it
2026-08-26 18:05:50 -07:00
mateo-berri
26b7bc3583 fix(prompts): propagate prompt deletes to every worker and pod 2026-08-26 18:01:08 -07:00
tin-berri
1df25e26cf
revert(proxy): remove router_model_name from auto-routed response bodies (#38429)
Reverts #37725. The field existed so SDK callers that cannot read
`x-litellm-model-id` could tell which tier an auto-router picked, and the
framework that motivated it was LangChain. `@langchain/openai` builds
`additional_kwargs` and `response_metadata` from fixed key allowlists and drops
unknown fields at both the chunk top level and inside `delta`, so no
proxy-side placement of a namespaced key can reach a LangChain caller.

The complexity router's existing `return_raw_model_name` already covers that
case: it puts the resolved model in the standard `model` field, which
LangChain does propagate (`model_name` is on its metadata allowlist), and the
proxy honors it on both the streaming and non-streaming paths.

Keeps the unrelated cleanup from #37725 that dropped the redundant
function-local `ProxyBaseLLMRequestProcessing` import shadowing the
module-level one in `async_data_generator`.

`TestModelGroupAliasReachesPreRoutingStrategies` asserted on the marker as a
proof of strategy dispatch; the surviving `response.model == "gemini-flash"`
assertion already proves it.
2026-08-26 17:58:30 -07:00
Yuneng Jiang
315144c9cc
test(budget): annotate the new locals with Final 2026-08-26 17:55:33 -07:00
Mateo Wang
5175fda0af
Merge pull request #38407 from BerriAI/litellm_fix_dotprompt_model_swap
fix(prompts): apply prompt templates before routing on /v1/responses and honor ignore_prompt_manager_model
2026-08-26 17:50:49 -07:00
Yuneng Jiang
595ada1ef7
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/budget-update-e2e-skip-81b8fa 2026-08-26 17:46:28 -07:00
Yuneng Jiang
4d6786d420
fix(budget): serialize model_max_budget before the /budget/update write
/budget/update handed prisma the raw update dict, so a model_max_budget
payload reached the Json? column as a nested python dict. prisma-client-py
renders that into the GraphQL mutation as bare object keys rather than a
JSON string, and the query engine rejects it, so every per-model budget
update returned a 500 and the cap was never stored. Model ids carrying
punctuation (glm-5.2) also produced an invalid GraphQL name.

/budget/new already ran its payload through jsonify_object for exactly this
reason. Do the same on the update path. Team member and organization member
budget updates route through this handler too, so they were failing the same
way.

The existing unit tests mocked the prisma table with an AsyncMock that
accepts any dict, which is why this never showed up outside a live proxy.
The new test asserts on what the endpoint hands prisma.
2026-08-26 17:46:17 -07:00
yucheng-berri
ecc49764af
feat(guardrails): track Azure Prompt Shield usage and cost with spend isolation (#38387)
* Track Azure Prompt Shield guardrail usage and cost with spend isolation (LIT-5917)

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

* Resolve credential references and pydantic extras in in-place guardrail updates

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

* Suppress LIT001 on the dict-accepting update helper signature

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 17:42:17 -07:00
mateo-berri
8697a9ffa9 Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_fix_dotprompt_model_swap 2026-08-26 17:36:03 -07:00
mateo-berri
ac2e07f6f4 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_dotprompt_model_swap
# Conflicts:
#	litellm/responses/main.py
2026-08-26 17:36:01 -07:00
yuneng-jiang
f677292901
Merge pull request #38392 from BerriAI/litellm_/search-tools-sync-issue-e522a2
fix(proxy): sync search tools into the router on management writes
2026-08-26 17:34:51 -07:00
Mateo Wang
53a607e088
Merge pull request #38411 from BerriAI/litellm_fix_prompt_patch_sync
fix(prompts): propagate PATCHed prompt templates to every worker and pod
2026-08-26 17:20:27 -07:00