feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799

Merged
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail
Jul 23, 2026
Merged

feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back#799
jarvis9443 merged 5 commits into
mainfrom
feat/1070-aliyun-ai-guardrail

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds guardrail kind aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, action MultiModalGuard). This is a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.

What it does

  • Same endpoint + signing, new action.MultiModalGuard on green-cip.<region>.aliyuncs.com (verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carries region/endpoint/access_key_id/access_key_secret/timeout_ms plus service_level: pro|basicpro (default) calls query_security_check_pro / response_security_check_pro, basic the non-Pro codes.
  • Suggestion-driven verdicts. The block decision follows the returned Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold. block → 422; pass/watch → release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData's S0–S3 vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.
  • mask honored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment so masked[i] stays aligned with texts[i] (MultiModalGuard takes one content string per call). mask with no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel) mask maps to Block so un-masked content is never released.
  • Streaming: windowed like the TMP kind; every window carries the stream-stable sessionIdandchatId so Aliyun stitches one console record per response. done is not sent (optional per Aliyun docs; the trait has no end-of-stream signal).
  • Diagnostics: upstream RequestId on every path (x-acs-request-id header first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); business Code 408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolic Code), and matched content (Ext, risk words) has no deserialized path into a log.

Testing

  • Unit (wiremock): suggestion mapping (pass/block/watch), blob-path mask→Block, segment mask write-back (single + multi-segment realign + string-encoded Ext + missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.
  • E2E (tests/e2e, real aisix + etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stable sessionId/chatId across windows, basic-tier service codes.
  • An --ignored live smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; the Ext.Desensitization accumulation assumption (doc-derived) is flagged there for live re-verification.

LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.

CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.

Fixes api7/AISIX-Cloud#1070

Summary by CodeRabbit

  • New Features
    • Added Aliyun AI Guardrails support for multimodal input and output moderation.
    • Added configurable service levels, regions, endpoints, timeouts, fail-open behavior, and streaming controls.
    • Added support for blocking, allowing, monitoring, and masking sensitive content.
    • Added streaming moderation with stable session tracking and buffered output handling.
  • Documentation
    • Updated the guardrail schema and API documentation labels for Aliyun AI Guardrails.
  • Tests
    • Added coverage for moderation verdicts, masking, streaming errors, routing, and service-level behavior.

… MultiModalGuard)
Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏)
— a different Aliyun product from the existing aliyun_text_moderation
(TextModerationPlus / Content Moderation): separately activated (commodity
lvwang_guardrail_public_cn), separately billed, policy-configured in its
own console, and — the point of the feature — its calls appear in that
console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070).
- Action=MultiModalGuard on the same green-cip endpoint and RPC v1
signature (signing helpers shared with the TextModerationPlus
dispatcher).
- Input hook: query_security_check_pro (query_security_check at
service_level: basic); output hook: response_security_check_pro /
response_security_check.
- Verdict follows the returned Data.Suggestion (computed by Aliyun from
the console-configured policy): block blocks; pass/watch/mask release.
No local risk threshold. Full per-dimension detail
(Type/Level/Suggestion, both level vocabularies incl. sensitiveData
S0–S3) lands in logs; only detected labels are logged, and matched
content (Ext) has no deserialized path to a log (#153).
- Streaming output: windowed like the TMP kind; each window carries the
stream-stable sessionId AND chatId so Aliyun stitches one console
record per response. done is not sent (optional per docs; the trait
has no end-of-stream signal).
- Code 408 (commodity not activated) logs a fix-naming error, the most
common first-run failure.
- Upstream RequestId diagnostics follow the #781 pattern
(x-acs-request-id header first, body fallback).
Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask
(Aliyun-side desensitization policies), following the Bedrock pattern:
- Segment path (non-streaming bodies, held-back tail of a streamed
response): moderates_segments()=true; a mask suggestion rewrites the
body with Aliyun's Ext.Desensitization text. A single segment (the
common case) reuses the verdict call's rewrite directly; multiple
segments are re-checked one call per segment because MultiModalGuard
takes one content string per call — the re-calls keep masked[i]
aligned with texts[i] by construction. mask with no usable
Desensitization fails closed. A clipped (>2000 chars) segment gets
its unscanned tail re-attached on write-back.
- Blob path (check_input/check_output — the mid-stream windows): mask
maps to Block, since there is no write-back channel and releasing
un-masked content would defeat the operator's policy.
- Ext is parsed tolerantly (object, or a JSON document nested in a
string — Aliyun types it inconsistently); the desensitized text
travels outside AigDiagnostics so the diagnostics type stays
structurally content-free (#153).
- The Desensitization accumulation assumption (last non-empty rewrite
wins across Result[] entries) is doc-derived and flagged for live
re-verification during #1070 QA.
- Test-side: cross-module lock for log-capturing tests — a concurrently
dropped capture subscriber recomputes tracing's GLOBAL max-level hint
and can fast-path away another thread's events mid-capture.
Ref AISIX-Cloud#1070
@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an aliyun_ai_guardrail configuration and Aliyun MultiModalGuard runtime integration, including signed requests, input/output moderation, masking, streaming controls, diagnostics, schema updates, feature wiring, unit tests, and end-to-end coverage.

Changes

Aliyun AI Guardrails

Layer / File(s)Summary
Configuration contract and schema
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json, crates/aisix-admin/src/openapi.rs
Adds the aliyun_ai_guardrail configuration and discriminator, exposes it publicly, updates the JSON schema, and adds its ReDoc title.
MultiModalGuard protocol implementation
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, crates/aisix-guardrails/src/aliyun.rs
Implements signed RPC requests, service-level routing, tolerant response parsing, diagnostics, failure classification, and shared Aliyun helper access.
Moderation and streaming behavior
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
Adds input/output checks, segment masking and rewrite handling, fail-open policies, content limits, correlation IDs, and stream buffering policies.
Feature wiring and supported kinds
crates/aisix-guardrails/src/build.rs, crates/aisix-guardrails/src/lib.rs, crates/aisix-server/src/heartbeat.rs
Registers and exports the runtime guardrail, conditionally builds it, reports its supported kind, and updates related expectations.
Validation coverage
crates/aisix-guardrails/src/aliyun_ai_guardrail.rs, tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Adds unit, log-safety, stream-policy, live-smoke, and mocked end-to-end tests for moderation, masking, failures, streaming, and service tiers.

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

Sequence Diagram(s)

sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Loading

Possibly related PRs

  • api7/aisix#781: Touches shared Aliyun request-ID handling and helper plumbing used by this integration.

Suggested reviewers:moonming

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningThe E2E suite still depends on the first test warming propagation and shares unisolated mock state, making later assertions order-sensitive.Move the readiness probe into beforeAll or baseline-isolate it, and make each test assert only its own request/response effects.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is specific and accurately summarizes the new Aliyun AI Guardrails MultiModalGuard support with suggestion verdicts and mask write-back.
Linked Issues check✅ PassedThe PR implements the requested MultiModalGuard path with Pro/Basic service codes, suggestion-based verdicts, streaming/session IDs, diagnostics, and tests.
Out of Scope Changes check✅ PassedThe OpenAPI tab label and test-lock changes support the new guardrail flow and test stability rather than unrelated functionality.
Security Check✅ PassedNo secrets, auth, DB, TLS, or ownership regressions found; Aliyun logs avoid Ext/prompt content and tests assert no leak.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1070-aliyun-ai-guardrail

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
schemas/resources/guardrail.schema.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

…uardrail
# Conflicts:
#	crates/aisix-core/src/models/mod.rs
… live endpoint
#1070 QA against the real green-cip endpoint (commodity now activated)
verified the block path end to end and corrected the one mask assumption
that was doc-derived:
- Real sensitiveData mask contract: the dimension returns the COMPLETE
desensitized text (every span already masked) in exactly ONE Result's
Ext.Desensitization; a sibling Result for a second matched sub-type
carries only Ext.SensitiveData (the original values — a leak vector)
with no Desensitization. Not the per-item accumulation the old comment
assumed. desensitization()'s scan-for-the-carrier is correct either
way; comment rewritten to the observed contract and a unit test added
with the exact captured multi-Result body (landline + two mobiles →
one full-text carrier + one metadata-only sibling).
- Ext is a JSON object live (confirmed); the string-encoded path stays as
a defensive tolerance.
- live_smoke now exercises the real mask + the full segment write-back
path (moderate_input_segments), asserting the rewrite carries the mask
token and never the original value. Benign assertion relaxed to
pass|watch: with the sensitive-data module's action set to observe,
even a no-match prompt bubbles a top-level watch (both release).
Also confirmed live and unchanged: block on top-level Suggestion,
per-dimension diagnostics (S0–S3 seen for sensitiveData), header
request_id, and that matched content in Ext (RiskWords/SensitiveData)
never reaches a log or the write-back.
Ref AISIX-Cloud#1070

@coderabbitaicoderabbitaiBot 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: 1

🤖 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-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 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: 7a31e51f-660d-4854-a499-b50854308daa

📥 Commits

Reviewing files that changed from the base of the PR and between b3ac72c and 01dcd12.

📒 Files selected for processing (10)
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-core/src/models/guardrail.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-guardrails/src/aliyun.rs
  • crates/aisix-guardrails/src/aliyun_ai_guardrail.rs
  • crates/aisix-guardrails/src/build.rs
  • crates/aisix-guardrails/src/lib.rs
  • crates/aisix-server/src/heartbeat.rs
  • schemas/resources/guardrail.schema.json
  • tests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts

Comment threadcrates/aisix-core/src/models/guardrail.rs
@jarvis9443
jarvis9443 merged commit 6413a77 into mainJul 23, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/1070-aliyun-ai-guardrail branch July 23, 2026 10:30
membphis added a commit that referenced this pull request Aug 24, 2026
`add_variant_titles` still assigns titles by position, and that is
deliberate — but it leaves one edit uncovered, so cover it where it
matters.
Positional title assignment is safe for the edits that actually happen.
`title_schema_variants` bails on a length mismatch, so adding or removing
a kind makes every title in that family disappear and
`openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a
variant in place edits the same line the title sits beside. The record
bears this out: #799 inserted a guardrail kind and updated the titles
correctly while silently breaking the descriptions (#1037), and #997
swapped `weighted` for `consistent_hash` with the titles staying right.
The descriptions had no such forcing function, which is the whole reason
the two tables diverged.
That leaves a length-preserving reorder as the only silent edit. The two
families that grow now pin their titles by discriminator in tests — the
guardrail kinds already did; the exporter kinds do now. Verified by
reordering two exporter titles: the new assertion names the offending
kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green,
which is exactly the window being closed.
The remaining families are small, stable string enums, plus `Model`,
whose branches are mutual-exclusion shapes carrying no discriminator to
key on at all. Converting those would split `add_variant_titles` into two
mechanisms for a reorder that has never happened, and whose result is
self-contradictory on sight — the tab would read `Lakera Guard` over a
branch whose `kind` says `openai_moderation`, since kinds and
descriptions no longer travel by position.
membphis added a commit that referenced this pull request Aug 24, 2026
The generated Admin API reference documented the `aliyun_ai_guardrail`
guardrail with the `pii` branch's sentence — "in-process sensitive-data
detection and redaction" — telling readers that a cloud-backed,
policy-driven moderation service runs inside the gateway and redacts
content, and giving them no way to tell the two kinds apart. Dispatch
always routed the kinds correctly; only the reference was wrong.
Two sources produced these sentences and they disagreed.
`guardrail_kind_description` keys on the kind value and had no
`aliyun_ai_guardrail` arm, leaving that branch undescribed.
`add_missing_property_descriptions` then backfilled it from a hardcoded
positional list of `/Guardrail/oneOf/0..8` pointers written when the enum
had nine variants; `aliyun_ai_guardrail` was later inserted at position
six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes
where a description is absent, so every other kind kept its correct text
and the stale entries stayed silent — the one hole in the kind-keyed
function was exactly where the stale list landed. A positional pointer
can only mislabel, never fail.
Add the missing arm, and remove positional addressing from the file
rather than just the branch that had broken. Of the twenty pointers in
`add_missing_property_descriptions`, sixteen were dead — the guardrail,
Bedrock and keyword-pattern families all describe their variants beside
the type in `aisix-core`. The four live ones, the observability exporter
kinds, had no other source and carried the identical latent defect, as
did the `sample_rate` default in `add_schema_defaults`, where an index
shift lands on a branch without that field and silently drops a
documented default. Both now select the branch by its `kind`
discriminator through a new `variant_property_mut` helper.
Regenerating the OpenAPI proves the sweep behavior-preserving: flattened
to leaf pointers, the document differs from the base in exactly two
values, both on the `aliyun_ai_guardrail` branch.
That branch's own description was also silent on what the guardrail does
to traffic, while every sibling states its side — `pii`, `presidio` and
`lakera` say they mask, `openai_moderation` and `semantic` say they never
rewrite content. This one does rewrite: on a `mask` suggestion the
caller-visible content is replaced with the provider's desensitized text,
and blocked when none comes back. That is now stated, and a non-English
product name and a regional endpoint template the `region` field already
documents are gone.
Four tests cover a binding nothing checked before. The pre-existing
checks only assert that metadata is present, not that it belongs to the
branch carrying it, which is why the defect shipped. The guardrail test
pins the source rather than the text — each description must equal
`guardrail_kind_description(kind)`, so a second source writing these
fails however plausible its sentence — plus the kind set and non-empty
text. Two OpenAPI-side tests check both carriers of a branch's identity
by discriminator, since the site of the original defect is in
`aisix-admin` where an `aisix-core` test cannot see it.
`add_variant_titles` stays positional, deliberately. It has a forcing
function the descriptions lacked: `title_schema_variants` bails on a
length mismatch, so adding or removing a kind makes every title in the
family vanish and fails loudly. That asymmetry is why #799 updated the
titles correctly while silently breaking the descriptions. Only a
length-preserving reorder slips through, so the two families that grow
pin their titles by discriminator in tests.
Two older assertions that reached for `ObservabilityExporter.oneOf[0]`
are converted too. `sample_rate` exists only on `otlp_http`, so a reorder
failed them while the document was entirely correct — the mirror image of
this bug, reporting a defect that is not there. A reorder should fail
exactly one test, the one checking metadata still belongs to its branch.
Closes#1037
Sign up for freeto 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

@jarvis9443