Skip to content

feat(guardrails): add Airia guardrail integration (internal copy of #40784) - #43657

Draft
devin-ai-integration[bot] wants to merge 6 commits into
mainfrom
litellm_airia_guardrail
Draft

devin-ai-integration[bot] wants to merge 6 commits into
mainfrom
litellm_airia_guardrail

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

How it solves it:

  • Carries the contributor's airia guardrail from feat(guardrails): add Airia guardrail integration #40784 unchanged, merged with main
  • Reports Airia's supported modes (pre_call, post_call) to the dashboard's add-guardrail settings
  • Adds two-worker proxy integration tests with a local Airia sink and scripted upstream

User Flow

Before: a proxy admin who configures guardrail: airia gets no enforcement

  1. They add guardrail: airia with api_base and api_key under guardrails: and start the proxy
  2. Startup logs Skipping guardrail 'airia' ... Unsupported guardrail: airia
  3. POST http://litellm-domain/v1/chat/completions with ada@example.test in the prompt reaches the model unchanged

After: the same config calls Airia before and after the model

  1. They add the same config and start the proxy, which boots with Airia active
  2. The same POST with ada@example.test reaches the model as [EmailAddress1] when Airia redacts it
  3. A prompt Airia blocks returns 400 with Airia's reason, and the model is never called
  4. On http://litellm-domain/ui/?page=guardrails the Airia mode picker offers only pre_call and post_call

Relevant issues

Internal copy of #40784 by @airia-open-source, credited with a Co-authored-by trailer. The contributor branch could not be pushed to from our side

Pre-Submission checklist

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

This is the deterministic audit run: a real two-worker proxy, real Postgres and Redis, a scripted upstream and a local sink that speaks the Airia /v1/guardrails/litellm protocol. A live run against the real Airia service is still pending an Airia credential and will replace nothing below, only add a case

Base is eae8ed7f3c448d9dde3ecc3291e0f92ddc70a2e4 (merge base with main). Head is 1d0fbcbb2feb5de551a00e6ee64c10cb8d8b7567; the PR tip adds an empty credit commit and 75d2ded, which only drops a stale # mutable-ok comment, so the code under test is unchanged. Runs used tests/integration/run.py extensions with seed 4106601 and order seed 0. The unit mutation check removed get_supported_event_hooks and the new unit test went red, then green on restore

Before (eae8ed7)

  1. Run 1d0fbcb-base over the two Airia files
  2. 2 passed (R18, R28), 54 failed because airia is not a known guardrail and traffic is unchecked, 3 skipped as BUG markers

After (1d0fbcb)

  1. Runs 1d0fbcb-headA and 1d0fbcb-headB over the same files
  2. Each run: 56 passed, 0 failed, 3 skipped as BUG markers, 0 reruns, identical collected and passed node lists
Inventory row Test node ID HEAD A HEAD B BASE
R1 test_airia_guardrail.py::test_r1_pre_call_none_chat_clients[httpx] PASS PASS FAIL
R1 test_airia_guardrail.py::test_r1_pre_call_none_chat_clients[openai_async] PASS PASS FAIL
R1 test_airia_guardrail.py::test_r1_pre_call_none_chat_clients[openai_sync] PASS PASS FAIL
R2 test_airia_guardrail.py::test_r2_blocked_pre_call_never_reaches_provider[httpx] PASS PASS FAIL
R2 test_airia_guardrail.py::test_r2_blocked_pre_call_never_reaches_provider[openai_async] PASS PASS FAIL
R2 test_airia_guardrail.py::test_r2_blocked_pre_call_never_reaches_provider[openai_sync] PASS PASS FAIL
R3 test_airia_guardrail.py::test_r3_blocked_without_reason_uses_default_message PASS PASS FAIL
R4 test_airia_guardrail.py::test_r4_pre_call_redaction_reaches_provider PASS PASS FAIL
R5 test_airia_guardrail.py::test_r5_structured_message_redaction_is_positional PASS PASS FAIL
R6 test_airia_guardrail.py::test_r6_multiple_texts_are_rewritten_without_reordering PASS PASS FAIL
R7 test_airia_guardrail.py::test_r7_multi_choice_response_redaction_rewrites_each_choice PASS PASS FAIL
R8 test_airia_guardrail.py::test_r8_streamed_chat_post_call_redaction_has_no_leak PASS PASS FAIL
R9 test_airia_guardrail.py::test_r9_streamed_chat_post_call_block_has_no_provider_error_leak PASS PASS FAIL
R10 test_airia_guardrail.py::test_r10_messages_pre_call_verdict_matrix[anthropic_async-REDACT] PASS PASS FAIL
R10 test_airia_guardrail.py::test_r10_messages_pre_call_verdict_matrix[anthropic_sync-BLOCK] PASS PASS FAIL
R10 test_airia_guardrail.py::test_r10_messages_pre_call_verdict_matrix[httpx-NONE] PASS PASS FAIL
R11 test_airia_guardrail.py::test_r11_messages_post_call_redaction[anthropic_async] PASS PASS FAIL
R11 test_airia_guardrail.py::test_r11_messages_post_call_redaction[anthropic_sync] PASS PASS FAIL
R11 test_airia_guardrail.py::test_r11_messages_post_call_redaction[httpx] PASS PASS FAIL
R12 test_airia_guardrail.py::test_r12_streamed_messages_post_call_redaction_has_no_leak SKIP SKIP SKIP
R13 test_airia_guardrail.py::test_r13_responses_pre_call_verdict_matrix[httpx-NONE] PASS PASS FAIL
R13 test_airia_guardrail.py::test_r13_responses_pre_call_verdict_matrix[openai_async-REDACT] PASS PASS FAIL
R13 test_airia_guardrail.py::test_r13_responses_pre_call_verdict_matrix[openai_sync-BLOCK] PASS PASS FAIL
R14 test_airia_guardrail.py::test_r14_nonstream_responses_post_call_redaction PASS PASS FAIL
R14 test_airia_guardrail.py::test_r14_streamed_responses_post_call_redaction_has_no_leak SKIP SKIP SKIP
R15 test_airia_guardrail.py::test_r15_default_on_and_default_off_are_distinct PASS PASS FAIL
R16 test_airia_guardrail.py::test_r16_key_attached_guardrail_runs_without_request_field PASS PASS FAIL
R17 test_airia_guardrail.py::test_r17_both_mode_runs_exactly_once_per_phase PASS PASS FAIL
R18 test_airia_guardrail.py::test_r18_during_call_is_skipped_and_request_is_unchecked PASS PASS PASS
R19 test_airia_guardrail.py::test_r19_logging_only_records_block_but_does_not_reject PASS PASS FAIL
R20 test_airia_guardrail.py::test_r20_missing_credentials_skip_and_environment_fallback_works PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-401] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-403] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-404] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-500] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-INVALIDJSON] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-NO-ACTION] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-NOFIELD] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-NONLIST-TEXTS] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-NONOBJECT-LIST] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-NONOBJECT-NULL] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-NONOBJECT-STRING] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-UNKNOWN-ACTION] PASS PASS FAIL
R21 test_airia_guardrail.py::test_r21_malformed_or_failed_verdicts_fail_closed_and_proxy_recovers[AIRIA-WRONGLEN] PASS PASS FAIL
R22 test_airia_guardrail.py::test_r22_transport_and_timeout_fail_closed PASS PASS FAIL
R23 test_airia_guardrail.py::test_r23_hostile_inputs_are_forwarded_without_proxy_crash[5kb] PASS PASS FAIL
R23 test_airia_guardrail.py::test_r23_hostile_inputs_are_forwarded_without_proxy_crash[empty] PASS PASS FAIL
R23 test_airia_guardrail.py::test_r23_hostile_inputs_are_forwarded_without_proxy_crash[image_part] PASS PASS FAIL
R23 test_airia_guardrail.py::test_r23_hostile_inputs_are_forwarded_without_proxy_crash[text_part] PASS PASS FAIL
R23 test_airia_guardrail.py::test_r23_repeated_identical_requests_each_reach_airia_once PASS PASS FAIL
R24 test_airia_guardrail.py::test_r24_caller_master_key_never_reaches_airia PASS PASS FAIL
R25 test_airia_guardrail.py::test_r25_tool_only_post_call_is_still_guarded PASS PASS FAIL
R26 test_airia_guardrail.py::test_r26_streamed_tool_call_deltas_do_not_leak_before_post_verdict SKIP SKIP SKIP
R27 test_airia_guardrail.py::test_r27_management_and_ui_surfaces_include_airia PASS PASS FAIL
R28 test_airia_guardrail.py::test_r28_existing_generic_guardrail_control_still_fires PASS PASS PASS
C1 test_airia_guardrail_chaos.py::test_c1_sink_outage_mid_mixed_endpoint_burst_fails_closed_and_recovers PASS PASS FAIL
C2 test_airia_guardrail_chaos.py::test_c2_slow_sink_is_bounded_and_proxy_recovers PASS PASS FAIL
C3 test_airia_guardrail_chaos.py::test_c3_surviving_worker_keeps_enforcing_airia PASS PASS FAIL
C4 test_airia_guardrail_chaos.py::test_c4_owned_proxy_restart_resumes_airia_enforcement PASS PASS FAIL

Type

🆕 New Feature
✅ Test

Caveats (if any)

High

  • Streamed /v1/messages post_call redaction never reaches the client (R12)
    • The caller sees the original email; Airia also receives model: null
    • The streaming rewrite path only supports chat routes, so it falls back to block only
  • Streamed /v1/responses post_call leaks the original text in deltas (R14)
    • Only the final response.completed event is redacted
  • Streamed tool-call arguments reach the client before the post_call block (R26)

Medium

  • A missing key or mode: during_call boots the proxy without Airia
    • The docs PR says startup is refused; it is not
  • unreachable_fallback: fail_open is ignored; Airia always fails closed
  • No guardrail garden card, preset or logo yet; needs an Airia logo

Low

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/7749cfdda367473a8133a29ac0415803
Open in Devin Desktop: https://app.devin.ai/desktop/session/7749cfdda367473a8133a29ac0415803?variant=devin
Requested by: @yucheng-berri

airia-open-source and others added 5 commits September 23, 2026 15:23
Adds `airia` as a built-in guardrail provider. The hook sends the prompt under `pre_call` and the
model's response under `post_call` to the Airia AI Gateway, which answers allow, block, or redacted
content; the only configuration is `api_base`, `api_key`, and an optional `timeout`.

`during_call` is not offered: it runs concurrently with the model call, so a block could land after
the prompt has already reached the provider.

A BLOCKED verdict, or any action this version does not recognise, raises with
`blocked_content=True`; a transport error, a non-2xx, or a response body that is not a JSON object
raises with `blocked_content=False`, so callers can tell "could not evaluate" from "evaluated and
blocked" while both still fail closed. On GUARDRAIL_INTERVENED a copy of the inputs is returned with
every rewritten field substituted; an intervention carrying no applicable rewrite, a rewrite of the
wrong shape, or a rewrite whose item count does not match what was sent (which would misalign the
positional write-back downstream) blocks instead of letting the original through.

Streamed responses are moderated whole and then emitted redacted: the hook opts into the unified
hook's `incremental_diff` mode (the default `block_only` drops rewrites) with
`streaming_end_of_stream_only`, so a redaction can never span transform rounds and underflow. Covered
by a test that drives the real streaming iterator hook end to end and checks the assembled client
output, not just the guardrail's own config attributes.

The HTTP client is dependency-injected (an `async_handler` constructor parameter, defaulting to a
real client) rather than mutated post-construction, so tests inject a fake client instead of
monkeypatching an instance method.

Known limitation, not fixable from this guardrail alone: LiteLLM's unified guardrail framework
yields a tool-call chunk to the client immediately during `incremental_diff` streaming, inspecting
the assembled tool calls against the guardrail's verdict only once the stream ends
(`_run_incremental_transform_stream` in `unified_guardrail.py`). A blocked tool call can therefore
reach an executing client before the terminal error arrives. Switching to `block_only` with
`streaming_buffer_until_moderated` does not close this without a regression: that flag has no effect
under `incremental_diff` (the mode branches away before it is read), and `block_only` itself replays
the original, unredacted chunks on release, silently dropping any REDACT verdict on streamed text.
Relatedly, LiteLLM's OpenAI chat completions guardrail translation skips `post_call` guardrails
entirely for a response with no text content, so a tool-call-only response bypasses this guardrail
(and every other post-call guardrail) even outside streaming. Both are shared-framework behavior,
not specific to Airia; fixing either is out of scope for an isolated single-guardrail contribution.
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>
Co-authored-by: Airia Open Source <opensource@airia.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@codspeed

codspeed Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_airia_guardrail (75d2ded) with main (39d14bd)

Open in CodSpeed

@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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

Copy link
Copy Markdown
Contributor

Hi @airia-open-source, can you send me the relevant longstanding credentials e.g api key for airia guardrail through email so that I can verify this PR e2e? Also we require them for maintaining the integration going forward.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants