Skip to content

[Bugfix][Tool Parser] Extract every InternLM2 tool-call block - #63354

Open
zavxai wants to merge 1 commit into
vllm-project:mainfrom
zavxai:fix/internlm2-parallel-tool-calls
Open

zavxai wants to merge 1 commit into
vllm-project:mainfrom
zavxai:fix/internlm2-parallel-tool-calls

Conversation

@zavxai

@zavxai zavxai commented Oct 23, 2026 •

Copy link
Copy Markdown

Overview

Internlm2ToolParser raised ValueError: too many values to unpack (expected 2) on any output containing a second <|action_start|><|plugin|> block, and its streaming path dropped every block after the first. Extract a tool call from every block, in both paths. Fixes #44139.

Claims

  • extract_tool_calls returns one ToolCall per block, in order, with the text before the first block preserved as content.
  • extract_tool_calls_streaming emits one DeltaToolCall per block with an incrementing index, including when the whole output arrives in a single delta.
  • Multi-block output no longer raises and no block is silently dropped; single-block output is unchanged.

Validation

Three new tests in tests/tool_parsers/test_internlm2_tool_parser.py:

test path exercised
test_multiple_blocks_extract_every_tool_call non-streaming: two blocks plus leading text
test_multiple_blocks_stream_across_deltas streaming: one block per delta (the case requested in #44140)
test_multiple_blocks_stream_in_one_delta streaming: both blocks in one delta

Fails before the fix — base commit, parser change stashed, new tests only:

$ git stash push vllm/tool_parsers/internlm2_tool_parser.py
$ .venv/bin/python -m pytest tests/tool_parsers/test_internlm2_tool_parser.py -q -k multiple_blocks
>       text, action = new_delta.split("<|action_start|><|plugin|>")
E       ValueError: too many values to unpack (expected 2)
vllm/tool_parsers/internlm2_tool_parser.py:80: ValueError
FAILED tests/tool_parsers/test_internlm2_tool_parser.py::test_multiple_blocks_extract_every_tool_call
FAILED tests/tool_parsers/test_internlm2_tool_parser.py::test_multiple_blocks_stream_across_deltas
FAILED tests/tool_parsers/test_internlm2_tool_parser.py::test_multiple_blocks_stream_in_one_delta
3 failed, 20 deselected

Passes after:

$ .venv/bin/python -m pytest tests/tool_parsers/test_internlm2_tool_parser.py -q
15 passed, 8 xfailed

The 8 xfails are pre-existing and unrelated: with the gpt2 tokenizer used by the test harness the special markers are split across tokens, so a prefix of <|action_start|> leaks as content. test_parallel_tool_calls[True] still fails for that reason (its non-streaming variant now covers both blocks and passes), so it stays xfailed rather than becoming an XPASS.

Whole directory: 1210 passed, 1 skipped, 30 xfailed, 15 errors. All 15 errors are test_llama3_json_tool_parser.py and are environmental on this CPU-only machine — OSError: You are trying to access a gated repo ... meta-llama/Llama-3.2-1B-Instruct ... 401.

Lint/type: ruff check, ruff format --check, and tools/pre_commit/mypy.py local are clean on both changed Python files. CI selection: .buildkite/test_areas/misc.yaml covers vllm/tool_parsers/ and tests/tool_parsers.

No GPU was available, so validation is at the parser unit level, not end-to-end against an InternLM2 checkpoint.

Details

Root cause. Both paths split accumulated text on <|action_start|><|plugin|> and unpack the result into exactly two names, which only holds for one marker. Streaming additionally had if self.current_tool_id > 0: return DeltaMessage(content=""), which stopped parsing for good after the first call, and self.prev_tool_call_arr = [tool_call_arr], which reset its bookkeeping to a single entry.

Fix. The non-streaming path splits on every marker and builds one ToolCall per block. The streaming path loops over the blocks present in current_text, clearing current_tool_name_sent when a new block starts and appending to prev_tool_call_arr / streamed_args_for_tool instead of replacing them. Most of the diff is the existing partial-JSON argument streaming, moved unchanged into _stream_action so it can run per block; _split_action and _stream_blocks are the two new seams.

Deliberate behaviour changes:

  • A block that is already complete when its name is first seen now emits its arguments together with the name. Those arguments were previously only flushed at end of stream by get_remaining_unstreamed_args, which inspects the last tool call only, so earlier blocks' arguments would otherwise be lost.
  • A whole output arriving in one delta now yields both tool calls instead of one.

Unchanged: text before the first block is still returned as content; parameters/arguments handling is untouched; malformed JSON in extract_tool_calls still raises JSONDecodeError; parallel_tool_calls=False is still filtered upstream.

Also removed. In extract_tool_calls, the if not tools or name not in [...]: branch constructed an ExtractedToolCallInformation and never returned it, so the result was discarded and the request's tool list was never actually consulted. It is removed rather than copied into the new per-block loop. Whether that whitelist should be enforced is the subject of #47968, not of this crash fix.

Tradeoff / limitation. The official InternLM2 chat template emits one tool call per turn, so multi-block output is a model-misbehaviour case rather than the normal protocol (same honesty note as on #44140). The value is not crashing and not discarding data when it does happen — and matching the Rust frontend.

Rust. rust/src/parser/src/tool/json/internlm2.rs documented "Python silently drops every <|action_start|> block after the first" as an intentional divergence. That divergence no longer exists, so the stale bullet is dropped. No Rust behaviour changed.

Non-duplication. Checked immediately before opening:

No open PR extracts every block in both paths.

AI assistance. Prepared with AI assistance (OpenCode). The submitter is responsible for the change and the commands above were run locally.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

InternLM2 wraps each tool call in its own
`<|action_start|><|plugin|>...<|action_end|>` block. Both
`extract_tool_calls` and `extract_tool_calls_streaming` split the
accumulated text on the marker and unpack the result into two values,
so an output holding a second block raises
`ValueError: too many values to unpack (expected 2)`. The streaming path
additionally returned an empty delta for every block after the first,
silently dropping it.

Extract a tool call from every block instead. The non-streaming path
emits one `ToolCall` per block; the streaming path emits one
`DeltaToolCall` per block with an incrementing index, resetting the
per-call state when a new block starts. Text before the first block is
still returned as content, and the partial-JSON argument streaming for
the block being generated is unchanged.

This matches the Rust frontend, which already extracts every block and
documented the Python behaviour as a divergence.

The result of the `tools` membership check in `extract_tool_calls` was
discarded rather than returned, so it had no effect; drop it instead of
carrying a dead expression through the new per-block loop.

Fixes vllm-project#44139

Co-authored-by: OpenCode <opencode@users.noreply.github.com>
Signed-off-by: johnvesslyalti <altijohnvessly@gmail.com>

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-23T14:26:48.573956Z
We couldn’t complete this review. Comment @codex review to try again.
4c14c04 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Labels

bug Something isn't working rust tool-calling

Projects

Status: Unsorted queue

Development

Successfully merging this pull request may close these issues.

BUG: ValueError too many values to unpack in internlm2_tool_parser when output contains multiple tool-call markers

1 participant