Summary
Same underlying defect as #31, observed from a different angle: the parallel image tool calls were issued by one BYOK model, and the 400 appears only after the session is switched to a different model that replays the history. The new model never produced the image reads itself.
Once the shape is in history, the session is permanently broken on that model until it is switched back to an image-stripping route.
Environment
- Droid CLI 0.218.1 (daemon,
subcommand: exec); 0.217.0 and 0.216.0 also appear in the same logs
- macOS 24.6.0 (darwin), Apple Silicon
- Custom model:
provider: "generic-chat-completion-api", noImageSupport: false
- Gateway:
https://api.commandcode.ai/provider/v1, model deepseek/deepseek-v4.1-flash
- Image-capable source model:
custom:agy-gemini-0
Steps to reproduce
- Run a session on an image-capable BYOK model. In one assistant turn, have it issue 4 parallel
Read calls where the results carry image content.
- While the session is still live, switch the model to a text/vision-capable model that serializes through the same
generic-chat-completion-api path.
- The first request on the new model replays the full history and returns:
400 {"error":{"message":"Messages with role 'tool' must be a response to a preceding message with 'tool_calls'"}}
Live log line for the failing turn:
WARN: [Chat route failure] | {"sessionId":"<redacted>","reason":"invalidRequest",
"error":{"message":"400 {\"error\":{\"message\":\"Messages with role 'tool' must be a response
to a preceding message with 'tool_calls'\"}}"},"modelId":"deepseek/deepseek-v4.1-flash",
"tags":{"modelId":"custom:deepseek-v4-flash-0","isByok":"true","version":"0.218.1"}}
Retrying the prompt does not help: the invalid ordering is persisted, so every subsequent turn re-sends it and fails identically.
Evidence from a real session
The offending turn (4-way parallel image read, issued by the previous model):
msgs[2643] assistant (modelId: custom:agy-gemini-0)
tool_calls: tc-1p1cqxbgvv3, tc-9mx6m3ds21v, tc-8ytom2k1vb7, tc-dbm6y04qc3 (4 x Read)
msgs[2644] user
tool_result x4 — every result carries image content
Serialized with the shipped generic-chat-completion-api conversion, the assistant's later tool_calls are trimmed down to the ids present in the contiguous run of following tool messages. The image bridge user message interrupts that run, so tc-9mx6m3ds21v, tc-8ytom2k1vb7 and tc-dbm6y04qc3 are removed from the assistant while their tool messages stay in the array.
Result — 3 orphan role:"tool" messages, each one a result that carried an image:
orphan tool_call_id=tc-9mx6m3ds21v prev-message role=user
orphan tool_call_id=tc-8ytom2k1vb7 prev-message role=user
orphan tool_call_id=tc-dbm6y04qc3 prev-message role=user
Isolation experiment
Rebuilt the real window into the exact outbound payload and toggled only the image bridge. The head of the payload is a clean user turn in both cases, so this is not a window-boundary artifact:
| Payload |
Result (direct to upstream) |
| Clean head, no image bridge |
200 |
| Clean head, with image bridge |
400 (same error text) |
The image bridge alone flips the result.
Why this only appears once image support is enabled
With noImageSupport: true the images are stripped, no bridge user message is inserted, the tool run stays contiguous, and the trim never produces orphans. That matches the report: the same history was accepted for a long time while images were being dropped, and started failing immediately after vision was enabled for that model.
Repair that works
Tested against the real broken payload:
| Transform |
Result |
| As-is (what Droid sends) |
400 |
Reorder image-bridge user messages after the tool run |
400 — not sufficient, the ids are already gone |
Reorder + re-attach the trimmed tool_call ids to the assistant |
200 |
Demote orphan tool messages to user |
200, but loses tool semantics |
| Reorder + re-attach, replayed through a local protocol relay |
200, and the image still reaches the model |
Two observations that may help the fix:
- Reordering alone is not enough. Because the ids are removed before the request leaves the client, the assistant message must also get its
tool_calls back. Those calls did execute, so restoring the declarations restores the true structure rather than inventing content.
- The repaired request preserves the image: with the fix applied, the model still read the test image correctly. So this does not require trading vision for message validity.
Suggested fix
Same as #31, now with a second observation in support:
- Emit image content only after the entire run of
tool messages for that assistant turn (tool, tool, user[images]), not after each individual result.
- Harden the trimming pass so that when it removes an id from an assistant's
tool_calls, it also drops the now-unmatched role:"tool" message — or better, does not remove ids at all when the corresponding result is present.
A capture of the outbound body for the failing turn would settle which layer performs the removal, but the client-side reproduction above is enough to show the client builds an invalid order.
Related
Summary
Same underlying defect as #31, observed from a different angle: the parallel image tool calls were issued by one BYOK model, and the 400 appears only after the session is switched to a different model that replays the history. The new model never produced the image reads itself.
Once the shape is in history, the session is permanently broken on that model until it is switched back to an image-stripping route.
Environment
subcommand: exec); 0.217.0 and 0.216.0 also appear in the same logsprovider: "generic-chat-completion-api",noImageSupport: falsehttps://api.commandcode.ai/provider/v1, modeldeepseek/deepseek-v4.1-flashcustom:agy-gemini-0Steps to reproduce
Readcalls where the results carry image content.generic-chat-completion-apipath.Live log line for the failing turn:
Retrying the prompt does not help: the invalid ordering is persisted, so every subsequent turn re-sends it and fails identically.
Evidence from a real session
The offending turn (4-way parallel image read, issued by the previous model):
Serialized with the shipped
generic-chat-completion-apiconversion, the assistant's latertool_callsare trimmed down to the ids present in the contiguous run of followingtoolmessages. The image bridgeusermessage interrupts that run, sotc-9mx6m3ds21v,tc-8ytom2k1vb7andtc-dbm6y04qc3are removed from the assistant while theirtoolmessages stay in the array.Result — 3 orphan
role:"tool"messages, each one a result that carried an image:Isolation experiment
Rebuilt the real window into the exact outbound payload and toggled only the image bridge. The head of the payload is a clean user turn in both cases, so this is not a window-boundary artifact:
The image bridge alone flips the result.
Why this only appears once image support is enabled
With
noImageSupport: truethe images are stripped, no bridgeusermessage is inserted, thetoolrun stays contiguous, and the trim never produces orphans. That matches the report: the same history was accepted for a long time while images were being dropped, and started failing immediately after vision was enabled for that model.Repair that works
Tested against the real broken payload:
usermessages after the tool runtool_callids to the assistanttoolmessages touserTwo observations that may help the fix:
tool_callsback. Those calls did execute, so restoring the declarations restores the true structure rather than inventing content.Suggested fix
Same as #31, now with a second observation in support:
toolmessages for that assistant turn (tool, tool, user[images]), not after each individual result.tool_calls, it also drops the now-unmatchedrole:"tool"message — or better, does not remove ids at all when the corresponding result is present.A capture of the outbound body for the failing turn would settle which layer performs the removal, but the client-side reproduction above is enough to show the client builds an invalid order.
Related