Skip to content

feat(guardrails): add Qwen3Guard and Qwen3GuardStream - #192

Merged
dni138 merged 6 commits into
mainfrom
feat/issue-93-qwen3guard
Jul 6, 2026
Merged

feat(guardrails): add Qwen3Guard and Qwen3GuardStream#192
dni138 merged 6 commits into
mainfrom
feat/issue-93-qwen3guard

Conversation

@dni138

@dni138 dni138 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the Qwen3Guard family (Apache-2.0, tech report arXiv:2510.14276) as two guardrails:

  • Qwen3Guard — the generative Qwen3Guard-Gen models (0.6B default / 4B / 8B). Decoder LLM whose chat template embeds the classifier instruction; follows the PolyGuard/Nemotron generate_chat() decoder pattern. Prompt moderation with validate(text), response moderation with validate(text, output_text=...).
  • Qwen3GuardStream — the token-level Qwen3Guard-Stream models (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

  • Ternary verdict (Safe / Controversial / Unsafe) maps onto the existing output contract: score carries the canonical risk (0.0 / 0.5 / 1.0), extra["severity"] the verbatim label, and valid is computed with a strict default — only Safe passes; strict=False lets Controversial pass.
  • Categories are Qwen3Guard's own 9-name taxonomy, surfaced verbatim as CategoryResults. Gen parses them from the Categories: line only, with a case-sensitive card-order alternation (so Violent can't match inside Non-violent Illegal Acts).
  • Refusal (Gen, response mode only): the model's Refusal: Yes|No verdict about the judged response is surfaced as a refusal category; it never affects valid, and a missing line tolerantly yields triggered=None.
  • Spans (Stream, response mode): runs of consecutive flagged response tokens become SpanResults with character offsets into output_text (fast-tokenizer offset mapping; degrades to no spans rather than wrong offsets). Prompt mode has no per-token verdicts, so no spans.
  • Fail-closed: unparseable Gen output / missing Stream risk levels return valid=False with extra={"parse_failure": True} (repo convention).
  • Gen defensively strips <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_id config 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, Qwen3GuardStream construction raises an actionable ImportError on transformers >= 5 (install transformers>=4.51,<5), its integration test skips on 5.x environments, and test_model_load asserts 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

Test plan

  • Unit: 19 new parse/aggregation tests (severity mapping, strict vs lenient, category scoping, refusal modes, span merge/split/offsetless handling, fail-closed) — pytest tests/unit, 384 passed
  • pre-commit run --all-files clean (ruff, mypy strict, codespell)
  • Docs snippets: pytest tests/docs passed; API pages regenerated via scripts/generate_api_docs.py
  • e2e smoke on real 0.6B weights for both guardrails: Gen on transformers 5.8.0; Stream on transformers 4.57.6 (prompt + response modes, span offsets verified against real output)
  • Version gate verified on transformers 5.8.0 (raises actionable ImportError)

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

  • True incremental streaming (feed tokens as they are generated) needs new library API surface; Qwen3GuardStream currently exposes the streaming classifier through the batch validate() contract.
  • Qwen3Guard-Stream prompt-side spans aren't possible (the model judges the prompt as one unit).

Closes #93

🤖 Generated with Claude Code

dni138 and others added 6 commits July 6, 2026 12:18
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>

Copilot AI 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.

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) and Qwen3GuardStream (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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 &lt;think&gt;-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.

Comment on lines +187 to +189
text: str = tokenizer.apply_chat_template(
model_inputs.data["messages"], tokenize=False, add_generation_prompt=False, enable_thinking=False
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dni138
dni138 merged commit c578494 into main Jul 6, 2026
10 checks passed
@dni138
dni138 deleted the feat/issue-93-qwen3guard branch July 6, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Qwen3Guard

2 participants