Skip to content

fix(tokenizers): don't end a Qwen3-Coder function at </function> inside a parameter value - #9097

Open
pcbeingused333 wants to merge 2 commits into
NVIDIA:mainfrom
pcbeingused333:fix/qwen3-coder-function-close
Open

pcbeingused333 wants to merge 2 commits into
NVIDIA:mainfrom
pcbeingused333:fix/qwen3-coder-function-close

Conversation

@pcbeingused333

Copy link
Copy Markdown

Fixes #8802

What does this PR do?

Qwen3CoderToolParser no longer cuts a tool call at a </function> that sits inside a <parameter>…</parameter> value.

Root cause

_get_function_calls extracted function bodies with <function=(.*?)</function>, which stops at the first </function> even when it is part of a parameter value (e.g. the model writes a snippet containing </function>). The call was truncated and every later parameter was dropped. </tool_call> inside a parameter was already handled by _find_tool_call_end; </function> was not.

Fix

_find_tool_call_end now takes the closing-tag pattern, and function extraction reuses that parameter-aware scan for </function>. Unclosed functions still take the rest of the text, as before.

Test

Added test_qwen3_coder_function_end_inside_value_does_not_terminate_the_function (fails before, passes after); the existing Qwen3-Coder/tool-parser tests in tests/unit_tests/tokenizers/test_text_parsers.py still pass. With the issue's repro, both </function> and </function><function=other></function> now yield a single write_file call with content and path intact.

Linked issue: Fixes #8802

Code provenance

No code copied from other repositories.

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • If this PR adds or changes a GPU kernel … (N/A: no kernel changes)
  • I have added proper typing to my code
  • I have added relevant documentation (N/A)
  • I have run the autoformatter (black/isort pass on the changed files)

Disclosure: this change was prepared with the assistance of an AI coding agent (Claude Code).

…de a parameter value

The function extraction used a non-greedy regex that stopped at the first
</function>, even inside a complete <parameter>...</parameter> block, which
truncated the call and dropped later parameters. Reuse the parameter-aware
scan already used for </tool_call>.

Fixes NVIDIA#8802

Signed-off-by: Alex Castillo <alex.castillog33@gmail.com>
@pcbeingused333
pcbeingused333 requested review from a team as code owners November 7, 2026 15:21
@copy-pr-bot

copy-pr-bot Bot commented Nov 7, 2026

Copy link
Copy Markdown

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.

@svcnvidia-nemo-ci
svcnvidia-nemo-ci marked this pull request as draft November 7, 2026 15:21
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

This PR has been automatically converted to draft because all PRs must start as drafts.

When you are ready for review, click Ready for Review to begin the review process. This will:

  1. Add the oncall reviewer (optional reviewer)
  2. Add required review teams based on your changes

See the contribution guide for more details.

@pcbeingused333
pcbeingused333 marked this pull request as ready for review November 8, 2026 15:21
@svcnvidia-nemo-ci
svcnvidia-nemo-ci requested a review from a team November 8, 2026 15:22
body_start = start_match.end()
# Like `</tool_call>`, a `</function>` inside a complete
# <parameter>...</parameter> block is part of the value.
end_index = self._find_tool_call_end(tool_call, body_start, self._function_end_re)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Using _find_tool_call_end for every </function> makes long, otherwise valid function payloads quadratic in their parameter count: inside the loop, end_re.search(text, pos) scans from each parameter to the final closing tag. With '<function=fn>' + '<parameter=p>x</parameter>'*N + '</function>', I measured ~0.006 s at N=1,000, ~0.022 s at 2,000, ~0.086 s at 4,000 and ~0.34 s at 8,000 (roughly 4x per doubling). This undermines the claimed linear scan and can stall larger generated calls. Could we locate the candidate end tag once and advance the parameter scan without re-searching the same suffix, with a scaling regression?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the update, @pcbeingused333. I rechecked c7ffa3e using the exact _find_tool_call_end method in an isolated Python harness. A 2,000-parameter payload took about 1.7 ms and 16,000 parameters about 5.8 ms (best of three), and an embedded </function> stayed inside its parameter value. That resolves the repeated-suffix-scan issue I raised. Thanks for adding the scaling regression. I haven't run the full Megatron suite.

Reuse the candidate end tag across parameters instead of re-searching the
same suffix after every parameter, which made long calls quadratic in their
parameter count. Add a scaling regression test.

Signed-off-by: Alex Castillo <alex.castillog33@gmail.com>
@pcbeingused333

Copy link
Copy Markdown
Author

Thanks for the review. _find_tool_call_end is no longer re-run for every </function>: the function extraction now reuses the single parameter-aware scan and the existing end_match, so the cost is linear in the number of parameters. Pushed in c7ffa3e, with a scaling test (2000 vs 16000 parameters) that failed on the previous parser and passes now.

This comment was drafted with Claude Code.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Qwen3-Coder parser treats function tags inside string arguments as call boundaries

2 participants