litellm/litellm
Noah Nistler 8177230a29
feat(guardrails): add run_in_parallel opt-in for concurrent pre_call and post_call guardrails (#33770)
* feat(guardrails): add run_in_parallel opt-in for concurrent pre_call guardrails

Pre-call guardrails run sequentially because each may mutate the request
payload and later guardrails depend on earlier mutations. Deployments with
several slow block-only pre_call guardrails (external moderation, Bedrock,
LLM-judge) therefore pay the sum of their latencies. during_call guardrails
run concurrently but alongside the LLM call, so a violating payload has
already been sent, which is unacceptable when the request must never reach
the model.

This adds a per-guardrail run_in_parallel flag (default off). Guardrails that
opt in are pulled out of the sequential loop and run concurrently via
asyncio.gather after every sequential (payload-mutating) guardrail has run, so
they observe the mutated payload and still form a hard barrier before the LLM
call; the first to raise blocks the request. Their returned data is discarded
since they are declared block-only.

The flag is wired from LitellmParams onto the guardrail instance at the same
generic choke point in initialize_guardrail that already sets
skip_system_message_in_guardrail, so no per-provider initializer needs to
change.

* feat(guardrails): extend run_in_parallel opt-in to post_call guardrails

post_call_success_hook ran guardrails sequentially for the same reason
pre_call did: response-modifying guardrails thread the response forward. But
block-only output scanners (which read the response and reject on violation
without changing it) serialize for no benefit and add latency.

This reuses the existing run_in_parallel flag for the post_call hook. Opted-in
post_call guardrails are pulled out of the sequential loop and run concurrently
via asyncio.gather after the sequential (response-modifying) guardrails and
before the non-guardrail CustomLogger callbacks, so they inspect the final
response and still block it from reaching the client if any raises. Their
returned response is discarded since they are block-only.

The apply_guardrail path sets data["guardrail_to_apply"] immediately before
awaiting, and unified_guardrail pops it before its first suspension point, so
concurrent guardrails never race on that key under asyncio's cooperative
scheduling.

* fix(guardrails): await all parallel guardrails and prioritize blocks over reroutes

Addresses review feedback on the run_in_parallel opt-in.

asyncio.gather propagated the first exception without cancelling or awaiting
the siblings, so a block at t=0 left the other guardrails running as
unobserved background tasks (wasted external calls plus event-loop warnings),
and a fast SensitiveDataRouteException/ModifyResponseException could return a
reroute or passthrough before a slower block finished, letting crafted input
bypass the block. Both the pre_call and post_call parallel batches now gather
with return_exceptions=True so every guardrail runs to completion, then raise
any blocking exception ahead of a flow-changing one.

The registry choke point wrote bool(None)==False onto every instance when the
config omitted run_in_parallel, silently disabling a constructor-set default;
it now only writes when the config provides an explicit value.

* fix(guardrails): record lifecycle logs for every concurrently-run guardrail

The log_guardrail_information decorator skipped its auto-record when it saw
that the count of standard_logging_guardrail_information entries in the shared
request_data had grown during the wrapped call, taking that as proof the
wrapped function had recorded its own richer entry. That heuristic breaks the
moment guardrails run concurrently (parallel pre_call/post_call, during_call):
a sibling guardrail's append inflates the shared count, so a guardrail that did
not self-record wrongly concludes it already did and drops its own entry. The
result is that enabling run_in_parallel silently loses per-guardrail lifecycle
logs, so the Admin UI Request Lifecycle timeline and downstream loggers
(Datadog, Langfuse, OTEL, spend logs) show only one of the concurrent
guardrails.

Replace the shared-count heuristic with a ContextVar flag set when a guardrail
records its own entry. asyncio copies the context into each gathered task, so
the flag is isolated per concurrent guardrail while still catching the
self-record-then-skip-auto-record case within a single invocation.

* test(guardrails): declare run_in_parallel on post_call guardrail mocks

The post_call partition reads run_in_parallel on every CustomGuardrail
callback. A MagicMock(spec=CustomGuardrail) has no run_in_parallel (it is
set in __init__, not on the class) so the attribute access raised, and even
a class-level default would return a truthy child mock that wrongly routes
the double into the parallel batch. Declare the flag False on the shared
mock factories so these pre-existing hook tests exercise the sequential
path they assert on.

* fix(guardrails): harden run_in_parallel reads and address review feedback

Read run_in_parallel via getattr(..., False) in the pre_call and post_call
partitions so a third-party CustomGuardrail subclass that overrides __init__
without chaining super().__init__() no longer raises AttributeError on a path
that previously worked. Drop the redundant in-function GuardrailEventHooks
import in _run_parallel_post_call_guardrails (already imported module-level).
Remove the flaky wall-clock upper-bound assertions from the two concurrency
tests; the all-start-before-any-end overlap assertion is the timing-independent
signal that actually proves concurrency.
2026-07-24 13:25:58 -07:00
..
a2a_protocol fix(responses): stop scheduling sync success_handler concurrently with async_success_handler (#32239) 2026-07-07 09:13:50 -07:00
anthropic_interface style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
assistants style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
batch_completion style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
batches Revert "chore(ci): sync litellm_internal_staging into daily OSS branch (#33337)" (#33339) 2026-07-14 19:32:25 -07:00
caching fix(cache): make in-memory and disk cache increments atomic (#34013) 2026-07-20 15:51:01 -07:00
completion_extras style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
compression style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
containers style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
endpoints/speech/speech_to_completion_bridge style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
evals style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
experimental_mcp_client fix(mcp): sanitize Anthropic tool schemas and stop encoding gateway names 2026-07-17 10:33:28 -07:00
files style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
fine_tuning chore(lint): zero out crash-class pyright rules and ban new type: ignore comments (#32152) 2026-07-04 16:56:12 -07:00
google_genai fix(logging): classify async anthropic_messages and generate_content as async (#33589) 2026-07-16 20:56:47 -07:00
images style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
integrations feat(guardrails): add run_in_parallel opt-in for concurrent pre_call and post_call guardrails (#33770) 2026-07-24 13:25:58 -07:00
interactions fix(responses): stop scheduling sync success_handler concurrently with async_success_handler (#32239) 2026-07-07 09:13:50 -07:00
litellm_core_utils fix(logging): stop scheduling sync failure_handler concurrently with async_failure_handler (#34306) 2026-07-24 11:06:55 -07:00
llms fix(vertex): incrementally parse accumulated Gemini stream JSON to prevent multi-value wedge (#34320) 2026-07-24 11:07:02 -07:00
models feat(mcp): issuer-anchored OAuth discovery (RFC 8414 §3.3) as the trust anchor 2026-07-15 16:59:01 -07:00
ocr test: litellm fix failing tests (#32577) 2026-07-09 13:54:45 -07:00
passthrough fix(logging): classify allm_passthrough_route as async to prevent duplicate success callbacks (#32265) 2026-07-06 15:04:05 -07:00
proxy feat(guardrails): add run_in_parallel opt-in for concurrent pre_call and post_call guardrails (#33770) 2026-07-24 13:25:58 -07:00
proxy_auth style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
rag fix(rag): track LLM completion usage and spend for /v1/rag/query (#32438) 2026-07-17 17:45:27 +00:00
realtime_api fix(vertex): forward realtime health check params (#32550) 2026-07-08 17:30:46 -07:00
repositories fix(team): make team member add atomic to prevent concurrent-add member loss (#34185) 2026-07-22 21:47:22 +00:00
rerank_api fix(rerank): log optional_rerank_params at debug to stop leaking request content (#32533) 2026-07-08 18:48:03 -07:00
responses fix(cost_tracking): map cache_write_tokens on Responses API usage path 2026-07-23 19:07:08 -07:00
router_strategy feat(complexity-router): add return_raw_model_name toggle for response model field (#33875) 2026-07-18 19:24:56 -07:00
router_utils fix(router): keep team wildcard routers fresh and prioritize them over global patterns 2026-07-17 18:19:02 -07:00
rust_bridge perf(bedrock): audio transcription via rust core (py->rust bridge) (#33990) 2026-07-20 14:09:41 -07:00
sandbox style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
search style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
secret_managers fix(proxy): harden secret name validation for external secret manager integrations (LIT-4201) (#32092) 2026-07-08 10:36:00 -07:00
skills style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
types feat(guardrails): add run_in_parallel opt-in for concurrent pre_call and post_call guardrails (#33770) 2026-07-24 13:25:58 -07:00
vector_store_files style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
vector_stores style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
videos style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
__init__.py feat(proxy): add overwrite_user_with_key_hash to stamp outgoing user param with key hash (#34417) 2026-07-23 16:38:01 -07:00
_internal_context.py fix(proxy): harden request parameter handling 2026-04-16 01:38:12 +00:00
_lazy_imports.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
_lazy_imports_registry.py feat(tencent): add Tencent TokenHub as a provider (#31903) 2026-07-02 18:31:59 -07:00
_logging.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
_redis.py fix(redis): honor ssl value instead of key presence when building async connection pool (#32590) 2026-07-16 13:38:01 -07:00
_redis_credential_provider.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
_service_logger.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
_uuid.py build(deps-dev): bump black to 26.3.1 and apply formatting (#28525) 2026-05-21 17:24:18 -07:00
_version.py
anthropic_beta_headers_config.json fix(bedrock-invoke): retain clear_tool_uses_20250919 context_management edits and emit context-management-2025-06-27 beta (LIT-3393) (#32658) 2026-07-09 14:31:27 -07:00
anthropic_beta_headers_manager.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
blog_posts.json docs(blog): add WebRTC blog post link 2026-03-13 17:54:33 +05:30
budget_manager.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
constants.py feat(anthropic): add Claude Opus 5 2026-07-24 10:43:49 -07:00
cost.json
cost_calculator.py fix(cost_calculator): sum mirrored cache token fields once in combine_usage_objects 2026-07-23 19:07:09 -07:00
exceptions.py feat(logging): add structured budget fields to budget rejection failure logs (#33460) 2026-07-16 12:39:04 -07:00
main.py fix(router): stop per-deployment num_retries from double-counting as provider max_retries (#34129) 2026-07-21 21:13:32 +00:00
model_prices_and_context_window_backup.json feat(anthropic): add Claude Opus 5 2026-07-24 10:43:49 -07:00
policy_templates_backup.json feat(add-new-block_code_execution-guardrail): prevent agent from executing code (#22154) 2026-02-25 22:02:14 -08:00
provider_endpoints_support_backup.json chore: litellm oss staging (#30968) 2026-06-23 07:31:44 -07:00
py.typed
router.py fix(router): stop custom model_info leaking onto shared backend cost map key 2026-07-20 15:49:19 -07:00
scheduler.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
setup_wizard.py feat(anthropic): add Claude Opus 5 2026-07-24 10:43:49 -07:00
timeout.py style: unify ruff format width on 120 (#31518) 2026-06-27 12:39:29 -07:00
utils.py fix(router): honor request-level num_retries over global litellm_settings.num_retries (#34124) 2026-07-21 21:32:34 +00:00