mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
fix(responses): reorder function_call_output adjacent to function_call before chat-completion conversion
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 <<EOF`` heredoc is
detected inside an ``exec_command``, Codex short-circuits via the native
``apply_patch`` tool and inserts a user-visible warning message between
the ``function_call`` and the synthesised ``function_call_output``.
When a Responses payload like that is converted to Chat Completions and
then to Converse, the adjacency invariant is silently lost and Bedrock
returns HTTP 400::
BedrockException - The number of toolResult blocks at messages.N.content
exceeds the number of toolUse blocks of previous turn.
Restore the adjacency at the Responses → Chat Completions boundary.
This is the right layer for the fix: the Responses API and most
downstream providers tolerate the reordered input, only Bedrock
*requires* it. Doing it once here means every provider sees a payload
that satisfies the strictest contract, without coupling the conversion
layer to provider-specific knowledge.
The new helper is purely structural: it pairs items by ``call_id``, is
idempotent, preserves relative order for everything else, and is a
no-op when the input is already adjacent. It only touches items that
can be paired; orphan ``function_call_output`` items keep their
original position.
A dedicated test module covers: idempotence, the Codex heredoc trigger,
multiple interleaved call/output pairs, orphan outputs, and mixed
non-dict inputs. The test suite is added in a follow-up commit so this
commit can be reviewed in isolation.
This commit is contained in:
parent
e58a561caa
commit
543762a95a
1 changed files with 70 additions and 0 deletions
|
|
@ -382,6 +382,16 @@ class LiteLLMCompletionResponsesConfig:
|
|||
if isinstance(input, str):
|
||||
messages.append(ChatCompletionUserMessage(role="user", content=input))
|
||||
elif isinstance(input, list):
|
||||
# Bedrock Converse rejects requests where a ``toolResult`` is not
|
||||
# adjacent to its ``toolUse``. The Responses API itself does not
|
||||
# require strict adjacency, so upstream clients sometimes inject a
|
||||
# ``message`` between a ``function_call`` and its
|
||||
# ``function_call_output``. Normalise that here, before the
|
||||
# conversion runs, so every downstream provider sees a payload
|
||||
# that satisfies the strictest contract.
|
||||
input = LiteLLMCompletionResponsesConfig._reorder_function_call_outputs_adjacent(
|
||||
list(input)
|
||||
)
|
||||
existing_tool_call_ids: Set[str] = set()
|
||||
for _input in input:
|
||||
chat_completion_messages = LiteLLMCompletionResponsesConfig._transform_responses_api_input_item_to_chat_completion_message(
|
||||
|
|
@ -1009,6 +1019,66 @@ class LiteLLMCompletionResponsesConfig:
|
|||
"tool_result", # Anthropic/MCP format
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _reorder_function_call_outputs_adjacent(
|
||||
items: List[Any],
|
||||
) -> 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:
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue