fix(responses): normalize overlong tool call IDs in chat-to-responses bridge

This commit is contained in:
agustin18 2026-10-18 01:48:14 +00:00
parent 7db8fc81f9
commit 038b826d95
2 changed files with 207 additions and 4 deletions

View file

@ -2,6 +2,7 @@
Handler for transforming /chat/completions api requests to litellm.responses requests
"""
import hashlib
import json
import os
from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence
@ -796,6 +797,16 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
# Unknown or unsupported type
return None, index
@staticmethod
def _normalize_tool_call_id(tool_call_id: str | None) -> str | None:
if tool_call_id is None:
return None
if len(tool_call_id) <= 64:
return tool_call_id
prefix: Final = tool_call_id[:31]
digest: Final = hashlib.sha256(tool_call_id.encode("utf-8")).hexdigest()[:32]
return f"{prefix}_{digest}"
def convert_chat_completion_messages_to_responses_api(
self,
messages: list["AllMessageValues"],
@ -836,6 +847,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
if isinstance(tool_call, dict)
and not tool_call.get("function")
and isinstance(tool_call.get("custom"), dict)
and "id" in tool_call
)
leading_system_count: Final = next(
@ -893,7 +905,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
input_items.append(
ResponseCustomToolCallOutputParam(
type="custom_tool_call_output",
call_id=tool_call_id,
call_id=(
self._normalize_tool_call_id(f"{tool_call_id}") or ""
if tool_call_id is not None
else ""
),
output=content if isinstance(content, str) else tool_output,
)
)
@ -901,7 +917,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
input_items.append(
FunctionCallOutput(
type="function_call_output",
call_id=tool_call_id,
call_id=(
self._normalize_tool_call_id(f"{tool_call_id}") if tool_call_id is not None else None
),
output=tool_output,
)
)
@ -923,7 +941,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
if function:
input_tool_call: dict[str, object] = {
"type": "function_call",
"call_id": tool_call["id"],
"call_id": (
self._normalize_tool_call_id(f"{tool_call['id']}")
if "id" in tool_call and tool_call["id"] is not None
else None
),
}
if "name" in function:
input_tool_call["name"] = function["name"]
@ -938,7 +960,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
input_items.append(
ResponseCustomToolCallParam(
type="custom_tool_call",
call_id=tool_call["id"],
call_id=(
self._normalize_tool_call_id(f"{tool_call['id']}") or ""
if "id" in tool_call and tool_call["id"] is not None
else ""
),
name=custom.get("name", ""),
input=custom.get("input", ""),
)

View file

@ -6060,3 +6060,180 @@ def test_stream_structured_output_drops_text_after_first_message_item(
content: Final = "".join(chunk.choices[0].delta.content or "" for chunk in chunks)
assert content == expected_content
assert chunks[-1].choices[0].finish_reason == "stop"
@pytest.mark.parametrize(
"tool_call_id,is_custom",
[
(None, False),
("call_short_123", False),
("call_" + "a" * 59, False),
("call_" + "a" * 60, False),
(
"call_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
False,
),
(
"custom_tool_call_id_exceeding_the_standard_responses_api_sixty_four_character_length_limit",
True,
),
],
)
def test_convert_chat_completion_messages_to_responses_api_normalizes_overlong_tool_call_ids(
tool_call_id: str | None,
is_custom: bool,
):
import hashlib
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
expected_id: Final = (
None
if tool_call_id is None
else tool_call_id
if len(tool_call_id) <= 64
else f"{tool_call_id[:31]}_{hashlib.sha256(tool_call_id.encode('utf-8')).hexdigest()[:32]}"
)
handler: Final = LiteLLMResponsesTransformationHandler()
assistant_tool_call: Final[dict[str, object]] = (
{
"id": tool_call_id,
"type": "custom",
"custom": {"name": "example_custom_tool", "input": "{}"},
}
if is_custom
else {
"id": tool_call_id,
"type": "function",
"function": {"name": "example_tool", "arguments": "{}"},
}
)
messages: Final[list[dict[str, object]]] = [
{"role": "user", "content": "Run tool"},
{
"role": "assistant",
"tool_calls": [assistant_tool_call],
},
{
"role": "tool",
"tool_call_id": tool_call_id,
"content": "tool execution result",
},
]
input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
if is_custom:
tool_call_item: Final = next(
item for item in input_items if isinstance(item, dict) and item.get("type") == "custom_tool_call"
)
call_id: Final = tool_call_item.get("call_id")
else:
func_tool_call_item: Final = next(
item for item in input_items if isinstance(item, dict) and item.get("type") == "function_call"
)
call_id = func_tool_call_item.get("call_id")
assert call_id == expected_id
if call_id is not None:
assert len(str(call_id)) <= 64
if is_custom:
custom_output_item: Final = next(
item for item in input_items if isinstance(item, dict) and item.get("type") == "custom_tool_call_output"
)
output_call_id: Final = custom_output_item.get("call_id")
else:
func_output_item: Final = next(
item for item in input_items if isinstance(item, dict) and item.get("type") == "function_call_output"
)
output_call_id = func_output_item.get("call_id")
assert output_call_id == expected_id
assert output_call_id == call_id
def test_convert_chat_completion_messages_to_responses_api_overlong_collision_resistance():
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
handler: Final = LiteLLMResponsesTransformationHandler()
id_1: Final = "call_" + "x" * 60 + "_1"
id_2: Final = "call_" + "x" * 60 + "_2"
messages: Final[list[dict[str, object]]] = [
{
"role": "assistant",
"tool_calls": [
{"id": id_1, "type": "function", "function": {"name": "f1", "arguments": "{}"}},
{"id": id_2, "type": "function", "function": {"name": "f2", "arguments": "{}"}},
],
},
{"role": "tool", "tool_call_id": id_1, "content": "res1"},
{"role": "tool", "tool_call_id": id_2, "content": "res2"},
]
input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
calls: Final = [item for item in input_items if isinstance(item, dict) and item.get("type") == "function_call"]
outputs: Final = [
item for item in input_items if isinstance(item, dict) and item.get("type") == "function_call_output"
]
assert len(calls) == 2
assert len(outputs) == 2
assert calls[0]["call_id"] != calls[1]["call_id"]
assert calls[0]["call_id"] == outputs[0].get("call_id")
assert calls[1]["call_id"] == outputs[1].get("call_id")
assert len(str(calls[0]["call_id"])) <= 64
assert len(str(calls[1]["call_id"])) <= 64
def test_convert_chat_completion_messages_to_responses_api_mixed_custom_and_function_output_types():
import hashlib
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
handler: Final = LiteLLMResponsesTransformationHandler()
custom_raw_id: Final = "custom_call_" + "y" * 60
function_raw_id: Final = f"{custom_raw_id[:31]}_{hashlib.sha256(custom_raw_id.encode('utf-8')).hexdigest()[:32]}"
messages: Final[list[dict[str, object]]] = [
{
"role": "assistant",
"tool_calls": [
{
"id": custom_raw_id,
"type": "custom",
"custom": {"name": "c_tool", "input": "{}"},
},
{
"id": function_raw_id,
"type": "function",
"function": {"name": "f_tool", "arguments": "{}"},
},
],
},
{"role": "tool", "tool_call_id": custom_raw_id, "content": "custom_res"},
{"role": "tool", "tool_call_id": function_raw_id, "content": "func_res"},
]
input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
custom_outputs: Final = [
item for item in input_items if isinstance(item, dict) and item.get("type") == "custom_tool_call_output"
]
function_outputs: Final = [
item for item in input_items if isinstance(item, dict) and item.get("type") == "function_call_output"
]
assert len(custom_outputs) == 1
assert len(function_outputs) == 1
assert custom_outputs[0].get("call_id") == function_raw_id
assert function_outputs[0].get("call_id") == function_raw_id