Commit graph

15506 commits

Author SHA1 Message Date
mateo-berri
3920cf4dfe fix(image_handling): tell callers when the image host did not resolve instead of blaming the URL policy
validate_url raises HostResolutionError, a SSRFError subclass, for the two
DNS outcomes (lookup failed, no addresses). The image fetch helper maps
that to a "host could not be resolved" message and keeps the
user_url_allowed_hosts hint for the policy verdicts it can actually fix.
2026-09-04 23:45:37 -07:00
mateo-berri
0ee3bec046 fix(image_edit): await an async transform hook and hide the URL policy verdict from callers
The image edit handler now awaits BaseImageEditConfig.async_transform_image_edit_request, and
Black Forest Labs overrides it so URL images and masks download through async_safe_get instead of
the blocking safe_get on the event loop. Rejected image fetches raise a fixed policy message with
the user_url_allowed_hosts hint rather than echoing the resolver's verdict (resolved IP, DNS
failure) back to the caller. The test fixture also fails any request-path call of the sync
convert_url_to_base64 so a regression cannot pass unnoticed.
2026-09-04 23:20:21 -07:00
mateo-berri
53178486a0 style(tests): wrap realtime cost test lines to the 120-column limit 2026-09-04 22:59:22 -07:00
mateo-berri
74613f9bd4 fix(realtime): redact credentials from the relayed upstream close
The handshake error path already runs client-facing error strings through
_redact_string; the relay's _close_client did not, so a secret echoed in an
upstream close reason could reach the client verbatim. Mirror the handshake
path and scrub the close message and reason before relaying them.
2026-09-04 22:52:26 -07:00
mateo-berri
004a820116 fix(ocr): send each provider a health-check document it accepts
Health checks probed every OCR deployment with a PDF, which Cohere Parse
rejects, so /health, background health checks, and the UI Test Connection
button marked Cohere Parse deployments unhealthy. BaseOCRConfig gains a
get_health_check_document hook (PDF by default) that CohereParseConfig
overrides with a 1x1 PNG data URI. cohere also gains ocr in the provider
endpoint matrix
2026-09-04 22:52:12 -07:00
mateo-berri
e1d900d1c2 fix(realtime): store text_tokens without the nested reasoning share
The realtime usage writer passed the provider's output_token_details through as sent, so spend logs and callbacks kept a text_tokens that still contained reasoning_tokens while every other completion_tokens_details producer stores the partitioned share. The writer now applies the same rule the cost calculator uses, moved to litellm/types/utils.py so both read one definition, and the calculator keeps it for usage objects that arrive nested from elsewhere
2026-09-04 22:47:09 -07:00
mateo-berri
d98cc2993c chore: merge litellm_internal_staging into litellm_lit_6348_fireworks_responses_api 2026-09-04 22:23:21 -07:00
mateo-berri
e9a40ad4d2 fix(fireworks_ai): map developer items after pydantic input items are dumped 2026-09-04 22:16:14 -07:00
tin-berri
78ad88f52c
fix(responses): decode JSON-string tool schemas before sending to the provider (#39844)
* fix(responses): decode JSON-string tool schemas before sending to the provider

A caller that hands a tool schema over already JSON-encoded reached the
Responses API with a string `parameters`, and the provider rejected the
request with a 400 naming the routed model instead of the offending tool.
Decode it at the one place every Responses request converges, and refuse
anything that is neither an object nor a string encoding one.

Collapses the duplicated input/tool sanitization block shared by the
request and compact-request builders into a single owner, so the decode
cannot be wired into one path and not the other.

* test(responses): pin null tool schemas as accepted, and type the parametrized cases

The Responses API serves `parameters: null` and an omitted schema alike, so
neither may raise. Pin both against a future tightening, annotate the
parametrized inputs, and trim the docstrings back to what the code does not
already say.
2026-09-05 05:14:44 +00:00
mateo-berri
d3a179f988 fix(azure_ai): route only cohere parse deployment names to Cohere Parse 2026-09-04 22:07:26 -07:00
mateo-berri
525d9fb14c chore: merge litellm_internal_staging into litellm_lit_6348_fireworks_responses_api 2026-09-04 21:30:31 -07:00
mateo-berri
8426235290 feat(ocr): add Cohere Parse support for cohere and azure_ai 2026-09-04 21:25:46 -07:00
mateo-berri
412c36bb8e fix(realtime): detect an upstream refusal from received frames, not the session log
The refusal predicate also required the session log to be empty, but that
log is not limited to upstream frames. With gemini_live_defer_setup the
handler stores a synthetic session.created before the relay starts, and
the transcription usage flush appends a usage event before the check
runs, so an upstream policy close with no received frames was still
logged as a $0 success. Key the check off the received-frames flag only
2026-09-04 21:24:36 -07:00
moe-berri
f03f82381e merge origin/litellm_internal_staging, keep the reportPrivateUsage suppression 2026-09-04 21:03:28 -07:00
mateo-berri
14f8677bfc fix(realtime): mark realtime sessions async so failure hooks fire once
The relay's failure dispatch runs the async handler and then the legacy sync
failure_handler for the proxy's callable callbacks. The realtime logging object
carried no async marker, so failure_handler treated the session as a sync SDK
call and fired every CustomLogger's sync failure hook on top of the async one:
Langfuse recorded two ERROR observations per refused session, and OpenTelemetry,
MLflow, Braintrust, Literal AI, DeepEval and New Relic implement the same sync
hook. Plant the _arealtime marker in litellm_params the way aanthropic_messages
and agenerate_content already do, so both dispatchers classify the session async.
2026-09-04 20:57:51 -07:00
mateo-berri
5c8016e99e Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_async_remote_image_fetch 2026-09-04 20:54:41 -07:00
mateo-berri
f09992b730 fix(image_handling): keep DNS, signing, and Vertex Gemini fetches off the event loop
The SSRF check in async_safe_get resolved DNS on the event loop and a blocked
address was retried three times; validate_url now runs in a thread and an
SSRFError fails the fetch on the first attempt in both fetchers. The shared
HTTP handler signed the request and ran pre_call logging on the loop after an
async transform; both now run in a thread. Vertex AI Gemini still fetched
http:// images and https images without an inferrable mime type with the sync
converter inside its async body builder; the walker takes a should_inline
predicate and Vertex AI inlines exactly those URLs, leaving https images with a
known mime type and Files API refs to Google. When one download fails the
other in-flight downloads for that request are now cancelled instead of
finishing in the background
2026-09-04 20:54:40 -07:00
Mateo Wang
3d00ad3f29
Merge pull request #39847 from BerriAI/litellm_lit_5730_bedrock_batch_cancel_e2e
test(e2e/batches): assert Bedrock batch cancel and list in the lifecycle
2026-09-04 20:54:27 -07:00
moe-berri
955baf8a5c Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_shadow_eval_judge_output_cap 2026-09-04 20:54:26 -07:00
tin-berri
8b6ea72845
feat(shadow_eval): scope a job to model groups, ANDed with its key, team, and user targets (#39828)
A shadow eval job could only be scoped by identity, so "this user's traffic on model X
across every key they own" was not expressible and a models field on the start body was
silently dropped. The job now carries a models list that every target is narrowed to,
matched on the requested model group with model_group_alias resolved on both sides. An
unresolvable name is a 400 at start. Empty means every model, which is what every existing
row reads as. The dashboard start form gains an "Only on models" picker and the job
headline shows the scope.
2026-09-04 20:50:46 -07:00
mateo-berri
6294367920 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_realtime_reasoning_double_bill 2026-09-04 20:49:12 -07:00
mateo-berri
92e449d4d0 test(cost): cover reasoning nested in text_tokens beside audio output 2026-09-04 20:35:21 -07:00
devin-ai-integration[bot]
e7dd524a3c
feat(otel): stamp litellm.request.route on the LLM call span (#39698)
* feat(otel): stamp litellm.request.route on the LLM call span

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

* refactor(otel): drop redundant comment on REQUEST_ROUTE

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

* style(otel): Final-annotate route test locals, drop field comment

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

* fix(otel): read litellm.request.route off the server span

The LLM call span took the auth-normalized literal path from logging
metadata, which disagrees with the SERVER span wherever FastAPI matched a
template: on /engines/{model:path}/chat/completions the LLM span spelled the
model name while http.route carried the template, so the two spans grouped
into different buckets and the PR's premise did not hold.

Read the value off the span that already holds it. The request's root SERVER
span is anchored per request for parenting, and its attributes stay readable
after it ends, so request_root_http_route() answers from the async close
callback with the same http.route the SERVER span exports: the route template
on a normal route, the literal path where the passthrough hook rewrote it, and
the mount point on an MCP call. Nothing has to re-derive any of that, so the
two spans cannot drift apart.

The route the proxy recorded at auth stays as the backstop for a deployment
whose FastAPI instrumentation never mounted, where there is no server span to
disagree with. Off the proxy the attribute is omitted rather than empty.

---------

Co-authored-by: shivam <shivam@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yucheng He <yucheng@berri.ai>
2026-09-05 03:33:44 +00:00
Mateo Wang
377b87c59c
Merge pull request #39306 from BerriAI/litellm_deflake_20260902
test: deflake JWT tamper, fuzzy picker, tag routing, liveliness, redis stall burst, and pre-commit interrupt tests
2026-09-04 20:30:50 -07:00
Mateo Wang
8d4a63d2a1
Merge pull request #39435 from BerriAI/litellm_debug_claude_session_report
feat(cli): add `lite debug claude` session report and /debug-lite slash command
2026-09-04 20:30:47 -07:00
Mateo Wang
b77b7f158f
Merge pull request #39842 from BerriAI/litellm_lit_6949_unknown_model_spend_log_test
test(store_model_in_db): assert the 400 contract in the unknown-model spend log test
2026-09-04 20:29:56 -07:00
moe-berri
6385c7b3c5 fix(auto-router compression): close three review findings on the per-hop policy
Suppression state moves out of request metadata into a request-scoped ContextVar.
refresh_proxy_server_request_body_snapshot copies metadata into
proxy_server_request.body, which deployments persist to spend logs, so the marker
naming each suppressed guardrail was readable by the caller whose request produced
it. Recovering it was enough to replay {token}:{name} for any CustomGuardrail and
switch off a PII or content-filter guardrail, since the check never verified the
named guardrail was a compression one. Nothing is read from metadata now, so there
is no marker to forge and the per-process token is no longer needed.

Routing-side compression reads the live messages instead of a pre-guardrail copy.
arm_pre_call runs before the pre-call hook, so its snapshot held the prompt as it
was before any masking guardrail rewrote it, and messages_for_routing handed that
to a compression guardrail which POSTs it to an external service. Masked content
left the proxy anyway. The cost is one combination: when the model hop compressed
and the hops differ, routing now classifies on the compressed text, since no
uncompressed copy survives that a masking guardrail has already seen.

policy_for_model no longer falls back to a marker scoped to tags the request does
not carry, which applied an 'eu' policy to a 'us' request on config order alone.

Each fix carries a regression test; all three fail when the fix is reverted.
2026-09-04 20:28:17 -07:00
mateo-berri
036d104533 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_govcloud_profiles_lit6421 2026-09-04 20:08:16 -07:00
Mateo Wang
4af62a38c1
Merge branch 'litellm_internal_staging' into litellm_mistral_voxtral_tts_speech 2026-09-04 20:07:45 -07:00
mateo-berri
b6a3cba25c Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_deflake_20260902 2026-09-04 20:03:51 -07:00
mateo-berri
9cde3d21b0 Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_debug_claude_session_report 2026-09-04 20:03:49 -07:00
mateo-berri
0c14777069 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_lit_5730_bedrock_batch_cancel_e2e 2026-09-04 20:03:47 -07:00
mateo-berri
41c8969f0a test(router): drive tag routing tests through acompletion until both deployments are seen 2026-09-04 19:54:40 -07:00
mateo-berri
da9dbdba96 fix(realtime): treat any client receive failure as a client hangup
client_ack_messages classified a websockets ConnectionClosed raised by
the client socket as the backend closing, so bidirectional_forward kept
waiting on the upstream instead of ending the session. Starlette clients
raise WebSocketDisconnect, but the realtime test client in
tests/llm_translation/realtime raises websockets.exceptions.ConnectionClosed,
which hung test_openai_realtime_simple.py until the run was killed.

Only the receive_text call now maps every exception to
CLIENT_DISCONNECTED; the loop body keeps ConnectionClosed as
BACKEND_CLOSED, since the backend socket is the only websockets socket
touched there.
2026-09-04 19:52:48 -07:00
ryan-crabbe-berri
1d375d8ada fix(guardrails): flag unpriced units per team and key, sort unknown cost last
The detail endpoint now returns untracked_usage_units_by_team and
untracked_usage_units_by_key next to the cost breakdowns, and the By team and
By key tables show them in an Unpriced Units column, so a row that pairs its
total units with a partial cost says how many units that cost leaves out.

The overview comparator no longer treats a missing cost as zero: guardrails
with no known cost sort last in both directions instead of mixing in with
genuinely free ones.

Refs LIT-5652
2026-09-04 19:46:14 -07:00
mateo-berri
296cd8c1f5 fix(cli): read CLAUDE_CODE_SESSION_ID and skip subagent transcripts when detecting the Claude Code session 2026-09-04 19:45:42 -07:00
mateo-berri
5a7e938f37 test(cost): type the cost map fixture parameter on the new tests 2026-09-04 19:40:46 -07:00
mateo-berri
a0b2e7fca6 fix(proxy): only a provably missing user row counts as unrestricted in the websocket passthrough gate 2026-09-04 19:35:08 -07:00
mateo-berri
85d45fbb4b fix(realtime): relay the upstream close even when a client message hit the closed socket first
When the upstream closes while the proxy is forwarding a client message,
the client loop ends before the backend relay sees the close, and the
relay skipped closing the client because it read the client loop's exit
as the client hanging up. The client loop now reports why it stopped, so
a close observed on the backend send still reaches the client with the
error event and the upstream close code
2026-09-04 19:33:30 -07:00
mateo-berri
6ee33df952 fix(realtime): relay the upstream websocket close to the client instead of hanging
When the provider closes the realtime websocket (for example Vertex Live
refusing the session with 1008 "Publisher model ... was not found"), the
proxy swallowed the close and kept waiting on the client, so the client
sat on an open socket with nothing coming back and the session was logged
as a $0 success

The backend relay now returns the upstream close, and bidirectional_forward
sends the client an OpenAI-style error event naming the upstream code and
reason, then closes the client socket with the same code (or 1011 when the
upstream code is one a server may not send). A session the upstream refused
before sending any frame is logged through the failure handlers instead of
as a success
2026-09-04 19:25:12 -07:00
mateo-berri
f846388bb1 fix(proxy): treat a missing user row as unrestricted in the websocket passthrough gate 2026-09-04 19:24:30 -07:00
mateo-berri
5bd4da0389 test(health): score the liveliness probe on the median of five warm polls 2026-09-04 19:16:51 -07:00
mateo-berri
02cb3daf26 fix(cli): return debug failures as values, survive transport errors, size report fences to content 2026-09-04 19:15:21 -07:00
mateo-berri
001b531ae9 fix(cost): bill realtime reasoning tokens nested in text_tokens once
OpenAI and Azure realtime usage reports output_tokens == text_tokens + audio_tokens
with reasoning_tokens counted inside text_tokens, so generic_cost_per_token billed
the reasoning share twice. When the output token details sum past completion_tokens,
the nested reasoning overlap is now subtracted from text_tokens before pricing;
shapes where text_tokens already excludes reasoning are unchanged.
2026-09-04 19:11:27 -07:00
mateo-berri
822c8629ae Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_deflake_20260902
# Conflicts:
#	tests/test_litellm/caching/test_redis_cache.py
2026-09-04 19:06:33 -07:00
mateo-berri
2a7fc8de01 fix(proxy): keep the token's team model list in the websocket passthrough gate without a database 2026-09-04 19:05:35 -07:00
ryan-crabbe-berri
2151dcbd73
Merge pull request #39822 from BerriAI/litellm_lit_6594_access_group_resource_names
feat(access-groups): resolve resource names on access group responses
2026-09-04 19:03:20 -07:00
tin-berri
d8ca43a800
feat(complexity_router): let the LLM classifier see request images (#39825)
The classifier scores extracted text, so a turn whose complexity lives in
its image is invisible to it: a screenshot of a stack trace classifies on
its caption, and an image-only turn flattens to empty text and never
reaches the classifier at all.

classifier_llm_config.vision opts in, off by default, with max_images
bounding what one turn can add. Images are still dropped when the
classifier model is declared supports_vision false. Anthropic and
Responses image parts are rewritten into chat-completions dialect before
they reach the classifier call, since /v1/messages hands the pre-routing
hook its own dialect untranslated.

The local scorer no longer short-circuits heuristic_first or hybrid on a
turn carrying forwarded images, because it reads text alone and its
confidence describes a request it has only partly seen.
2026-09-04 18:59:50 -07:00
mateo-berri
0157808187 fix(fireworks_ai): keep file_search on LiteLLM's emulated search for the Responses API 2026-09-04 18:58:09 -07:00
moe-berri
d1fd3a3457 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_shadow_eval_judge_output_cap
# Conflicts:
#	tests/test_litellm/integrations/test_shadow_eval_logger.py
2026-09-04 18:57:19 -07:00