diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 05c0b796633..9be66f088e6 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -360,14 +360,6 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): @staticmethod def _normalize_tool_call_id(tool_call_id: object) -> str | None: - """Normalize tool call ID to ensure it does not exceed 64 characters. - - Responses API downstream providers (e.g. OpenAI Responses API, AWS Bedrock) - enforce a 64-character limit on ``call_id``. IDs within the limit are preserved - verbatim. Overlong IDs are deterministically mapped to the first 31 characters - of the ID followed by '_' and a 32-character SHA-256 digest of the full ID - (31 + 1 + 32 = 64 characters), preserving readability and guaranteeing collision resistance. - """ if tool_call_id is None: return None tool_call_id_str: Final = str(tool_call_id) @@ -383,17 +375,14 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): input_items: Final[list[object]] = [] instructions: str | None = None custom_tool_call_ids: Final = frozenset( - ident + tool_call["id"] for msg in messages if msg.get("role") == "assistant" and isinstance(msg.get("tool_calls"), list) for tool_call in msg.get("tool_calls") or () if isinstance(tool_call, dict) and not tool_call.get("function") and isinstance(tool_call.get("custom"), dict) - for raw_id in (tool_call.get("id"),) - if raw_id is not None - for ident in (raw_id, self._normalize_tool_call_id(raw_id)) - if ident is not None + and "id" in tool_call ) leading_system_count: Final = next( @@ -446,7 +435,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): # Fallback: convert unexpected types to input_text tool_output = [{"type": "input_text", "text": str(content)}] normalized_tool_call_id: Final = self._normalize_tool_call_id(tool_call_id) - if tool_call_id in custom_tool_call_ids or normalized_tool_call_id in custom_tool_call_ids: + if tool_call_id in custom_tool_call_ids: input_items.append( ResponseCustomToolCallOutputParam( type="custom_tool_call_output", diff --git a/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py index 4f428340926..a8ea05f9704 100644 --- a/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py +++ b/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -4357,18 +4357,13 @@ def test_map_optional_params_verbosity_merges_into_text(): @pytest.mark.parametrize( "tool_call_id,is_custom", [ - # Short tool call ID: stays unchanged ("call_short_123", False), - # Exactly 64 characters: boundary case, stays unchanged ("call_" + "a" * 59, False), - # Overlong tool call ID (65 characters): normalized to <= 64 chars ("call_" + "a" * 60, False), - # Overlong tool call ID from issue #42765 (85 characters): normalized to 64 chars ( "call_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", False, ), - # Custom tool call with overlong ID: normalized to <= 64 chars ( "custom_tool_call_id_exceeding_the_standard_responses_api_sixty_four_character_length_limit", True, @@ -4379,11 +4374,7 @@ def test_convert_chat_completion_messages_to_responses_api_normalizes_overlong_t tool_call_id: str, is_custom: bool, ): - """ - Overlong tool call IDs (> 64 chars) must be deterministically normalized to <= 64 characters - consistently across assistant tool_calls and matching tool result messages, - while IDs <= 64 chars must be preserved unchanged. - """ + """Overlong tool call IDs (> 64 chars) must be deterministically normalized to <= 64 characters.""" import hashlib from litellm.completion_extras.litellm_responses_transformation.transformation import ( LiteLLMResponsesTransformationHandler, @@ -4425,7 +4416,6 @@ def test_convert_chat_completion_messages_to_responses_api_normalizes_overlong_t input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages) - # Validate tool call item 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" @@ -4440,7 +4430,6 @@ def test_convert_chat_completion_messages_to_responses_api_normalizes_overlong_t assert call_id == expected_id assert len(str(call_id)) <= 64 - # Validate matching tool output item 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" @@ -4491,3 +4480,48 @@ def test_convert_chat_completion_messages_to_responses_api_overlong_collision_re 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(): + """Mixed custom and function tool call outputs must maintain respective types without collision.""" + 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