Summary
We run a fork of e2e-chatbot-app-next against Databricks model serving endpoints and periodically hit provider 400s caused by a tool_use / tool_result mismatch in the converted model messages.
Caveat up front: I have not reproduced this against the unmodified template. Please treat this as a field report plus an inferred mechanism rather than a confirmed template bug. I am filing it because the relevant code path looks unguarded upstream and the mitigation is cheap.
Symptom
Anthropic-family endpoints reject the request with a 400 complaining that tool_use blocks have no corresponding tool_result blocks. There is an OpenAI / AI Gateway flavour of the same failure. It appears mid-conversation, after tool calls have happened, and tends to recur for the affected thread because the bad state has been persisted.
Inferred mechanism
server/src/routes/chat.ts L251 calls:
const modelMessages = await convertToModelMessages(uiMessages);
with no options and no de-duplication. uiMessages is assembled from persisted DB messages and/or requestBody.previousMessages (L169-198). Two states appear able to survive that round-trip:
- Duplicate
tool-<name> parts sharing one toolCallId, at different states.
- Parts stuck at
input-available — a call with no result, e.g. the stream ended early, the tool errored, or an approval was denied.
Either one produces a tool_use with no matching tool_result once converted.
Mitigation we use
Two changes, both cheap and drop-only:
- De-duplicate UI tool parts by
toolCallId before conversion, keeping the most-resolved state (output-available > output-error > input-available > input-streaming). Never synthesise a part, only drop.
- Pass
ignoreIncompleteToolCalls: true to convertToModelMessages.
Since adding those we have not seen the 400 recur.
Ask
Would you consider guarding this path upstream? Happy to send a PR if that would be useful. Equally, if you already know this is handled elsewhere or is specific to how our fork persists messages, feel free to close.
Version
Template e2e-chatbot-app-next at ab30424be796009d34a0b760c25395211fe08e91
Summary
We run a fork of
e2e-chatbot-app-nextagainst Databricks model serving endpoints and periodically hit provider400s caused by atool_use/tool_resultmismatch in the converted model messages.Caveat up front: I have not reproduced this against the unmodified template. Please treat this as a field report plus an inferred mechanism rather than a confirmed template bug. I am filing it because the relevant code path looks unguarded upstream and the mitigation is cheap.
Symptom
Anthropic-family endpoints reject the request with a
400complaining thattool_useblocks have no correspondingtool_resultblocks. There is an OpenAI / AI Gateway flavour of the same failure. It appears mid-conversation, after tool calls have happened, and tends to recur for the affected thread because the bad state has been persisted.Inferred mechanism
server/src/routes/chat.tsL251 calls:with no options and no de-duplication.
uiMessagesis assembled from persisted DB messages and/orrequestBody.previousMessages(L169-198). Two states appear able to survive that round-trip:tool-<name>parts sharing onetoolCallId, at different states.input-available— a call with no result, e.g. the stream ended early, the tool errored, or an approval was denied.Either one produces a
tool_usewith no matchingtool_resultonce converted.Mitigation we use
Two changes, both cheap and drop-only:
toolCallIdbefore conversion, keeping the most-resolved state (output-available>output-error>input-available>input-streaming). Never synthesise a part, only drop.ignoreIncompleteToolCalls: truetoconvertToModelMessages.Since adding those we have not seen the
400recur.Ask
Would you consider guarding this path upstream? Happy to send a PR if that would be useful. Equally, if you already know this is handled elsewhere or is specific to how our fork persists messages, feel free to close.
Version
Template
e2e-chatbot-app-nextatab30424be796009d34a0b760c25395211fe08e91