server : support input_image in Responses API tool call output - #27958
server : support input_image in Responses API tool call output#27958Fino-wind wants to merge 1 commit into
Conversation
The Responses API converter rejects any tool call output that is not
'input_text', so a client that returns an image from a tool (e.g. a
screenshot tool) always gets:
400 Output of tool call should be 'Input text'
This is inconsistent with input messages, which already accept
'input_image' a few lines above in the same file.
Accept 'input_image' in function_call_output as well. Because chat
templates cannot render media inside a `tool` message, the image is
forwarded as a following user message, which keeps it in the same
position in the conversation and lets the existing multimodal path
pick it up unchanged.
|
Hi @Fino-wind, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
|
I don't think the statement that chat templates cannot render media originating from a We have a concrete counterexample in our Qwen3.8/Codex setup: In our Responses converter, The resulting That is also what the current branch diff implements. On the template side, the companion Qwen3.8/Codex template accepts both
The important part is that:
is executed for every message before role-specific handling: The template then handles There is an important distinction here: Qwen's template ultimately projects tool responses into its own user-side ChatML framing. So I am not claiming that the final serialized prompt literally contains a native The point is that this projection happens in the model-specific template, while the Responses -> Chat Completions conversion preserves the tool association of the content. That separation seems architecturally cleaner to me. The image can remain semantically where it originated: in the tool result. The protocol converter does not need to turn part of that result into an additional standalone That is also the relevant difference compared with the approach in #27958:
To be clear, this does not show that all existing llama.cpp chat templates already support multimodal tool content. Some may not. It does show that multimodal content originating from a tool result is technically workable without requiring the generic protocol conversion layer to rewrite that content into a separate user turn. For that reason, I would prefer keeping In other words: the protocol converter should preserve the semantic message structure and tool association; the model-dependent projection belongs in the template. |
Overview
The Responses API converter rejects any tool call output whose type is not
input_text, so a client that returns an image from a tool always gets:This is inconsistent with input messages:
input_imageis already accepted a hundred lines above in the same file. A model with a working vision encoder cannot see an image simply because it arrived as the result of a tool call rather than in a user turn.This is reachable with any Responses client that has a screenshot / image-returning tool — I hit it with Codex driving a local Qwen3.8-27B (vision) via
--mmproj, where every screenshot step failed.Changes
Accept
input_imageinfunction_call_outputas well, mirroring the existing branch for input messages.Chat templates cannot render media inside a
toolmessage, so the image is emitted as a following user message rather than placed in the tool message itself. This keeps it at the same position in the conversation and lets the existing multimodal path consume it unchanged. When the tool output contains only images, a short placeholder text is kept in the tool message so the tool result is not empty.output_typeis now read withjson_value(...), so a malformed entry withouttypeproduces the regular error path instead of relying oncontains().Unknown output types still throw; the message is updated to mention both accepted types.
Testing
Built with CUDA and run against
Huihui-Qwen3.8-27B-abliterated-Q8_0+mmproj-model-bf16.gguf. Same request, same model, two servers:d7bd3bf)HTTP 400 Output of tool call should be 'Input text'HTTP 200— model replied "这是宝可梦游戏画面" ("this is a Pokémon game screen"), 211 → 6 tokensThe request carried a real 50 KB JPEG screenshot as
input_imageinsidefunction_call_output. The reply describes the actual picture content, so the image reaches the vision encoder rather than merely passing validation.Text-only tool outputs are unaffected: the
input_textpath is the same conversion as before (typerewritten totext), and the string form ofoutputis untouched.Requirements