fix(guardrails): make pii mask real on rerank/images/audio; scan+mask audio transcripts - #703
Conversation
… 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
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis 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. ChangesPII mask redaction and guardrail telemetry across endpoints
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
Possibly related PRs
Estimated code review effort: 4 (Complex) | ~60 minutes Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
crates/aisix-proxy/src/redact.rs (1)
457-490: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
words[].wordnot covered whenverbose_json+ word-level timestamps are requested.This masks
textandsegments[].text, but OpenAI'sverbose_jsonformat withtimestamp_granularities: ["word"]adds a top-levelwords[]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 viawords[]even with amask-action output guardrail configured — the same silent-bypass class#696is fixing, just on a field this helper doesn't touch.Since this mirrors the same scope as
transcription_output_text(the block-check helper inaudio.rs, which also only reads top-leveltext), the gap may be a pre-existing/accepted scope limit for word-granularity requests specifically — worth confirming whether this proxy forwardstimestamp_granularitiesto 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
📒 Files selected for processing (4)
crates/aisix-proxy/src/audio.rscrates/aisix-proxy/src/images.rscrates/aisix-proxy/src/redact.rscrates/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).
Problem
Audit finding #696 (parent: api7/AISIX-Cloud#950). A
kind: piidetector configured with the mask action was a silent no-op on/v1/rerank,/v1/images/generationsand/v1/audio/*:check_inputran (so block worked), but noredact_*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/completionswas 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(JSONtext+segments[].text, or the rawtext/srt/vttbody).redacted_entity_counts.promptform 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 422content_filterenvelope while keeping the billed usage markedguardrail_blocked(the completions #911 [23] convention: the upstream already charged for the transcription), and mask rewrites the transcript before the caller sees it.applied_guardrailsis now recorded on these events (refactor: #302 Phase A pure clean cut — delete Provider enum + with_name + per-vendor consts #379 parity).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
guardrail_blockedon the UsageEvent. Verified fail-before/pass-after (wiring removed → test fails).Fixes #696
Summary by CodeRabbit
New Features
Bug Fixes