Skip to content

fix(tool_parser): serialize tool-call arguments with vLLM/json.dumps spacing - #1836

Closed
key4ng wants to merge 2 commits into
mainfrom
fix/tool-parser-arg-spacing
Closed

key4ng wants to merge 2 commits into
mainfrom
fix/tool-parser-arg-spacing

Conversation

@key4ng

@key4ng key4ng commented Jun 24, 2026 •

Copy link
Copy Markdown
Member

Description

Problem

vLLM serializes tool_calls[].arguments with Python json.dumps — spaced separators ({"folder": "tmp"}). SMG's XML-derived parsers used serde_json::to_string — compact ({"folder":"tmp"}).

A byte-for-byte parity test (SMG's ChatTemplateProcessor vs HF apply_chat_template for a Qwen3 multi-turn conversation) matches except for this spacing. Because the chat template emits arguments verbatim, every multi-turn step that feeds back a prior tool call renders to different prompt tokens on the SMG arm than on pure vLLM — diverging generations that compound across turns (one driver of the BFCL multi_turn gap; the other, sampling nondeterminism, is addressed by the greedy A/B change).

Solution

Add helpers::args_to_json_string (a serde_json Formatter with Python json.dumps separators) and use it in the XML-style parsers that build args from <parameter> tags — qwen_xml, minimax_m2, deepseek_dsml. Type coercion is unchanged; only the output spacing changes. kimik2 already passes the model's raw arguments string through, so it's untouched. (String escaping stays serde's UTF-8 default = ensure_ascii=False.)

Changes

  • crates/tool_parser/src/parsers/helpers.rs — args_to_json_string + SpacedFormatter.
  • qwen_xml.rs, minimax_m2.rs, deepseek_dsml.rs — use it for the final argument serialization.
  • tool_parser_minimax_m2.rs — assert spaced output.

Test Plan

  • cargo test -p tool-parser — all pass (existing parser tests compare parsed Value, so they're spacing-agnostic; added an explicit spaced-output assertion).
  • cargo +nightly fmt + cargo clippy -p tool-parser clean.
  • Behavioral validation: greedy nightly A/B (see the temperature=0 PR) — multi_turn tool steps should now feed back prompts identical to vLLM's.

Context

From the multi_turn deep-dive: SMG's chat-template rendering is byte-identical to HF/vLLM; the only systematic frontend difference was this argument spacing.

Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Summary by CodeRabbit

  • Refactor

    • Standardized JSON formatting for tool-call arguments across parsers to use Python-style spacing conventions (spaces after colons and commas) for improved consistency.
  • Tests

    • Added test coverage to verify correct JSON spacing formatting in tool-call argument serialization.

key4ng added 2 commits June 23, 2026 17:35
vLLM serializes tool_calls[].arguments with Python json.dumps (spaced ", "/": "),
while SMG's XML-style parsers used serde_json::to_string (compact). The Qwen3 chat
template emits arguments verbatim, so on every multi-turn step that feeds back a
prior tool call, the SMG arm rendered different prompt tokens than pure vLLM —
diverging generations that compound across turns (confirmed: a byte-for-byte
template-parity test matches HF except for this arg spacing).

Add helpers::args_to_json_string (spaced separators, UTF-8 escaping unchanged) and
use it in the XML-derived parsers that build args from <parameter> tags: qwen_xml,
minimax_m2, deepseek_dsml. kimik2 already passes the model's raw arguments string
through, so it's unchanged. Type coercion is preserved (only spacing changes).

Signed-off-by: key4ng <rukeyang@gmail.com>
Signed-off-by: key4ng <rukeyang@gmail.com>
@github-actions github-actions Bot added tests Test changes tool-parser Tool/function call parser changes labels Jun 24, 2026
@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new SpacedFormatter struct and args_to_json_string public function are added to helpers.rs, producing Python json.dumps-style JSON with spaced separators. Three parsers (deepseek_dsml, minimax_m2, qwen_xml) replace their serde_json::to_string calls with this helper. A test verifies the spacing format on minimax_m2 output.

Changes

Spaced JSON serialization for tool-call arguments

Layer / File(s) Summary
SpacedFormatter and args_to_json_string utility
crates/tool_parser/src/parsers/helpers.rs
Adds a private SpacedFormatter implementing serde_json::ser::Formatter to emit , and : separators, and a public args_to_json_string<T: Serialize> function that serializes with this formatter, mapping errors to ParserError::ParsingFailed.
Parser call-sites switched to args_to_json_string
crates/tool_parser/src/parsers/deepseek_dsml.rs, crates/tool_parser/src/parsers/minimax_m2.rs, crates/tool_parser/src/parsers/qwen_xml.rs, crates/tool_parser/tests/tool_parser_minimax_m2.rs
Replaces serde_json::to_string with helpers::args_to_json_string in three parsers; adds an integration test asserting spaced separators in minimax_m2 output.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • lightseekorg/smg#1824: Modifies minimax_m2.rs's parse_tool_call serialization and error handling path, directly overlapping with the call-site change in this PR.

Suggested labels

tool-parser, tests

Suggested reviewers

  • slin1237
  • CatherineSue

Poem

🐇 A comma with space, a colon with flair,
Python-style JSON floats lightly through air.
Three parsers now hum with a tidier beat,
args_to_json_string makes formatting neat.
Hop hop hooray, the separators align!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing vLLM/json.dumps spacing for tool-call argument serialization across the tool parser module.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tool-parser-arg-spacing

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a custom JSON serialization helper, args_to_json_string, which formats JSON with Python-like spacing (", " and ": ") to match vLLM's json.dumps behavior. This helper is integrated into the DeepSeek DSML, Minimax M2, and Qwen XML parsers. The review feedback points out that the streaming/incremental parameter parsing implementations in both the Qwen XML and Minimax M2 parsers still use compact serialization for nested objects, which could cause a mismatch with the final parsed arguments. Additionally, it is recommended to use String::from_utf8 instead of String::from_utf8_lossy in the helper function to avoid unnecessary scanning and allocation overhead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.


let arguments = serde_json::to_string(&parameters)
.map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
let arguments = helpers::args_to_json_string(&parameters)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

While using helpers::args_to_json_string here ensures spaced serialization for complete parsing, the incremental/streaming implementation in parse_and_stream_parameters (specifically on lines 255 and 279) still uses serde_json::to_string(value) to serialize individual parameter values. If a parameter value is a nested object or array, it will be serialized compactly (e.g., {"nested":"value"}), leading to a mismatch/divergence between the streamed arguments and the final parsed arguments. Consider updating parse_and_stream_parameters to also use helpers::args_to_json_string(value) for serializing parameter values.

let parameters = self.parse_parameters(params_text, &param_types);

match serde_json::to_string(&parameters) {
match helpers::args_to_json_string(&parameters) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

While using helpers::args_to_json_string here ensures spaced serialization for complete parsing, the incremental/streaming implementation in parse_and_stream_parameters (specifically on lines 273 and 300) still uses serde_json::to_string(value) to serialize individual parameter values. If a parameter value is a nested object or array, it will be serialized compactly (e.g., {"nested":"value"}), leading to a mismatch/divergence between the streamed arguments and the final parsed arguments. Consider updating parse_and_stream_parameters to also use helpers::args_to_json_string(value) for serializing parameter values.

Comment on lines +439 to +442
value
.serialize(&mut ser)
.map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
Ok(String::from_utf8_lossy(&buf).into_owned())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using String::from_utf8_lossy scans the entire buffer for invalid UTF-8 sequences and may allocate/copy. Since serde_json serialization is guaranteed to produce valid UTF-8, we can use String::from_utf8 to perform a zero-copy conversion of the Vec<u8> buffer into a String and avoid this overhead.

Suggested change
value
.serialize(&mut ser)
.map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
Ok(String::from_utf8_lossy(&buf).into_owned())
value
.serialize(&mut ser)
.map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
String::from_utf8(buf).map_err(|e| ParserError::ParsingFailed(e.to_string()))

/// Serialize tool-call arguments like Python `json.dumps` (spaced separators), so
/// they match vLLM — chat templates emit `arguments` verbatim, so compact vs spaced
/// would diverge the prompt across multi-turn tool calls.
pub fn args_to_json_string<T: serde::Serialize>(value: &T) -> ParserResult<String> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: The JSON-based parsers (llama.rs:82, json.rs:146, qwen.rs:93, deepseek.rs:123, deepseek31.rs:110, glm4_moe.rs:142, step3.rs:396, pythonic.rs:284, mistral.rs:191) also parse model output and re-serialize with serde_json::to_string (compact). If vLLM re-serializes those with json.dumps (spaced) too, they'd have the same spacing divergence in multi-turn prompts.

Intentionally out of scope for this PR, or worth a follow-up?

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clean, well-scoped change. The SpacedFormatter correctly replicates Python json.dumps separators, the three XML parser call sites are straightforward substitutions, and the test validates the output format.

One nit posted: the JSON-based parsers also re-serialize with compact spacing — worth considering whether they need the same treatment in a follow-up.

0 🔴 Important · 1 🟡 Nit · 0 🟣 Pre-existing

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tool_parser/tests/tool_parser_minimax_m2.rs`:
- Around line 982-986: The assertions in the test starting at line 982 only
verify spaced key-value separators (`: `) but do not check item separators in
the JSON output. Expand the test assertions to also verify that item separators
are properly spaced by adding checks that confirm the presence of spaced item
separators (`, `) and the absence of compact item separators (`,`) in the
variable `a`, similar to the existing key-value separator assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e34d7a95-afce-42a4-bab7-add84f3d2526

📥 Commits

Reviewing files that changed from the base of the PR and between 10a085f and 641db26.

📒 Files selected for processing (5)
  • crates/tool_parser/src/parsers/deepseek_dsml.rs
  • crates/tool_parser/src/parsers/helpers.rs
  • crates/tool_parser/src/parsers/minimax_m2.rs
  • crates/tool_parser/src/parsers/qwen_xml.rs
  • crates/tool_parser/tests/tool_parser_minimax_m2.rs

Comment on lines +982 to +986
assert!(
a.contains("\": \""),
"expected spaced key/value separators, got {a}"
);
assert!(!a.contains("\":\""), "must not be compact, got {a}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Expand the spacing assertion to cover item separators too.

The test currently checks ": " only; compact item separators (",") could regress without failing this test.

Suggested test assertion update
     assert!(
         a.contains("\": \""),
         "expected spaced key/value separators, got {a}"
     );
     assert!(!a.contains("\":\""), "must not be compact, got {a}");
+    assert!(a.contains(", "), "expected spaced item separators, got {a}");
+    assert!(!a.contains(",\""), "must not use compact item separators, got {a}");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tool_parser/tests/tool_parser_minimax_m2.rs` around lines 982 - 986,
The assertions in the test starting at line 982 only verify spaced key-value
separators (`: `) but do not check item separators in the JSON output. Expand
the test assertions to also verify that item separators are properly spaced by
adding checks that confirm the presence of spaced item separators (`, `) and the
absence of compact item separators (`,`) in the variable `a`, similar to the
existing key-value separator assertions.

@key4ng key4ng closed this Jun 24, 2026
@lightseek-bot
lightseek-bot deleted the fix/tool-parser-arg-spacing branch June 24, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Test changes tool-parser Tool/function call parser changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant