Repository navigation
[Bugfix][Frontend] Fix tool_reference results on DeepSeek V4.1 and /v1/messages - #64971
Conversation
…names The V4.1 encoder accepts only text and image parts, so a tool_reference part fails the request with a 400. /v1/messages turns the tool_reference items of a tool_result (Claude Code's tool search) into such parts, and the block stays in the history, so every later turn of that conversation fails too. The system prompt already lists every tool, deferred ones included, so rendering the tool name is enough. Signed-off-by: Maroon Ayoub <mayoub@redhat.com>
/v1/messages sends a tool_result with tool_reference items as an empty tool message followed by a reference message for the same call. V4.1 pairs tool results with calls by position, so the extra result put the tool search output and every later parallel result on the wrong call. Merge consecutive tool messages that answer the same call. Signed-off-by: Maroon Ayoub <mayoub@redhat.com>
sfeng33
left a comment
There was a problem hiding this comment.
Thanks for digging into this! I think the root cause is a bit upstream though. The Anthropic converter (_convert_user_tool_result) turns one tool_result with tool_reference parts into two tool messages: an empty one, then one with the references. So every renderer that pairs results with calls by position sees one extra result, not just V4.1.
I checked with vllm launch render. DeepSeek V4 and GLM-5.1 (the template tool_reference support was first built for) both render an extra empty result for a plain tool search result on main. The V4.1 merge also misses two cases: a result with an image (the image's user message sits between the two tool messages) and a result with no tool_use_id.
What do you think about fixing it in the converter instead? It would emit a single tool message per tool_result, with the tool_reference parts first and then any text. Then the merge pass in the V4.1 encoder isn't needed, and you keep your tool_reference-to-name rendering.
A tool_result with tool_reference items became two tool messages for the same call: one with the text (empty for a plain tool search), then one with the references. Templates and encoders that render one result per tool message, such as GLM-5.3's and DeepSeek V4.1's, rendered an extra result, and an image in the result landed between the two messages. Emit one tool message with the references first, which templates that expand references check for, then the text. This replaces the merge pass in the DeepSeek V4.1 encoder. Signed-off-by: Maroon Ayoub <mayoub@redhat.com>
|
Agreed, the split in |
|
/ci run |
|
✅ Triggered Buildkite CI #99040 for commit |
…sults [Backport] Fix V4.1 tool_reference results (upstream vllm-project#64971)
Overview
The DeepSeek V4.1 encoder rejects
tool_referencecontent parts with a 400, and/v1/messagesturns atool_resultwithtool_referenceitems into twotoolmessages for one call. This renders each reference on V4.1 as the tool's name and emits onetoolmessage pertool_result, so conversations that use Claude Code's tool search work on DeepSeek V4.1 and render one result per tool call.Claims
tool_referenceparts render on DeepSeek V4.1. On main they fail withDeepSeek V4.1 supports text and image content only; got 'tool_reference'. This coverstool_resultblocks withtool_referenceitems on/v1/messages, andtool_referenceparts on/v1/chat/completions.<tool_result>. Several references are joined with\n\n, the separator the encoder already uses between parts./v1/messagesemits onetoolmessage pertool_result: thetool_referenceparts first, then the text. On main, a tool search result renders an extra empty result before the references, on GLM-5.3 as well as V4.1, so after parallel tool calls the results no longer line up with the calls.tool_resultwithouttool_referenceitems converts as before, and other unsupported part types still return 400 on V4.1._convert_user_tool_result(vllm/entrypoints/anthropic/serving.py) and_normalize_messages(vllm/tokenizers/deepseek_v41.py): 15 lines added and 11 removed, plus three tests.Validation
5d55ef1590pytest tests/entrypoints/anthropic/test_anthropic_messages_conversion.py::TestToolResultContent::test_tool_result_with_tool_references tests/entrypoints/scale_out/render/test_render.py::test_messages_render_tool_search_result_for_deepseek_v41 tests/tokenizers_/test_deepseek_v41.py::test_tool_references_render_as_tool_namespytest tests/entrypoints/anthropic/test_anthropic_messages_conversion.py tests/entrypoints/scale_out/render/test_render.py tests/tokenizers_/test_deepseek_v41.py tests/tokenizers_/test_deepseek_v4.pyvllm launch renderwith the DeepSeek-V4.1-Flash config and tokenizer, 4 requests (below)<tool_result>per callvllm launch renderwith the GLM-5.3 config, tokenizer and chat template, 3 requests (below)<tool_response>each<tool_response>per callThe conversion test sends a
tool_resultwith text, twotool_referenceitems and an image, and checks for onetoolmessage with the references first, followed by the image'susermessage. The render test startsvllm launch render deepseek-ai/DeepSeek-V4.1-FlashthroughRemoteLaunchRenderServer, which downloads only the config and tokenizer, like the existing GPT-OSS render test. It sends a/v1/messagesturn where a tool search and a file read ran in parallel, and checks the rendered user turn through/detokenize. On main the new tests fail like this:With only the V4.1 encoder change and main's converter, the render test still fails: the parallel turn renders three
<tool_result>s for two calls.The one other failure in the changed files is
test_tokenize_matches_chat_render_for_gpt_oss, which fails the same way on main here because the container cannot download the Harmony vocab file. Tests ran on CPU invllm-ci-postmerge-repo:074c83a09a0b872cec92e244d91adf2b3c55e41b-arm64-cpuwith the branch tree onPYTHONPATH. The repo's pre-commit hooks pass, includingruff,mypyand the test tethering check. No model evaluation is included: the change only affects prompts withtool_referenceitems.Render server checks
DeepSeek V4.1. The server runs
vllm launch render deepseek-ai/DeepSeek-V4.1-Flash --tokenizer-mode deepseek_v41 --reasoning-parser deepseek_v41 --tool-call-parser deepseek_v41 --enable-auto-tool-choice --language-model-only, withconfig.jsonand the tokenizer from revision314113f8and no weights. Every request declares aToolSearchtool, plusget_weatherandget_timewithdefer_loading: true./v1/messages/render, shaped like a Claude Code tool search: the last user turn is atool_resultwhose content is[{"type": "tool_reference", "tool_name": "get_weather"}].get_weatherwas called and returned./v1/chat/completions/renderwith atoolmessage whose content is[{"type": "tool_reference", "name": "get_weather"}]./v1/messages/renderwhere one assistant turn callsToolSearchandReadin parallel, and the user turn returns the reference result and a text result.On main all four return the 400 above. On this branch, requests 1 and 3 end with
<tool_result>get_weather</tool_result>before the generation prompt, request 2 has one result for each of its two calls, request 4 ends with<tool_result>get_weather</tool_result>\n\n<tool_result>NOTES-FILE-BODY</tool_result>, and the system prompt of every request lists all tools.GLM-5.3. The server runs
vllm launch renderwith the GLM-5.3config.jsonand tokenizer, the chat template from Hub revisionba32f8c19c,--tool-call-parser glm47 --reasoning-parser glm45 --enable-auto-tool-choice, and requests 1 and 4 above plus a third/v1/messagesrequest whosetool_resultholds a text block and atool_reference.<tool_response></tool_response><tool_response><tools>{get_weather}</tools></tool_response><tool_response><tools>{get_weather}</tools></tool_response>ToolSearch+Read<tool_response>s for 2 calls, the first one empty<tool_response><tools>{get_weather}</tools></tool_response><tool_response>NOTES-FILE-BODY</tool_response><tool_response>Found 1 tool</tool_response><tool_response><tools>{get_weather}</tools></tool_response><tool_response><tools>{get_weather}</tools></tool_response>{get_weather}stands for the tool's full JSON definition, which both arms render identically.Details
Root cause. #40190 added
tool_referenceparts._convert_user_tool_resultemits atoolmessage with the result's text (empty for a plain tool search), then a secondtoolmessage with thetool_referenceitems, both for the sametool_use_id. An image in the result goes into ausermessage between the two. Templates and encoders that render one result pertoolmessage render one result too many. Separately, V4.1 renders with its own encoder, and_normalize_messagesraisesValueErrorfor any part type other than text and image. Thetool_resultstays in the conversation history, so on V4.1 every later request in that conversation fails too, and an agent session that runs a tool search cannot continue.Why one message, references first. Templates that expand references look for a
toolmessage whose first part is atool_reference(GLM-5.3's does), so the references lead and the text follows. Images stay in theusermessage after thetoolmessage. Limitation: GLM-5.3's template expands such a message and skips its other parts, so text that sits beside references in onetool_resultno longer renders there; on main it rendered as an extra result. The tool search docs show results that hold onlytool_referenceblocks.Why the name.
apply_chat_templateputs every request tool into the V4.1 system prompt, deferred ones included, so the definitions a reference points at are already in context. The name tells the model which tools the search returned. The Cohere renderer renders these parts the same way (vllm/renderers/cohere.py). The model's Jinja template on the Hub drops unknown block types, which would render the search result empty.Related. #52498 and #52610 (open, for #52489) rewrite
tool_referenceitems as text in the Anthropic converter for every model. That would also stop the V4.1 400 on/v1/messages, but/v1/chat/completionsrequests with these parts would still fail, and the structured parts that templates expanding references rely on would be gone.AI assistance. Claude Code was used for the root-cause analysis, the fix, the tests and this description.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (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.
BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)