Skip to content

feat: support Gemma 4 tool calling with the <|"|> string delimiter - #923

Merged
Seven-Streams merged 4 commits into
mlc-ai:mainfrom
asanomitakanori:asanomi/gemma4-structural-tag
Sep 24, 2026
Merged

Seven-Streams merged 4 commits into
mlc-ai:mainfrom
asanomitakanori:asanomi/gemma4-structural-tag

Conversation

@asanomitakanori

Copy link
Copy Markdown
Contributor

Summary

  • Add a "gemma" JSON-schema style that emits Gemma 4's tool-call argument spelling: unquoted object keys and strings delimited by the single <|"|> token with no escape sequences, at every nesting level.
  • Re-enable the gemma_4 builtin structural tag (auto / required / forced tool choice, <|channel>thought reasoning prefix, parallel_tool_calls), which has been unregistered with a "we will support it later" TODO since refactor: unify reasoning parameter and rename model keys #609.
  • Validate against the released Gemma-4 chat templates in the alignment test, plus token-level matcher tests.

Closes #587. Continues #684 by @iwanhae, which was closed unmerged after the converter was rewritten to build grammar trees directly (#745) and the branch fell into conflict; the original implementation and authorship are preserved through Co-authored-by.

Why

Engines currently fall back to unconstrained decoding for Gemma 4 tool calls because the existing converters cannot express <|"|>-delimited strings. This blocks tool_choice="required" and JSON-schema-constrained arguments for Gemma 4 in SGLang and vLLM (vllm-project/vllm#53363, sgl-project/sglang users on #684).

What changed since #684

Ported to the current int32_t-based converter API and reworked around JSONSchemaFormat.excludes (#912):

  • The delimiter is registered as an exclusion. GemmaToolCallingConverter appends <|"|> to excludes_, so:
    • pattern / format / minLength-maxLength string bodies are intersected with "does not contain <|"|>" through ExcludingString. A permissive pattern such as .* can no longer close the string early — this was the documented limitation of feat: support Gemma tool calling format with <|"|> string delimiters  #684. (Built-in format regexes the FSM engine cannot build keep the CFG expansion, as in the JSON converter.)
    • const / enum literals that would have to spell the delimiter become unsatisfiable instead of emitting a string that parses differently.
    • Declared property names are excluded from additionalProperties keys (the original PR fell back to the plain identifier pattern).
    • Caller-provided JSONSchemaFormat.excludes work for style="gemma" too.
  • Two small virtual hooks on JSONSchemaConverter: CreatePatternKeyRule (replaces the local lambda in GenerateObject) and CreatePropertyNamesKeyRule. Formats with unquoted keys override them to emit bare key bodies for patternProperties / propertyNames; the JSON and XML converters keep their behaviour unchanged.
  • Length-bounded strings are built as (.|\n){m,n} intersected with the exclusion, so they count codepoints and allow lone < / delimiter prefixes without hand-written lookahead rules.
  • The alignment test renders the vendored Gemma-4 templates with jinja2 under the GEMMA: model-id prefix (no HF token or Gemma-4-capable transformers needed), reusing the tokenizer prompt-diff path; a token-gated drift guard checks the vendored templates against google/gemma-4-E2B-it / google/gemma-4-31B-it.
  • Docs (structural_tag.md, tool_calling_and_reasoning.md), the TypeScript JSONSchemaFormat.style union, and the style error message are updated.

Test plan

  • tests/python/test_function_calling_converter.py -k gemma (59 cases: strings, scalars, nesting, any, enum/const incl. large-int literal preservation, additionalProperties, patternProperties, propertyNames, length bounds incl. multi-byte and newline, .* pattern cannot emit the delimiter, format: date, excludes)
  • tests/python/test_builtin_structural_tag.py (612 passed incl. the shared get_*_structural_tag matrix, a single-token <|"|> / <|tool_call> tokenizer walk, and the auto-dispatch regression)
  • tests/python/test_builtin_structural_tag_alignment.py -k gemma (34 passed: e2b + 31b templates × thinking on/off × tool scenarios, plus the drift guard against the official templates with a HF token)
  • Full test_function_calling_converter.py, test_structural_tag_converter.py, test_json_schema_excludes.py, test_grammar_matcher_json_schema.py, test_json_schema_converter.py: 2933 passed (-m "not hf_token_required")
  • pre-commit run (black, isort, clang-format 19.1.7, taplo) and ruff check pass

Known limitations

🤖 Generated with Claude Code

Re-enable the `gemma_4` builtin structural tag by adding a `"gemma"` JSON-schema
style that emits Gemma 4's argument spelling: unquoted object keys and strings
delimited by the single `<|"|>` token with no escape sequences, at every nesting
level.

This picks up mlc-ai#684 (closed unmerged after the converter was
rewritten to build grammar trees directly in mlc-ai#745) and ports it to
the current converter API:

- `GemmaToolCallingConverter` (cpp/converter_ext/gemma.cc) registers the
  delimiter as an exclusion, so pattern/format/length-bounded strings are
  intersected with "does not contain <|"|>" through `ExcludingString` and can no
  longer close a string early (the known limitation of mlc-ai#684), literals that would
  have to spell the delimiter become unsatisfiable, and declared property names
  are excluded from `additionalProperties` keys.
- Two small virtual hooks on `JSONSchemaConverter` (`CreatePatternKeyRule`,
  `CreatePropertyNamesKeyRule`) let formats with unquoted keys emit bare key
  bodies for `patternProperties` / `propertyNames`.
- `get_gemma_4_structural_tag` uses `style="gemma"` and keeps `<|tool_call>`
  excluded from the thought channel while leaving it available as the trigger in
  free text.
- The alignment test renders the vendored Gemma-4 chat templates with jinja2
  (no HF token or Gemma-4-capable transformers needed) and guards them against
  drift from the official templates when a token is available.

Closes mlc-ai#587.

Co-authored-by: iwanhae <iwanhae@gmail.com>
Co-authored-by: wanhae.lee <wanhae.lee@linecorp.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: yuchuan <yuchuan.7streams@gmail.com>
Signed-off-by: yuchuan <yuchuan.7streams@gmail.com>
Signed-off-by: yuchuan <yuchuan.7streams@gmail.com>
@Seven-Streams
Seven-Streams merged commit 4221346 into mlc-ai:main Sep 24, 2026
90 of 91 checks passed
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 builtin structural tag support for Gemma 4 models

2 participants