Commit graph

450 commits

Author SHA1 Message Date
Mateo Wang
80250807db
Merge pull request #38808 from BerriAI/litellm_headroom_ccr_streaming_responses
fix(headroom): resolve CCR retrieval on streaming /v1/responses
2026-09-03 13:13:18 -07:00
yucheng-berri
ecabfbd5af
fix(guardrail): hide-secrets playground redaction and guardrail telemetry (#39398)
* Fix hide-secrets guardrail: playground redaction, UI dropdown entry, spend-log telemetry

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

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

Resolves LIT-3548

* Fix hide-secrets passthrough telemetry and JSON config input

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

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

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

walk_user_text and the prompt redaction now report how many non-empty
strings they visited; when neither inspected anything (image-only
content, empty strings), the run records no guardrail entry instead of
an 'allow' row that counts a check which never saw any text.
2026-09-03 00:01:03 -07:00
yucheng-berri
7a81ae98e6
fix(model_armor): handle Anthropic Messages and Responses streams in post_call (#39181)
* fix(model_armor): handle Anthropic Messages and Responses streams in post_call

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: yassin <yassin@berri.ai>
2026-09-02 19:15:14 -07:00
devin-ai-integration[bot]
8065ede40b
test(guardrails): expect the deduped end-of-stream scan in crowdstrike cadence test (#39467)
Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 02:11:08 +00:00
Mateo Wang
4f7b20ec10
fix(guardrails): skip streaming guardrail rounds that re-scan cleared output (#39386)
* fix(guardrails): skip streaming guardrail rounds that re-scan cleared output

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

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

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

Update sampled cadence expectations and use tuple-backed scan state

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-02 18:25:28 -07:00
Mateo Wang
9e8e486f1c
Merge pull request #38877 from BerriAI/litellm_fix_guardrail_put_inmemory_sync
fix(guardrails): apply PUT /guardrails/{id} to the serving worker immediately and reject invalid configs with 422
2026-09-02 18:13:15 -07:00
mateo-berri
45f44fea60 fix(headroom): leave background responses requests uncompressed and unconverted 2026-09-02 17:53:39 -07:00
devin-ai-integration[bot]
8e3566d2f7
fix(guardrails): forward mode and streaming params to crowdstrike_aidr handler (#39317)
* fix(guardrails): forward mode and streaming params to crowdstrike_aidr handler

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

* test(guardrails): drop stream_chunk_builder patch from crowdstrike cadence test

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

* test(guardrails): type test params and cover unsupported crowdstrike mode rejection

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-02 17:38:43 -07:00
mateo-berri
0fb83995dd Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_fix_guardrail_put_inmemory_sync
# Conflicts:
#	litellm/proxy/guardrails/guardrail_registry.py
2026-09-02 17:35:06 -07:00
yassin
6d5a3ab42c merge: litellm_internal_staging into litellm_headroom_ccr_streaming_responses
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-03 00:24:17 +00:00
Mateo Wang
86ca146ea2
Merge pull request #39271 from BerriAI/litellm_fix_presidio_sibling_callback_leak
fix(guardrails): track and tear down presidio sibling callbacks on delete and update
2026-09-02 14:39:47 -07:00
mateo-berri
6fae4b3c39 fix(guardrails): keep the presidio output masker from unmasking after an in-memory update 2026-09-02 13:32:21 -07:00
Ali Ahmed
a677242d6f
fix(headroom): stop re-compressing retrieved CCR content in client tool loops (#38591)
When the headroom_retrieve tool is exposed to a client that runs its own
tool-execution loop (the LiteLLM MCP gateway path), the client executes the
retrieve call and sends the recovered original content back as a tool result
on the next turn. The guardrail then compressed that row again, and because
CCR is content-addressed it collapsed back to the exact same hash it was just
retrieved from. The model never saw the expansion and the agent looped.

Hold tool-result rows that carry headroom_retrieve output back from the
compression service, the same way the live turn and trailing tool exchange are
already protected, so the expansion survives. Retrieve calls are matched by the
direct headroom_retrieve name and the mcp__<server>__headroom_retrieve gateway
name. Because a long gateway name is truncated past 64 chars in the
OpenAI-translated view the guardrail scans, the pairing also falls back to the
tool-call id read from the request's own untranslated messages, which is never
truncated.

Fixes #38558
2026-09-02 12:07:13 -07:00
mateo-berri
7cde2cd77f test(guardrails): type the presidio sibling test helpers precisely 2026-09-01 22:39:17 -07:00
mateo-berri
c18511be7d fix(guardrails): track and tear down presidio sibling callbacks
initialize_presidio registers up to three callbacks per guardrail but the
registry only kept the first, so deleting or re-syncing the guardrail left
the post_call siblings serving the old config. The initializer now returns
every callback it registered, the registry tracks primary and siblings per
guardrail id, delete purges all of them from every callback list, and
update pushes the new params into each while siblings keep their stage.
2026-09-01 22:28:38 -07:00
mateo-berri
4a5d0b8163 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_headroom_ccr_streaming_responses
# Conflicts:
#	litellm/llms/openai/responses/guardrail_translation/handler.py
#	tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py
2026-09-01 22:25:59 -07:00
mateo-berri
a38dfecd96 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_stream_modify_response_chunks 2026-09-01 14:49:06 -07:00
Mateo Wang
deb67ce6e2 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_bedrock_buffered_responses_stream
# Conflicts:
#	tests/test_litellm/proxy/guardrails/guardrail_hooks/test_bedrock_guardrails.py
2026-09-01 14:11:29 -07:00
mateo-berri
4c7dd0b522 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_stream_modify_response_chunks
# Conflicts:
#	type-discipline-budget.json
2026-09-01 12:36:48 -07:00
Sean Yasnogorodski
8a4ba78869
feat(guardrails): add Alice guardrail (#38898)
* feat(guardrails): add Alice by ActiveFence guardrail

Adds `guardrail: alice` — policy-based guardrails for prompts and model
responses, evaluated against ActiveFence's Alice.

What makes this different from the other providers: Alice evaluates against
policies configured per *application*, and a proxy typically fronts several of
them, so the application cannot be a static config value. It is named on the
LiteLLM virtual key instead:

    curl $PROXY/key/generate -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
      -d '{"key_alias": "payments-bot",
           "metadata": {"alice_app_id": "payments-bot"}}'

read via `CustomGuardrail._get_admin_metadata`, with `key_alias` as the
fallback. That helper is what makes it trustworthy: it reads whichever metadata
holder the proxy wrote the authenticated key's values into — which differs by
route — and the proxy strips caller-supplied `user_api_key_*` from both, so a
caller cannot point its own traffic at an application with laxer policies than
the one its key was issued for. A request whose key names no application is
refused rather than evaluated against a guess.

Implements `apply_guardrail` only, so pre_call, during_call, post_call and
streaming all come from UnifiedLLMGuardrails. Blocks with
GuardrailRaisedException; masks by substituting Alice's redacted text; a MASK
carrying no replacement blocks rather than passing the original through. A
verdict reporting `errors[]` is treated as a failure, not a pass — otherwise a
half-evaluated message would be allowed. `unreachable_fallback` (already on
LitellmParams) chooses fail-closed or fail-open on transport failure.

Config:

    guardrails:
      - guardrail_name: alice
        litellm_params:
          guardrail: alice
          mode: [pre_call, post_call]
          api_key: os.environ/ALICE_API_KEY

21 tests in tests/test_litellm/proxy/guardrails/guardrail_hooks/test_alice.py
cover registration, credential resolution, the app-id ladder including the
forged-metadata case, every verdict, and both unreachable policies.

No new LitellmParams field, so no schema.d.ts regeneration is needed.

* refactor(guardrails): post to Alice's LiteLLM endpoint and forward verbatim

Switches from `/v2/evaluate/message` — Alice's single-text endpoint — to
`/v2/evaluate/litellm`, which takes the hook's arguments as they arrive and
answers with a verdict.

That inverts where the work happens, and shrinks this plugin accordingly. It
now selects nothing and renames nothing: it posts `{input_type, inputs,
request_data}` and enforces `{verdict, categories, correlation_id, message,
replacements}`. Which parts of a conversation are worth evaluating, and how a
verdict is reached, are decided by Alice — so changing either is a change on
their side rather than a LiteLLM upgrade for every user.

The app-id resolution this plugin carried is gone with it. Alice reads the
application off the authenticated key's metadata itself, from the payload it is
handed, so the ladder here was duplicating a decision the far side already
makes. The security property is unchanged and still comes from the proxy
stripping caller-supplied `user_api_key_*` before a guardrail sees the request.

Masking is now positional — the far side chose which texts it was answering
for, so it says which by index. Only `texts` is written; a new
`structured_messages` object would make the chat translation layer skip the
`texts` write-back and silently drop the edits. A mask that lands nowhere
blocks rather than passing the original through.

`request_data` carries live Python objects (an OpenTelemetry span among them),
so `_json_safe` copies it into something serialisable by a mechanical rule
rather than a field list — a list drifts from what the far side needs, a rule
cannot. Serialising naively raises, and that error would read as "guardrail
unavailable" on every request.

26 tests, covering verbatim forwarding, each verdict, positional masking, the
`structured_messages` identity trap, both unreachable policies, and the
serialiser's handling of unserialisable values and cycles.

* fix(alice guardrail): satisfy lint and code-quality CI gates

- Bound _json_safe's recursion and register it in recursive_detector's
  ignore list (it already caps depth and dedupes cycles by id, matching
  the repo's established pattern for legitimate bounded recursion).
- Clear ruff-strict budget breaches: annotate __init__'s return type,
  raise TypeError (not ValueError) for a bad response body, type
  _json_safe's payload as object instead of Any, and file-scope-ignore
  ANN401 for **kwargs (forwarding it as object broke the call into
  CustomGuardrail.__init__, confirmed via basedpyright).
- Clear type-discipline budget breaches: suppress the construction/
  annotation checks on one-shot HTTP payloads, the module-level
  guardrail registries, and _json_safe's bounded accumulator; narrow
  AliceVerdict's list fields to tuples and _evaluate's request_data to
  Mapping[str, object] where nothing downstream mutates them.

* test(alice guardrail): assert the guardrail actually registers

The registration test called init_guardrails_v2 and asserted nothing, so it
passed whether or not the guardrail was ever registered — TQ001 in the
test-quality gate, and a fair catch: a test that cannot fail is not covering
the thing it names.

Now asserts exactly one AliceGuardrail lands in litellm.callbacks under the
configured name.

This surfaced only after the ruff-strict and type-discipline gates stopped
failing ahead of it; the lint job runs its gates in sequence, so an earlier
failure masks every later one.

* fix(alice guardrail): reach 100% patch coverage, drop the ActiveFence naming

Codecov flagged 10 uncovered lines, all of them error paths — which is where a
guardrail most needs covering, since each one decides whether traffic flows
unscreened.

Two of the ten turned out to be dead rather than untested, and are removed:

- `except GuardrailRaisedException: raise` in apply_guardrail. `_evaluate`
  raises httpx errors, Timeout and TypeError, never that — so the clause could
  never fire.
- the trailing `json.dumps` probe in `_json_safe`. Everything json.dumps
  handles natively is caught by the isinstance branches above (a dict or list
  subclass included), so anything reaching the bottom — bytes, datetime, an
  OpenTelemetry span — cannot cross the wire regardless. It now says so and
  returns None.

The rest are now tested: a timeout, 502/503/504 as unreachable, a 4xx as NOT
unreachable (a rejected credential is our misconfiguration, not an outage, and
must not fail open), a non-object response body, and a model whose model_dump
raises.

Also drops "by ActiveFence" throughout — the product is Alice — and points the
header at alice.io. `ui_friendly_name` is now "Alice", which is the key
guardrailLogoMap and the garden card look up, so all three moved together.

* fix(alice guardrail): strip caller credentials, widen unreachable detection, block partial MASK

Addresses PR review: request_data no longer forwards secret_fields.raw_headers or
the root api_key to Alice (the caller's Authorization token in the clear otherwise);
HTTP 500, malformed JSON, and a non-object body now route through the configured
unreachable_fallback instead of raising raw, so fail_open still fails open on those;
a MASK verdict with even one out-of-range replacement now blocks entirely instead of
silently letting the rest through unmasked. Also tightens request_data's type and
documents the known streaming-mask limitation on the class.

* fix(alice guardrail): strip credentials at any depth, stop filtering on texts

secret_fields/api_key/headers/provider_specific_header can appear nested
under proxy_server_request, metadata, litellm_metadata, and their
requester_metadata/body sub-paths in a real captured payload — a
top-level-only strip missed all of those. _json_safe now drops these keys
by name wherever they occur during serialization, so a new nesting path
can't reintroduce the leak.

apply_guardrail also stopped skipping the call whenever texts was empty,
even when tool_calls/images/structured_messages carried content — that
was the plugin making a selection decision Alice's design says belongs on
the far side. It now only skips when none of the selectable fields have
anything in them.

* fix(alice guardrail): route an undecodable response body through the fallback

`response.json()` raises UnicodeDecodeError when the body carries bytes that
are not valid UTF-8, and that escaped the except clause: UnicodeDecodeError is
a *sibling* of json.JSONDecodeError under ValueError, not a subclass of it, so
naming only JSONDecodeError left it uncaught. Both fallback modes surfaced a
raw decoding error instead of applying unreachable_fallback — which for a
fail_open deployment meant a hard failure where it had asked for an allow.

Named explicitly rather than widening to ValueError, so the clause still says
which three conditions it means. Tested under both policies.
2026-09-01 12:33:39 -07:00
mateo-berri
c01ef712a2 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_headroom_ccr_streaming_responses
# Conflicts:
#	tests/test_litellm/responses/litellm_completion_transformation/test_streaming_iterator_transformation.py
2026-09-01 11:27:03 -07:00
mateo-berri
329654765a test(guardrails): update chat eos block tests for finish-chunk withholding 2026-08-31 17:31:59 -07:00
mateo-berri
76cfa6339b test: give the mocked prepared request real headers for the masked debug log 2026-08-31 17:28:12 -07:00
mateo-berri
e601114383 fix(bedrock): mask signed request headers in guardrail debug log 2026-08-31 17:15:54 -07:00
mateo-berri
78b57fb427 fix(guardrails): withhold chat finish chunk in end_of_stream_only mode and close open Responses items before a mid-stream block 2026-08-31 17:05:45 -07:00
mateo-berri
31a9f7e6ad test(guardrails): type streaming-block test helpers and drop mutable accumulators 2026-08-31 16:34:21 -07:00
mateo-berri
158220f151 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_stream_modify_response_chunks 2026-08-31 16:20:29 -07:00
mateo-berri
7edf5b36cf fix(guardrails): deliver modify_response block as valid SSE on streaming chat and Responses
A guardrail modify_response verdict on a streaming request only produced a
proper replacement on /v1/messages: the chat completions and Responses API
translations had no build_block_sse_chunks, so the ModifyResponseException
re-raised and surfaced as an in-stream 500 error frame (or a whole-request
500 in buffered mode) instead of the documented 200 replacement.

Implement build_block_sse_chunks for both OpenAI translations: chat emits a
content delta plus a finish_reason content_filter chunk with real usage;
Responses emits the typed event sequence (standalone via
build_synthetic_response_events pre-stream, or an output-item continuation
under the in-progress response id mid-stream) ending in response.completed.
2026-08-31 16:01:39 -07:00
Ashton Sidhu
9f9236e8d5
fix(guardrails): exclude images from HiddenLayer v1 scans (#29210)
* Don't scan images

* Fix failing tests

* Fix lint: typed image-part filter, restore monkeypatch-based tests

---------

Co-authored-by: Yucheng Zhu <yucheng@berri.ai>
2026-08-31 12:50:42 -07:00
davida-ps
9f67a58198
fix(guardrails): configure Prompt Security file timeout policy (#38083)
* fix(guardrails): fail open on Prompt Security file timeouts

* fix(guardrails): configure Prompt Security timeout policy
2026-08-31 12:05:57 -07:00
mateo-berri
db1e0717f9 fix(guardrail_translation): assemble responses stream text from delta events for terminal-failure scans 2026-08-30 12:52:03 -07:00
mateo-berri
4261198b2f Merge branch 'litellm_internal_staging' into litellm_headroom_ccr_streaming_responses 2026-08-30 12:47:33 -07:00
mateo-berri
24c5846c75 Merge branch 'litellm_internal_staging' into litellm_fix_bedrock_buffered_responses_stream 2026-08-30 12:35:39 -07:00
mateo-berri
eac5dc10f3 fix(guardrails): apply PUT /guardrails/{id} to the serving worker immediately and reject invalid configs with 422 2026-08-29 22:11:36 -07:00
mateo-berri
60296cb540 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_bedrock_guardrail_stream_audit 2026-08-29 21:49:13 -07:00
mateo-berri
a23f0fc3c3 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_bedrock_guardrail_stream_audit
# Conflicts:
#	litellm/proxy/common_request_processing.py
2026-08-29 21:23:35 -07:00
yucheng-berri
842c423ccd
fix(guardrails): stop Lakera monitor mode forwarding unmasked PII on Responses-API bodies (#38841)
#34940 widened the mask-in-place safety guard so a Responses-API
`instructions` field (and a combined messages+input body) skips the
PII masking branch. With `on_flagged: "monitor"` that fell straight
through to "allow", so PII that used to be masked now reaches the
model unredacted.

Monitor means "don't block", not "don't redact". Recover the one shape
whose payload is still fully writable: mask it and write the redacted
instructions back into `data["instructions"]` directly, since
apply_redacted_messages_back has no path for that field and would
otherwise fold the instructions text into `data["input"]`.

The combined messages+input and multimodal shapes stay unmasked - both
are unsafe to write back, not merely unwritable - and now log an error
naming the reason instead of passing silently.

No block/allow decision changes: block and inject_system_message keep
the exact outcomes #34940 shipped.
2026-08-29 17:07:39 -07:00
mateo-berri
6bd3699d43 fix(responses): keep guardrailed input items and bridge stream usage intact
- _write_back_structured_messages now patches only the rewritten rows back
  into the original input items, so reasoning items (encrypted_content),
  function_call ids, and web_search_call items survive a guardrail rewrite
  verbatim; rewrites that cannot be row-mapped fall back to the previous
  full conversion
- the responses bridge stream snapshot restores usage hidden in
  _hidden_params when stream_options is unset, so converted fake streams
  report real input_tokens instead of 0
2026-08-29 16:39:14 -07:00
mateo-berri
1695b7f7b1 fix(headroom): fake-stream converted sync /v1/responses calls too
The sync response_api_handler agentic branch returned the completed ResponsesAPIResponse for a request the Headroom guardrail had converted from streaming, so litellm.responses(stream=True) handed callers a non-iterable object. Wrap it in the same fake stream the async path uses
2026-08-29 15:02:20 -07:00
mateo-berri
ef96af5121 fix(headroom): resolve CCR retrieval on streaming /v1/responses
Streaming /v1/responses requests that carried Headroom's retrieve tool were
sent upstream as streams, so the model's headroom_retrieve function_call was
streamed straight back to a client that never declared the tool and the
retrieval never resolved. Chat completions already avoid this by converting
the request to non-stream in the pre-call deployment hook, letting the
agentic loop resolve the retrieve, and fake-streaming the final answer.

The hook now converts responses call types too, the responses handler wraps
the resolved result as a fake stream whenever any interception converted the
stream (shared converted_stream_requested helper instead of per-integration
key checks), and the follow-up request filter drops every non-code-interpreter
interception key through is_interception_internal_key.

Resolves LIT-6481
2026-08-29 14:34:42 -07:00
Mateo Wang
c62c2afa09
Merge pull request #38234 from BerriAI/litellm_request_timeouts
fix(proxy): give every `requests` call a timeout so a silent server cannot hang the caller
2026-08-29 14:30:46 -07:00
mateo-berri
429ad06972 fix(guardrails): write structured_messages rewrites back into /v1/responses input
Message-rewriting guardrails such as Headroom return their rewrite in
structured_messages and leave texts untouched. The responses guardrail
translation only mapped texts back, so compression never reached the
upstream request on /v1/responses while the retrieve tool still got
injected. Convert the returned messages back to Responses input (plus
instructions) the way the chat and Anthropic handlers already do, and
keep developer messages as input_text in the chat-to-responses bridge.

Resolves LIT-6494
2026-08-29 14:23:57 -07:00
mateo-berri
8aba6e9203 Merge branch 'litellm_internal_staging' into litellm_request_timeouts 2026-08-29 13:32:33 -07:00
mateo-berri
2affd800ec test(headroom): cover stream conversion after deployment-level compression 2026-08-29 12:45:56 -07:00
mateo-berri
4d4cf40334 fix(headroom): delegate to the parent deployment hook so deployment-level configs still compress 2026-08-29 12:06:38 -07:00
mateo-berri
2457e60cfc Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_lit_4913_headroom_streaming_ccr 2026-08-29 10:54:24 -07:00
mateo-berri
229970c500 fix(guardrails): unwrap HiddenParamsAsyncIteratorWrapper before deferred dispatch class sniffing 2026-08-29 02:30:34 -07:00
mateo-berri
f60ccf6234 fix(guardrails): match deferred stream dispatch shape per stream owner and defer passthrough logging until guardrail eos 2026-08-29 02:23:12 -07:00
mateo-berri
64eec53fd8 fix(guardrails): surface post-flush stream blocks as in-stream error frames and keep guardrail_information in spend logs
A guardrail block or failed scan that fires after SSE chunks have been
flushed can no longer set an HTTP status, so raising HTTPException there
silently truncated the stream. _emit_streaming_http_error now routes
post-flush failures through the endpoint translation's
build_stream_error_items, emitting the surface-correct error frame on
chat completions (data: {error}), /v1/messages (event: error), and
/v1/responses (ErrorEvent with the next sequence number). Pre-flush
blocks still raise with a real HTTP status.

Successful flags-on scans also logged metadata.guardrail_information as
null: the chat handler planted litellm_metadata on a route whose bucket
is metadata, flipping the bucket for every later write, and responses
streams fired their spend log before the eos scan ran. The chat handler
now merges user_api_key metadata through get_or_create_metadata_bucket,
and deferred stream-complete logging is armed for aresponses like it
already was for anthropic_messages.
2026-08-29 01:25:08 -07:00
mateo-berri
7eb757a49b fix(bedrock): route streamed responses-API output through the unified guardrail
Streamed /v1/responses returned 500 whenever a Bedrock post_call guardrail
was enabled: the hook fed responses-API events into stream_chunk_builder,
which only understands chat-completions chunks, and the wrapped KeyError
surfaced as litellm.APIError before any ApplyGuardrail scan ran.

Delegate responses-API routes to UnifiedLLMGuardrails, whose translation
layer scans the assembled response at end of stream and only then releases
the buffered events, so flagged content never reaches the client.
2026-08-29 00:01:02 -07:00