Repository navigation
fix(tool_parser): preserve Qwen XML streamed arguments - #2490
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 SummarySummary by CodeRabbit
WalkthroughThe Qwen XML parser now isolates parameters for coalesced tool calls, closes completed streamed arguments consistently, and preserves incomplete parameters for end-of-stream recovery. Tests cover chunking patterns, nested values, empty calls, and incomplete arguments. ChangesQwen XML streaming
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to Qwen XML streamed tool-call arguments now remain isolated per call and close consistently, including at end of stream. No actionable current-head merge risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
| for cap in self.xml_param_pattern.captures_iter(&self.buffer) { | ||
| for cap in self | ||
| .xml_param_pattern | ||
| .captures_iter(&self.buffer[..parameter_end]) |
There was a problem hiding this comment.
🟡 Nit: Truncating the scan at the first </tool_call> silently changes behavior for parameter values that contain the literal </tool_call>, and nothing pins the new behavior.
For <tool_call><function=process><parameter=snippet>use </tool_call> tag</parameter></function></tool_call>:
- Before: the regex ran over the whole buffer, captured
snippet = "use </tool_call> tag", and the brace counter appended}— the client received the complete, valid{"snippet": "use </tool_call> tag"}(plus the leftovertag</parameter>…leaking as normal text). - After:
parameter_endlands mid-value, the slice is<parameter=snippet>usewith no</parameter>, so the parameter never matches and the call closes as{}— the argument is dropped entirely.
This is arguably the right call: parse_complete_inner's non-greedy (?s)<tool_call>\s*(.*?)\s*</tool_call> extractor already truncates at the same point and yields {}, so streaming and non-streaming now agree, and vLLM/SGLang split the same way. But the PR description lists this under "existing delimiter ambiguity … remain outside this change", when the change actually flips it from "correct args" to "empty args". Worth a short regression test in tool_parser_qwen_xml.rs asserting json!({}) for that fixture, so the alignment with parse_complete is intentional and stays that way.
Bound parameter extraction to the current tool call, close streamed root objects at tool boundaries, and recover a safe final brace at EOS. Add focused regressions for literal braces, truncation, and adjacent calls. Signed-off-by: ai-jz <ai-jz@users.noreply.github.com>
8b91d25 to
1a3ff34
Compare
Description
Problem
QwenXmlParser can emit invalid or cross-contaminated tool arguments depending on the argument content and chunk boundaries:
}inside a parameter string is counted as a structural JSON brace. For<tool_call><function=get_weather><parameter=city>echo '}'</parameter></function></tool_call>, the streamed arguments omit the root closing brace.<tool_call><function=get_weather><parameter=city>Tokyo</parameter>leaves an open argument object. The generic recovery helper compares compact serialized JSON with the parser's spaced fragments and finds no matching prefix.Minimal reproducer
On a clean SMG checkout at
9c87ef5d499604089b25bd1900ea7f6804cbf1db, save the following temporary file ascrates/tool_parser/tests/qwen_xml_minimal_repro.rs. It uses one tool, one string parameter containing}, and one complete chunk; no model server or GPU is needed.Run from the repository root:
cargo test --locked -p tool-parser --test qwen_xml_minimal_repro -- --nocaptureBefore the fix: the assertion fails with
arguments: {"x": "}"; the root closing brace is missing. Result: 0 passed / 1 failed (exit 101), including the terminal argument getter.With this fix: the same snippet passes and the collected arguments parse to
{"x":"}"}. Result: 1 passed / 0 failed (exit 0). Both runs were executed against the same base, changing only the parser implementation. This temporary reproducer is documentation material; the committed patch still adds six regression tests.Solution
Limit parameter extraction to the current
</tool_call>boundary and close each argument object when that boundary is consumed, emitting{}for an empty call. At EOS, append only the root}when the resulting JSON equals exactly the already parsed argument object. Unfinished parameter values remain absent.The repair stays within QwenXmlParser; the shared recovery helper, argument coercion, and caller finish-reason policies are unchanged. Existing tests retain coverage for the schema coercion from #1841 and literal argument values from #1899. The existing
qwen_coderandnemotronaliases use the same parser implementation.Prior work
This change follows earlier work on final argument validity, per-call isolation, and streaming completion:
The three Qwen mechanisms were present in the initial XML implementation. Following its mainline history through renaming and value-coercion changes found no intervening repair-and-revert sequence.
Changes
Test Plan
Rebase validation
Rebased onto main
a8dc4f4088974925ee6e05f8a4c8aae65b011a23as1a3ff347dc3ed12dbc94b64d1dec21a50f99146f.git range-diffconfirms the original parser/test patch is unchanged: two files, +186/-20. On this new commit, the CPU run ofcargo test --locked -p tool-parserpassed 501 tests / 0 failures / 0 ignored across 26 completed harnesses.cargo clippy --locked -p tool-parser --all-targets --all-features -- -D warnings, nightly whole-repository formatting, and the base-to-head whitespace check all returned exit 0. The initial full-workspace result below is historical; new-head hosted CI, including the GPU lanes, remains required.The previous CI run had two independent failures plus the aggregate
finishfailure:cudaIpcMemHandle_t.reserved. The rebase includes the exact dependency pin and canary from merged #2494.finishfailed because those two prerequisite jobs failed. No parser or CI-gate changes were added for these failures.Original red/green evidence
Reproduction baseline:
a5901cb5905eb929ec60448f39b3c082d5940b91. The initial publication base was9c87ef5d499604089b25bd1900ea7f6804cbf1db; neither Qwen XML file changed upstream, and the reviewed patch is byte-for-byte unchanged. Applying only the final test-file changes to the unchanged production baseline gives 39 passed / 5 failed; adding the production fix gives 44 passed / 0 failed. All 38 pre-existing tests pass in both runs. Of the six new tests, five expose defects in the baseline and one protects already-correct handling of an unfinished parameter. All fixtures are synthetic.New regression tests: baseline versus fix
All test names below have the prefix
test_qwen_xml_and live incrates/tool_parser/tests/tool_parser_qwen_xml.rs. The baseline column records the first observed failing assertion in each test; later subcases in that test are not claimed as independently reproduced failures.streaming_braces_inside_string_do_not_close_objectecho '}'value leaves streamed JSON without its root closing brace.eos_closes_only_complete_parameter_valuescity=Tokyoparameter followed by EOS leaves the argument object open.eos_does_not_invent_unfinished_parametercity=Tok, retains the existing{}fallback.eos_closes_last_of_multiple_callscoalesced_calls_do_not_share_parametersunits=celsiusfrom the second call.empty_calls_and_nested_values_are_closed_once{}; the following nested value remains exact; completed calls need no additional argument flush or duplicate closure.The six tests cover whole input, character-by-character input, and every valid two-chunk split. Assertions reconstruct exact semantic JSON per tool index, verify names and terminal getters, and check reset behavior. Completed fixtures require no pending closure. Each test protects a distinct boundary: payload braces, single-call EOS, no completed value, last-call EOS, adjacent-call isolation, or an earlier empty call. Existing suites supply nonstream and factory-mapping coverage.
Original-base commands and broader compatibility checks
Run from the repository root with Rust 1.95 and nightly rustfmt:
The original focused CPU run passed 166 tests with zero failures:
--lib)tool_parser_qwen_xmltool_parser_qwen_dottedtool_parser_nemotrontool_parser_streaming_flushThe family suites verify parser selection and representative nonstream parsing; they do not establish model-serving qualification.
The initial full publication gate was run on
9c87ef5d499604089b25bd1900ea7f6804cbf1dbplus the unchanged patch, on a CPU builder with Rust 1.95:cargo test --lockedcargo clippy --locked --workspace --all-targets -- -D warningscargo +nightly fmt --all -- --checkgit diff --checkRepresentative output from the full workspace test run and final workspace Clippy run:
The first line is the Qwen XML test harness, not the workspace total. The earlier 166-test focused run is overlapping evidence and is not added to the 5,481-test total. Cargo emitted existing workspace-manifest warnings; there were no Clippy lint failures.
The gRPC ChatCompletion streaming caller's terminal getters were checked in source and exercised by the parser tests. The workspace run includes mock transport/gRPC tests, but no live-model Qwen gRPC or GPU serving canary was run. Existing delimiter ambiguity and duplicate parameter names within a single call remain outside this change. There is no configuration migration. Deployment validation remains a separate step; reverting this change restores the previous parser behavior.
Checklist
cargo +nightly fmtpassescargo clippy --workspace --all-targets -- -D warningspasses (documented alternative without OpenCV); tool-parser scoped--all-featuresalso passes