From 29f757490e004f4e2d70b68e0e6f6f7a451f65d0 Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Sat, 8 Aug 2026 22:58:31 +0300 Subject: [PATCH] fix: don't drop a tool call whose id an earlier round already used The tool loop builds `output` once per request but runs many rounds inside it, and each round skips a function_call whose id appears anywhere in `output` - including rounds already completed. Results are rebuilt per round and appended regardless, so an id a later round reuses loses its call and keeps its result. The turn then holds function_call_output items with no function_call, and the next request fails at the provider with "tool message tool_call_id '...' does not match any tool call in the preceding assistant messages". The malformed turn is persisted, so retrying cannot succeed and every later message in that chat fails the same way. Renaming a reused id restores the pairing. Scoped to ids that are already answered, so the case the dedup exists for - Responses API items already in `output` and still awaiting a result - is untouched. Co-Authored-By: Claude Opus 5 --- backend/open_webui/utils/middleware.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index a13f43193d..f2ccb7946a 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -4920,6 +4920,15 @@ async def streaming_chat_response_handler(response, ctx): response_tool_calls = tool_calls.pop(0) + # Some providers restart tool-call numbering each round, so a later round can reuse an id an + # earlier one already answered - the dedup below would then drop the call but keep its result. + answered_call_ids = { + item.get('call_id') for item in output if item.get('type') == 'function_call_output' + } + for tc in response_tool_calls: + if tc.get('id') in answered_call_ids: + tc['id'] = f'{tc.get("id")}-r{tool_call_iterations}' + # Append function_call items for each tool call # (Responses API already has them from streaming, so skip duplicates) existing_call_ids = {item.get('call_id') for item in output if item.get('type') == 'function_call'}