Skip to content

server: hold strict tools to their schema under tool_choice auto - #1688

Merged
ashhart merged 3 commits into
ashhart:mainfrom
jayleaton:dsv41-up/strict-tools-auto
Oct 31, 2026
Merged

ashhart merged 3 commits into
ashhart:mainfrom
jayleaton:dsv41-up/strict-tools-auto

Conversation

@jayleaton

@jayleaton jayleaton commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Under tool_choice: "auto" (or none given), a tool marked "strict": true was not held to its schema. The model could call it with arguments that fail validation. Now, when any offered tool is strict, the server compiles an xgrammar triggered_tags grammar. Free text stays free. At the template's call opener a call tag begins, in which strict tools' arguments follow their schema and non-strict tools take any JSON object. Required and named calls are unchanged. LFM2, and engines without the grammar library, stay unconstrained.

This changes default behaviour for every family that has a grammar runtime, not only DeepSeek: an auto request with a strict tool is now constrained. TENSORFOLD_TOOL_GRAMMAR=1 holds every auto tools request the same way. TF_DSV41_TOOL_GRAMMAR is read as an alias, because the published DeepSeek recipe sets it. Say if you would rather not carry that alias.

A second commit makes the non-strict fallback explicitly open (additionalProperties: true). The pinned xgrammar v0.2.8 already treats the bare {"type":"object"} as open, so this fixes no observed bug. It makes the intent explicit and adds a test against the real library: zig build test-grammar-tools -Dxgrammar=DIR compiles the mixed strict lookup(q, n) / non-strict now(tz) auto grammars through a real libtfgrammar in DeepSeek DSML, hermes JSON and Qwen XML markup. It checks that now(tz="UTC") and nested arguments are accepted, and that wrong-type, extra and missing strict lookup arguments are refused, with parallel calls on and off.

Issue: none open upstream. Stacked on: #1684 (DSML required tools), whose deepseek style this uses for the call opener. Review only e9f668512bb2..d5855e035461.

Receipt

  • Environment, with the TensorFold commit, MLX or PyTorch version, machine and GPU, checkpoint and revision: head d5855e035461 on main d2ad5a7f2; Zig 0.17.0; host checks on a Linux aarch64 DGX Spark (GB10, CUDA 13.0). Real-pack boot on two GB10s, TP=2 over RoCE, checkpoint jayleaton/DeepSeek-V4.1-Flash-1017-EXL3-2.8bpw revision 859396591c34, TF_EXPERIMENTAL=deepseek.
  • Exactness: n/a; this changes which outputs are admitted for strict tools, not token arithmetic.
  • Decode speed before and after: n/a for requests without strict tools. With strict tools, the grammar mask runs on each token.
  • Prompt speed before and after: n/a.
  • Host tests (tool_grammar_auto_test.zig, tool_required_test.zig): the grammar shape for strict-only and mixed tools, the opener per style, the env override, and unchanged required and named calls. test-grammar-tools runs the real-library cases above.
  • Tests run, with pass, fail and skip counts:
    zig fmt --check build.zig zig                      exit 0
    git diff --check d2ad5a7f2..HEAD                    exit 0
    python3 tools/zig/lean_check.py                     base 424 problems, head 424, new 0 (lean_new.py)
    zig build test-cuda-host --summary all              exit 0, 273/273 steps, 4086/4095 tests passed, 9 skipped (with test-server-cpu)
    zig build test-server-deepseek-chat-host test-tool-visibility-http-host test-golden test-grammar-tools -Dxgrammar=<xgrammar v0.2.8>  exit 0, 16/16 steps, 82/82 tests passed
    
  • Real-pack TP=2 boot of main + every PR in this series (and the DeepSeek image path that is not yet proposed), at 83ba53298924: passed. Both ranks reached readiness; the recipe smoke, the canary and a strict tool call passed; MissingTritonVariant 0 in both rank logs; 2048 greedy tokens in 51.1 s with token_sha256 7538b20557b1…, the same as the downstream build of this series. On that boot, a required strict lookup returned {"q":"tensor parallelism","n":3}, and an auto request to the non-strict now returned {"tz":"UTC"}.
  • Not run, and why: a tool-call accuracy comparison against main on a model with strict tools.
  • Overlap: atc overlap reports the same additive tool_grammar.zig and reply_text.zig conflicts with minimax_m3: a CUDA backend for MiniMax-M3.1-Flash at TP over GB10s, from the release's own formats #1388, minimax_m3: serve MiniMax-M3.1-Flash (428B-A23B MoE, images) natively on Metal #1235 and minimax_m3 (CUDA): draft several streams' DSpark blocks in one pass #1426 as the PR this sits on.

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (Not measured for this change alone.)
  • No precision traded for speed. If the bits change, the description says which sums change and why.
  • A new or changed GPU kernel is registered with the kernel contract (docs/kernel-contract.md); each backend's SUMMARY line is in the receipt. (No kernel changes.)
  • Prompt processing is no slower than the last release. (Server grammar only.)
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency. (No reverted-fix control was run for this PR.)
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

@jayleaton
jayleaton force-pushed the dsv41-up/strict-tools-auto branch from 8700ab5 to d5855e0 Compare October 31, 2026 16:01
@jayleaton

Copy link
Copy Markdown
Contributor Author

Restacked on the rebased #1684 (e9f668512bb2), with no changes of its own. The host gates, including test-grammar-tools against xgrammar v0.2.8, were re-run on the new head and pass; the description has the new range and numbers.

jayleaton and others added 3 commits October 31, 2026 21:02
Under tool_choice auto (or none given) with a tool marked "strict": true, compile an xgrammar triggered_tags grammar: free text, and at the template's call opener a call tag whose strict tools' arguments follow their schema while other tools take any JSON object. TENSORFOLD_TOOL_GRAMMAR=1 (legacy TF_DSV41_TOOL_GRAMMAR) holds every auto tools request. LFM2 and engines without a grammar library stay unconstrained; required and named calls are unchanged.

(cherry picked from commit 17eef71)
…e auto

Structural tags compile argument schemas in strict mode, where an object that
omits additionalProperties is closed. The fallback for a tool that is not
strict now says additionalProperties: true, so it takes any JSON object.

zig build test-grammar-tools -Dxgrammar=DIR walks the strict lookup(q, n)
and non-strict now(tz) fixture's auto grammars through a real libtfgrammar
in DeepSeek DSML, hermes JSON and Qwen XML markup.

(cherry picked from commit c01e4cc)
@ashhart
ashhart force-pushed the dsv41-up/strict-tools-auto branch from d5855e0 to c669910 Compare October 31, 2026 21:02
@ashhart
ashhart merged commit cd217f4 into ashhart:main Oct 31, 2026
9 checks passed
@ashhart

ashhart commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Thank you @jayleaton. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes.

@ashhart

ashhart commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Thanks @jayleaton! Strict tools held to their schema under tool_choice: "auto" are in 1.2.1: https://github.com/ashhart/TensorFold/releases/tag/v1.2.1

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.

2 participants