Repository navigation
Conversation
DeepSeek V4.1 tool arguments are raw XML strings, so a malformed closing-control prefix such as `</| parameter>` stayed ordinary text inside an unfinished string. When special-token exclusion is enabled, add `JSONSchemaFormat.excludes: ["</|"]` to every V4.1 tool-argument schema. The exclusion applies in auto, required, and named tool-choice modes; `exclude_special_tokens: false` suppresses it. V4, other parser families, and structured-output response schemas keep their current wire format. XGrammar 0.2.8 honors the exclusion but drops `minLength`/`maxLength` on strings while it is active, so the public options and crate docs state that limitation. Refs ai-dynamo#530. Signed-off-by: Stu Cao <stu@primeintellect.ai>
Run the emitted V4.1 grammar through the real XGrammar backend and verify that malformed closing prefixes are rejected while valid parameter, invoke, and calls endings are accepted. Cover auto, required, and named choices, the literal-marker opt-out, Unicode strings, string length bounds, token-boundary crossings, and the absence of leakage into V4 or structured-output response schemas. The verifier now reports its XGrammar version and whether it honors `JSONSchemaFormat.excludes`, so the 0.2.7 compatibility job asserts the documented legacy behavior while 0.2.8 asserts rejection. The length test records that 0.2.8 drops `minLength`/`maxLength` under exclusion. Refs ai-dynamo#530. Signed-off-by: Stu Cao <stu@primeintellect.ai>
A real tokenizer can split the valid closing `</|DSML| parameter>` after `</` or after `</|`. Add both token-boundary cases to the V4.1 exclusion test. XGrammar 0.2.8 accepts the completed marker in each case with the exclusion active, so the guard does not reject valid structural closings that span tokens. Refs ai-dynamo#530. Signed-off-by: Stu Cao <stu@primeintellect.ai>
stu-cao
force-pushed
the
fix/530-v41-raw-arg-excludes
branch
from
November 5, 2026 09:14
d8176a0 to
7fc3828
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Closes #530. Follow-up to #479 and #493. The downstream runtime repair this follows is PrimeIntellect-ai/frontend-crates
d76e37f.DeepSeek V4.1 tool arguments are raw XML strings (
style: deepseek_v4_1_xml). A malformed closing-control prefix such as</| parameter>can remain ordinary content inside an unfinished string, so generation can continue to the token limit without producing a call. This PR reserves the closing-control prefix in V4.1 argument strings at the grammar level. It is a generation restriction, not payload-aware parser recovery.Affected crate:
dynamo-parsers-v20.9.10 (parsers/v2).Details
JsonSchemaFormatgains an optionalexcludes: Vec<String>field, serialized only when non-empty. XGrammar's structural-tag wire format already defines it.reserve_v41_closing_prefixwalks the built format and adds</|only toJsonSchemaFormatnodes whose style isdeepseek_v4_1_xml.StructuralTagBuilder::build_with_optionsapplies the walk after tool-choice policy resolution and before reasoning wrapping, only when the resolvedexclude_special_tokensvalue is true. This covers auto, required, and named tool choice.StructuralTagOptions::exclude_special_tokens: falseemits no argument exclusion, so a literal</|in a string argument remains representable.deepseek_xml), other parser families, and structured-output (response_format) schemas are unchanged. No force-closing of an unfinished but valid prefix is added.Bug Fix Evidence
1. Minimal Sample Input
Crate:
dynamo-parsers-v20.9.10, DeepSeek V4.1 builder. Named tool choicereplywith:{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false}Default
exclude_special_tokens. The raw argument text contains a malformed close with two spaces:inside an unfinished
<|DSML| parameter name="text" string="true">payload. The runnable case isv41/named/guarded=true/malformed-double-spaceinv41_argument_exclusion_xgrammar:CONFORMANCE_GRAMMAR_PYTHON=/tmp/st-530-028/bin/python \ cargo test -p dynamo-parsers-v2 --test structural_tag_xgrammar \ v41_argument_exclusion_xgrammar -- --ignored2. Buggy Output (Before)
Without the fix, the named-choice grammar emits the argument schema as:
{"type":"json_schema","json_schema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false},"style":"deepseek_v4_1_xml"}There is no
excludesfield. XGrammar 0.2.8 acceptshello</| parameter> worldas string content (accepted: true,complete: truefor the enclosing call), so the malformed close never becomes a parameter end. The runtime symptom recorded in #530 is a missing tool call followed by generation to the unchanged 2,048-token limit.3. Expected/Fixed Output (After)
The emitted schema now includes:
{"type":"json_schema","json_schema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false},"style":"deepseek_v4_1_xml","excludes":["</|"]}On XGrammar 0.2.8, the malformed close is rejected (
accepted: false), while the valid ending</|DSML| parameter>is still accepted (accepted: true,complete: true). Token-boundary cases where the valid terminator is split after</or after</|are also accepted both guarded and unguarded. Withexclude_special_tokens: false, noexcludesfield is emitted and the literal marker remains accepted.Validation
cargo fmt --all -- --checkcargo clippy -p dynamo-parsers-v2 --all-targets -- -D warningscargo test -p dynamo-parsers-v2CONFORMANCE_GRAMMAR_PYTHON=/tmp/st-530-028/bin/python cargo test -p dynamo-parsers-v2 --test structural_tag_xgrammar -- --ignoredCONFORMANCE_GRAMMAR_VERSION=0.2.7 CONFORMANCE_GRAMMAR_PYTHON=/tmp/st-530-027/bin/python cargo test -p dynamo-parsers-v2 --test structural_tag_xgrammar -- --ignoredTests and what they prove:
src/structural_tag/tests.rs::v41_argument_exclusion_is_scoped_and_honors_the_opt_out— exact wire shape across V4.1, V4, Qwen3, and GLM; auto/required/named; default/true/falseexclude_special_tokens; with and without prompt-opened reasoning; with a structured-output schema. Only V4.1 argument schemas carryexcludes; response schemas do not.v41_argument_exclusion_xgrammar— real backend, all three tool-choice modes. Malformed closes (</| parameter>,</| unknown>,</| invoke>inside a string) are rejected only when guarded; valid parameter/invoke/calls endings, empty strings, and Unicode strings are accepted; an unfinished valid prefix is accepted but not completed (no force-closing).v41_exclusion_token_boundaries_xgrammar—accept_tokenwith a byte vocabulary plus supplied multi-byte tokens. Proves content+terminator in one token, terminator starting a new token, the newterminator-split-after-slashandterminator-split-after-prefixcases (both(true, true)guarded and unguarded), malformed closes split across tokens rejected under guard, literal markers split across tokens accepted only with the opt-out, andhello</|accepted as a valid unfinished prefix.v41_exclusion_length_bounds_xgrammar— bounded schema (minLength: 2,maxLength: 3). Pins the current 0.2.8 behavior: withexcludesactive the bounds are dropped (0/1/4-character values accepted), while the unguarded grammar enforces them; Unicode strings in range are accepted.v41_argument_exclusion_does_not_leak_xgrammar— V4 argument text containing</| parameter>is still accepted, and a V4.1 structured-output response containing</|is accepted.tests/check_structural_tag_xgrammar.py— addstokensrequests throughaccept_tokenand reportsbackend_versionplusexcludes_supportedfrom a V4.1-style probe, so the 0.2.7 compatibility job asserts its documented legacy behavior.Note: the 0.2.8 run prints
Ignoring the length constraints of string root_prop_0 (minLength=2, maxLength=3)while the exclusion is active. That is the known limitation described below, not a test failure.Conformance
Not applicable. This PR does not change parser events; it changes only the structural-tag grammar emitted for generation.
conformance/fixtures-unified-v2/families/deepseek_v41contains noinit.structural_tagcases, so noUNIFIED.*IDs, captures, or report columns are added, andconformance/CONFORMANCE_v2.htmlwas not regenerated.Why this stays a draft
XGrammar 0.2.8 honors
excludes, but its converter dropsminLength/maxLengthon strings while an exclusion is active (mlc-ai/xgrammar#912). The length-bound test above pins that behavior so the gap is visible. Constraint-preserving work is in mlc-ai/xgrammar#901. Per #530, this PR stays a draft until a backend that keeps those bounds is available and the dependency/version policy is agreed. The length-bound test will flag the fix when such a backend lands.Where should the reviewer start?
parsers/v2/src/structural_tag/wire.rs—JsonSchemaFormat.excludesandreserve_v41_closing_prefix.parsers/v2/src/structural_tag/builders/mod.rs— when the walk is applied.parsers/v2/tests/structural_tag_xgrammar.rs— real-backend contracts, including the two token-boundary cases.parsers/v2/tests/check_structural_tag_xgrammar.py— token mode and backend capability probe.Downstream repair
The runtime repair this follows is PrimeIntellect-ai/frontend-crates
d76e37f(fix(parsers): reserve V4.1 raw-string closing prefixes). Its evidence — 816/816 Kimi Vendor Verifier cases plus 8/8 tool-choice checks on DeepSeek V4.1 with Dynamo 1.6.1 and vLLM 0.32 — is recorded in #530.🤖 Generated with Prime Agent (DeepSeek-V4.1-Flash-1017)