Skip to content

[Bugfix] Fix tool_calls Iterable consumed when debug logging is enabled#34844

Open
wojciech-wais wants to merge 4 commits intovllm-project:mainfrom
wojciech-wais:fix/tool-calls-iterable-consumed-debug-logging
Open

[Bugfix] Fix tool_calls Iterable consumed when debug logging is enabled#34844
wojciech-wais wants to merge 4 commits intovllm-project:mainfrom
wojciech-wais:fix/tool-calls-iterable-consumed-debug-logging

Conversation

@wojciech-wais
Copy link
Copy Markdown

@wojciech-wais wojciech-wais commented Feb 18, 2026

Fixes #34792.

When VLLM_LOGGING_LEVEL=debug is set, tool calling with Mistral models fails with ValueError: Unexpected tool call id ....

Root cause

The OpenAI Python library types tool_calls as Iterable[...] in ChatCompletionAssistantMessageParam. When Pydantic v2 validates a ChatCompletionRequest from Python objects (not from a raw JSON body, as happens in the Anthropic → OpenAI conversion path inside _convert_anthropic_to_openai_request), it wraps Iterable fields in a one-shot lazy iterator. Debug logging then calls chat_req.model_dump_json(), which serialises the model and exhausts the iterator. Subsequent readers — notably the Mistral tokenizer's apply_chat_template — see an empty tool_calls sequence and raise the "Unexpected tool call id" error.

Fix

  1. vllm/entrypoints/chat_utils.py — change tool_calls from Iterable[...] to list[...] in both vLLM-owned TypedDicts (CustomChatCompletionMessageParam and ConversationMessage). This corrects the type annotation and prevents Pydantic from creating lazy iterators for these vLLM-specific message types.

  2. vllm/entrypoints/openai/chat_completion/protocol.py — add a field_validator on ChatCompletionRequest.messages that eagerly materialises any non-list tool_calls to list. This covers the external OpenAI library types (e.g. ChatCompletionAssistantMessageParam) that still annotate tool_calls as Iterable[...] and therefore can produce lazy iterators during Python-object validation.

Purpose

Test Plan

Test Result


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.
  • (Optional) Release notes update. If your change is user facing, please update the release notes draft in the Google Doc.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request effectively resolves the issue where tool_calls iterators were being prematurely exhausted when debug logging was enabled. By changing the type annotation to list in vLLM's internal message types and adding a Pydantic field_validator to eagerly materialize tool_calls in ChatCompletionRequest, the fix ensures that these calls can be accessed multiple times without data loss. The implementation is correct, follows Pydantic v2 best practices for field normalization, and includes comprehensive regression tests. No high or critical severity issues were identified during the review.

Copy link
Copy Markdown
Member

@DarkLight1337 DarkLight1337 left a comment

Choose a reason for hiding this comment

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

Thanks for fixing!

@DarkLight1337 DarkLight1337 enabled auto-merge (squash) February 19, 2026 02:32
@github-actions github-actions bot added the ready ONLY add when PR is ready to merge/full CI is needed label Feb 19, 2026
@NickLucche
Copy link
Copy Markdown
Collaborator

cc @dtrifiro

@DarkLight1337
Copy link
Copy Markdown
Member

auto-merge was automatically disabled February 20, 2026 10:33

Head branch was pushed to by a user without write access

@wojciech-wais wojciech-wais force-pushed the fix/tool-calls-iterable-consumed-debug-logging branch from 8124b2d to 9fac590 Compare February 20, 2026 10:33
@wojciech-wais wojciech-wais force-pushed the fix/tool-calls-iterable-consumed-debug-logging branch from 9fac590 to e665f7a Compare March 18, 2026 20:38
The OpenAI Python SDK types tool_calls as Iterable[...] in
ChatCompletionAssistantMessageParam. When Pydantic v2 validates from
Python objects (not JSON), it wraps Iterable fields in a one-shot
ValidatorIterator. Debug logging via model_dump_json() consumed that
iterator, causing the Mistral tokenizer to see empty tool_calls and
raise "ValueError: Unexpected tool call id ...".

Fix: add a model_validator(mode='after') on ChatCompletionRequest that
materialises any ValidatorIterator tool_calls back to a list after
Pydantic validation completes. Also change vllm's own TypedDicts
(CustomChatCompletionMessageParam, ConversationMessage) to use
list[...] instead of Iterable[...] for tool_calls.

Fixes vllm-project#34792

Signed-off-by: Wojciech Wais <wojciech.wais@gmail.com>
@wojciech-wais wojciech-wais force-pushed the fix/tool-calls-iterable-consumed-debug-logging branch from e665f7a to 6f83a13 Compare March 18, 2026 20:54
@wojciech-wais
Copy link
Copy Markdown
Author

I've rebased changes and fixed failures in test_tool_calls_serialization.py test.

@bbrowning
Copy link
Copy Markdown
Contributor

bbrowning commented Apr 1, 2026

These pydantic lazy iterators have bit me a few times in the past in other places, so solving this generally would be reasonable. There's already some code in both mistral and gpt-oss model paths that do some of this materialization, but only for those models and perhaps not in all cases.

However, some of the added unit tests are still failing. Those will need to be investigated and fixed for this to merge. If you need help root-causing those, let me know and I'm happy to help out.

Thanks!

…ators

Generators and iterators passed as tool_calls were consumed during
Pydantic union type validation before the mode=after validator could
materialise them. Switch to mode=before so we convert iterables to
lists before Pydantic touches them.

Fixes 3 failing tests: test_tool_calls_from_generator_are_materialised,
test_multiple_tool_calls_materialised[1], test_multiple_tool_calls_materialised[3]

Signed-off-by: Wojciech Wais <wojciech.wais@gmail.com>
@wojciech-wais wojciech-wais force-pushed the fix/tool-calls-iterable-consumed-debug-logging branch from 0bcfeb1 to 039068a Compare April 1, 2026 18:02
wojciech-wais and others added 2 commits April 1, 2026 20:03
mode=before: converts generators/iterators to lists before Pydantic
consumes them during union type validation.

mode=after: converts Pydantic ValidatorIterator wrappers back to
plain lists. Pydantic re-wraps even already-list tool_calls in a
ValidatorIterator when validating against the Iterable[...] type
in ChatCompletionAssistantMessageParam.

Both stages are needed: before prevents iterator exhaustion, after
unwraps Pydantic's own lazy wrapper.

Signed-off-by: Wojciech Wais <wojciech.wais@gmail.com>
@wojciech-wais
Copy link
Copy Markdown
Author

wojciech-wais commented Apr 2, 2026

Thanks @bbrowning!

I've investigated and fixed the failing unit tests.

The initial mode="after" model validator wasn't sufficient because Pydantic v2 consumes one-shot generators/iterators during union type validation of ChatCompletionMessageParam (before the "after" validator runs). Switching to mode="before" alone also didn't work - Pydantic re-wraps even plain lists in a ValidatorIterator when validating against Iterable[...] typed fields in the union. The fix uses both a mode="before" validator (catches generators before Pydantic consumes them) and a mode="after" validator (unwraps Pydantic's ValidatorIterator back to lists).

Fixed tests:

  • test_tool_calls_list_preserved_after_model_dump
  • test_tool_calls_from_generator_are_materialised
  • test_tool_calls_list_passthrough
  • test_messages_without_tool_calls_unaffected
  • test_multiple_tool_calls_materialised[1] / test_multiple_tool_calls_materialised[3]

Remaining CI failures:
The only failing job is async-engine-inputs-utils-worker-config-cpu. The failures there are test_extract_tool_calls_anyof_type_conversion and test_extract_tool_calls_anyof_type_conversion_streaming in tests/tool_parsers/test_qwen3coder_tool_parser.py. IMO these are pre-existing issues with the qwen3coder tool parser's anyOf type conversion. This PR does not touch qwen3coder_tool_parser.py or any related code.

@bbrowning
Copy link
Copy Markdown
Contributor

I agree the CI failures you hit here were caused by the merging of #37831 and it looks like it was reverted by #38751. So, if you merge latest main into this you should get that revert and end up with green tests here.

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

Labels

bug Something isn't working frontend ready ONLY add when PR is ready to merge/full CI is needed tool-calling

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Bug]: setting VLLM_LOGGING_LEVEL=debug breaks tool calling

5 participants