diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index b7d5340d8d4..5e7a501bf5f 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -126,9 +126,21 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): litellm_params: GenericLiteLLMParams, headers: dict, ) -> Dict: - """No transform applied since inputs are in OpenAI spec already""" + """Strip Anthropic-only `cache_control` markers before sending to OpenAI. + + OpenAI's Responses API rejects unknown fields on input content blocks + with HTTP 400 ("Unknown parameter: 'input[0].content[0].cache_control'"). + Chat Completions strips these in + `remove_cache_control_flag_from_messages_and_tools`; mirror that here. + """ input = self._validate_input_param(input) + tools = response_api_optional_request_params.get("tools") + input, tools = self.remove_cache_control_flag_from_input_and_tools( + model=model, input=input, tools=tools + ) + if tools is not None: + response_api_optional_request_params["tools"] = tools final_request_params = dict( ResponsesAPIRequestParams( model=model, input=input, **response_api_optional_request_params @@ -137,6 +149,38 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): return final_request_params + def remove_cache_control_flag_from_input_and_tools( + self, + model: str, # allows overrides to selectively run this + input: Union[str, ResponseInputParam], + tools: Optional[List[ALL_RESPONSES_API_TOOL_PARAMS]] = None, + ) -> Tuple[ + Union[str, ResponseInputParam], + Optional[List[ALL_RESPONSES_API_TOOL_PARAMS]], + ]: + """Sibling of `remove_cache_control_flag_from_messages_and_tools` on + the chat path. Strips Anthropic-only `cache_control` markers from + Responses API input content blocks and tools. + + `filter_value_from_dict` mutates each dict in place, so the same + objects are returned. + """ + from litellm.litellm_core_utils.prompt_templates.common_utils import ( + filter_value_from_dict, + ) + + if isinstance(input, list): + for item in input: + if isinstance(item, dict): + filter_value_from_dict(cast(dict, item), "cache_control") + + if tools is not None: + for tool in tools: + if isinstance(tool, dict): + filter_value_from_dict(cast(dict, tool), "cache_control") + + return input, tools + def _validate_input_param( self, input: Union[str, ResponseInputParam] ) -> Union[str, ResponseInputParam]: diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py index acb9fa9b64c..4b2e9471fb7 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py @@ -86,6 +86,90 @@ class TestOpenAIResponsesAPIConfig: self.validate_responses_api_request_params(result, expected_fields) + def test_transform_strips_cache_control_from_input_content_blocks(self): + """`cache_control` markers (Anthropic-only) must be stripped from + Responses API input content blocks before sending to OpenAI. + + OpenAI rejects unknown params on input content blocks with HTTP 400: + "Unknown parameter: 'input[0].content[0].cache_control'" + Chat Completions strips these via + `remove_cache_control_flag_from_messages_and_tools`; the Responses + path must do the same. + """ + input_with_cache_control = [ + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Hello", + "cache_control": {"type": "ephemeral"}, + } + ], + } + ] + + result = self.config.transform_responses_api_request( + model=self.model, + input=input_with_cache_control, + response_api_optional_request_params={}, + litellm_params={}, + headers={}, + ) + + assert "cache_control" not in result["input"][0]["content"][0] + assert result["input"][0]["content"][0]["type"] == "input_text" + assert result["input"][0]["content"][0]["text"] == "Hello" + + def test_transform_strips_cache_control_from_tools(self): + """`cache_control` markers must also be stripped from tools for + symmetry with the Chat Completions path. OpenAI currently accepts + cache_control on tools silently but stripping keeps the wire payload + clean and matches `remove_cache_control_flag_from_messages_and_tools`. + """ + tools_with_cache_control = [ + { + "type": "function", + "name": "get_weather", + "description": "Get the weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + }, + "cache_control": {"type": "ephemeral"}, + } + ] + + result = self.config.transform_responses_api_request( + model=self.model, + input="hi", + response_api_optional_request_params={"tools": tools_with_cache_control}, + litellm_params={}, + headers={}, + ) + + assert "cache_control" not in result["tools"][0] + assert result["tools"][0]["name"] == "get_weather" + + def test_transform_preserves_input_without_cache_control(self): + """Inputs without cache_control must pass through unmodified.""" + input_clean = [ + { + "role": "user", + "content": [{"type": "input_text", "text": "Hello"}], + } + ] + + result = self.config.transform_responses_api_request( + model=self.model, + input=input_clean, + response_api_optional_request_params={}, + litellm_params={}, + headers={}, + ) + + assert result["input"] == input_clean + def test_transform_streaming_response(self): """Test streaming response transformation""" # Test with a text delta event