From 31fbe6fc3354e35e79e848b471b972128d570394 Mon Sep 17 00:00:00 2001 From: "Alexandr A. Alexandrov" Date: Thu, 17 Sep 2026 21:38:01 +0000 Subject: [PATCH 1/2] fix(chatgpt): normalize foreign function call item ids --- .../llms/chatgpt/responses/transformation.py | 27 +++++++++++++++++ .../test_chatgpt_responses_transformation.py | 30 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/litellm/llms/chatgpt/responses/transformation.py b/litellm/llms/chatgpt/responses/transformation.py index 34953a79c60..6a724065e8b 100644 --- a/litellm/llms/chatgpt/responses/transformation.py +++ b/litellm/llms/chatgpt/responses/transformation.py @@ -1,3 +1,4 @@ +import hashlib from collections.abc import Mapping from typing import TYPE_CHECKING, Any, Final, cast @@ -97,6 +98,7 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): if "reasoning.encrypted_content" not in include: include.append("reasoning.encrypted_content") request["include"] = include + request["input"] = self._normalize_function_call_item_ids(request.get("input")) allowed_keys: Final = { "model", @@ -118,6 +120,31 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): filtered["service_tier"] = service_tier return filtered + @classmethod + def _normalize_function_call_item_ids(cls, input_value: Any) -> Any: + if not isinstance(input_value, list): + return input_value + + normalized_input: list[Any] = [] + changed = False + for item in input_value: + if isinstance(item, dict) and item.get("type") == "function_call": + current_id = item.get("id") + normalized_id = cls._normalize_function_call_item_id(current_id) + if normalized_id != current_id: + item = dict(item) + item["id"] = normalized_id + changed = True + normalized_input.append(item) + return normalized_input if changed else input_value + + @staticmethod + def _normalize_function_call_item_id(item_id: Any) -> Any: + if not isinstance(item_id, str) or not item_id or item_id.startswith("fc"): + return item_id + digest: Final = hashlib.sha256(item_id.encode("utf-8")).hexdigest()[:32] + return f"fc_{digest}" + def transform_response_api_response( self, model: str, diff --git a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py index e47d7fc702c..c929c80b747 100644 --- a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py +++ b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py @@ -189,6 +189,36 @@ class TestChatGPTResponsesAPITransformation: assert "reasoning.encrypted_content" in request["include"] assert request["instructions"].startswith("You are Codex, based on GPT-5.") + def test_chatgpt_normalizes_foreign_function_call_item_id(self): + config = ChatGPTResponsesAPIConfig() + input_items = [ + { + "type": "function_call", + "id": "toolu_fixture_123", + "call_id": "call_fixture_123", + "name": "read_file", + "arguments": '{"path":"README.md"}', + } + ] + + request = config.transform_responses_api_request( + model="chatgpt/gpt-5.6-sol", + input=input_items, + response_api_optional_request_params={}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert request["input"] == [ + { + "type": "function_call", + "id": "fc_064009edc4c975f0fa4b447f8ea388e0", + "call_id": "call_fixture_123", + "name": "read_file", + "arguments": '{"path":"README.md"}', + } + ] + @pytest.mark.parametrize( "model_name", [ From f2dae20e49cf901248f682bde8eb4df41eaae3bc Mon Sep 17 00:00:00 2001 From: "Alexandr A. Alexandrov" Date: Thu, 17 Sep 2026 21:40:28 +0000 Subject: [PATCH 2/2] fix(chatgpt): omit replayed private response items --- .../llms/chatgpt/responses/transformation.py | 21 +++++++----- .../test_chatgpt_responses_transformation.py | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/litellm/llms/chatgpt/responses/transformation.py b/litellm/llms/chatgpt/responses/transformation.py index 6a724065e8b..8a7d377602a 100644 --- a/litellm/llms/chatgpt/responses/transformation.py +++ b/litellm/llms/chatgpt/responses/transformation.py @@ -98,7 +98,7 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): if "reasoning.encrypted_content" not in include: include.append("reasoning.encrypted_content") request["include"] = include - request["input"] = self._normalize_function_call_item_ids(request.get("input")) + request["input"] = self._normalize_responses_input_for_chatgpt(request.get("input")) allowed_keys: Final = { "model", @@ -121,20 +121,25 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): return filtered @classmethod - def _normalize_function_call_item_ids(cls, input_value: Any) -> Any: + def _normalize_responses_input_for_chatgpt(cls, input_value: Any) -> Any: if not isinstance(input_value, list): return input_value normalized_input: list[Any] = [] changed = False for item in input_value: - if isinstance(item, dict) and item.get("type") == "function_call": - current_id = item.get("id") - normalized_id = cls._normalize_function_call_item_id(current_id) - if normalized_id != current_id: - item = dict(item) - item["id"] = normalized_id + if isinstance(item, dict): + item_type = item.get("type") + if item_type in {"reasoning", "web_search_call"}: changed = True + continue + if item_type == "function_call": + current_id = item.get("id") + normalized_id = cls._normalize_function_call_item_id(current_id) + if normalized_id != current_id: + item = dict(item) + item["id"] = normalized_id + changed = True normalized_input.append(item) return normalized_input if changed else input_value diff --git a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py index c929c80b747..2ce568a4e46 100644 --- a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py +++ b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py @@ -219,6 +219,38 @@ class TestChatGPTResponsesAPITransformation: } ] + def test_chatgpt_omits_replayed_private_output_items(self): + config = ChatGPTResponsesAPIConfig() + input_items = [ + {"type": "reasoning", "id": "rs_fixture", "encrypted_content": "opaque"}, + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "continue"}], + }, + {"type": "web_search_call", "id": "ws_fixture", "status": "completed"}, + { + "type": "function_call", + "id": "fc_fixture", + "call_id": "call_fixture", + "name": "read_file", + "arguments": '{"path":"README.md"}', + }, + ] + + request = config.transform_responses_api_request( + model="chatgpt/gpt-5.6-sol", + input=input_items, + response_api_optional_request_params={}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert [item["type"] for item in request["input"]] == [ + "message", + "function_call", + ] + @pytest.mark.parametrize( "model_name", [