Skip to content

feat(symphony): keyed arguments written from the table's tags, for Ling's newlines and Hy4's suffixed tags - #2882

Merged
slin1237 merged 2 commits into
mainfrom
feat/symphony-grammar-keyed
Oct 8, 2026
Merged

slin1237 merged 2 commits into
mainfrom
feat/symphony-grammar-keyed

Conversation

@slin1237

@slin1237 slin1237 commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

What

Stacked on #2881 (base refactor/symphony-grammar-schema): the keyed spellings the engines' glm_xml style does not write, inside crates/symphony/ only.

  • grammar/keyed.rs: a call written from the table's own Tags: the call's opener and the tool's name, one <arg_key>/<arg_value> pair per property of the schema in the schema's order (optional unless required), and the call's close, with the spelling's newline (Tags::between) after the name, after each key's close and before the call's close, which is where Ling's template writes it; Hy4's tags carry their :opensource suffix and its calls sit in a block, framed by the block's ways in as Kimi K3's are. The key is the property's name; the value follows the property's type as the templates write it (a string as it is, any text up to the value's close or a marker that ends the call or the block; enum as its values; the rest as JSON through the schema module); a property without a type, or a schema without properties, takes any text.
  • Format::grammar: Keyed(PLAIN) keeps the glm_xml style (GLM, IQuest; parity with the gateway), every other Keyed spelling takes the new path. Ling and Hy4 leave the None list; with the reasoning open they take the same prefix as GLM.
  • block_of (in grammar.rs): the block framing Kimi K3 built inline, now shared with Hy4.

Spellings, from the templates

Table Template Call as written
Ling 3.0 Flash '<tool_call>' + name, newline, per argument '<arg_key>' + k + '</arg_key>', '\n<arg_value>', the value (v if a string, else tojson), '</arg_value>'; then '\n</tool_call>' <tool_call>get_weather\n<arg_key>city</arg_key>\n<arg_value>Paris</arg_value>\n</tool_call>
Hy4 preview toolcalls_begin_token, per call toolcall_begin_token ~ name, per argument argkey_begin ~ key ~ argkey_end, argvalue_begin ~ value ~ argvalue_end (tojson unless a string), toolcall_end_token, then toolcalls_end_token, every expression whitespace-stripped <tool_calls:opensource><tool_call:opensource>get_weather<arg_key:opensource>city</arg_key:opensource><arg_value:opensource>Paris</arg_value:opensource></tool_call:opensource></tool_calls:opensource>

No old builder is an oracle for either: the gateway takes the JSON-schema path for Ling, and its Hy4 builder spells the tags without the suffix. The tests pin the derived payloads literally against the templates above.

Gates

By exit code (.claude/symphony/gates.sh, base refactor/symphony-grammar-schema): fmt, clippy -D warnings, test (334 lib, 10 fixture, 7 contract, 5 README), doc, width, codespell, all 0.

Not in this PR

  • The tagged syntax (Qwen 3.5 and later, Seed-OSS), MiniMax M3's XML, the pythonic tables, xLAM's list: still None.
  • Acceptance of the derived tags by xgrammar itself (bellwether, once the recorder has a constrained run).

Merge

After #2881, rebased onto main. Squash under rule 25 (rule 18: nothing of Format's changes; the maintainer merges). Subject: the PR title plus (#<n>). Body, with the authors of the old Kimi K3 parser whose property walk (build_args_format) moves into grammar/schema.rs here and which the keyed syntaxes now build on (rule 27, as on #2881):

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
Co-authored-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
Co-authored-by: Keyang Ru <rukeyang@gmail.com>
Co-authored-by: key4ng <key4ng@users.noreply.github.com>
Co-authored-by: Chen Hongtao <56470055+chenht2022@users.noreply.github.com>
Co-authored-by: LightSeek Foundation <243258330+lightseek-bot@users.noreply.github.com>

@slin1237
slin1237 requested a review from CatherineSue as a code owner October 8, 2026 09:03
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Team
  • Run ID: dbb4ee69-b25d-423c-baa8-2d5c26590a80
📥 Commits

Reviewing files that changed from the base of the PR and between 3c430bd and 9009f6a.

📒 Files selected for processing (4)
  • crates/symphony/src/grammar.rs
  • crates/symphony/src/grammar/keyed.rs
  • crates/symphony/src/grammar/schema.rs
  • crates/symphony/src/grammar/xtml.rs
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the symphony crates/symphony: the unified output parser label Oct 8, 2026
@claude

claude Bot commented Oct 8, 2026

Copy link
Copy Markdown

👋 The PR description doesn't fully follow
PULL_REQUEST_TEMPLATE.md:

  • Missing header: ## Description
  • Missing header: ### Problem
  • Missing header: ### Solution
  • Missing header: ## Changes
  • Missing header: ## Test Plan

Please update the PR description so reviewers have the context they need.

Comment thread crates/symphony/src/grammar/keyed.rs
Comment thread crates/symphony/src/grammar/keyed.rs
Comment thread crates/symphony/src/grammar/keyed.rs

@hello-alexmcc hello-alexmcc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rule-17 review of #2882 at a4ce888: comment. I'll approve once the three bot threads are answered; the data below settles the first two. With no old builder to check against, I checked the derived grammars against what each model's own template writes.

Template oracle. I used the parse sets recorded locally from each model's template at its pinned revision: inclusionAI/Ling-3.0-flash, and tencent/Hy4-preview at 705d81ee. The cases with tool calls, up to 1,500 per set, came from BFCL's seven sets, glaive-v2's first six, SWE-bench's call sets and the shapes call sets: 13,628 per model. For each case I derived the grammar from its request's tools with this PR. A throwaway matcher for the structural-tag JSON then checked each rendered output against it, using jsonschema for the json_schema leaves.

A case counts as one the grammar must accept when every call:

  • names a tool of the request,
  • uses only the schema's keys, in the schema's order, with every required one,
  • has each value valid against its property's schema,
  • and has no string value containing a close.

Results:

  • All 26,758 such cases are accepted (13,379 per model). That includes 259 zero-argument calls per model, BFCL's parallel calls, and SWE-bench's long code strings.
  • Each case was paired with the grammar its prompt calls for. Hy4 has 12,784 whose output starts inside the thought, matched with the reasoning prefix, and 595 that open their own thought, matched without it. All 13,379 of Ling's start inside the thought.
  • Calls that break their schema are rejected (extra keys, a missing required key, keys out of order, values outside their schema), as a strict grammar should. The exception is 4 per model on BFCL's is_unisex. Its schema is {"type": "boolean", "enum": ["True", "False", "dontcare"]}, and the string enum wins as in the old Kimi K3 builder.

On the threads:

  1. A Ling call without arguments (keyed.rs:107): the template writes <tool_call>version_api_VersionApi_get_version\n\n</tool_call>, two newlines, in every one of the 259. The grammar's \n\n is right. formats/ling.rs's single-newline sample is not what the template writes.
  2. Hy4 with the reasoning open (keyed.rs:296): the 12,784 outputs above match the prefix. It's any_text excluding all ten :opensource tags and ends at </think:opensource>, followed by the block's single way in from content, which has no close. A test pinning that, like Ling's, is still worth having.
  3. arguments shared with xtml (keyed.rs:113): your call. Both copies follow the schema's order and the required keys the same way today.

Gates and earlier probes, at a4ce888 merged on main 21d5e44, with Rust 1.98.0, its own target dir and -j 2:

  • fmt, clippy and doc each exit 0.
  • cargo test -p smg-symphony: 332 lib, 10 fixture, 7 contract and 5 README tests.
  • GLM and DSML vs the old builders: 36 of 36 equal. Kimi K3 is unchanged, equal in all 56 payloads but for "type": "tag".
  • Every table's grammar vs #2880's: only Ling's and Hy4's 12 lines each change, from None to a tag. No table goes from a tag to None.

Rule 17 so far:

  • Fixture ids: none changed.
  • Protected paths: the crate's grammar module.
  • Trailers: the author's Signed-off-by and Co-authored-by: Chang Su, and no AI trailer.

@hello-alexmcc hello-alexmcc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rule-17 re-review of #2882 at ab7f762: approve. All three threads are answered and resolved, and my checks pass at this head.

The round, checked:

  1. A Ling call without arguments: a_ling_call_without_arguments_writes_both_newlines pins <tool_call>ping\n\n</tool_call>, as the template writes it in all 259 recorded zero-argument calls.
  2. Hy4 with the reasoning open: hy4_with_the_reasoning_open_takes_the_prefix_and_its_one_way_in pins the prefix's end, its ten excludes, and the block's single trigger and way in.
  3. The shared walk: schema::arguments is the walk the two syntaxes shared, moved as it was. xtml passes its argument function and keyed its pair closure.

No output moves with the refactor. The template oracle's 27,256 per-case grammars (13,628 recorded tool-call cases per model) are byte-identical to the first round's, with the reasoning both open and closed. Paired with each prompt's reasoning state, all 26,758 schema-conforming cases are accepted:

  • Hy4: 12,784 starting inside the thought, and 595 not.
  • Ling: 13,379, all starting inside the thought.

Parity:

  • Kimi K3 vs KimiK3Parser::build_structural_tag: all 56 payloads are still equal except for "type": "tag".
  • GLM and DSML: 36 of 36 equal.
  • Every table's grammar vs #2880's: only Ling's and Hy4's 12 lines each differ.

Gates. I ran them at ab7f762 merged on main 21d5e44, with Rust 1.98.0, its own target dir and -j 2; each exited 0:

  • cargo +nightly fmt -p smg-symphony --check
  • cargo clippy -p smg-symphony --all-targets --all-features -- -D warnings
  • cargo test -p smg-symphony: 334 lib, 10 fixture, 7 contract and 5 README tests
  • RUSTDOCFLAGS="-D warnings" cargo doc -p smg-symphony --no-deps

Rule 17:

  • Fixture ids: none changed.
  • Property tests: no new format.
  • Protected paths: the crate's grammar module. Nothing public changes, so rule 18 isn't touched.
  • Trailers: the squash body carries the author's Signed-off-by and five Co-authored-by lines, one each for Chang Su, Keyang Ru, key4ng, Chen Hongtao and the LightSeek Foundation account. As on #2881, that's for rule 27, since this moves the port of the old builder's argument walk. No AI trailer.
  • Threads: three, all answered and resolved.

The merge is the maintainer's (rule 25), after #2881.

@slin1237
slin1237 force-pushed the refactor/symphony-grammar-schema branch from b041332 to 39e22c5 Compare October 8, 2026 12:26
@slin1237
slin1237 force-pushed the feat/symphony-grammar-keyed branch from ab7f762 to fb3dffe Compare October 8, 2026 12:26
@slin1237
slin1237 force-pushed the refactor/symphony-grammar-schema branch from 39e22c5 to d173b5e Compare October 8, 2026 12:38
@slin1237
slin1237 force-pushed the feat/symphony-grammar-keyed branch 2 times, most recently from 9e03a58 to 9acf6bf Compare October 8, 2026 12:47
Base automatically changed from refactor/symphony-grammar-schema to main October 8, 2026 12:51

@hello-alexmcc hello-alexmcc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rule-17 re-review of #2882 at 9acf6bf (rebased on #2881 ad49353): approve. The rebase changes nothing this PR derives. Its own diff passes, and all its threads are resolved.

  • Template oracle: I matched the recorded Ling and Hy4 tool-call cases (13,628 per model) against this head's grammars, each case paired with its prompt's reasoning state. All 26,758 schema-conforming cases are accepted: Hy4 12,784 with the thought open and 595 without, Ling 13,379. Calls that break their schema are rejected, as before.
  • Dump: only Ling's and Hy4's 12 lines each change against #2881.
  • Parity: GLM and DSML 36 of 36. Kimi K3 is as at #2881.
  • Gates: fmt, clippy and doc exit 0; tests 336 lib, 10 fixture, 7 contract and 5 README.
  • Trailers: six in the squash body, and no AI trailer.

#2881's open threads (the unbounded pointer walk) are its own. This PR picks up their fix through its base.

slin1237 and others added 2 commits October 8, 2026 06:00
…ng's newlines and Hy4's suffixed tags

The engines' `glm_xml` style writes the compact spelling of keyed
arguments, GLM 4.7's and IQuest's, and nothing else. Ling writes a
newline after the call's name, after each `</arg_key>` and before
`</tool_call>`, and Hy4 suffixes every tag `:opensource` and puts the
calls in a block, so their tags are now written from the table's own
tags: the call's opener and the tool's name, one `<arg_key>` and
`<arg_value>` pair per property of the schema in its order, optional
unless required, the newline where the spelling has it, and the call's
close; the key is the property's name, the value follows the property's
type as the templates write it (a string as it is, up to the value's
close or a marker that ends the call; the rest as JSON, through the
schema module); a property without a type, or a schema without
properties, takes any text. Hy4's block takes the block's ways in, as
Kimi K3's does, through a block builder the two now share. GLM and
IQuest keep the `glm_xml` style, the shape the gateway writes today.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
Co-authored-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
…two keyed cases are pinned

The Kimi K3 and keyed modules walked a tool's schema the same way: the
non-empty properties, the required names, the definitions, one argument
per property in the schema's order, optional unless required, and a
fallback for a schema without properties. The walk moves to the schema
module, and each syntax gives it only how one property becomes a grammar
and what the fallback is. Two tests pin what the review asked: a Ling
call without arguments writes both of the template's newlines, as every
one of the 259 such calls in its recorded sets does, and Hy4 with the
reasoning open takes the prefix and then its block's one way in.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
Co-authored-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
@slin1237
slin1237 force-pushed the feat/symphony-grammar-keyed branch from 9acf6bf to 9009f6a Compare October 8, 2026 13:11
@slin1237
slin1237 merged commit 227bd5b into main Oct 8, 2026
11 of 14 checks passed
@slin1237
slin1237 deleted the feat/symphony-grammar-keyed branch October 8, 2026 13:16

@hello-alexmcc hello-alexmcc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rule-17 re-review of #2882 at 9009f6a (rebased on main 3c430bd): approve. It's the 9acf6bf I approved plus the rebase.

  • Template oracle: all 26,758 schema-conforming recorded Ling and Hy4 cases are accepted, each paired with its prompt's reasoning state: Hy4 12,784 with the thought open and 595 without, Ling 13,379.
  • Dump: only Ling's and Hy4's 12 lines each change against main.
  • Parity: GLM and DSML 36 of 36. Kimi K3 is as on main.
  • Gates: fmt, clippy and doc exit 0; tests 336 lib, 10 fixture, 7 contract and 5 README.
  • Trailers: six in the squash body, and no AI trailer. Threads: all resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

symphony crates/symphony: the unified output parser

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants