Skip to content

feat(guardrails): azure_content_safety_text_moderation — config + schema + dispatcher (P2 of #379) - #461

Merged
moonming merged 6 commits into
mainfrom
feat/p2-text-moderation
Jun 1, 2026
Merged

feat(guardrails): azure_content_safety_text_moderation — config + schema + dispatcher (P2 of #379)#461
moonming merged 6 commits into
mainfrom
feat/p2-text-moderation

Conversation

@moonming

@moonming moonming commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Status: draft. DP runtime for P2 text moderation (api7/AISIX-Cloud#379). Pairs with the CP slice in api7/AISIX-Cloud#602.

What this adds

Config + schema + dispatcher (input + non-streaming output):

  • GuardrailKind::AzureContentSafetyTextModeration + AzureContentSafetyTextModerationConfig (aisix-core). Reuses the P1 connection block (endpoint + api_key + timeout_ms) and adds the text:analyze moderation params + streaming controls; serde defaults match the cp-api validator.
  • schema::validate_guardrail: kind enum + per-kind oneOf branch (closes guardrails DP: accept kind=azure_content_safety_text_moderation (PR2 of #379) #459).
  • text_moderation.rs (TextModerationGuardrail): text:analyze dispatch, per-category severity (override → general → default 2), blocklist, 10k chunking, text_source-aware input collection, OUTPUT-direction fail-closed.
  • build.rs wires the kind (feature-gated azure-content-safety).

Streaming hold-back (build_sse_stream):

  • New Guardrail::stream_output_policy()EndOfStreamCheck (default) | Window { size_chars, overlap_chars } | BufferFull { max_buffer_bytes, on_exceeded_fail_open }. GuardrailChain folds members to the strictest. azure_content_safety_text_moderation returns Window/BufferFull from config.
  • build_sse_stream consumes it: hold-back policies buffer content and release a window (or the whole response) only after a clean scan; on Block emit only the content_filter frame; held unscanned content (Block or upstream error) never reaches the wire. The EndOfStreamCheck path (keyword / prompt_shield / no guardrail) is preserved byte-for-byte.

Scope / dependencies

Independent self-audit (cold) — merge gate

Core guarantee independently verified: blocked / errored hold-back streams leak no content; EndOfStreamCheck unchanged; borrow/async-safe; clippy clean.

  • H1 (mid-stream Window + BufferFull paths were untested) → fixed: 4 new wire-level tests (mid-stream block, multi-window clean release, BufferFull cap fail-closed + fail-open).
  • M1 (overlap ≥ window → per-token rescan) → fixed: overlap clamped to cc-1.
  • M2 (ttft_ms records first-upstream not first-delivered under hold-back) → deferred + tracked at build_sse_stream: ttft_ms misreports under streaming hold-back (first-upstream, not first-delivered) #467 (observability, not correctness/security).
  • L1 (window boundary detection bounded by overlap_chars) → inherent windowing limitation; operators wanting zero boundary-evasion risk use buffer_full.

Tests

aisix-core + aisix-guardrails unit suites; aisix-proxy 356 incl. wire-level streaming hold-back (block leaks nothing + content_filter + no [DONE]; clean releases + [DONE]; mid-stream Window; BufferFull cap fail-open/closed). clippy --all-targets clean under default features (AWS SDK + bedrock arm). Real-Azure / dashboard e2e land in later slices.

Summary by CodeRabbit

  • New Features

    • Added Azure Content Safety text moderation guardrail with category-based filtering and severity thresholds.
    • Added streaming output moderation policies: end-of-stream, sliding window, and full buffering modes with configurable overflow behavior.
    • Text content is automatically chunked for efficient processing.
  • Tests

    • Added comprehensive test coverage for streaming moderation scenarios and policy handling.

…+ schema + dispatcher (P2 of #379)

DP runtime for P2 text moderation — PR2, increment 1 of 2:

- New GuardrailKind::AzureContentSafetyTextModeration variant +
  AzureContentSafetyTextModerationConfig (aisix-core). Reuses the P1
  connection block (endpoint + api_key + timeout_ms) and adds the
  text:analyze moderation params (output_type, categories,
  severity_threshold[_by_category], blocklist_names, halt_on_blocklist_hit,
  text_source) + streaming controls (stream_processing_mode, window_size,
  window_overlap_size, max_buffer_bytes, on_buffer_exceeded,
  output_fail_open). serde defaults match the cp-api validator so a minimal
  projected row still moderates correctly.
- schema::validate_guardrail: add the kind to the top-level enum + a
  per-kind oneOf branch (closes the schema gate, #459).
- text_moderation.rs (TextModerationGuardrail): text:analyze dispatch,
  per-category severity resolution (override -> general -> default 2),
  blocklist match, 10k-char chunking, text_source-aware input collection,
  and OUTPUT-direction fail-closed by default (an Azure outage can't release
  unscanned model output). Forks the P1 transport (chunk_text / AcsFailure /
  fail-open mapping); shared-module extraction tracked as a follow-up so this
  slice doesn't touch the shipped P1 dispatcher.
- build.rs wires the new kind through build_one (input + non-streaming
  output), feature-gated under azure-content-safety like P1.

Deferred to increment 2: the build_sse_stream window/buffer_full streaming
restructure in aisix-proxy.

Tests: aisix-core 186 + aisix-guardrails 87 pass; clippy clean under default
features (incl. the bedrock arm). Depends on the cp-api projection in
api7/AISIX-Cloud#602.
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 19c0ae5a-2991-4702-9fd5-01191ca50f79

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd3d25 and 3e70bb2.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/chain.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-guardrails/src/text_moderation.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/lib.rs
  • schemas/resources/guardrail.schema.json

📝 Walkthrough

Walkthrough

This PR introduces Azure Content Safety text moderation for streaming chat completions. It adds a new StreamOutputPolicy abstraction enabling guardrails to declare streaming moderation behavior (end-of-stream, sliding window, or full buffering). The TextModerationGuardrail implementation chunks text, submits it to Azure's text:analyze API, applies per-category severity thresholds and blocklist matching, and maps failures to bypass or blocking verdicts. The proxy's SSE handler extracts the stream policy from the guardrail chain and enforces hold-back semantics: withholds SSE events during window scanning or buffer checks, releases them only on non-blocking verdicts, or emits error frames on policy violations.

Changes

Azure Content Safety Text Moderation with Streaming

Layer / File(s) Summary
Streaming Output Policy Abstraction
crates/aisix-guardrails/src/lib.rs
StreamOutputPolicy enum (EndOfStreamCheck, Window, BufferFull) and stream_output_policy() trait method enable guardrails to declare streaming hold-back requirements; stricter() combines policies for chain-level consistency.
Configuration Model and Schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, crates/aisix-core/src/models/schema.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyTextModerationConfig struct with serde defaults for moderation categories, thresholds, blocklist controls, text source, and streaming mode; JSON schema validation for required endpoint/api_key and all configurable properties.
TextModerationGuardrail Implementation
crates/aisix-guardrails/src/text_moderation.rs
Azure text:analyze dispatcher: chunks text to 10k chars, applies per-category severity and blocklist thresholds, handles timeout/5xx/throttling failures via fail-open/fail-closed mapping, selects messages by text_source, exposes stream policy; includes chunking, threshold, blocklist, fail-open, message filtering, and wiremock integration tests.
Builder and Chain Integration
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/chain.rs
Build.rs dispatches AzureContentSafetyTextModeration to TextModerationGuardrail; chain.rs aggregates individual stream policies via stricter fold across guardrails.
Proxy SSE Streaming with Hold-Back Policies
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/lib.rs
build_sse_stream extracts stream_policy and introduces hold-back state: Window mode accumulates and scans at size_chars, BufferFull enforces max_buffer_bytes; SSE events are withheld during policy checks, released on Allow/Bypass, or blocked with error frames; end-of-stream respects hold-back. Tests verify content_filter error on harmful text, clean content released with [DONE], window/buffer modes enforce policies.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 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.

@moonming

moonming commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

PR2 (DP) — independent §7 audit

Independent cold review of the DP slice (commit ed72c4f). Verdict: APPROVE — no HIGH or MEDIUM findings.

Verified (not just claimed):

  • CP↔DP wire contract (the key risk) — clean, field-by-field against api7/AISIX-Cloud#602. All 16 inner config field names match snake-for-snake; the new variant rides the same #[serde(flatten)] + tag="kind" path as P1, so outer fields (name/enabled/fail_open/…) don't trip the inner struct's deny_unknown_fields; an explicit 0 (severity / window_overlap) survives via Go *int → Rust #[serde(default)].
  • oneOf mutual exclusivity — a azure_content_safety_text_moderation payload matches exactly one branch; a P1 azure_content_safety payload still matches only its own (the kind const disambiguates).
  • Securityapi_key is never logged; the Block reason carries category + severity but is redacted to a generic message on all three surfacing paths (input / non-streaming output / streaming output), so the harm taxonomy doesn't leak to the client.
  • Fail policycheck_output uses output_fail_open (default false = fail-closed); an Azure outage can't release unscanned model output.
  • No breaking changes; build.rs match is exhaustive across the azure-content-safety flag; P1 prompt_shield.rs is untouched. aisix-core + aisix-guardrails tests pass; clippy clean under default features.
Sev Finding Disposition
LOW-1 azure oneOf branches don't set additionalProperties:false (schema laxer than the struct) Defense-in-depth: the inner struct's deny_unknown_fields already rejects stray keys at deserialize; pre-existing for the P1 branch. Cleanup, not addressed here to avoid touching P1.
LOW-2 explicit categories: [] → DP defaults to all four Benign (Azure treats omitted categories as analyze-all). Informational.
LOW-3 multimodal text in content_blocks is not moderated (only flat content is read) Pre-existing across the codebase incl. P1; tracked at #465.
test gap no concatenate_all_content includes-assistant test Fixed in ed72c4f.

Merge gate: no HIGH/MEDIUM; the LOWs are pre-existing / benign / deferred. Streaming (build_sse_stream window/buffer_full — increment 2), real-Azure, and dashboard e2e land in later slices.

…hold-back policy (P2 of #379)

Adds the plumbing the SSE builder will consume to actually hold streamed
output back until it scans clean:

- StreamOutputPolicy enum (EndOfStreamCheck | Window | BufferFull) +
  Guardrail::stream_output_policy() (default EndOfStreamCheck = today's
  live-forward, no behavior change for keyword/prompt_shield).
- GuardrailChain folds members to the STRICTEST policy (any hold-back
  member → the whole stream holds back, full chain re-checks the held
  content).
- TextModerationGuardrail returns Window/BufferFull from its config.

Plumbing only; build_sse_stream consumes it in the next commit.
@moonming
moonming force-pushed the feat/p2-text-moderation branch from f1045ba to 08d23e4 Compare June 1, 2026 03:34
moonming added 2 commits June 1, 2026 12:01
…aming hold-back (P2 of #379)

Wires the per-guardrail streamed-output policy into the SSE builder so
azure_content_safety_text_moderation actually holds streamed content back
until it scans clean:

- EndOfStreamCheck (keyword / prompt_shield / no guardrail) path preserved
  byte-for-byte — content forwarded live, checked at end. The pre-existing
  leak for those kinds is tracked at #466.
- Window: buffer content, scan per window_size (carrying overlap_chars),
  release a window only after it scans clean; on Block emit only the
  content_filter frame.
- BufferFull: hold the whole response, scan once; max_buffer_bytes cap ->
  on_buffer_exceeded (fail-closed default blocks; fail-open releases held +
  forwards the rest live).
- On block (or upstream error), held unscanned content is dropped — the
  offending content never reaches the wire.

Tests: HTTP/SSE wire-level — blocked stream leaks nothing + content_filter
+ no [DONE]; clean stream releases content + [DONE]. Full aisix-proxy suite
(352) green incl. the unchanged EndOfStreamCheck guardrail test; clippy clean.
…erlap (PR2 self-audit H1/M1)

- H1: add streaming hold-back tests for the previously-untested novel
  paths — mid-stream Window block (no leak), multi-window clean release
  (overlap retention), and both BufferFull cap branches (fail-closed
  blocks; fail-open releases held + forwards rest live).
- M1: clamp the retained window overlap to cc-1 so a misconfigured
  overlap >= window can't keep the whole buffer and re-scan every
  subsequent token (cost/latency guard).
#379)

Fixes the 'schema drift (resources)' CI check — the new
AzureContentSafetyTextModerationConfig was missing from the committed
canonical schema. Generated by 'cargo run -p aisix-core --bin dump-schema'.
@moonming
moonming marked this pull request as ready for review June 1, 2026 07:17
@moonming
moonming merged commit 71bb76b into main Jun 1, 2026
7 of 8 checks passed
@jarvis9443
jarvis9443 deleted the feat/p2-text-moderation branch June 25, 2026 06:25
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.

guardrails DP: accept kind=azure_content_safety_text_moderation (PR2 of #379)

1 participant