From 543762a95a63ce61f47be95ebbe9272ecb931aeb Mon Sep 17 00:00:00 2001 From: parisni Date: Thu, 14 May 2026 21:13:03 +0200 Subject: [PATCH] fix(responses): reorder `function_call_output` adjacent to `function_call` before chat-completion conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bedrock Converse requires every ``toolResult`` block to live in the ``user`` message *immediately* following the assistant message that emitted the matching ``toolUse``. The OpenAI Responses API does not enforce this, so upstream clients sometimes inject a ``message`` between a ``function_call`` and its ``function_call_output``. One concrete trigger is OpenAI Codex CLI: when an ``apply_patch < List[Any]: + """Move every ``function_call_output`` immediately after its matching + ``function_call`` (by ``call_id``). + + OpenAI's Responses API tolerates other input items (for example a + ``message`` injected by an upstream client) between a ``function_call`` + and its ``function_call_output``. Bedrock Converse, however, requires + each ``toolResult`` block to live in the ``user`` message *immediately* + following the assistant message that emitted the matching ``toolUse``. + + When a Responses-API request is converted to Chat Completions and then + to Converse, that adjacency invariant is silently lost and Bedrock + rejects the request with HTTP 400:: + + BedrockException - The number of toolResult blocks at messages.N.content + exceeds the number of toolUse blocks of previous turn. + + Restoring the adjacency at the Responses → Chat Completions boundary is + provider-agnostic: OpenAI accepts the reordered input as well, so this + is safe for every backend that consumes the converted request. + + The helper preserves relative order for everything else, is idempotent, + and only touches items it can pair by ``call_id``. + """ + if not isinstance(items, list) or not items: + return items + + result = list(items) + i = 0 + while i < len(result): + it = result[i] + if ( + isinstance(it, dict) + and it.get("type") == "function_call_output" + ): + call_id = it.get("call_id") + if call_id: + # Find the most recent matching function_call before i. + fc_idx: Optional[int] = None + for k in range(i - 1, -1, -1): + prev = result[k] + if ( + isinstance(prev, dict) + and prev.get("type") == "function_call" + and prev.get("call_id") == call_id + ): + fc_idx = k + break + if fc_idx is not None and fc_idx != i - 1: + out = result.pop(i) + result.insert(fc_idx + 1, out) + # Position i now holds what was at i+1, so don't + # advance the cursor. + continue + i += 1 + return result + @staticmethod def _is_input_item_function_call(input_item: Any) -> bool: """