Skip to content

[#17574][fix] Complete zero-argument tool calls in the streaming tool parser - #17575

Merged
zhaoyangwang-nvidia merged 3 commits into
NVIDIA:mainfrom
Yigtwxx:fix/base-tool-parser-zero-arg-streaming
Aug 26, 2026
Merged

[#17574][fix] Complete zero-argument tool calls in the streaming tool parser#17575
zhaoyangwang-nvidia merged 3 commits into
NVIDIA:mainfrom
Yigtwxx:fix/base-tool-parser-zero-arg-streaming

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #17574.

BaseToolParser.parse_streaming_increment gates the argument-streaming and
call-completion branch on the truthiness of the parsed argument object
(base_tool_parser.py:226). A tool call with no arguments never satisfies that gate, so
the branch at :239-253 is never entered. Two things follow: the arguments are never
streamed, so the client is left with arguments="", which is not valid JSON, and
self._buffer is never advanced past the completed call, so the call never finishes and
the buffer only grows for the remainder of the request.

A model can express "no arguments" in three ways, and all three hit this. Driving
Qwen3ToolParser chunk by chunk, followed by a trailing All done.:

emitted object streamed parameters final _buffer
{"name": "get_time", "arguments": {}} '' '<tool_call>\n{"name": "get_time", "arguments": {}}\n</tool_call> All done.'
{"name": "get_time"} '' '<tool_call>\n{"name": "get_time"}\n</tool_call> All done.'
{"name": "get_time", "arguments": null} '' '<tool_call>\n{"name": "get_time", "arguments": null}\n</tool_call> All done.'

An empty object is falsy; a missing key and an explicit null both make
current_tool_call.get("arguments") return None. detect_and_parse on the same text
returns parameters='{}' for the first two, so this is a divergence between the streamed
and non-streamed response for the same generation.

The change is in two parts:

  • Gate on is not None rather than truthiness, which covers the empty object.
  • Normalize a missing or null arguments object to {}, but only once the call's JSON is
    complete. Restricting it to is_current_complete keeps the partial path intact: while
    the JSON is still unclosed a missing key only means "not streamed yet", so it must not
    be mistaken for an empty object and flushed early. An unclosed object still falls
    through to the elif prev_arguments: branch at :256, which keeps its own falsy guard.

After the change all three shapes emit {} and advance the buffer past the call.

Qwen3ToolParser is the parser that reaches this code, through _wrapped_streaming, so
this covers the models that resolve to the qwen3 tool parser: qwen2, qwen3,
qwen3_moe, qwen3_5, qwen3_5_moe and qwen3_next. Glm4ToolParser and
Glm47ToolParser implement their own streaming paths and already emit "{}" here, which
is where the expected behaviour comes from. No API change.

A third part was added on review, on the non-streaming side. parse_base_json:83 reads
act.get("parameters") or act.get("arguments", {}), and that default only applies when
the key is absent, so a present-but-null value was dumped as "null" while streaming
completed the same call with "{}" — the same stream/non-stream divergence this PR is
about, left in place for a third of the cases. It now reads
act.get("parameters") or act.get("arguments") or {}. Two consequences worth stating:
or {} normalizes every falsy value, so "arguments" of [], "" or 0 become {}
where they used to be dumped as-is — all malformed for a tool call, and the
act.get("parameters") or ... on the left already behaved that way — and
parse_base_json is shared, so this reaches Qwen3, GLM4, GLM4.7 and DeepSeek
V3/V3.1/V3.2. The DeepSeek and GLM parsers build their match_result with a
"parameters" key, so the arguments leg is unreachable from them, and no test asserts
"null" for this shape.

Out of scope

Review raised a second symptom that an earlier revision of this description attributed to
the same cause: after a completed call, later chunks are routed back into the tool-call
branch instead of being emitted as content. That is real, but it has a different cause and
this PR does not fix it. base_tool_parser.py:136-138 also enters the tool-call branch
when current_tool_id > 0 and current_text.startswith(self.tool_call_separator). For
Qwen3ToolParser the separator is "\n" and eot_token is "\n</tool_call>", so the
remainder after any completed call begins with the separator by construction of the
format. It reproduces on main with ordinary non-empty arguments, independently of the
argument-truthiness gate, and fixing it means changing when that clause is allowed to
match — a separate concern in a method shared by every parser inheriting the base
streaming path. Tracked separately as #17740.

Test Coverage

tests/unittest/llmapi/apps/test_tool_parsers.py, already registered as cpu_only in
tests/integration/test_lists/test-db/l0_cpu.yml:

  • TestQwen3ToolParser::test_streaming_zero_arg_tool streams a zero-argument call in four
    chunks and asserts the emitted arguments are {}, that detect_and_parse on the same
    text agrees, and that the call is consumed from the buffer. It is parametrized over the
    three shapes above (empty_object, key_absent, explicit_null). It mirrors
    TestGlm47ToolParser::test_streaming_zero_arg_tool and
    TestGlm4ToolParser::test_streaming_no_args, which pin the same behaviour for those
    parsers.

  • TestBaseToolParser::test_parse_base_json_null_arguments pins the non-streaming half,
    next to the existing test_parse_base_json_missing_parameters: an explicit
    "arguments": None now resolves to {} like an absent key does.

All three streaming cases fail on main with assert '' == '{}' and pass with this
change; all three now also assert parameters == "{}" on the one-shot path, which no
longer diverges. Reverting the one-token parse_base_json change fails exactly
test_parse_base_json_null_arguments and test_streaming_zero_arg_tool[explicit_null].
The rest of the file is unchanged and still passes, including every other parser that
inherits the base streaming path.

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

Dev Engineer Review

  • Normalizes missing and null arguments to {}.
  • Completes zero-argument streaming tool calls and clears the buffer.
  • Keeps streaming and non-streaming behavior consistent.
  • Preserves the public API.
  • No configuration or test-list files changed.

QA Engineer Review

  • Updated tests/unittest/llmapi/apps/test_tool_parsers.py.
  • Added coverage for empty-object, omitted, and null arguments.
  • Tests verify {} output, one-shot parsing, call completion, and buffer cleanup.
  • No corresponding test-db/ or qa/ entries were identified.
  • Verdict: needs follow-up.

@Yigtwxx
Yigtwxx requested a review from a team as a code owner August 12, 2026 19:16
@Yigtwxx

Yigtwxx commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@zhaoyangwang-nvidia @JunyiXu-nv could one of you trigger a pipeline run when you have a moment? /bot run does not work from my account.

The change is one condition in BaseToolParser.parse_streaming_increment and the new test is cpu_only. It fails on main with assert '' == '{}' and passes here, and the rest of test_tool_parsers.py is unaffected, including the other parsers that go through the base streaming path.

Related but separate: #17572 / #17573 covers a text-loss bug in the DeepSeek tool parsers. The two do not overlap in files.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 305beffb-f54e-4fb2-85da-b4df5f9377fe

📥 Commits

Reviewing files that changed from the base of the PR and between 3d4e919 and 5d3e4f5.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/tool_parser/base_tool_parser.py
  • tests/unittest/llmapi/apps/test_tool_parsers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unittest/llmapi/apps/test_tool_parsers.py

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


Walkthrough

The tool parser normalizes null, omitted, and empty arguments to {} in non-streaming and completed streaming calls. Qwen3 tests verify emitted parameters, one-shot parsing, call completion, and buffer cleanup.

Changes

Tool argument normalization

Layer / File(s) Summary
Normalize tool arguments
tensorrt_llm/serve/tool_parser/base_tool_parser.py
Non-streaming and completed streaming calls normalize null or missing arguments to {}. Streaming parsing processes empty argument objects and clears completed-call buffers.
Validate normalized arguments
tests/unittest/llmapi/apps/test_tool_parsers.py
Tests cover null arguments in base parsing and empty, omitted, and null arguments in Qwen3 streaming calls. Tests verify streamed {} parameters, one-shot parsing, call completion, and buffer cleanup.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 5d3e4

This localized change completes zero-argument tool calls consistently in streaming and non-streaming parsing and adds focused coverage; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: junyixu-nv

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the fix for zero-argument tool calls in the streaming parser and uses the required issue and type format.
Description check ✅ Passed The description explains the problem, solution, scope, test coverage, and checklist status. It provides sufficient technical and validation detail.
Linked Issues check ✅ Passed The changes satisfy issue #17574 by completing zero-argument streaming calls, emitting normalized "{}" arguments, advancing the buffer, aligning non-streaming behavior, and adding coverage for empty, …
Out of Scope Changes check ✅ Passed The code changes remain within the linked issue scope. The non-streaming null normalization supports the required stream/non-stream consistency, and the separately identified post-completion content-r…
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 2 files.
Full details: Linked Issues check

Explanation

The changes satisfy issue #17574 by completing zero-argument streaming calls, emitting normalized "{}" arguments, advancing the buffer, aligning non-streaming behavior, and adding coverage for empty, omitted, and null arguments.

Full details: Out of Scope Changes check

Explanation

The code changes remain within the linked issue scope. The non-streaming null normalization supports the required stream/non-stream consistency, and the separately identified post-completion content-routing issue is explicitly excluded.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
tests/unittest/llmapi/apps/test_tool_parsers.py (1)

1053-1057: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover response content after the completed tool call.

Line 1057 checks that the opening marker is absent, but the test does not send response content after </tool_call>. Add a follow-up chunk and assert that it produces normal_text with no tool calls. This directly covers the PR objective for subsequent response content.

Suggested assertion
         assert "<tool_call>" not in parser._buffer
+        follow_up = parser.parse_streaming_increment(
+            "The current time is 12:00.", tools)
+        assert follow_up.calls == []
+        assert follow_up.normal_text == "The current time is 12:00."
🤖 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 `@tests/unittest/llmapi/apps/test_tool_parsers.py` around lines 1053 - 1057,
Extend the completed tool-call test around parser.detect_and_parse by appending
a response-content chunk after </tool_call>, then assert the follow-up result
contains that content as normal_text and has no tool calls. Keep the existing
buffer-consumption assertions intact.
🤖 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.

Nitpick comments:
In `@tests/unittest/llmapi/apps/test_tool_parsers.py`:
- Around line 1053-1057: Extend the completed tool-call test around
parser.detect_and_parse by appending a response-content chunk after
</tool_call>, then assert the follow-up result contains that content as
normal_text and has no tool calls. Keep the existing buffer-consumption
assertions intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 302aa629-aef1-472d-af42-860b32485833

📥 Commits

Reviewing files that changed from the base of the PR and between 3612c80 and 8a9aa86.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/tool_parser/base_tool_parser.py
  • tests/unittest/llmapi/apps/test_tool_parsers.py

@zhaoyangwang-nvidia zhaoyangwang-nvidia 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.

Approve with nits.

Comment thread tensorrt_llm/serve/tool_parser/base_tool_parser.py
Comment thread tests/unittest/llmapi/apps/test_tool_parsers.py
@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66612 [ run ] triggered by Bot. Commit: d58427d Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66612 [ run ] completed with state FAILURE. Commit: d58427d
/LLM/main/L0_MergeRequest_PR pipeline #54235 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Yigtwxx

Yigtwxx commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@zhaoyangwang-nvidia the fold-in is pushed as 8f763f59b5, so this needs another
/bot run when you have a moment.

On the previous one: #66612 came back FAILURE 32 minutes after it was
triggered, which is far short of what a full L0 pre-merge takes here (the runs on
my other PR take three to four hours), so I read it as the pipeline stopping
before the test stage rather than in it. Every GitHub-side check on d58427d was
green, and the Jenkins, CI-report and failure-analysis links are internal-only
from outside NVIDIA, so I cannot tell what actually failed.

If the new run is red too, could you paste the failing stage name or the first
few lines of its log? #17573 has now ended in FAILURE four times with the same
signature on a completely disjoint change, which makes me suspect it is not the
diff — but I would rather see it than guess.

@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66921 [ run ] triggered by Bot. Commit: 8f763f5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #66921 [ run ] completed with state FAILURE. Commit: 8f763f5
/LLM/main/L0_MergeRequest_PR pipeline #54473 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Yigtwxx

Yigtwxx commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@zhaoyangwang-nvidia when you have a moment, could I ask for one more /bot run on
8f763f5? No code change from me — the reason is evidence from my other PR rather than
an argument.

#17573 ran three pipelines on its final commit 8b382f6 with no push between them:
#66129 FAILURE, #66605 FAILURE (--disable-fail-fast), then #66989 SUCCESS, and
it merged this morning as bbfeecf8. All three failures carried the same signature the
runs on this PR carry — no L0 test tally published in the blossom-ci description, every
GitHub-side check on the commit green — and the tree that produced them was byte-identical
to the one that passed.

Both runs here match that signature and stopped early: #66612 on d58427d finished 32
minutes after the trigger and #66921 on 8f763f5 34 minutes, against the three to four
hours a run that reaches the test stage takes. Since the failure detail links are on the
internal network I still cannot see the stage, so I would rather not change the diff on
the strength of those two runs — the one thing that has now been measured three times in
this repo (#17159, #17157, #17573) is that a red verdict here is not a property of the
commit.

Happy to act immediately if a run does surface something in the code.

@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #67235 [ run ] triggered by Bot. Commit: 8f763f5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #67235 [ run ] completed with state FAILURE. Commit: 8f763f5
/LLM/main/L0_MergeRequest_PR pipeline #54765 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Yigtwxx
Yigtwxx force-pushed the fix/base-tool-parser-zero-arg-streaming branch from 8f763f5 to 1ae1af9 Compare August 19, 2026 14:03
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@Yigtwxx

Yigtwxx commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@zhaoyangwang-nvidia I rebased onto current main (2c1be7d5) and force-pushed as 1ae1af9. Could I ask for a /bot run on that commit when you have a moment?

The reason I touched the branch this time rather than asking for a fourth run on 8f763f5: the old merge base was 3d3d7c91 (Aug 12) and main had moved 221 commits past it, over the 200-commit limit in .github/scripts/pr_base_freshness_check.py:49. The cached green PR Base Freshness on this PR was from the Aug 17 push — the workflow only runs on synchronize, so it had not re-evaluated since the branch went stale. It has now re-run on the new head and passes.

The diff is unchanged. base_tool_parser.py has had no upstream commit since the old merge base, so both hunks applied byte-identically; test_tool_parsers.py picked up the +132 lines that #17573 and #17903 added, with no conflict. git diff main..HEAD is still the same 2 files, +76/-2.

Re-verified against the rebased tree, not just the old one: the four cases this PR adds pass, reverting base_tool_parser.py to main fails exactly those four (test_parse_base_json_null_arguments and the three test_streaming_zero_arg_tool ids), and the rest of the file is unaffected either way.

On the three runs against 8f763f5 — #66612, #66921 and #67235 — I still cannot say what failed. All three ended 32-35 minutes after the trigger, against the three to four hours a run that reaches the test stage took on my other PRs, and the Jenkins, CI-report and failure-analysis links are internal-only from outside NVIDIA. A current base at least removes one variable. If the new run is red too, the failing stage name or the first few lines of its log would be enough for me to act on.

@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #68655 [ run ] triggered by Bot. Commit: 1ae1af9 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #68655 [ run ] completed with state FAILURE. Commit: 1ae1af9
/LLM/main/L0_MergeRequest_PR pipeline #56061 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Yigtwxx

Yigtwxx commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@zhaoyangwang-nvidia thanks for running #68655 with --disable-fail-fast — that one behaved differently from the previous three. It ran 03:06 → 05:59 (~2h53m), where #66612, #66921 and #67235 all came back FAILURE after 32–35 minutes. So this looks like the first run that actually executed the pipeline rather than aborting early, which is useful progress even though the verdict is red.

I can't see what failed: nv/trt-llm-cicd/... and the pbss.s8k.io failure-analysis page are both unreachable from outside the NVIDIA network (the latter times out rather than 403s). Could you paste the failing stage names and test IDs, or the summary section of the failure analysis for pipeline #56061?

What I can verify locally on 1ae1af9, in case it helps narrow it down:

  • tests/unittest/llmapi/apps/test_tool_parsers.py is the only file in the repo that references BaseToolParser, parse_base_json or parse_streaming_increment, so it is the only test file that covers the changed code.
  • Running that file: 323 passed on the branch vs 319 passed on main at the same base. The +4 are this PR's new cases (test_parse_base_json_null_arguments plus the three parametrizations of test_streaming_zero_arg_tool). The 19 failures are byte-identical before and after the change — they need the compiled package (guided decoding, reasoning parsers, openai_server), which I can't build here, so they are a fixed baseline on this machine, not a regression.
  • pre-commit is clean on both changed files. ruff-legacy reports 28 "regressions", but ruff-legacy-baseline.json already records exactly 28 for these two files (base_tool_parser.py: D200×1, D205×2, D212×6, D301×1; test_tool_parsers.py: D202×18), and the reported codes match one-for-one. That is the known Windows path-separator artifact in the baseline lookup, not new lint.
  • main has moved 121 commits since the branch base (2c1be7d5), but none of them touch tensorrt_llm/serve/tool_parser/ or test_tool_parsers.py, so the branch is not semantically stale against those files. Happy to rebase onto current main anyway if the failures look like staleness rather than something in this diff.

If the failures turn out to be in stages unrelated to tool parsing, I'd rather not keep asking for re-runs blind — knowing which stage broke is enough for me to either fix it or say clearly that it's not from this PR.

@Yigtwxx

Yigtwxx commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my comment above, before anyone spends time on it.

I checked the two public signals and #68655 published no test tally. The blossom-ci commit status on 1ae1af9 is the bare Bot Pipeline Run FAILURE, L0 Pipeline FAILURE with no N passed, N failed, and there is no L0-Test workflow run anywhere in the 03:00–07:00Z window (that workflow is what publishes the counts, and it only runs for pipelines that reached the test stage).

So despite the ~2h53m runtime, this run does not look like it got to the tests either. The duration read I gave above was wrong — #68655 has the same countless-failure signature as #66612, #66921 and #67235, which points at build/infra rather than at this diff.

That narrows the ask rather than changing it: if you can see the stage list for pipeline #56061, all I really need is which stage broke. If it's a build or setup stage, another /bot run is probably all this needs and I shouldn't be touching the code. The local verification in my previous comment is unaffected.

…g tool parser

BaseToolParser.parse_streaming_increment gated the argument-streaming and
call-completion branch on the truthiness of the parsed argument object. An empty
object is falsy, so a tool call with no arguments never emitted its arguments and
was never consumed from the buffer: the client was left with arguments="", which
is not valid JSON, and has_tool_call stayed true for the rest of the request, so
every later chunk was routed back into the tool-call branch instead of being
emitted as content.

Gate on "is not None" instead. The partial path is unaffected because the
elif prev_arguments branch keeps its own falsy guard, so no premature "{}" is
emitted while the JSON is still incomplete.

Qwen3ToolParser is the parser that reaches this code, through _wrapped_streaming.
Glm4ToolParser and Glm47ToolParser already assert the expected behaviour for their
own streaming paths, so the new test mirrors theirs.

Signed-off-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
…e arguments key

Review follow-up. Gating on "is not None" fixed the empty-object shape but left
two equivalent ones dead-ending in exactly the same way: a model can express "no
arguments" by omitting the key entirely, or by emitting an explicit null. Both
make current_tool_call.get("arguments") return None, so the completion branch is
skipped, the buffer never advances past the call, and _buffer only grows for the
rest of the request.

Normalize a missing or null arguments object to {} once the call's JSON is
complete. Restricting it to is_current_complete keeps the partial path intact:
while the JSON is still incomplete a missing key only means "not streamed yet",
so it must not be mistaken for an empty object and flushed early.

This matches parse_base_json, which already resolves a missing key to {} on the
non-streaming path. It still dumps an explicit null as "null" there; the new
parametrization asserts that so the divergence stays visible.

Signed-off-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
…in parse_base_json

act.get("arguments", {}) only defaults when the key is absent, so a
present-but-null value was dumped as "null" while the streaming path
completes the same call with "{}". Both paths now agree on {}.

`or {}` normalizes every falsy value, not just None: "arguments" of [], ""
or 0 become {} where they used to be dumped as-is. All are malformed for a
tool call, and act.get("parameters") or ... on the left of the same
expression already has that property, so it is not a new hazard.

parse_base_json is shared by Qwen3, GLM4, GLM4.7 and DeepSeek V3/V3.1/V3.2;
none of their tests assert "null" for this shape.

Signed-off-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
@zhaoyangwang-nvidia
zhaoyangwang-nvidia force-pushed the fix/base-tool-parser-zero-arg-streaming branch from 1ae1af9 to 5d3e4f5 Compare August 25, 2026 01:43
@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

/bot run --disable-fail-fast

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

1 similar comment
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

Correction to my comment above, before anyone spends time on it.

I checked the two public signals and #68655 published no test tally. The blossom-ci commit status on 1ae1af9 is the bare Bot Pipeline Run FAILURE, L0 Pipeline FAILURE with no N passed, N failed, and there is no L0-Test workflow run anywhere in the 03:00–07:00Z window (that workflow is what publishes the counts, and it only runs for pipelines that reached the test stage).

So despite the ~2h53m runtime, this run does not look like it got to the tests either. The duration read I gave above was wrong — #68655 has the same countless-failure signature as #66612, #66921 and #67235, which points at build/infra rather than at this diff.

That narrows the ask rather than changing it: if you can see the stage list for pipeline #56061, all I really need is which stage broke. If it's a build or setup stage, another /bot run is probably all this needs and I shouldn't be touching the code. The local verification in my previous comment is unaffected.

@Yigtwxx Sorry for the slow turnaround — I was out on PTO the last couple of days.

Your read on #68655 is right: it never got to your tests. Here is the stage breakdown for pipeline #56061 (1ae1af9), and none of it points at your diff.

Failing orchestrator stages: [Test-x86_64-Single-GPU] Remote Run and [Test-SBSA-Single-GPU] Remote Run, plus the follow-on Collect Test Result / Rerun Report / Test Coverage. Four independent causes, all of them before any test executes:

Test collection produced an empty list. pytest --collect-only --test-list=.../l0_gb10_cleaned.txt returned no tests collected (4237 deselected), exit code 5, then Test collection failed for shard 1/1. Cannot proceed without valid test list. This is a CI test-db problem — the same family as the INVALID TEST NAME FILTERS error on unittest/disaggregated/test_openai_disagg_server.py that killed the earlier run #54765 in Check Test List.
Kubernetes pod launch timeout on the x86 job: KubernetesClientTimeoutException: Timed out waiting for [Pod] h100-cr-...-l0-test-1363-... in namespace sw-tensorrt.
Slurm monitor lost contact: InfraFailure: SLURM job 16476961 for DGX_H100-PyTorch-4 is still in non-terminal state RUNNING (8min of 240min walltime); the monitor lost contact while the job was alive. The pipeline itself classifies this as transient infra.
pip backtracked into a broken old uvicorn sdist while preparing the environment: uvicorn-0.2.1.tar.gz → FileNotFoundError: [Errno 2] No such file or directory: 'README.md'.
For what it's worth, the stages that did reach your file were green on both architectures: test_unittests_v2[unittest/llmapi/apps/test_tool_parsers.py] ... PASSED on CPU-Generic-x86-1 and on CPU-Generic-arm-1. So your local 323-vs-319 result is consistent with what CI saw — none of these failures ask you to touch the code.

That said, the branch had gone stale again — it was 134 commits behind main, and several of these test-list/test-db breakages are already fixed upstream, so re-running the same commit just keeps re-testing a known-bad environment. So I went ahead and rebased it onto current main for you and re-triggered CI

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #68945 [ run ] triggered by Bot. Commit: 5d3e4f5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #68945 [ run ] completed with state FAILURE. Commit: 5d3e4f5
/LLM/main/L0_MergeRequest_PR pipeline #56327 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Yigtwxx

Yigtwxx commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

@zhaoyangwang-nvidia thank you for the stage breakdown and for doing the rebase — that answered the question I was stuck on, and the test_unittests_v2[unittest/llmapi/apps/test_tool_parsers.py] ... PASSED line on both architectures is exactly the signal I could not see from outside.

#68945 on 5d3e4f5 looks like the same class of failure again, not a new one. The two public signals say it did not reach the test stage:

  • The blossom-ci status on 5d3e4f5 is the bare Bot Pipeline Run FAILURE, L0 Pipeline FAILURE with no N passed, N failed tally.
  • There is no L0-Test workflow run for 5d3e4f5 at all. The pipeline ran 01:49 → 04:18Z; the most recent L0-Test run before that window is 00:44Z on an unrelated commit, and none was created during it.

So the rebase did not change the outcome, which I think rules out staleness as the cause — the four things you listed (empty test-list collection on l0_gb10_cleaned.txt, the k8s pod timeout, the Slurm monitor losing contact, and pip backtracking to uvicorn-0.2.1) are all environment-side, and none of them care which commit is under test.

Re-verified locally on the rebased head, in case it is useful:

  • The rebase preserved the branch exactly. git range-diff 2c1be7d5..1ae1af9 3d4e9192..5d3e4f5 marks all three commits = — no content changed, and the diff is still 76 insertions / 2 deletions across base_tool_parser.py and test_tool_parsers.py.
  • The baseline moved with main, so my earlier numbers no longer apply: main has added cases to this file that need the compiled package. On the new base 3d4e9192 the file is 26 failed, 331 passed; on 5d3e4f5 it is 26 failed, 335 passed. The two failure lists are byte-identical (empty diff), and the +4 are this PR's own cases — test_parse_base_json_null_arguments plus the three parametrizations of test_streaming_zero_arg_tool. The 26 are the guided-decoding / reasoning-parser / Kimi-K3 tests my box cannot run, failing the same way before and after.
  • Nothing in main touches tensorrt_llm/serve/tool_parser/ or test_tool_parsers.py between the new base and current main (16 commits), so the branch is not drifting again in any way that matters here.

Given that, I do not think there is anything left on my side to fix — but I would rather hear that from you than assume it. If the test-db and infra fixes you mentioned have landed since, another /bot run on 5d3e4f5 is all this should need; if they have not, I am happy to just wait rather than keep burning pipeline slots.

@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

@Yigtwxx One correction: pipeline #56327 did reach the test stage, so the "no L0-Test run, no tally" signal did not hold this time. L0_Test-SBSA-Single-GPU #1481 was fully green; the only failure was on x86, in [DGX_B200-PyTorch-2] Run Pytest:

unittest/_torch/visual_gen/test_wan22_ti2v_5b_pipeline.py
  TestWan22TI2V5B_T2V_PipelineCorrectness::test_cosine_similarity
    :263  assert plain_call is not None and plain_call.call_count > 0   # _fused_pertoken_adaln call_count == 0
  TestWan22TI2V5BCombinedOptimizations::test_fp8_cache_dit_trtllm
    :479  assert all(block._pertoken_adaln._enabled for block in ...)   # False

That is the Wan2.2 TI2V-5B / Cache-DiT visual-generation path — no overlap with tool_parser/. Your own file passed on both architectures again.

It is also not specific to this PR: across builds 56290-56340 (different PRs) that same test file is 12 FAILED / 6 PASSED with an identical signature, and it is not in waives.txt. So it is an intermittent regression on main owned by the visual-gen side, not something your diff can influence.

Nothing to fix here. Re-triggering now — if it lands on the ~1/3 of runs where Wan2.2 passes, this should go green.

@zhaoyangwang-nvidia

Copy link
Copy Markdown
Collaborator

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69028 [ run ] triggered by Bot. Commit: 5d3e4f5 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69028 [ run ] completed with state SUCCESS. Commit: 5d3e4f5
/LLM/main/L0_MergeRequest_PR pipeline #56403 completed with status: 'SUCCESS'

CI Report

Link to invocation

@zhaoyangwang-nvidia
zhaoyangwang-nvidia merged commit 46f0b7e into NVIDIA:main Aug 26, 2026
10 checks passed
@Yigtwxx
Yigtwxx deleted the fix/base-tool-parser-zero-arg-streaming branch August 26, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Zero-argument tool calls never complete in the streaming tool parser

3 participants