Skip to content

fix(guardrails): make pii mask real on rerank/images/audio; scan+mask audio transcripts - #703

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/issue-696-pii-mask-parity
Jul 2, 2026
Merged

fix(guardrails): make pii mask real on rerank/images/audio; scan+mask audio transcripts#703
jarvis9443 merged 2 commits into
mainfrom
fix/issue-696-pii-mask-parity

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Audit finding #696 (parent: api7/AISIX-Cloud#950). A kind: pii detector configured with the mask action was a silent no-op on /v1/rerank, /v1/images/generations and /v1/audio/*: check_input ran (so block worked), but no redact_* call existed on these handlers, so the raw matched text was forwarded upstream with no error and no counts. The audio transcript response additionally bypassed output guardrails entirely — an output block/mask enforced on /v1/chat/completions was escapable by transcribing audio.

Fix

  • redact.rs: four new helpers — redact_rerank_request (query + documents[], plain-string and {text} shapes), redact_images_request (prompt), redact_speech_request (input), redact_transcription_response (JSON text + segments[].text, or the raw text/srt/vtt body).
  • rerank / images / speech: mask in place after the input block check (same #932 ordering as embeddings), counts threaded onto the emitted UsageEvent's redacted_entity_counts.
  • transcriptions / translations: the multipart prompt form field (caller text forwarded verbatim, previously unscanned) now runs the input chain — block + mask; the transcript response runs the output chain — a block returns the standard 422 content_filter envelope while keeping the billed usage marked guardrail_blocked (the completions #911 [23] convention: the upstream already charged for the transcription), and mask rewrites the transcript before the caller sees it. applied_guardrails is now recorded on these events (refactor: #302 Phase A pure clean cut — delete Provider enum + with_name + per-vendor consts #379 parity).
  • The transcription/translation handlers now log/emit the actual response status (the billed-then-blocked path is a 422, previously hardcoded 200).

LiteLLM baseline: presidio-based PII masking in LiteLLM applies through pre/post-call hooks across call types (embedding, transcription, image generation included), so enforcing mask semantics on these endpoints matches the reference behavior.

Tests

  • Handler-level (wiremock, real router): masked text asserted on the upstream-received body for rerank (query + both document shapes) / images / speech / transcription-prompt-field; transcript output mask asserted on the client response; prompt-field block (422, upstream never contacted); output block keeps billed tokens + guardrail_blocked on the UsageEvent. Verified fail-before/pass-after (wiring removed → test fails).
  • Unit tests for the four new redact helpers.

Fixes #696

Summary by CodeRabbit

  • New Features

    • Added PII masking support for audio, image, and rerank requests.
    • Audio transcription and translation responses can now be filtered when guardrails block content.
    • Audio and image usage reporting now includes redaction counts.
  • Bug Fixes

    • Audio endpoints now report the correct upstream HTTP status instead of always showing success.
    • Transcription responses now handle both structured and plain-text formats more reliably.

… audio transcripts

A mask-action pii detector was a silent no-op on /v1/rerank,
/v1/images/generations and /v1/audio/* — check_input ran (block worked)
but no redact_* call existed, so the raw text was forwarded upstream.
The audio transcript response additionally bypassed output guardrails
entirely: an output block/mask enforced on chat was escapable by
transcribing audio.

- redact.rs: redact_rerank_request (query + documents[], string or
  {text} shapes), redact_images_request (prompt), redact_speech_request
  (input), redact_transcription_response (JSON text/segments[].text or
  raw text/srt/vtt body)
- rerank/images/speech: mask after the input block check, counts
  threaded onto the emitted UsageEvent (redacted_entity_counts)
- transcriptions/translations: the multipart `prompt` form field (caller
  text forwarded verbatim) now runs the input guardrail chain (block +
  mask); the transcript response runs the output chain — a block returns
  the 422 envelope but keeps the billed usage marked guardrail_blocked
  (completions #911 [23] convention); mask rewrites text/segments before
  the caller sees it; applied_guardrails now recorded (#379 parity)
- transcription/translation handlers log/emit the actual response status
  (the blocked path is a 422, not a hardcoded 200)

Fixes #696
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9c92a8b1-2cb7-4d61-bd4c-b9a42ea5e46d

📥 Commits

Reviewing files that changed from the base of the PR and between 9308d7f and b204870.

📒 Files selected for processing (1)
  • crates/aisix-proxy/src/audio.rs
📝 Walkthrough

Walkthrough

This PR adds PII mask-action redaction to /v1/rerank, /v1/images/generations, and /v1/audio endpoints (previously silent no-ops), adds output guardrail scanning and redaction for audio transcription/translation responses, and extends UsageEvent telemetry with redaction counts and guardrail-blocked status across these handlers.

Changes

PII mask redaction and guardrail telemetry across endpoints

Layer / File(s) Summary
Shared redaction helper functions
crates/aisix-proxy/src/redact.rs
Adds redact_rerank_request, redact_images_request, redact_speech_request, and redact_transcription_response helpers that mask request fields or transcription response text, with new unit tests.
Audio transcription/translation input and output guardrail scanning
crates/aisix-proxy/src/audio.rs
Adds redactions/guardrail_blocked to AudioDispatchSuccess, scans and masks multipart prompt input, adds output guardrail enforcement and transcript redaction post-upstream, adds transcription_output_text helper, derives status from upstream response instead of hardcoding 200, and updates telemetry/tests.
Audio speech input redaction and telemetry
crates/aisix-proxy/src/audio.rs
Applies redact_speech_request to the TTS input field before forwarding upstream and passes redaction counts and guardrail_blocked into emit_usage_event.
Images generation prompt redaction and telemetry
crates/aisix-proxy/src/images.rs
Adds redactions to ImageDispatchSuccess, masks the prompt field via redact_images_request, refactors dispatch to own the body and validate model, and extends emit_usage_event with redaction counts.
Rerank query/document redaction and telemetry
crates/aisix-proxy/src/rerank.rs
Adds redactions to RerankDispatchSuccess, masks query and documents[] via redact_rerank_request, and extends emit_usage_event with redaction counts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AudioHandler
  participant GuardrailChain
  participant RedactModule
  participant Upstream

  Client->>AudioHandler: multipart request with prompt
  AudioHandler->>GuardrailChain: run input guardrails on prompt
  GuardrailChain-->>AudioHandler: block or mask result
  alt blocked
    AudioHandler-->>Client: ContentFiltered response
  else masked
    AudioHandler->>RedactModule: rewrite prompt field, accumulate counts
    AudioHandler->>Upstream: forward masked request
    Upstream-->>AudioHandler: transcription response
    AudioHandler->>GuardrailChain: run output guardrails on transcript text
    alt output blocked
      AudioHandler-->>Client: ContentFiltered, guardrail_blocked=true
    else
      AudioHandler->>RedactModule: redact_transcription_response, merge counts
      AudioHandler-->>Client: response with status and telemetry
    end
  end
Loading

Possibly related PRs

  • api7/aisix#647: Both PRs modify the same emit_usage_event telemetry emission in audio.rs, images.rs, and rerank.rs, adding new fields to UsageEvent.
  • api7/aisix#650: Both PRs modify audio.rs and rerank.rs to change UsageEvent/telemetry emission logic for request outcomes.
  • api7/aisix#584: The guardrail-block handling and ContentFiltered error construction used in this PR builds on guardrail verdict attribution changes from the referenced PR.

Estimated code review effort: 4 (Complex) | ~60 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security Check ❌ Error Audio multipart prompt only scans the first field, and transcript output scanning ignores segments[].text, letting PII bypass block/redaction. Aggregate all prompt parts before guardrail check/redaction, and build transcript scan text from top-level text plus every segments[].text.
E2e Test Quality Review ⚠️ Warning audio.rs still scans only the first multipart prompt and only top-level transcript text, so later prompts and verbose_json segments can bypass guardrails; tests miss both cases. Update the helper to aggregate all prompt fields and transcript segment text, then add e2e cases for multi-prompt multipart bodies and verbose_json/raw response formats.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main guardrail masking and transcript scanning changes in rerank, images, and audio.
Linked Issues check ✅ Passed The PR implements the #696 masking parity fix and audio transcript output scanning with usage redaction counts as requested.
Out of Scope Changes check ✅ Passed The changes stay focused on guardrail masking, transcript scanning, telemetry, and tests for the linked issue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-696-pii-mask-parity

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
crates/aisix-proxy/src/redact.rs (1)

457-490: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

words[].word not covered when verbose_json + word-level timestamps are requested.

This masks text and segments[].text, but OpenAI's verbose_json format with timestamp_granularities: ["word"] adds a top-level words[] array of {word, start, end} objects carrying the transcript as individual word tokens. If a PII match happens to align with a single word (e.g. an email rendered as one token), it can reach the caller unmasked via words[] even with a mask-action output guardrail configured — the same silent-bypass class #696 is fixing, just on a field this helper doesn't touch.

Since this mirrors the same scope as transcription_output_text (the block-check helper in audio.rs, which also only reads top-level text), the gap may be a pre-existing/accepted scope limit for word-granularity requests specifically — worth confirming whether this proxy forwards timestamp_granularities to upstream at all.

♻️ Possible extension to also mask word-level tokens
         if let Some(Value::Array(segments)) = json.get_mut("segments") {
             for seg in segments {
                 if let Some(text) = seg.get_mut("text") {
                     apply_to_value_string(chain, Direction::Output, text, &mut counts);
                 }
             }
         }
+        if let Some(Value::Array(words)) = json.get_mut("words") {
+            for w in words {
+                if let Some(word) = w.get_mut("word") {
+                    apply_to_value_string(chain, Direction::Output, word, &mut counts);
+                }
+            }
+        }

Can you confirm whether timestamp_granularities=["word"] is exposed/forwarded on this proxy's transcription path? If so, this gap should be closed alongside it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aisix-proxy/src/redact.rs` around lines 457 - 490, The transcription
response redaction in redact_transcription_response only handles top-level text
and segments[].text, so verbose_json responses with words[].word can bypass
output masking. Update this helper to also traverse and redact any words array
entries (using apply_to_value_string on each word field) before serializing the
JSON, and verify the transcription path that uses transcription_output_text /
audio.rs forwards or supports timestamp_granularities=["word"] so the new
coverage matches the exposed response shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aisix-proxy/src/audio.rs`:
- Around line 689-701: The output-guardrail scan in transcription_output_text
only checks the top-level JSON text field, so verbose_json responses with
blocked content inside segments[].text can slip through. Update
transcription_output_text to also collect and scan the text from each entry in
segments[] (while preserving the current raw-body fallback for plain-text
formats), and make sure redact_transcription_response still rewrites the same
fields used for scanning.
- Around line 430-469: The multipart handling in audio.rs only inspects the
first prompt field, so update the logic around prompt_text and the Guardrail
check in the audio request parser to process every multipart field named
"prompt" instead of stopping at the first match. Iterate through all prompt
entries, run aisix_guardrails::Guardrail::check_input on each, and apply
aisix_guardrails::Guardrail::redact_input_text to each prompt field when
redaction is enabled so no later blocked or sensitive prompt bypasses filtering.

In `@crates/aisix-proxy/src/images.rs`:
- Around line 233-237: The redaction counts computed in `redact_images_request`
are being lost on later error paths, so `emit_error_usage_event` ends up with
default `redacted_entity_counts` after quota or upstream failures. Update the
`images.rs` request flow so the `redactions` value is carried through the
post-redaction error path (for example via `ProxyError` or a dispatch error
wrapper) and reused when emitting error usage events. Make sure the affected
error handling around the upstream dispatch/quotas, including the later error
branch near the `emit_error_usage_event` call, preserves the counts for masked
prompts.

In `@crates/aisix-proxy/src/rerank.rs`:
- Around line 248-252: The rerank request error path is dropping post-redaction
telemetry because failures after `redact_rerank_request` do not carry
`redactions` into the `UsageEvent`. Update the `dispatch` flow in `rerank.rs` so
any downstream error returned from forwarding preserves the redaction metadata,
either by wrapping the dispatch error with the `redactions` value or by emitting
the error `UsageEvent` inside `dispatch` before returning. Use the existing
`redact_rerank_request`, `dispatch`, and `UsageEvent` sites to ensure
`redacted_entity_counts` is populated for upstream 4xx/5xx failures as well as
success paths.

---

Nitpick comments:
In `@crates/aisix-proxy/src/redact.rs`:
- Around line 457-490: The transcription response redaction in
redact_transcription_response only handles top-level text and segments[].text,
so verbose_json responses with words[].word can bypass output masking. Update
this helper to also traverse and redact any words array entries (using
apply_to_value_string on each word field) before serializing the JSON, and
verify the transcription path that uses transcription_output_text / audio.rs
forwards or supports timestamp_granularities=["word"] so the new coverage
matches the exposed response shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb83064d-aad9-4569-9a76-d35d2932b8cf

📥 Commits

Reviewing files that changed from the base of the PR and between cb1bcd0 and 9308d7f.

📒 Files selected for processing (4)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/redact.rs
  • crates/aisix-proxy/src/rerank.rs

Comment thread crates/aisix-proxy/src/audio.rs Outdated
Comment thread crates/aisix-proxy/src/audio.rs
Comment thread crates/aisix-proxy/src/images.rs
Comment thread crates/aisix-proxy/src/rerank.rs
… transcript scan

CodeRabbit on #703: (1) a multipart body with an empty first prompt
field and a later PII-carrying one skipped both scan and mask — scan all
prompt fields and run the mask loop independently of the scan gate;
(2) transcription_output_text now also collects segments[].text so a
verbose_json response carrying text only in segments can't bypass the
output check (redact already covered segments).
@jarvis9443
jarvis9443 merged commit 81800db into main Jul 2, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the fix/issue-696-pii-mask-parity branch July 2, 2026 13:55
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.

pii mask action is a silent no-op on /v1/rerank, /v1/images and /v1/audio (and audio transcript output is never scanned)

1 participant