feat(guardrails): add Qwen3Guard and Qwen3GuardStream - #192
Conversation
Generative safety classifier with a three-level severity verdict (Safe / Controversial / Unsafe). The chat template embeds the classifier instruction, so prompt moderation is a single user message and response moderation adds the assistant turn. Severity maps onto the canonical risk score (0.0 / 0.5 / 1.0) and is surfaced verbatim in extra["severity"]; strict=True (default) passes only Safe verdicts. In response mode the model's Refusal verdict is surfaced as a "refusal" category. Fails closed on unparseable output. Part of #93. The Qwen3Guard-Stream variants land separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qwen3Guard-Stream loads its classification heads as remote code (AutoModel + trust_remote_code) and judges the user prompt as a whole plus every assistant response token individually. validate() is a non-streaming facade over the streaming API: it aggregates the worst severity across all judged positions onto the same strict/score/extra contract as Qwen3Guard, and returns runs of flagged response tokens as character spans into output_text (offset-mapping based; degrades to no spans on tokenizers without offset support). HuggingFace-only; a user-supplied provider must set trust_remote_code=True. Part of #93. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gen: severity/score/extra mapping, strict vs lenient Controversial, category parsing scoped to the Categories line (no substring matches), refusal surfacing rules per mode, think-block stripping, fail-closed. Stream: worst-severity aggregation, category dedup, span merge/split semantics, offsetless-token handling, fail-closed on missing risk levels. Integration: both 0.6B defaults added to the non-heavy parametrized HuggingFace suite. Part of #93. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds both guardrails to the API-docs generator, GitBook navigation (SUMMARY.md), and the CLAUDE.md guardrail-shape lists, then runs scripts/generate_api_docs.py. The regeneration also emits the patronus.md / watsonx-guardian.md pages and their index rows that PR #184 forgot to commit (the index table is derived from the enum, so the catch-up cannot be split out). Part of #93. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PolyGuard's generated API page has existed since it landed, but the GitBook navigation never linked it, so it was absent from production docs navigation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Qwen3Guard-Stream model repos ship remote modeling code written against transformers 4.x; transformers 5 removed APIs it relies on (the implicit pad_token_id config default, ROPE_INIT_FUNCTIONS ["default"], the old rotary-embedding weight-init interface), so the checkpoint cannot load there. Construction now raises an actionable ImportError on transformers >= 5, the integration test skips on such environments, and test_model_load asserts the gate. Verified end to end on transformers 4.57.6: prompt and response moderation on the real 0.6B weights, including span offsets into output_text. Part of #93. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Qwen3Guard-based moderation guardrails (generative and token-level “stream” variants) to the any-guardrail library, including parsing/aggregation logic, version-gating for the Stream model’s remote code, and accompanying unit/integration coverage plus regenerated docs.
Changes:
- Introduces
Qwen3Guard(Gen) andQwen3GuardStream(Stream) implementations with ternary severity → canonical risk mapping and fail-closed behavior on parse failures. - Adds unit + integration tests, including a transformers-major-version gate for
Qwen3GuardStream. - Updates docs generation inputs and documentation index/SUMMARY to include newly added/previously missing guardrail pages.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_unit_new_guardrails.py | Adds unit tests for Qwen3Guard parsing and Qwen3GuardStream aggregation/span behavior. |
| tests/unit/test_api.py | Updates model-load test to account for Qwen3GuardStream’s transformers<5 gate. |
| tests/integration/test_huggingface_guardrails.py | Adds Qwen3Guard integration coverage; skips Qwen3GuardStream on transformers>=5. |
| src/any_guardrail/guardrails/qwen3_guard/qwen3_guard.py | Implements generative Qwen3Guard parsing + output mapping. |
| src/any_guardrail/guardrails/qwen3_guard/init.py | Exports Qwen3Guard. |
| src/any_guardrail/guardrails/qwen3_guard_stream/qwen3_guard_stream.py | Implements token-level streaming moderation + span extraction + transformers version gate. |
| src/any_guardrail/guardrails/qwen3_guard_stream/init.py | Exports Qwen3GuardStream. |
| src/any_guardrail/base.py | Adds new GuardrailName enum values for Qwen3Guard and Qwen3GuardStream. |
| scripts/generate_api_docs.py | Adds Qwen3Guard/Qwen3GuardStream entries to docs generation inputs. |
| docs/SUMMARY.md | Adds missing PolyGuard entry and new Qwen3Guard pages to the docs nav. |
| docs/api/guardrails/watsonx-guardian.md | Adds/regenerates WatsonxGuardian API documentation page. |
| docs/api/guardrails/qwen3-guard.md | Adds Qwen3Guard API documentation page. |
| docs/api/guardrails/qwen3-guard-stream.md | Adds Qwen3GuardStream API documentation page (incl. transformers<5 requirement). |
| docs/api/guardrails/patronus.md | Adds/regenerates Patronus API documentation page. |
| docs/api/guardrails/index.md | Updates guardrails index table with additional entries. |
| CLAUDE.md | Updates contributor guidance to include Qwen3Guard/Qwen3GuardStream references. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # match inside "Non-violent Illegal Acts". | ||
| _CATEGORY = re.compile("|".join(re.escape(category) for category in QWEN3GUARD_CATEGORIES)) | ||
| _REFUSAL = re.compile(r"Refusal:\s*(Yes|No)", re.IGNORECASE) | ||
| _THINK_PATTERN = re.compile(r"<think>.*?</think>", re.DOTALL) |
There was a problem hiding this comment.
Triage: declining this one — the premise doesn't hold for any supported backend. The HF path returns raw tokenizer.decode text and the llamafile path returns JSON (whose unescaping yields a literal <), so <think>-escaped tags can't reach the parser; nothing in the PR description says otherwise. The literal pattern also matches the repo's existing convention (_THINK_PATTERN in nemotron_content_safety.py). Happy to extend the regex if a real backend that HTML-escapes output ever gets added — it's a one-liner.
| text: str = tokenizer.apply_chat_template( | ||
| model_inputs.data["messages"], tokenize=False, add_generation_prompt=False, enable_thinking=False | ||
| ) |
There was a problem hiding this comment.
Triage: declining — PreTrainedTokenizerBase.apply_chat_template has a **kwargs catch-all (forwarded to the Jinja template context) throughout the gated transformers>=4.51,<5 range, so an unknown enable_thinking kwarg is silently ignored by templates that don't use it; it cannot raise TypeError. This exact call is exercised end-to-end on transformers 4.57.6 (see the PR test plan), and on transformers >=5 construction is version-gated so this line is unreachable.
Summary
Adds the Qwen3Guard family (Apache-2.0, tech report arXiv:2510.14276) as two guardrails:
Qwen3Guard— the generativeQwen3Guard-Genmodels (0.6B default / 4B / 8B). Decoder LLM whose chat template embeds the classifier instruction; follows the PolyGuard/Nemotrongenerate_chat()decoder pattern. Prompt moderation withvalidate(text), response moderation withvalidate(text, output_text=...).Qwen3GuardStream— the token-levelQwen3Guard-Streammodels (0.6B default / 4B / 8B), loaded as remote code (AutoModel+trust_remote_code).validate()is a non-streaming façade: it judges the prompt as a whole and every response token individually, aggregating the worst severity.Design decisions
Safe/Controversial/Unsafe) maps onto the existing output contract:scorecarries the canonical risk (0.0 / 0.5 / 1.0),extra["severity"]the verbatim label, andvalidis computed with a strict default — onlySafepasses;strict=FalseletsControversialpass.CategoryResults. Gen parses them from theCategories:line only, with a case-sensitive card-order alternation (soViolentcan't match insideNon-violent Illegal Acts).Refusal: Yes|Noverdict about the judged response is surfaced as arefusalcategory; it never affectsvalid, and a missing line tolerantly yieldstriggered=None.SpanResults with character offsets intooutput_text(fast-tokenizer offset mapping; degrades to no spans rather than wrong offsets). Prompt mode has no per-token verdicts, so no spans.valid=Falsewithextra={"parse_failure": True}(repo convention).<think>...</think>before parsing (Nemotron pattern) for provider-swapped backends; the HF chat template pre-fills an empty think block so the HF path never generates one.transformers-version constraint (Stream only)
The Qwen3Guard-Stream repos ship remote modeling code written against transformers 4.x; transformers 5 removed APIs it relies on (the implicit
pad_token_idconfig default,ROPE_INIT_FUNCTIONS["default"], the old rotary-embedding weight-init interface), so the checkpoint cannot load there — this is the model repo's code, not ours. Rather than stacking monkeypatches on private APIs,Qwen3GuardStreamconstruction raises an actionableImportErroron transformers >= 5 (installtransformers>=4.51,<5), its integration test skips on 5.x environments, andtest_model_loadasserts the gate. If Qwen updates the repo for 5.x, the gate can simply be removed.Qwen3Guard(Gen) is unaffected and runs on both majors.Also included
patronus.md/watsonx-guardian.mdpages and index rows that feat(guardrails): add commercial-API guardrails (Watsonx Guardian, Patronus, Qualifire, Pioneer) #184 forgot to commit (the index table derives from the enum, so the catch-up can't be split out).docs/SUMMARY.mdwas missing the PolyGuard entry (pre-existing gap).Test plan
pytest tests/unit, 384 passedpre-commit run --all-filesclean (ruff, mypy strict, codespell)pytest tests/docspassed; API pages regenerated viascripts/generate_api_docs.pyImportError)Both integration params are non-heavy (0.6B), so CI's default integration job exercises them (the Stream one skips while the lock resolves transformers 5.x).
Follow-up candidates
Qwen3GuardStreamcurrently exposes the streaming classifier through the batchvalidate()contract.Qwen3Guard-Streamprompt-side spans aren't possible (the model judges the prompt as one unit).Closes #93
🤖 Generated with Claude Code