Skip to content

fix(guardrails): input guardrails on /v1/completions, /v1/rerank, /v1/images, /v1/audio/speech (#545) - #551

Merged
moonming merged 2 commits into
mainfrom
fix/issue-545-input-guardrails-more-surfaces
Jun 8, 2026
Merged

fix(guardrails): input guardrails on /v1/completions, /v1/rerank, /v1/images, /v1/audio/speech (#545)#551
moonming merged 2 commits into
mainfrom
fix/issue-545-input-guardrails-more-surfaces

Conversation

@moonming

@moonming moonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

Closes the input half of #545: four inbound surfaces carry user-controlled text but ran no guardrails, so a content/DLP block configured on /v1/chat/completions was bypassable by switching surface — the same content-safety bypass class as #719.

Surface Scanned field(s) Output hook?
/v1/completions prompt (string / array of strings; token-id arrays skipped) text output → follow-up (see below)
/v1/images/generations prompt no (returns an image)
/v1/rerank query + documents (strings or {text}) no (returns scores)
/v1/audio/speech input (text → TTS) no (returns audio)

/v1/audio/speech was not in the original #545 list — the independent audit surfaced it as a 4th surface in the same class.

How

Per surface, mirroring the /v1/embeddings pattern: resolve the per-request chain (RequestContext{model_id, api_key_id, team_id}), synthesize a ChatFormat from the surface's text fields (scan-only, never sent upstream), run check_input; BlockProxyError::ContentFiltered (422 content_filter). Matched-pattern detail stays in tracing only (#153) — the wire message is generic.

The check runs before quota::enforce, so a content-policy refusal doesn't burn a rate-limit slot (matching /v1/chat/completions, and avoiding the #542 ordering issue on these new surfaces from the start).

Exemption (documented)

/v1/messages/count_tokens is intentionally exempt: it's a pre-flight sizing call that never reaches a model and returns only an integer token count — there's nothing to moderate on either hook, and the same messages payload is scanned on the actual /v1/messages request. Documented in count_tokens.rs.

Follow-up (not in this PR)

/v1/completions also returns generated choices[].text, so it warrants an output guardrail too (the analog of the /v1/responses output work in #544). Out of scope here to keep the diff focused on the demonstrated input bypass; will file/track separately.

Reference

In-repo reference pattern: /v1/embeddings (embeddings.rs) input check. Upstream field shapes: OpenAI completions, images, audio/speech; rerank follows the Cohere/Jina query+documents shape.

Test plan

Per-surface wiremock tests: blocked input → 422 + error.type == content_filter with the upstream never contacted (expect(0)); benign input still forwards (expect(1)) → 200. /v1/rerank covers both the query and documents channels. The blocked literal must not leak into the wire message. cargo fmt + clippy clean; 401 aisix-proxy lib tests pass.

Independent audit (CLAUDE.md §8) — findings + resolution

A fresh agent reviewed this PR cold and returned MERGE. It verified the core contract: each surface scans the right text, the check runs before quota::enforce (matching chat — no burned slot, no #542 issue), the forwarded body is unchanged, the no-guardrail path is a pure no-op, #153 holds, and the tests prove block (422 + expect(0)) and benign (expect(1) + 200).

Merge gate: no HIGH; the MEDIUM is justified + tracked; LOWs addressed.

Refs #545, #719, #554, #555.

Summary by CodeRabbit

Release Notes

New Features

  • Input content guardrails are now enforced across audio generation, text completions, image generation, and rerank endpoints. Requests with inappropriate content are blocked with a 422 response before reaching upstream providers.

… /v1/images, /v1/audio/speech (#545)

These four inbound surfaces carry user-controlled text but ran NO guardrails,
so a content/DLP block configured on /v1/chat/completions was bypassable by
switching surface — the same class as #719. Add the input guardrail check
(mirroring /v1/embeddings): resolve the per-request chain, synthesize a
ChatFormat from the surface's text fields, run check_input, Block -> 422
content_filter. The check runs BEFORE quota::enforce so a content-policy
refusal doesn't burn a rate-limit slot (matching /v1/chat/completions, and
avoiding the #542 ordering issue on these new surfaces).

- completions: `prompt` (string | array of strings; token-id arrays skipped)
- images/generations: `prompt`
- rerank: `query` + `documents` (strings or `{text}` objects)
- audio/speech: `input` (text synthesized to audio)

Matched-pattern detail stays in ops logs only (#153). Output is not scannable
text on images (image)/rerank (scores)/speech (audio) — no output hook.

/v1/messages/count_tokens is intentionally EXEMPT (documented): it's a
pre-flight sizing call that never reaches a model and returns only an integer
token count; the same `messages` payload is scanned on the actual /v1/messages
request.

Follow-up (not in this PR): /v1/completions also returns generated `choices[].text`,
so it warrants an OUTPUT guardrail too (the analog of the /v1/responses output
work) — tracked separately.

Tests: per-surface wiremock tests — blocked input -> 422 content_filter with the
upstream never contacted (expect(0)); benign input still forwards (expect(1)) ->
200. rerank covers both the query and documents channels. fmt + clippy clean;
401 aisix-proxy lib tests pass.
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 018d9440-1feb-41bc-8b02-014144d8c67d

📥 Commits

Reviewing files that changed from the base of the PR and between a494cd5 and 9291788.

📒 Files selected for processing (5)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs

📝 Walkthrough

Walkthrough

This PR adds input guardrail enforcement (#545) to four API endpoints (audio, completions, images, rerank) by converting endpoint-specific inputs to ChatFormat, running guardrail checks before quota/rate-limit reservation, and rejecting blocked content with 422 responses. It also documents why count_tokens is exempt from this guardrail chain.

Changes

Guardrail enforcement across endpoints

Layer / File(s) Summary
Audio endpoint guardrail enforcement
crates/aisix-proxy/src/audio.rs
Converts /v1/audio/speech input field to ChatFormat, checks guardrails in speech_dispatch before quota reservation, returns 422 on block. Tests verify blocked input is rejected and benign input forwards to upstream.
Completions endpoint guardrail enforcement
crates/aisix-proxy/src/completions.rs
Converts legacy /v1/completions prompt (string or string array) to ChatFormat, checks guardrails in dispatch before quota reservation, returns 422 on block. Tests verify blocked prompts are rejected and benign prompts forward to upstream.
Images endpoint guardrail enforcement
crates/aisix-proxy/src/images.rs
Converts /v1/images/generations prompt to ChatFormat, checks guardrails in dispatch before quota reservation, returns 422 on block with no pattern details in error. Tests verify blocked prompts are rejected and benign prompts forward to upstream.
Rerank endpoint guardrail enforcement
crates/aisix-proxy/src/rerank.rs
Converts rerank query and documents (string or { "text": ... }) to ChatFormat, checks guardrails in dispatch before quota reservation, returns 422 on block. Adds test import and tests verify blocked query/documents are rejected and benign input forwards to upstream.
Count tokens guardrail exemption documentation
crates/aisix-proxy/src/count_tokens.rs
Documents that /v1/messages/count_tokens is exempt from content-moderation guardrails as it is a pre-flight call forwarding to the provider for token counting.

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

… count_tokens doc (#545)

Independent audit of #551 (MERGE):
- LOW-1: rerank block tests now assert the matched literal is absent from the
  wire message (#153 parity with the other surfaces).
- LOW-2: add a benign-with-guardrail rerank test (guardrail present, clean
  query/documents -> expect(1) + 200).
- MEDIUM-1: scope the count_tokens guardrail-exemption doc to *content
  moderation*; note the DLP/egress nuance (messages forwarded to the provider
  count endpoint) is tracked in #555, out of scope for #545.

Deferred completions output guardrail tracked in #554. fmt + clippy clean;
20 input_guardrail tests pass.
@moonming
moonming merged commit fb1d79c into main Jun 8, 2026
11 of 13 checks passed
@moonming
moonming deleted the fix/issue-545-input-guardrails-more-surfaces branch June 8, 2026 07:08
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.

1 participant