Skip to content

fix: build schema grammar in Qwen3-Coder XML template when no tools#1652

Closed
markaalonzo wants to merge 1 commit into
ikawrakow:mainfrom
markaalonzo:fix/qwen3-coder-xml-grammar
Closed

fix: build schema grammar in Qwen3-Coder XML template when no tools#1652
markaalonzo wants to merge 1 commit into
ikawrakow:mainfrom
markaalonzo:fix/qwen3-coder-xml-grammar

Conversation

@markaalonzo

Copy link
Copy Markdown
Contributor

Summary

common_chat_params_init_qwen3_coder_xml previously only built a grammar when params.tools was non-empty. Requests that sent response_format.json_schema (or top-level json_schema) with no tools reached the sampler with an empty grammar string, so output was completely unconstrained regardless of strict=true.

After the fix, three branches are explicit:

  1. Tools present → XML tool-call grammar (existing behavior, unchanged).
  2. No tools, json_schema present → strict GBNF built from the schema. If the template kept <think>...</think> open (Step-3.5-Flash / Nemotron 3 Nano), the grammar is lazy with a </think> pattern trigger, mirroring the deepseek_r1 / deepseek_v3_1 / kimi_k2 thinking-enforced paths. Otherwise enforcement starts at token 0.
  3. Neither → no grammar, free generation.

Also adds a throw when both json_schema and grammar are passed, matching the common_chat_params_init_mistral_nemo precedent ("Either \"json_schema\" or \"grammar\" can be specified, but not both"). Previously the initializer would silently overwrite user-supplied grammar.

Why at the initializer level

The Qwen3-Coder XML dispatcher still routes Qwen3-Coder, Step-3.5-Flash, and Nemotron-3 Nano here when inputs.use_peg=false. Those users currently lose schema enforcement entirely. The autoparser refactor in #1376 centralizes this concern, at which point this fix dissolves into the new architecture; until it lands, the initializer-level fix is the minimal correct change.

Testing

Validated on Terra (RTX 3080 Ti, Qwen3.5-35B-A3B Q4_K_M, turbo K4V3 KV, -ger -ser 3,0 -c 512 -fa 1):

  • /v1/chat/completions with response_format={type: "json_schema", ...} and no tools now produces strictly schema-conformant JSON.
  • Previous behavior (no constraint) reproduced on the parent commit.
  • Tool-call path (tools=[...]) unchanged — XML tool-call grammar still built and triggered.
  • Passing both json_schema and grammar now raises runtime_error instead of silently dropping the caller's grammar.

Note on test coverage: tests/test-chat.cpp is currently commented out of the default build (unrelated pre-existing compile errors in legacy scaffolding, common_chat_parser_params symbol renamed to common_chat_syntax). Happy to add cases inside test_template_output_parsers in a follow-up if the maintainer wants test-chat revived as part of this change.

common_chat_params_init_qwen3_coder_xml previously only built a
grammar when params.tools was non-empty. Requests that sent
response_format.json_schema (or top-level json_schema) with no
tools reached the sampler with an empty grammar string, leaving
output completely unconstrained regardless of strict=true.

Now: when tools are absent and json_schema is provided, build a
strict GBNF from the schema. If the template has kept <think>...
open (thinking-forced), make the grammar lazy with a trigger on
</think> so the thinking block stays free and schema enforcement
starts at the first content token. Otherwise enforce from token 0.

Also: if json_schema and grammar are both set, throw instead of
silently overwriting user-supplied grammar. Mirrors the precedent
in common_chat_params_init_mistral_nemo and the deepseek_r1 /
deepseek_v3_1 / kimi_k2 schema-enforcement paths.
Comment thread common/chat.cpp
})();
build_grammar_xml_tool_call(data, params.tools, form);
} else if (!params.json_schema.is_null()) {
if (!params.grammar.empty()) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Do we really want to throw here? If json schema is the preferred way, why not just ignore the grammar if present?

@markaalonzo markaalonzo force-pushed the fix/qwen3-coder-xml-grammar branch from f6a7d1a to 14ca68f Compare April 18, 2026 21:19
@firecoperana

Copy link
Copy Markdown
Collaborator

I think we can just wait for #1376 to be merged.

@ikawrakow

Copy link
Copy Markdown
Owner

This PR is no longer relevant after merging #1376, correct?

@markaalonzo

Copy link
Copy Markdown
Contributor Author

Confirmed — #1376 supersedes this; the XML schema grammar path is handled there (commit 2df5caa). Closing.

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.

3 participants