Skip to content

Conversation

@2ez4bz
Copy link
Contributor

@2ez4bz 2ez4bz commented Nov 17, 2025

Overview:

This PR adds a tool call parser for Qwen3 coder using SGLang's implementation as a reference.

Details:

This PR tries to follow the pattern for existing parser implementations to add a new one for Qwen3 coder.

Where should the reviewer start?

Probably the unit tests serve as a good starting point.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

  • closes GitHub issue: #xxx

Summary by CodeRabbit

  • New Features

    • Added support for the Qwen3Coder tool-call format, including detection and parsing of tool calls in messages.
  • Tests

    • Added comprehensive tests for the new format covering detection, parsing, edge cases, and mixed scenarios.
  • Chores

    • Updated development dependencies.

@copy-pr-bot
Copy link

copy-pr-bot bot commented Nov 17, 2025

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

let function_regex = FUNCTION_REGEX.get_or_init(|| {
// Match <function=name>content</function> or partial <function=name>content
// (?s) makes . match newlines
Regex::new(r"(?s)<function=([^>]+)>(.*?)(?:</function>|$)").unwrap()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Per https://github.com/sgl-project/sglang/blob/e389f91decdad61653edc57c765ef6041506e4a2/python/sglang/srt/function_call/qwen3_coder_detector.py#L52, this matches both properly terminated function blocks like:

<function=foo>...</function>

as well as blocks that are missing a trailing </function>:

<function=foo>...

Ditto for the parameter_regex below.

}

/// Simple HTML unescape for common entities.
fn html_unescape(s: &str) -> String {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@2ez4bz 2ez4bz force-pushed the dev-qwen3-coder-parser branch from a4923fe to 6127f58 Compare November 18, 2025 06:14
@2ez4bz 2ez4bz marked this pull request as ready for review November 18, 2025 06:14
@2ez4bz 2ez4bz requested a review from a team as a code owner November 18, 2025 06:14
@2ez4bz
Copy link
Contributor Author

2ez4bz commented Nov 18, 2025

/ok to test

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 18, 2025

Walkthrough

Adds a new Qwen3Coder tool-calling parser (detection, end-position, and parsing) integrated into the tool-calling framework and a dev-test dependency update (rstest).

Changes

Cohort / File(s) Summary
Build Configuration
lib/parsers/Cargo.toml
Added rstest = "0.25" as a dev-dependency.
Tool-calling Configuration
lib/parsers/src/tool_calling/config.rs
Added Qwen3Coder variant to ToolCallParserType and pub fn qwen3_coder() -> Self constructor on ToolCallConfig.
Tool-calling Module Exports
lib/parsers/src/tool_calling/mod.rs
Added pub mod qwen3_coder; and pub use qwen3_coder::try_tool_call_parse_qwen3_coder;.
Tool-calling Parser Integration
lib/parsers/src/tool_calling/parsers.rs
Registered and routed the Qwen3Coder parser: added to parser map, extended detect_tool_call_start / find_tool_call_end_position branching, and dispatch in try_tool_call_parse; updated tests to include Qwen3Coder scenarios.
Qwen3Coder Parser Implementation
lib/parsers/src/tool_calling/qwen3_coder/mod.rs, lib/parsers/src/tool_calling/qwen3_coder/parser.rs
New submodule exposing detect_tool_call_start_qwen3_coder, find_tool_call_end_position_qwen3_coder, try_tool_call_parse_qwen3_coder, plus parsing implementation: extraction of <parameter=...>… blocks, safe value parsing, HTML entity unescape, and tests for many edge cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay attention to integration points and dispatch in parsers.rs.
  • Review parsing correctness and edge-case handling in qwen3_coder/parser.rs (parameter parsing, HTML unescape, JSON/primitive conversions).
  • Confirm public exposure and config constructor consistency in config.rs.

Poem

🐇 I hopped in code with eager paws,
Found tags and params and parsed their laws.
Qwen3Coder sings in XML rhyme,
JSON and entities parsed in time,
Tests clap softly — now that's applause!

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding a tool parser for Qwen3 coder, which aligns with the changeset.
Description check ✅ Passed The PR description covers all required template sections with appropriate details about the implementation approach, reference material, and review guidance.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
lib/parsers/src/tool_calling/qwen3_coder/parser.rs (2)

171-198: Consider broadening whitespace trimming for consistency.

The function has good defensive parsing with multiple fallbacks. One minor observation: line 197 uses .trim_matches('\n') to strip only newlines from strings, while other parsers in the codebase might use .trim() for all whitespace. This might be intentional for preserving spaces, but worth considering if it aligns with expected behavior.

     // Default to string, stripping newlines from start and end.
-    serde_json::Value::String(unescaped.trim_matches('\n').to_string())
+    serde_json::Value::String(unescaped.trim().to_string())

201-208: Consider a dedicated HTML entity library for completeness.

The simple replacement approach works for common entities and matches the SGLang reference (per your past comments). However, if the Qwen3 model outputs other entities like &nbsp;, &apos;, numeric entities (&#123;), or Unicode (&#x1F680;), they won't be decoded. Consider using a library like html-escape or quick-xml's unescape utilities if broader entity support becomes needed.

For now, this is fine given it matches the reference implementation and handles the expected cases.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3984bc and 7d5927e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • lib/parsers/Cargo.toml (1 hunks)
  • lib/parsers/src/tool_calling/config.rs (2 hunks)
  • lib/parsers/src/tool_calling/mod.rs (2 hunks)
  • lib/parsers/src/tool_calling/parsers.rs (9 hunks)
  • lib/parsers/src/tool_calling/qwen3_coder/mod.rs (1 hunks)
  • lib/parsers/src/tool_calling/qwen3_coder/parser.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T22:32:12.978Z
Learnt from: zhongdaor-nv
Repo: ai-dynamo/dynamo PR: 2999
File: lib/parsers/src/tool_calling/harmony/harmony_parser.rs:250-256
Timestamp: 2025-09-10T22:32:12.978Z
Learning: In lib/parsers/src/tool_calling/harmony/harmony_parser.rs, the team prefers to maintain identical code patterns between parse_tool_calls_harmony and parse_tool_calls_harmony_complete functions, including message.content[0] indexing, to ensure consistency between streaming and complete parser implementations.

Applied to files:

  • lib/parsers/src/tool_calling/parsers.rs
  • lib/parsers/src/tool_calling/qwen3_coder/parser.rs
  • lib/parsers/src/tool_calling/qwen3_coder/mod.rs
🧬 Code graph analysis (3)
lib/parsers/src/tool_calling/mod.rs (2)
lib/parsers/src/tool_calling/config.rs (1)
  • qwen3_coder (182-188)
lib/parsers/src/tool_calling/qwen3_coder/parser.rs (1)
  • try_tool_call_parse_qwen3_coder (51-63)
lib/parsers/src/tool_calling/parsers.rs (2)
lib/parsers/src/tool_calling/config.rs (1)
  • qwen3_coder (182-188)
lib/parsers/src/tool_calling/qwen3_coder/parser.rs (3)
  • detect_tool_call_start_qwen3_coder (17-34)
  • find_tool_call_end_position_qwen3_coder (38-46)
  • try_tool_call_parse_qwen3_coder (51-63)
lib/parsers/src/tool_calling/qwen3_coder/mod.rs (1)
lib/parsers/src/tool_calling/qwen3_coder/parser.rs (3)
  • detect_tool_call_start_qwen3_coder (17-34)
  • find_tool_call_end_position_qwen3_coder (38-46)
  • try_tool_call_parse_qwen3_coder (51-63)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: operator (amd64)
  • GitHub Check: operator (arm64)
  • GitHub Check: sglang (amd64)
  • GitHub Check: sglang (arm64)
  • GitHub Check: vllm (arm64)
  • GitHub Check: trtllm (arm64)
  • GitHub Check: trtllm (amd64)
  • GitHub Check: vllm (amd64)
  • GitHub Check: Build and Test - dynamo
  • GitHub Check: tests (lib/bindings/python)
  • GitHub Check: tests (launch/dynamo-run)
  • GitHub Check: clippy (.)
  • GitHub Check: tests (lib/runtime/examples)
  • GitHub Check: tests (.)
🔇 Additional comments (14)
lib/parsers/Cargo.toml (1)

42-43: LGTM! Appropriate test dependency addition.

The rstest dev-dependency enables clean parameterized testing (as seen in the qwen3_coder parser tests). Since it's test-only, there's no runtime impact.

lib/parsers/src/tool_calling/config.rs (2)

18-19: LGTM! Clear enum variant addition.

The Qwen3Coder variant is well-documented with the format example, making it easy for users to understand the expected structure.


182-188: LGTM! Consistent config constructor pattern.

The unused JsonParserConfig follows the same pattern as the pythonic() constructor, maintaining consistency across parser configs.

lib/parsers/src/tool_calling/mod.rs (1)

9-9: LGTM! Standard module integration.

The module declaration and public re-export follow the established pattern used by other parsers in the codebase.

Also applies to: 24-24

lib/parsers/src/tool_calling/qwen3_coder/mod.rs (1)

1-10: LGTM! Clean module structure.

The module correctly exposes the three main parser functions (detect, find_end, parse) following the same pattern as other parser modules.

lib/parsers/src/tool_calling/parsers.rs (4)

16-19: LGTM! Complete parser integration.

The Qwen3Coder parser is correctly integrated at all dispatch points:

  • Imports added
  • Registered in parser map
  • Handlers added to try_tool_call_parse, detect_tool_call_start, and find_tool_call_end_position

The integration follows the exact pattern used by other parsers.

Also applies to: 38-38, 67-70, 121-121, 157-157


1676-1711: LGTM! Good parallel tool call coverage.

The test verifies that multiple Qwen3Coder tool calls are correctly parsed and validated using the existing test helper infrastructure.


2437-2449: LGTM! Detection tests cover key scenarios.

The tests verify both complete token detection and partial token handling for streaming use cases.


2452-2767: LGTM! Comprehensive test suite.

The Qwen3Coder tests cover:

  • Simple and multiple parameters
  • Normal text handling
  • Parallel tool calls
  • JSON/numeric parameter values
  • HTML entity unescaping
  • Missing closing tags
  • Edge cases (no tool calls, compact format, mixed types, arrays)

This provides strong confidence in the parser implementation.

lib/parsers/src/tool_calling/qwen3_coder/parser.rs (5)

17-34: LGTM! Robust streaming detection.

The partial token matching logic correctly handles streaming scenarios where the start token arrives in chunks.


38-46: LGTM! Simple and correct end position logic.

Returns the position after the end token or the chunk length if not found, which is appropriate for streaming contexts.


66-107: LGTM! Careful extraction with graceful handling.

The function correctly:

  • Separates normal text from tool call blocks
  • Handles multiple tool calls
  • Gracefully handles missing end tokens by treating remaining text as normal content

111-167: LGTM! Well-structured parsing with efficient regex compilation.

The use of OnceLock for regex compilation is good for performance. The regex patterns intentionally handle missing closing tags (per your past review comments and SGLang reference), which provides robustness during streaming.


210-465: LGTM! Excellent test coverage with rstest parameterization.

The tests comprehensively cover:

  • Detection and end position logic
  • Value parsing with multiple types
  • HTML entity unescaping
  • Simple and complex tool calls
  • Missing closing tags (intentional tolerance)
  • Error handling

The use of rstest for parameterized tests (lines 237-261) makes the test cases clean and maintainable.

@rmccorm4 rmccorm4 added the frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` label Nov 18, 2025
Copy link
Contributor

@ayushag-nv ayushag-nv left a comment

Choose a reason for hiding this comment

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

The folder structure is not right. We are not putting model specific parser at top level folder. try to put them into one of the categories and then can have model specific file if required.

Typescript,
Xml,
/// Qwen3Coder format: `<tool_call><function=name><parameter=key>value</parameter></function></tool_call>`
Qwen3Coder,
Copy link
Contributor

Choose a reason for hiding this comment

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

There should not be new type added if its under Xml

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed in favor of Xml.

/// Format: <tool_call><function=name>...
pub fn detect_tool_call_start_qwen3_coder(chunk: &str) -> bool {
// Check for complete or partial start token.
let start_token = "<tool_call>";
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we think more on how to generalize structure for xml based parsers. Don't like harded start, end tokens in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed on slack: will do this in a follow-up. Left some TODO comments alluding to this.

/// Find the end position of a Qwen3Coder tool call.
/// Returns the position after </tool_call> or the length of the chunk if not found.
pub fn find_tool_call_end_position_qwen3_coder(chunk: &str) -> usize {
let end_token = "</tool_call>";
Copy link
Contributor

Choose a reason for hiding this comment

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

Same: Avoid harcoding

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed on slack: will do this in a follow-up. Left some TODO comments alluding to this.

Note: the current implementation is hardcoded for Qwen3 coder.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants