Commit graph

40072 commits

Author SHA1 Message Date
ryan-crabbe-berri
f427284c61 chore(ui): fix post-rebase type drift in useCreateAccessGroup test
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
The deriveErrorMessage mock was typed zero-arg but is called with a spread, which the current vitest/TS types reject after rebasing onto staging. Widen it to accept varargs
2026-06-30 16:49:09 -07:00
ryan-crabbe-berri
bab23bbbbc test(ui): restore empty-string searchEmail guard for useInfiniteUsers
Re-add the regression test verifying useInfiniteUsers(50, "") sends no
user_email query param, so a future change from the truthiness check to a
strict undefined check would fail instead of silently forwarding empty strings.
2026-06-30 16:45:25 -07:00
ryan-crabbe-berri
21bbba687f fix(ui): stop double-dispatching handleError from legacy query hooks
The new global QueryCache.onError already routes every query error through
handleError. The query fetch helpers in these hooks also called handleError
inline, so each failure dispatched twice; today the second call is swallowed
only by the 60s throttle in handleError, which is fragile if that throttle or
handleError ever changes. Drop the inline call from the query paths and let
the throw reach the global sink, which re-derives the same message.

Mutation hooks are left as-is because there is no global MutationCache.onError;
removing their inline handler would drop error reporting entirely.
2026-06-30 16:45:25 -07:00
ryan-crabbe-berri
62d3b5edc2 feat(ui): migrate access groups to the openapi-react-query client
Rebody the access-group hooks on $api so reads and mutations go through the
typed openapi-fetch client instead of hand-rolled fetch helpers. This drops
the local fetch wrappers, manual auth headers, manual error parsing, and the
createQueryKeys factory; the response/request types now come from schema.d.ts.
Public hook signatures are unchanged, so the page, modals, and selector that
consume them are untouched.

Reads rely on the global QueryCache.onError sink; mutations keep an explicit
onError since there is no global mutation sink yet. Tests adopt the vi.hoisted
fetch pattern already used by useUsers, asserting the outgoing request and the
cache invalidation. Stale raw-fetch suppressions for these files are pruned.
2026-06-30 16:45:25 -07:00
ryan-crabbe-berri
ffa3880b80 feat(ui): adopt openapi-react-query as the typed data-fetching client
Stands up the typed transport on top of the generated OpenAPI types and migrates
the first real hook to prove the pattern. src/lib/http/api.ts creates an
openapi-fetch client wrapped by openapi-react-query ($api); base URL stays
call-time via getProxyBaseUrl in an onRequest middleware (not a frozen
createClient baseUrl, which would break server-root-path and worker switching),
and the access token is passed per call via authHeader.

useInfiniteUsers now calls $api.useInfiniteQuery("get", "/user/list", ...) instead
of the hand-rolled userListCall: query params, the response body, and the user
rows are all typed from the spec, and the 11-arg positional call collapses into a
typed query object. Its infinite-query return shape is unchanged, so UsagePageView
is untouched. Global query errors route through QueryCache.onError into the
existing handleError, preserving the session-expiry logout behavior.

The test mocks fetch (installed via vi.hoisted because openapi-fetch captures
globalThis.fetch at client creation) and asserts the real request URLs, the bearer
header, pagination, and the error path.
2026-06-30 16:45:24 -07:00
yuneng-jiang
776b272689
Merge pull request #31735 from BerriAI/litellm_lit_4057_router_settings_routing_groups_save
fix(ui): fix Router Settings Loadbalancing tab save (LIT-4057)
2026-06-30 15:39:56 -07:00
yucheng-berri
41f9d8de7b
fix(proxy): extend banned-params + admin-clear lists for NVIDIA Riva (VERIA-493) (#31742)
Two NVIDIA-Riva-specific fields consumed by the audio-transcription
handler via the provider's `optional_params` passthrough were not
covered by the proxy's existing banned-request-body list or the
admin-config clearing list applied on `api_base` BYOK override:

* `nvcf_function_id`
* `use_ssl`

Add both to `_BANNED_REQUEST_BODY_PARAMS` in
`litellm/proxy/auth/auth_utils.py` and to the kwargs-only list in
`_admin_config_fields_to_clear_on_base_override()` in
`litellm/router_utils/clientside_credential_handler.py`, next to the
analogous provider-specific entries already there (`aws_bedrock_*`,
OCI provider fields, etc.). Same admin opt-ins as every other entry
on those lists (`general_settings.allow_client_side_credentials`
proxy-wide, or `configurable_clientside_auth_params` per deployment).

Regression tests in `tests/test_litellm/proxy/auth/test_auth_utils.py`
cover root-level rejection, the historical `api_key` bypass, both
admin opt-in paths (proxy-wide and per-deployment), nested-container
smuggling via the existing recursive walk, and clearing on
`api_base` override. Mutation check verified.

Resolves VERIA-493
2026-06-30 15:30:08 -07:00
Mateo Wang
a7d8c6f467
test(pass-through): de-flake vertex spend-log test by routing through the proxy (#31689)
* test(pass-through): de-flake vertex spend-log assertion by re-billing

The vertex pass-through spend-log test asserted that a single billed
generateContent call moved the global spend aggregate within a fixed
wait. CI failures show the call returning a valid response with real
usage, yet spend never increasing over a 240s poll.

Pass-through spend logging is best-effort: the success handler is
enqueued on a background worker that can drop or time out an individual
event under load and never retries it, so one billed call occasionally
never reaches LiteLLM_SpendLogs. Waiting longer cannot recover a dropped
event; only re-issuing the call can.

Re-bill the call up to a few times and require at least one to be
tracked, mirroring the sibling jest test that already retries. The test
still fails hard if cost tracking is actually broken, since then every
call records nothing. Also sum spend across all returned days instead of
matching the runner's local 'today', removing a separate UTC-rollover
flake.

* test(pass-through): route vertex spend-log test through proxy via direct HTTP

The vertexai SDK, configured with location="global" and an http api_endpoint
override, intermittently sends generateContent to the public Vertex endpoint
instead of the proxy. Proxy logs from a failing run show all 46 of the test's
own spend-log polls reaching the proxy while zero generateContent calls did, so
LiteLLM never saw the billed call and no spend was ever recorded; re-billing
through the SDK could not help because every retry bypassed the proxy too.

Issue the pass-through request directly over HTTP so it always hits the proxy,
minting a Google token from the same service-account credentials, then assert
that the specific call's own spend log lands with spend > 0, a gemini model, and
custom_llm_provider vertex_ai. A small best-effort retry covers the rare case
where the background logging worker drops a single event; failing every attempt
still fails hard so the test keeps its teeth if cost tracking breaks.

* test(pass-through): reuse LITE_LLM_ENDPOINT and drop needless async in get_tracked_spend
2026-06-30 15:27:48 -07:00
yuneng-jiang
ae8084de74
ci(codspeed): pin benchmark runner to ubuntu-24.04 (#31746)
* ci(codspeed): pin benchmark runner to ubuntu-24.04

ubuntu-latest resolves to different runner images between the BASE
(main/staging) and HEAD (PR) runs, so CodSpeed reports 'Different
runtime environments detected' and emits false-positive regressions
(e.g. a -25.2% swing on test_completion_multi_turn in #31684, an MCP
auth fix with no LLM code changes). Pinning the runner to a fixed
image keeps BASE and HEAD on the same hardware so 1 ms swings on a
~3 ms benchmark stop blocking unrelated PRs.

Fixes #31738

* ci(codspeed): stop running benchmarks on litellm_internal_staging

The CodSpeed check flip-flops on internal staging and on PRs targeting
it (e.g. "+11.75% improvement" on one run, "-25.36% regression" on the
next) because the comparison flags "different runtime environments" and
the benchmarks are only 3-4 ms, so sub-millisecond runner noise swings
the result by 25-30%. Pinning the runner to ubuntu-24.04 in this PR
helps the head side, but the internal_staging base is still recorded on
the old unpinned runner, so comparisons keep flapping until the pin
merges and the base is re-baselined.

Until that settles, the red X's on internal staging make the OSS
project look unhealthy and confuse contributors, so drop the
litellm_internal_staging push and pull_request triggers and keep
CodSpeed running on main only.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-30 15:25:55 -07:00
tin-berri
a0b26d2c3c
Revert "fix(presidio): stream SSE output incrementally instead of buffering t…" (#31764)
This reverts commit 94936a3922.
2026-06-30 21:37:11 +00:00
yucheng-berri
5d4bb7548f
fix(token_counter): count legacy function_call.arguments (VERIA-492) (#31741)
* fix(token_counter): count legacy function_call.arguments (VERIA-492)

token_counter handled the modern assistant tool_calls field but had no
branch for the legacy OpenAI function_call payload. The value is a dict,
so it skipped every special-cased branch in _count_messages and fell
through to the unsupported-key continue, letting arbitrary text in
function_call.arguments slip past the count.

Resolves VERIA-492

* refactor(token_counter): raise on unexpected key in _count_function_call_tokens

Address Greptile P2: the helper's fallback branch previously applied
function_call logic to any key that wasn't tool_calls. Make the contract
explicit so a future caller can't silently miscount.
2026-06-30 14:23:09 -07:00
yuneng-jiang
e337df4e43
Merge pull request #31757 from BerriAI/litellm_/gracious-bardeen-d32d47
chore: bump litellm to 1.92.0 and litellm-enterprise to 0.1.45
2026-06-30 13:55:18 -07:00
Yuneng Jiang
aaa58a72f7
chore: rebuild uv lock for version bumps 2026-06-30 13:21:49 -07:00
Yuneng Jiang
c736ec5285
bump: version 0.1.44 → 0.1.45 2026-06-30 13:21:13 -07:00
Yuneng Jiang
86fbe90f45
bump: version 1.91.0 → 1.92.0 2026-06-30 13:20:57 -07:00
yuneng-jiang
8beb68aa9f
Merge pull request #31740 from BerriAI/litellm_add-claude-sonnet-5-c71c
feat(anthropic): add Claude Sonnet 5
2026-06-30 13:14:44 -07:00
Yassin Kortam
94936a3922
fix(presidio): stream SSE output incrementally instead of buffering the whole response (#31503)
The Presidio streaming post-call hooks (_stream_apply_output_masking for
apply_to_output and _stream_pii_unmasking for output_parse_pii) collected every
upstream chunk, reassembled the full completion with stream_chunk_builder at
end-of-stream, ran Presidio over it, then emitted one reconstructed SSE chunk.
Time-to-first-token collapsed to the total generation time and token-by-token
streaming was lost whenever Presidio output handling was enabled. With the
default presidio_filter_scope both, an apply_to_output masking instance is always
created, so even the unmask configuration buffered the stream.

Both paths now transform and forward chunks as they arrive. The unmask path
replaces placeholder tokens per chunk, holding back only the trailing run that
could still grow into a token so a placeholder split across SSE chunks
(<PER + SON_1>) is still rewritten atomically. The mask path emits a prefix only
when masking it in isolation matches the corresponding prefix of masking the
whole buffer, with a lookahead margin still buffered past the cut, so an entity
straddling the cut is detected and held until complete; past
_PRESIDIO_STREAM_MAX_BUFFER the run is bounded without splitting an entity.
Tool-call and legacy function-call argument fragments are accumulated per choice
and transformed once the choice closes, content is buffered independently per
choice index for correct n>1 streaming, raw Anthropic SSE bytes and /v1/responses
events pass through with any held content flushed first so events never reorder,
and a masking error redacts only the affected chunk (fail closed, keeping
finish_reason) while the stream continues.

Resolves LIT-3222
2026-06-30 12:59:18 -07:00
mubashir1osmani
d4c33b2b59
fix(logging): route realtime success logging through the bounded worker (#31733)
RealTimeStreaming.log_messages dispatched the success handler with a bare
asyncio.create_task, bypassing GLOBAL_LOGGING_WORKER (which gives a per-coroutine
timeout and a concurrency cap). On a long-lived realtime websocket a slow logging
callback left one suspended task per logged turn, each pinning that turn's
assembled response, accumulating without bound (~12-15k in-flight under load in a
repro) until OOM. Route realtime success logging through the bounded worker so
in-flight logging is capped and a hung callback is cancelled at the worker
timeout.

The chat and responses streaming success-logging paths are intentionally left
unchanged: their success callbacks must complete within the call's event-loop run
(the non-streaming path pairs the worker with a synchronous callback; the
streaming path has no such companion), so deferring them through the worker would
drop logs for one-shot SDK calls and breaks test_async_custom_handler_stream.
Bounding those paths needs a load-shedding approach and is left to a follow-up.
2026-06-30 12:54:47 -07:00
ryan-crabbe-berri
4f41a9e140 test(ui): drop the e2e typecheck CI gate, keep the typed import for the editor
The e2e runs against the real proxy, so a contract drift already fails the test at
runtime; tsc only checks the spec against schema.d.ts, a generated snapshot, so a
backend change with a stale snapshot would pass tsc while the live test still
catches it. The dedicated tsconfig + script + CI step were circular ceremony for
that. Keep the zero-runtime-cost type-only import, which still catches mistakes in
the editor, and make its comment honest about what enforces the contract.
2026-06-30 12:50:21 -07:00
Yassin Kortam
be4d0d8439
fix(redis): re-establish async cluster connections after a node restart (#31577)
Some checks failed
GitHub Actions Security Analysis / zizmor (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Has been cancelled
When redis_startup_nodes is set the async cluster client was built with no health check and no TCP keepalive, so a connection silently dropped by a cluster restart (e.g. ElastiCache Serverless maintenance) stayed in the pool and got reused while dead; the first command after the restart stalled in re-initialization until the LoggingWorker timeout cancelled it, surfacing as CancelledError then TimeoutError on the spend-counter path

Build the async cluster client with a 25s health_check_interval and socket_keepalive so an idle connection is PING-validated and reconnected before reuse, and expose both through the cluster kwarg allow-list so an explicit value from config still wins

Resolves LIT-4083
2026-06-30 12:25:15 -07:00
Yassin Kortam
52dc15adfe
fix(proxy): isolate poison spend-log rows so one bad record can't drop the whole batch (#31705)
update_spend_logs flushes the queue with a single create_many per batch, so one
row carrying bytes Postgres refuses (a residual NUL byte is the canonical case)
fails the entire insert and drops every good spend log alongside it. PR #29515
strips NUL bytes from the JSON columns, but the scalar string columns (end_user,
model, session_id, ...) still flow through unsanitized, so a poisoned row can
still reach the write and take a batch of up to 1000 good rows down with it.

On a genuine data-layer rejection the batch is now bisected so the good rows
still persist and only the offending row is dropped and logged with its
request_id. The classification lives in PrismaDBExceptionHandler.is_prisma_data_error
(matched by exact type so systemic subclasses like a missing table are not
mistaken for a single poison row), which keeps prisma an in-function import and
litellm.proxy.utils importable without the proxy extra. Transport failures,
including the "can't reach database server" outage that prisma mislabels as a
DataError, are re-raised unchanged so the existing connection-retry path still
runs and a transient outage never turns into silent per-row data loss.

The bisection carries a per-batch isolation budget so an authenticated caller
flooding poisoned rows cannot amplify one failed bulk insert into ~2N failed
inserts and N log lines; once the budget is spent the still-failing remainder
is dropped wholesale under a single log line.

Resolves LIT-4103
2026-06-30 12:21:14 -07:00
mateo-berri
6d43c21ec6
fix(anthropic): drop redundant supports_output_config from Vertex/Azure Sonnet 5
The Vertex AI and Azure AI Sonnet 5 entries carried supports_output_config:
true, which the gen-5 siblings (vertex_ai/claude-opus-4-8, azure_ai/claude-fable-5,
etc.) do not. The flag only feeds AnthropicConfig._model_supports_effort_param,
which already returns true for these entries via supports_xhigh/max_reasoning_effort,
so output_config.effort still forwards on both routes. Removing it is behavior
neutral and matches the existing per-platform convention for gen-5 Claude.
2026-06-30 19:19:48 +00:00
Mateo Wang
6d828e5759
feat(messages): passthrough /v1/messages to native endpoints via supported_endpoints (#31685)
* feat(messages): passthrough /v1/messages to native endpoints via supported_endpoints

The unified /v1/messages proxy endpoint always translated inbound Anthropic
requests down to /v1/chat/completions (or the Responses API for openai) when the
deployment's provider lacked a native Anthropic-messages config, dropping
Anthropic-only features like cache_control and thinking. Some customers run
OpenAI-compatible servers (self-hosted vLLM, DeepSeek's Anthropic endpoint, etc.)
that also natively expose /v1/messages and want the raw Anthropic payload
forwarded untranslated, while keeping provider openai so /v1/chat/completions to
the same deployment stays native.

Opt in per deployment via model_info.supported_endpoints containing
/v1/messages. When present, the gate routes to a generic, provider-agnostic
OpenAILikeAnthropicMessagesConfig that POSTs the Anthropic payload to
{api_base}/v1/messages with Bearer auth, instead of translating. Default
behavior is unchanged. Generalizes and supersedes the hosted_vllm-only,
env-var-toggled PR #28745.

* fix(messages): preserve standard-cased caller headers in native passthrough

The OpenAI-like Anthropic passthrough config only checked for lowercase header
names before injecting Bearer auth, anthropic-version, and content-type
defaults. A caller sending standard-cased Authorization, Anthropic-Version, or
Content-Type was treated as missing those headers, so LiteLLM added duplicate
lowercase variants and overwrote the caller's credential/version at the HTTP
layer. Header presence is now checked case-insensitively and the merge no longer
mutates the caller dict.

Also moves the feature docs out of the main repo (docs live in litellm-docs).

* fix(openai_like/messages): delegate to parent transform and inject anthropic-beta headers

The passthrough config bypassed the parent transform and skipped header beta injection. Both gaps cause native /v1/messages features (context management, advisor tool, fast mode, structured outputs, reasoning_effort, advisor stripping) to silently degrade on opted-in deployments. Reuse the parent's pipeline and call _update_headers_with_anthropic_beta after merging defaults

* fix: normalize anthropic-beta header key case before beta injection

* style: collapse anthropic-beta header normalization to single line

ruff format --check requires the comprehension on one line (it fits within
the 120 char limit); fixes the lint job failure on the bugbot autofix commit

* fix(messages): forward anthropic-beta to native passthrough upstream

The shared anthropic_messages HTTP handler ran update_headers_with_filtered_beta
with the deployment's custom_llm_provider after validate. For the native
/v1/messages passthrough that provider is openai, which has no beta-header
mapping, so every anthropic-beta value (caller-supplied or feature-derived for
speed/context_management/etc.) was stripped to empty before the upstream
request, breaking beta passthrough to the Anthropic-compatible endpoint.

Beta filtering only makes sense on cross-provider translation paths where the
upstream cannot understand Anthropic betas. Gate it on a new
should_filter_anthropic_beta_headers() that defaults to True (bedrock, vertex_ai,
native anthropic unchanged) and is overridden to False by
OpenAILikeAnthropicMessagesConfig, whose upstream is a native Anthropic endpoint,
so betas pass through verbatim.

* chore: remove accidentally committed local QA logs and config

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-30 12:17:33 -07:00
michelligabriele
88c34a5bad
fix(email): apply EMAIL_SIGNATURE to budget alert emails (#31712) 2026-06-30 21:11:50 +02:00
ryan-crabbe-berri
3971469b71 test(ui): harden Router Settings e2e and make its typing a real CI gate
Address an adversarial review of the Loadbalancing e2e:

- The "typed against the backend schema" claim was hollow: nothing type-checked
  e2e_tests (the root tsconfig excludes it and no CI step runs tsc), so a
  contract drift would compile and run unchanged. Add e2e_tests/tsconfig.json, a
  typecheck:e2e script, and a CircleCI step so the schema typing actually gates.
- The two describe blocks both mutate the proxy's shared router_settings, and the
  Loadbalancing save echoes the whole settings object, so they could clobber each
  other under local fullyParallel. Run the file serially.
- patchRouterSettings swallowed a failed seed, which surfaced later as a
  misleading UI timeout. Assert the write succeeded, and rely on the server-side
  merge instead of echoing the whole settings object back (drops a cast and a GET).
- Empty routing_groups already reproduces the bug, so drop the non-empty seed and
  its model coupling.
2026-06-30 12:08:15 -07:00
Yassin Kortam
87f035b58f
perf(spend): gather independent per-scope spend-counter increments (#31578) 2026-06-30 12:07:47 -07:00
mateo-berri
d6f09c4f24
test(reasoning-effort-grid): bump cell-count assertion for claude-sonnet-5
The Sonnet 5 grid entry raised the Anthropic direct route to 30 model
combos, so test_grid_cell_count now expects 330 cells instead of 319.
2026-06-30 19:04:19 +00:00
tin-berri
87de0e80a8
fix(mcp): stop one unauthenticated server from emptying the aggregate tools/list (#31684)
* fix(mcp): stop one unauthenticated server from emptying the aggregate tools/list

On the aggregate MCP route (/mcp), the gateway fans out to every server the caller can access and
flattens their tools. _fetch_and_filter_server_tools re-raises MCPUpstreamAuthError unconditionally
(added with the OAuth passthrough feature in #28356) so it surfaces a 401 on single-server routes,
but on the aggregate route that exception propagates through the asyncio.gather fan-out and the
outer handler turns it into an empty list. The result: a single delegate/passthrough OAuth server
the user has not authenticated (e.g. a delegate-auth server) zeroes the tools of every other server,
including the ones that resolve fine, so the client connects and sees no tools.

Surface the upstream auth error only when a single server was explicitly targeted (so that route
still drives the upstream OAuth flow); across the aggregate, absorb it to [] for that one server so
the rest still list their tools. This restores the graceful per-server degradation that predated
#28356.

Adds regression tests: the aggregate keeps a healthy server's tools when a sibling raises
MCPUpstreamAuthError, and a single-server listing still surfaces it.

* fix(mcp): decide aggregate vs single-server listing by route scope, not server count

Addresses review: keying the surface-vs-absorb decision off the server count (len(allowed_mcp_servers),
and even len(mcp_servers)) misclassifies an aggregate /mcp request from a key that can access exactly
one server as a targeted single-server listing, so that one server's MCPUpstreamAuthError re-raises and
empties the aggregate again for one-server permission sets.

Use the path-derived single-server scope instead: _mcp_gateway_server_name, set by
_gateway_initialize_instructions_request_scope only when the request path names exactly one upstream
server (/<server>/mcp) and never from client headers, is None on the aggregate route (/mcp) regardless
of how many servers the key can access. Single-server routes still surface the upstream-auth challenge;
the aggregate absorbs it per server.

Adds a regression test that an aggregate request with a single accessible server still absorbs, plus
renames the single-server test to drive the route scope explicitly. The new test fails on the
count-based logic.

* fixing aggregation error

* style(mcp): collapse single-line debug log to satisfy ruff format
2026-06-30 11:58:47 -07:00
ryan-crabbe-berri
540c860a97 test(ui): add typed e2e for Router Settings Loadbalancing save (LIT-4057)
Drives the real save flow against a live proxy: seeds a present routing_groups
array (the LIT-4057 trigger) via the typed /config/update contract, changes
num_retries on the Loadbalancing tab, and asserts the POST returns 200 instead
of 422, the success toast appears, and the value still shows after a reload (the
ticket's "refresh shows old values" symptom). The round-trip is typed against the
OpenAPI-generated backend schema (ConfigYAML write, RouterSettingsResponse read)
through a type-only import, so a backend contract drift fails the type check.
2026-06-30 11:47:35 -07:00
Cursor Agent
a126cdf5b7
feat(anthropic): add Claude Sonnet 5
Register claude-sonnet-5 across the Anthropic, Bedrock (base + global/us/eu/au/jp
cross-region inference profiles), Vertex AI, and Azure AI cost-map entries in both
the root and bundled-backup model maps, plus BEDROCK_CONVERSE_MODELS and the
setup-wizard provider list.

Sonnet 5 ships with the gen-5 adaptive-thinking profile (adaptive thinking always
on, no extended thinking, effort defaults to high), so the entries mirror the
Fable 5 / Opus 4.8 sampling-param and prefill restrictions rather than the older
Sonnet 4.6 behavior: supports_sampling_params and supports_assistant_prefill are
false while supports_adaptive_thinking, supports_xhigh_reasoning_effort, and
supports_max_reasoning_effort are true. Pricing follows standard Sonnet rates
($3 / $15 per MTok) with the 10% regional premium on the us/eu/au/jp profiles.

Add a reasoning-effort grid entry for the Anthropic direct route and a regression
test pinning pricing, capabilities, regional premiums, backup parity, and bare-name
provider resolution.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-06-30 18:47:08 +00:00
ryan-crabbe-berri
30141f86f8 test(ui): make router settings save tests resilient to async timing
Address Greptile P2: the routing_groups test read setCallbacksCall.mock.calls[0][1]
immediately after the now-async save handler, so any latency in the mock would throw
an opaque TypeError instead of a clean assertion failure. Assert through
toHaveBeenCalledWith inside waitFor with expect.not.objectContaining, dropping the
index access and the cast. Also drop the ticket id from the test names.
2026-06-30 11:40:09 -07:00
ryan-crabbe-berri
9968499aab fix(ui): fix Router Settings Loadbalancing tab save (LIT-4057)
The Loadbalancing tab rendered routing_groups as a generic text input and
sent its array value back as the JSON string "[]", which fails Pydantic
list validation on POST /config/update and returns 422. routing_groups has
its own dedicated Routing Groups tab, so this tab must neither render nor
write it; exclude it the same way retry_policy and model_group_retry_policy
are excluded for the Model Retry Settings tab.

The save was also fire-and-forget: setCallbacksCall was not awaited, so the
rejected promise escaped the try/catch and the success toast fired
unconditionally, showing success even when the backend rejected the change.
Await the call, gate the success toast on resolution, and surface the error.
2026-06-30 11:28:51 -07:00
Mateo Wang
fecaf5c9e5
feat(router): tag routing denylist support via ! prefix (#31728)
Adds `!` prefix negation to tag-based routing so callers can exclude deployments by exact tag value without enumerating every allowed alternative. `!provider:anthropic` removes all deployments tagged exactly `provider:anthropic` before routing, and positive and negation tags compose. Matching is exact literal membership (frozenset intersection), so there is no regex or ReDoS surface for client-supplied tags. Ban-only requests that carry only negation tags stay within the default pool, mirroring untagged-request semantics so callers can't use negation to escape it. Fallback chains keep working because get_deployments_for_tag runs on each routing hop

Copy of #31680; implementation credit to @deepanshululla

Co-authored-by: deepanshululla <15312873+deepanshululla@users.noreply.github.com>
2026-06-30 11:00:30 -07:00
yucheng-berri
1815636e1c
feat(guardrails): expose streaming knobs on generic_guardrail_api (#31730)
* feat(guardrails): expose streaming knobs on generic_guardrail_api

Wire streaming_end_of_stream_only and streaming_sampling_rate through
optional params, initialize_guardrail, and get_config_model so the
generic guardrail API participates in UnifiedLLMGuardrails streaming
checks with configurable cadence and end-of-stream-only mode.

* fix(guardrails): use builtin type[] in get_config_model return

Avoids a new UP006 violation that tripped the ruff strict-rule budget
gate on the PR lint job.

* fix(guardrails): default optional streaming knobs to None

Non-None Pydantic defaults on GenericGuardrailAPIOptionalParams made
_get_config_value treat unset nested fields as explicit values, which
shadowed top-level litellm_params streaming flags whenever any other
optional_params key was present. Real defaults stay in the constructor.

* fix(guardrails): address review nits on generic_guardrail_api streaming

Validate streaming_sampling_rate >= 1 in the constructor and Pydantic
optional_params (ge=1), and add /v1/responses streaming coverage through
the unified post-call hook so Responses API usage is exercised alongside
chat completions.

* fix(guardrails): read nested streaming config from dict optional_params

Guardrail API/UI delivers optional_params as a plain dict, so getattr was
silently ignoring streaming_sampling_rate and streaming_end_of_stream_only.
Handle both dict and model shapes in _get_config_value with regression tests.

* fix(guardrails): clear ruff findings in generic_guardrail_api tests/types

* style(guardrails): ruff format generic_guardrail_api modules

---------

Co-authored-by: Marton Schneider <marton@schneider.co.nl>
2026-06-30 10:58:22 -07:00
Mateo Wang
59f51b2d72
chore: prevent CLAUDE.md comment bloat (#31729)
The existing comment rule is not strict enough
2026-06-30 10:46:47 -07:00
Yassin Kortam
6ab3742fa6
perf(spend): move cost-callback payload deepcopy off the request event loop (#31579) 2026-06-30 10:31:02 -07:00
ryan-crabbe-berri
7ed25de120
fix(ui): allow any git host on the skills add form (LIT-4053) (#31652)
* fix(ui): allow any git host on the skills add form (LIT-4053)

The skills add form only accepted GitHub URLs: its URL parser bailed on
any host that did not start with github.com, so GitLab, Bitbucket, and
self-hosted repos (and any repo subfolder on them) were rejected before a
request was ever sent. The backend already accepts arbitrary git hosts
via its url and git-subdir sources, with no host allowlist, so this was a
client-side restriction only.

Generalize the parser into an exported, host-agnostic parseSkillSource:
GitHub URLs keep their github / git-subdir shorthand, every other host is
treated as a raw repo url, and an optional Subfolder path field turns any
repo into a git-subdir source (url + path). When a pasted GitHub
tree/blob URL already encodes a subfolder, the field is cleared and
disabled so a contradictory source can never be submitted.

The parser is hardened to match the backend contract: query strings and
fragments are stripped, the host match is case-insensitive and drops a
leading www., the extracted and field-entered subfolder paths are both
validated against the same regex the server uses, a real file-extension
allowlist (not "any dot") decides whether a trailing blob segment is a
file, a branch-only tree URL falls back to the repo, non-GitHub URLs
require at least an org/repo, and the suggested skill name is kebab-cased
so it satisfies the name field's own rule.

The git-subdir source is now handled in the display helpers
(getSourceDisplayText, getSourceLink, formatInstallCommand), which
previously showed it as "Unknown source" with no link. The submit path
is fully typed (RegisterPluginRequest plus an AddPluginFormValues
interface), removing the two prior any usages; as a result an
author with an email but no name is dropped rather than sent, since the
backend requires the author name.

No backend changes. Tests cover the full host/subfolder matrix at the
parser level plus form-submit assertions on the exact source payload.

* refactor(ui): sync skill register types to the generated OpenAPI schema, surface backend errors

Replace the hand-maintained, already-drifted API types for the skills add
flow with the generated ones from schema.d.ts: PluginAuthor now aliases
components["schemas"]["PluginAuthor"], the registration payload is a new
SkillRegisterRequest (the generated RegisterPluginRequest envelope with
source narrowed to our PluginSource union, since the backend types source
as a loose string map, and version kept optional since the backend
defaults it), and the dead, mismatched RegisterPluginResponse is deleted.
registerClaudeCodePlugin's inline payload type (which was missing the
git-subdir path field entirely) is replaced with SkillRegisterRequest, so
the networking layer and the form can no longer drift from the backend.

Error handling: the add-skill form swallowed the real failure and always
showed "Failed to register skill". registerClaudeCodePlugin already
derives the backend message and throws it, so the form now surfaces it
("Failed to register skill: <reason>"), and the networking helper falls
back to the raw body / status when the error response is not JSON instead
of throwing a JSON parse error. A regression test asserts the backend
message reaches the user.

* fix(ui): reject credentialed git URLs on the skills form

A repo URL with embedded user-info (user:token@host) passed the raw-host
parser and was stored verbatim as the skill source, which is served on
the unauthenticated /public/skill_hub and marketplace.json feeds, leaking
the credentials. Reject any host segment containing '@'.

* fix(ui): validate skill repo URLs through one WHATWG URL gate

Replace the ad-hoc string parsing (stripScheme / splitHost / manual
scheme, @, ?# checks) with a single parseRepoUrl gate built on the URL
parser, so every malformed/unsafe class is handled in one place and the
URL stored on the public skill feeds is always canonical. It enforces
https (rejecting http/ssh/git/file/javascript/data and protocol-relative
//host), rejects embedded credentials (user:token@host, including
userinfo-confusion like github.com@evil.com), rejects IP-literal hosts
(loopback/private/metadata and obfuscated/IPv6 forms), and rebuilds the
stored url from origin+pathname so query strings, fragments, and trailing
slashes can never be published. The GitHub org/repo shorthand is now
charset-validated like the other paths, so junk can't reach the stored
repo. Closes both Veria findings (credentialed and http sources) plus the
adversarial-review follow-ups, with regression tests for each class.
2026-06-30 10:29:49 -07:00
Yassin Kortam
1eb7122465
test(benchmarks): add CodSpeed benchmarks for inference, MCP and A2A hot paths (#31716)
Guard the per-request CPU cost of the chat completion, MCP tool and A2A
message transforms against regressions on every commit. All benchmarks are
pure in-process work with no network I/O so they stay deterministic under
CodSpeed's simulation mode, and they import under the base dependency set the
benchmark job installs.

Inference covers the full SDK overhead via mock_response (simple, multi-turn,
tools, streaming) plus convert_to_model_response_object as a deterministic
anchor. MCP covers the client-side tool translation and the proxy server-side
tool-name prefix round-trip. A2A covers the client request/response transforms
and the proxy server-ingress message conversion.

Adds the mcp and a2a-sdk packages to the benchmark run since those transform
modules need them, and broadens the workflow triggers to litellm_internal_staging
so the internal branch flow is benchmarked too.
2026-06-30 10:27:12 -07:00
ryan-crabbe-berri
468d11f71d
feat(otel): emit a tools/list CLIENT span for MCP discovery under otel_v2 (#31525)
* feat(otel): emit a tools/list CLIENT span for MCP discovery under otel_v2

Under otel_v2 an MCP tools/call already produced a dedicated CLIENT span, but tools/list produced none. The discovery call surfaced only as the bare POST /{mcp_server_name}/mcp server span with no MCP attributes, indistinguishable from initialize and impossible to query by method

The list success event already reaches the v2 logger with call_type list_mcp_tools, but _emit_mcp_tool_call only matched call_mcp_tool, so listing fell through to the LLM-call path and emitted nothing. This adds a dedicated MCP_LIST_TOOLS span role with its own MCPListToolsSpanData, emitted from a sibling _emit_mcp_list_tools branch that mirrors the tools/call path

Per the OTel GenAI MCP semantic conventions the span is named tools/list (the method name alone, since there is no low-cardinality target), is a CLIENT span parented to the request span, and carries mcp.method.name plus the call id. It deliberately omits gen_ai.operation.name and gen_ai.tool.name, which the convention reserves for tool executions, since listing runs no tool

* fix(otel): anchor MCP spans to params._meta trace context, not the transport span

MCP streamable-HTTP multiplexes many JSON-RPC messages over one session, so the request-root anchor captured on initialize persisted and every later message's span (tools/call, tools/list) nested under it. A tools/list run 44s after the initialize rendered 44s to the right of its parent with a clock-skew warning, because the MCP message and the HTTP transport are independent lifecycles

Following the OTel GenAI MCP semantic conventions, an MCP span now parents to the W3C trace context the client propagated in the request's params._meta (a remote parent, per SEP-414), records the transport/session span as a span link rather than the parent, and starts its own root trace when nothing was propagated. The MCP gateway captures traceparent/tracestate/baggage from each message's params._meta into a per-message contextvar that the otel_v2 emitter reads; opentelemetry stays an optional dependency via guarded lazy imports

This applies to tools/call as well as the new tools/list span, since both shared the same transport-anchoring bug

* fix(otel): drop client baggage from MCP params._meta to prevent identity spoofing

The MCP trace propagation added a W3CBaggagePropagator, so resolve_mcp_span_context
extracted the client's W3C Baggage from params._meta into the span's parent context.
The LiteLLMBaggageSpanProcessor then stamps allowlisted baggage keys onto the span,
and the list-tools/tool-call mappers don't set those identity keys, so nothing
overwrites them. A malicious MCP client could send
params._meta.baggage: litellm.team.id=...,litellm.metadata.user_api_key_user_id=...
and have those identity attributes attributed to its spans.

Extract trace context only (traceparent/tracestate) in the propagator, and stop
collecting the baggage key at the source in _mcp_meta_trace_carrier. Parenting to the
client's trace context, the actual goal, needs only trace context; remote baggage had
no legitimate consumer here. Regression tests at both layers assert a spoofed
params._meta.baggage never lands as a span identity attribute.

* style(mcp): clear ruff strict-budget breach in otel trace-carrier helpers

The otel MCP trace-carrier helpers added in this branch pushed the BLE001 and
UP006 strict-rule totals past their ceilings. Use PEP 585 `dict[str, str]` instead
of `Dict`, and narrow the optional-import guards to `except ImportError` (the only
failure these can hit, matching the "when otel_v2 is unavailable" intent) instead of
a blind `except Exception`.

* fix(otel): stamp authenticated identity baggage onto MCP spans

Parenting MCP spans to the client's params._meta trace context over an empty
Context() meant the tool-call and tools/list spans carried no team/key/metadata
identity at all, so they couldn't be attributed or filtered by team in a traces
backend. The LLM-call span already re-seeds identity from the parsed, authenticated
StandardLoggingPayload rather than trusting ambient/remote context; extract that into
a shared _seed_identity_baggage helper and run both MCP emitters through it.

Identity comes only from the authenticated payload, never the client carrier, so this
keeps the earlier spoofing fix intact while restoring attribution. Regression tests
assert the authenticated team lands on both MCP spans and that a spoofed
params._meta.baggage value can't override it.

* refactor(otel): model MCP spans as roots that link the transport in SPAN_REGISTRY
2026-06-30 10:26:57 -07:00
Yassin Kortam
2e575d39f2
perf(otel): memoize per-request lazy import of otel runtime hooks (#31707)
The proxy auth path calls phase_span() and seed_request_identity() in
litellm/integrations/otel/runtime.py on every request, each doing a
try/except lazy import of litellm.integrations.otel.logger. When the
OpenTelemetry SDK is not installed (the default), that import raises, and
CPython never caches a failed import, so every request re-scanned sys.path
and contended on the import lock. At 750 concurrent users this cost about
12% throughput versus v1.85.0.

Resolve the hooks once and cache the outcome, absence included, with
functools.cache, so the import is attempted a single time instead of per
request. Throughput returns to the v1.85.0 baseline.
2026-06-30 10:26:20 -07:00
yuneng-jiang
f8a2ea7378
Merge pull request #31426 from BerriAI/litellm_/cranky-hamilton-21b5d0
fix(ui): stop Request Logs page from overflowing horizontally and size its columns
2026-06-30 10:23:38 -07:00
ryan-crabbe-berri
3dce3daff6
feat(proxy): type Customer Management response_model for OpenAPI coverage (#31043)
* feat(proxy): type Customer Management response_model for OpenAPI coverage

Add response_model to the five remaining untyped /customer operations
(block, unblock, new, update, delete) so the generated OpenAPI schema
documents a concrete response body. new/update reuse the canonical
LiteLLM_EndUserTable (matching info/list); block, unblock, and delete
get small dedicated models in
litellm/types/proxy/management_endpoints/customer_endpoints.py.

Together with the already-typed info/list/daily-activity routes this
brings the Customer Management group to full response_model coverage.
Regression tests assert each public /customer/* route declares the
expected response_model and that /customer/new surfaces a typed schema
in app.openapi(), so dropping a response_model fails CI.

* fix(proxy): keep budget_id in typed customer responses

Address review feedback on the Customer Management response_model typing.
Greptile flagged that response_model=LiteLLM_EndUserTable on /customer/new
and /customer/update silently drops fields the raw Prisma model_dump()
echoed. Checking the schema, budget_id is the only such scalar column that
was missing from the Pydantic model (created_at/updated_at/tpm_limit do not
exist on litellm_endusertable), so add budget_id to LiteLLM_EndUserTable.

This restores budget_id on new/update and also fixes the pre-existing gap
where /customer/info and /customer/list (already typed) dropped it, which
the UI Customer type expects. A regression test pins budget_id surviving the
response_model filter on /customer/update.

Also document UnblockUsersResponse.blocked_users via a Field description: it
holds the users that remain blocked after the call. The key name predates
this PR and is kept to avoid a backwards-incompatible rename on a beta route.

* fix(proxy): keep nested budget fields in customer responses

response_model=LiteLLM_EndUserTable nests the budget as the narrow write
allowlist LiteLLM_BudgetTable, which silently drops the server-managed
fields the customer endpoints used to return (budget_reset_at, created_at).

Introduce CustomerResponse, a thin response model that nests
LiteLLM_BudgetTableFull (the repo's budget response model), and apply it on
/customer/new, /customer/update, /customer/info and /customer/list. list
also builds CustomerResponse so its budget isn't narrowed at construction
time. created_by/updated_at/updated_by remain omitted, matching how budgets
are returned elsewhere.

The shared LiteLLM_EndUserTable is left untouched: it's constructed in many
places that pass narrow budget instances, and pydantic v2 won't coerce a
budget instance into a wider nested model. Typing only at the response
boundary (where the handler hands FastAPI a dict) sidesteps that. A
regression test pins budget_reset_at + created_at through the filter and
asserts the internal audit fields stay out.

* test(proxy): add golden-master characterization tests for customer responses

Lock the exact JSON body each customer-object endpoint (info/list/new/update)
and delete return today, so the upcoming type-safety refactor of the handlers
is only allowed to land if it reproduces these byte for byte. Pins null-field
inclusion, the nested budget shape (server fields kept, audit fields dropped),
and object_permission reverse-relation stripping. Green against current code.

* refactor(proxy): make the customer response flow type-safe

Replace the untyped dict + bolt-on response_model pattern on the customer
object endpoints with explicit typed construction. A single mapper,
_to_customer_response, validates a DB row into CustomerResponse at one
Any -> typed seam; new/update/info/list now return it (or a list of it) and
carry real -> CustomerResponse / -> List[CustomerResponse] return
annotations, and delete returns DeleteCustomersResponse. basedpyright now
verifies the handlers' return shapes instead of a runtime filter doing it
silently.

This also deletes the four copy-pasted object_permission reverse-relation
cleanup loops: pydantic's extra=ignore drops those undeclared fields during
validation, so the loops were dead code (proven by the golden-master tests,
which stay byte-for-byte green). basedpyright errors on the file drop from
140 to 116, all from removed dict plumbing.

CustomerResponse stays a thin subclass of LiteLLM_EndUserTable so it inherits
the existing validators/config unchanged (behavior preservation); only the
nested budget type is widened.

* refactor(proxy): annotate customer response mapper param as BaseModel

Address review nit: the mapper's untyped `record` added an ANN001 violation.
The incoming rows are pydantic v2 models, so type the param as BaseModel
rather than object (object has no model_dump, which would just move the
problem to basedpyright). This clears the ANN001 and also drops three
basedpyright unknown-type violations the untyped param was adding.

* style(test): ruff format customer endpoint tests

* test(proxy): give customer budget test update mocks a valid model_dump

The type-safe response refactor validates the update result via
_to_customer_response (CustomerResponse.model_validate(record.model_dump())).
These budget tests mocked the end-user update to return a bare MagicMock,
so model_dump() yielded a MagicMock that fails validation. Give each update
mock a minimal valid dict; the tests assert on the prisma calls, not the body.

* chore(ui): regenerate API types from proxy OpenAPI spec

* fix(ui): make generated API types stable across Python versions

Python 3.13 strips a docstring's common leading indentation at compile
time while 3.12 keeps it, so app.openapi() emits differently-indented
description strings depending on the interpreter. The dashboard type
generator ran locally on 3.13 and in CI on 3.12, so schema.d.ts drifted
and the "Verify schema.d.ts matches the proxy OpenAPI spec" check failed

Normalize every description through inspect.cleandoc in the spec dump so
the output is identical regardless of interpreter, then regenerate
2026-06-30 09:58:01 -07:00
Mateo Wang
0965a4d1f4
chore: shift CI lint left with an opt-in make pre-commit and CLAUDE.md rule (#31544)
* chore: shift CI lint left with a pre-commit hook and CLAUDE.md rule

Add an opt-in pre-commit hook (.githooks/pre-commit, active after
make install-hooks) that runs the CI-equivalent checks against staged
files: make lint for Python, prettier plus eslint for the dashboard,
and a gen:api drift check for the proxy OpenAPI types. Document the
same expectation in CLAUDE.md so reds surface locally instead of in CI.

* fix: make `make lint` isomorphic to the CI lint job

`make lint` diverged from test-linting.yml in ways that produced both
false reds and false greens: its format-check ran over the whole repo
(CI scopes it to changed files vs the base), its ruff-strict budget ran
in absolute mode (CI runs it as a delta vs base), and it omitted the
type-discipline gate entirely. Recompose `lint` to replay CI's exact
sequence: diff-scoped ruff format check, whole-tree ruff check, the
strict / type-discipline / basedpyright budgets as a delta resolved the
same way CI resolves it (merge-base with origin/litellm_internal_staging),
then circular-import and import-safety. Factor the repeated base fetch
into one shared prerequisite so the chain hits the network once.

Align the pre-commit hook's eslint invocation with the CI frontend-lint
job (`--pass-on-unpruned-suppressions`) and fix the CLAUDE.md guidance to
point at the diff-scoped frontend commands instead of the whole-folder
npm scripts, which are broader than CI.

* fix(githooks): make pre-commit 1:1 with CI frontend-lint, lint, and type-gen

The shift-left pre-commit hook diverged from the CI jobs it claims to mirror, so a clean commit did not actually mean a green CI lint.

The dashboard block only ran prettier and eslint over js/jsx/ts/tsx/mjs/cjs, but CI's frontend-lint runs prettier over a wider set (also json, css, scss, md, mdx, yml, yaml, html) and additionally gates the whole-folder eslint lint budgets via scripts/check-lint-budgets.mjs. The hook now mirrors that split and runs the budget check, so a dashboard commit that passes locally passes the job.

The API-types block ran npm run gen:api without LITELLM_PYTHON, so it shelled out to the system python3 which has no litellm installed and always failed with a false 'could not regenerate API types' red. It now passes LITELLM_PYTHON="uv run --no-sync python" the way check-ui-api-types.yml does.

make lint format-checks the files in origin/base...HEAD, which at pre-commit time predates the staged change, so a brand-new commit's formatting went unchecked. The Python block now also runs ruff format --check over the staged litellm files directly to cover that case, and its trigger is scoped to staged litellm/ files (the only tree CI's lint job inspects) so a tests-only or scripts-only commit skips the slow make lint instead of wasting time on a run that could not catch anything.

CLAUDE.md's shift-left rule was cut off mid-sentence and understated the frontend checks; it now describes all three gates accurately and points agents at make install-hooks to run them automatically before each commit.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(githooks): scope the API-types check to all of check-ui-api-types.yml's triggers

spec_files was filtered from the staged Python files, so the gen:api drift
check only fired for .py changes under litellm/proxy or litellm/types. CI's
check-ui-api-types.yml triggers on any file under those directories (Prisma
schema, configs) plus the generator script and the dashboard package files,
so a non-Python proxy/types change could pass the hook and still fail CI.
Match the workflow's full trigger set instead.

* fix(pre-commit): run prisma generate before gen:api to mirror CI

* refactor(githooks): run shift-left lint via on-demand make pre-commit, not an auto-firing hook

The pre-commit hook ran make lint plus the dashboard eslint budgets, which are minutes of work (basedpyright over litellm/, a whole-folder eslint . pass at ~40s). Wiring that into core.hooksPath via make install-hooks meant every human commit, not just an agent's, paid that cost, which is real friction for interactive committers.

Move the staged-file checks out of .githooks/ into scripts/pre_commit_lint.sh and expose them as make pre-commit, and keep .githooks/ to only the fast Conventional Commits / Branches hooks so make install-hooks no longer makes commits slow. Agents run make pre-commit right before each commit (CLAUDE.md instructs this), so the slow gates fire only for the commits an agent is making and never auto-fire for a human typing git commit. The script stays hook-compatible for anyone who still wants it to fire automatically via a symlink.

Preferred this over sniffing an agent env var to auto-fire only for agents: that is fragile (misses agents when the var is unset, fires on humans when it leaks into their shell, and silently no-ops a hook a human deliberately installed), whereas an on-demand command achieves the same humans-never, agents-per-commit outcome deterministically.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(pre-commit): run make lint last so it can't prune the proxy deps gen:api needs

make lint's install-dev prerequisite runs uv sync --frozen, which prunes the proxy extras (prisma, websockets, ...) from the venv. With the Python block running first, the subsequent API-types block then failed: gen:api imports litellm.proxy.proxy_server, which needs those deps, so every litellm/proxy change (the main trigger for the API-types check) hit a false 'could not regenerate API types' red. Run the dashboard and API-types blocks before the Python block so gen:api sees an intact env; CI is unaffected because there the lint and check-ui-api-types jobs run in separate environments.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix: make CLAUDE.md more concise

* fix(makefile): give make lint the CI lint env and stop it pruning the venv

make lint diverged from test-linting.yml's lint job in two ways: it never generated the Prisma client (so basedpyright resolved the DB wrappers as Unknown, drifting from CI's counts), and its bare uv sync --frozen pruned the proxy extras (prisma, websockets, ...) out of the venv on every run, which broke the gen:api step that imports litellm.proxy.proxy_server and left a dev unable to run the proxy until re-syncing.

Add a lint-install target that mirrors the job's environment (the proxy-dev group plus prisma generate) and runs before the checks, and make both it and install-dev use uv sync --inexact so they top up the venv instead of tearing packages out. CI is unaffected since it installs its own env per job.

Because make lint no longer prunes, the pre-commit reorder that ran it last (to dodge the prune) is no longer needed, so restore the original block order.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(makefile): drop lint-install so make lint matches CI's slimmer env

test-linting.yml's lint job installs deps with a bare uv sync --frozen
(default dev group only, no proxy-dev, no prisma generate), but the
lint-install target chained into make lint pulled in --group proxy-dev
and ran prisma generate. Because the basedpyright budget step compares
head and base counts against fixed thresholds, the extra symbols and
Prisma client locally resolved can shift error counts away from CI's,
producing false greens or false reds on the type-check gate.

Remove the lint-install target and its slot in lint. The remaining
sub-targets already chain install-dev, which now uses
uv sync --inexact --frozen, so the venv still isn't pruned but the
installed set stays aligned with what CI sees.

* ci(linting): install proxy-dev and generate prisma in lint job, matching make lint

make lint now installs the proxy-dev group and generates the Prisma client so basedpyright resolves the DB wrappers; the lint job here still installed only the base env, so a local pre-commit could pass while the required CI lint failed (or vice versa). Bring this job in line, which is the same environment litellm_internal_staging's lint job already uses.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(makefile): keep make lint on the proxy-dev + prisma env to match CI

A concurrent change dropped lint-install to match what looked like CI's slim env, but test-linting.yml's lint job (and the merge ref this PR's CI actually runs) installs --group proxy-dev and generates the Prisma client. With make lint slim and CI fat, basedpyright resolves fewer symbols locally than CI, so a prisma-typed error can stay Unknown locally (green) while CI catches it (red). Restore lint-install so make lint installs the same env CI does; the previous commit also brought this PR's test-linting.yml in line with that env, so the two now match.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-06-30 09:37:12 -07:00
Yassin Kortam
70eb4e5d00
feat(prometheus): add litellm_total_overhead_latency_metric (SDK overhead + guardrails) (#31593)
litellm_overhead_latency_metric only covers the SDK wrapper window and excludes
proxy guardrails. Add a histogram that sums SDK overhead plus pre/post-call
guardrail durations (during-call excluded since it runs concurrently with the LLM
call, alongside logging_only and MCP modes that never block the response),
recorded next to the existing overhead metric with the same labels and buckets.
No existing metric's value is changed.
2026-06-30 17:34:17 +08:00
Mateo Wang
72bcb748b9
chore: remove _experimental/out (#31546)
* chore: remove _experimental/out

* fix(ci): recreate _experimental/out before copying UI build output

The build scripts cp the Next.js output into litellm/proxy/_experimental/out,
which was removed from git. cp failed because the target directory no longer
existed; mkdir -p recreates it before the copy.

* fix(proxy): make UI serving resilient to a missing _experimental/out

Removing the committed UI export means the source/test tree no longer
ships litellm/proxy/_experimental/out. Three things assumed it was always
present and broke once it was gone:

- get_favicon hard-coded the built favicon path and 404'd without it; it
  now falls back to the bundled swagger/favicon.ico
- the /_next and /ui static mounts raised at construction when the export
  was absent, so the whole UI-setup block was swallowed and no mounts
  registered; they now use check_dir=False
- _restructure_ui_html_files was a nested function only exposed as a
  module attribute when that block happened to succeed; it is now a real
  module-level function

test_admin_ui_export_serves_nested_extensionless_routes validated the
committed artifact, whose premise this PR removes; it now drives the same
MCP OAuth callback restructure guarantee through a synthetic export.

* chore(greptile): ignore generated _experimental/out so review fits the file limit

* Revert "chore(greptile): ignore generated _experimental/out so review fits the file limit"

ignorePatterns is applied after Greptile counts the files changed, so it
does not bring the diff under the file limit; the config had no effect.
2026-06-29 21:42:58 -07:00
yuneng-jiang
adf6eb75a4
chore(docs): remove docs accidentally committed to litellm repo (#31691)
Docs live in BerriAI/litellm-docs; these four files were swept into the
repo by unrelated PRs. The Crusoe provider and XecGuard guardrail pages
are migrated to litellm-docs (BerriAI/litellm-docs#438); plugin_architecture.md
is already covered there by docs/proxy/plugins.md, and the orphaned image
was referenced by no doc.
2026-06-29 21:41:25 -07:00
Yuneng Jiang
256b5aadfb
fix(ui): revert Request ID width to default, tighten Session ID
Drop the explicit size on Request ID so it falls back to the default
width like the other reverted columns. Narrow Session ID from 160px to
120px since its truncated value needs less room
2026-06-29 21:16:59 -07:00
Yuneng Jiang
84d7a32020
fix(ui): revert Duration and TTFT column widths to default
The explicit 90px/80px sizes were too narrow for the Duration (s) and
TTFT (s) headers once the sort arrows were factored in, cramping the
header labels. Dropping the size lets these two columns fall back to the
default width like before
2026-06-29 21:08:40 -07:00
Mateo Wang
884cdc1537
fix(anthropic): drop unsignable thinking blocks and allow null signature in logging (LIT-4007) (#31654)
* fix(anthropic): drop unsignable thinking blocks and allow null signature in logging (LIT-4007)

Open-source reasoning models (DeepSeek-R1 and distills, Qwen3/QwQ, IBM
Granite 3.2 via vLLM/Ollama/OpenRouter/DeepSeek) return reasoning_content
with no Anthropic-style signature, which LiteLLM represents as a thinking
block with a null signature.

Two failures resulted. First, ChatCompletionThinkingBlock.signature was a
required str, so building the StandardLoggingObject raised a ValidationError
on signature=None and the success log record was silently dropped while the
request still returned 200; relaxing it to Optional[str] lets the log build.
Second, replaying such a turn to a real Anthropic model forwarded the
null-signature thinking block unchanged and Anthropic rejected it with
400 thinking.signature.str; since Anthropic verifies the signature
cryptographically, a null, empty, or missing signature cannot be repaired,
so anthropic_messages_pt now drops the unsignable thinking block while
preserving the assistant text and keeping genuinely signed blocks.

* style: use builtin generics for thinking-block filter helpers

* fix(ui): regenerate schema.d.ts for nullable thinking-block signature
2026-06-29 21:01:03 -07:00
Sameer Kankute
6d5de74447
feat(cost_calculator): log per-token-type reasoning and cache cost breakdown (#31623) (#31686)
Reasoning-token cost was computed but folded into output_cost, and cache
cost was only populated from the top-level cache_read_input_tokens attribute,
so providers that report cache tokens under prompt_tokens_details (Gemini,
OpenAI, Vertex) never got a cache breakdown.

Adds a provider-agnostic get_token_type_cost_breakdown helper that derives
reasoning, cache-read and cache-creation cost from the normalized usage object
using the same rate-resolution primitives as the total-cost path, so the
breakdown reconciles with the totals. completion_cost stores these via
set_cost_breakdown, surfacing reasoning_cost (new), cache_read_cost and
cache_creation_cost in StandardLoggingPayload.cost_breakdown and the spend logs.

Co-authored-by: Kunal Nayyar <48790070+kunal2002@users.noreply.github.com>
2026-06-30 09:17:08 +05:30