From 255ec81c4a16287a232fcf027ac46d42cdceb5ee Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:44:45 -0800 Subject: [PATCH] fix(responses): forward only message cache points through the bridge on native-config routes Routes with their own Responses config keep only chat completion request fields on the way into the bridge. Forwarding every injection point there let a tool_config point reach the provider body through the generic unknown-param copy, where no transformation reads it. Forward only the location: message points the bridge's second pass places. Cherry-picked from 008e91a5c68c2f89ab95075b98bd70615b3db01a, adapted to the line's test imports. --- litellm/responses/main.py | 18 +++++++++++++++--- .../test_responses_api_bridge_flag.py | 7 ++++--- .../test_responses_api_request_body.py | 5 +++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 7295d2be2f9..97674961c98 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -66,7 +66,6 @@ from litellm.types.responses.streaming_websocket import ResponsesWebSocketReques from litellm.types.router import GenericLiteLLMParams from litellm.types.utils import all_litellm_params from litellm.utils import ( - LITELLM_ONLY_PARAMS, ProviderConfigManager, client, ) @@ -443,6 +442,15 @@ def _bridges_to_chat_completions( _RESPONSES_ONLY_REQUEST_FIELDS_NEVER_BRIDGED: Final = frozenset({"client_metadata"}) +def _message_cache_control_points(kwargs: Mapping[str, object]) -> Mapping[str, object]: + points: Final = tuple( + point + for point in configured_injection_points(kwargs.get("cache_control_injection_points")) + if point.get("location") == "message" + ) + return {"cache_control_injection_points": points} if points else {} + + def _bridge_kwargs( kwargs: Mapping[str, object], responses_api_provider_config: BaseResponsesAPIConfig | None, @@ -458,11 +466,15 @@ def _bridge_kwargs( *DEFAULT_CHAT_COMPLETION_PARAM_VALUES, *all_litellm_params, *GenericLiteLLMParams.model_fields, - *LITELLM_ONLY_PARAMS, *(allowed_openai_params or ()), ) ).difference(_RESPONSES_ONLY_REQUEST_FIELDS_NEVER_BRIDGED) - return MappingProxyType({key: value for key, value in kwargs.items() if key in forwarded_keys}) + return MappingProxyType( + { + **{key: value for key, value in kwargs.items() if key in forwarded_keys}, + **_message_cache_control_points(kwargs), + } + ) _ResponsesCompatibilityFailure: TypeAlias = Literal["encrypted_task_unsupported"] diff --git a/tests/unit/responses/test_responses_api_bridge_flag.py b/tests/unit/responses/test_responses_api_bridge_flag.py index 74cfbe55cc8..5cf1f8ce42c 100644 --- a/tests/unit/responses/test_responses_api_bridge_flag.py +++ b/tests/unit/responses/test_responses_api_bridge_flag.py @@ -429,17 +429,18 @@ class TestUseResponsesApiBridgeFlag: {"type": "function_call_output", "call_id": "call_a", "output": "contents of a"}, ], use_chat_completions_api=True, - cache_control_injection_points=[{"location": "message", "index": -1}], + cache_control_injection_points=[{"location": "tool_config"}, {"location": "message", "index": -1}], api_key="fake-provider-api-key", num_retries=0, ) - sent_messages: Final = json.loads(upstream.calls[0].request.read())["messages"] + sent_body: Final = json.loads(upstream.calls[0].request.read()) assert [ message.get("tool_call_id") or message["role"] - for message in sent_messages + for message in sent_body["messages"] if "cache_control" in json.dumps(message) ] == ["call_a"] + assert "cache_control_injection_points" not in sent_body def test_bridge_keeps_deployment_credentials_while_dropping_unknown_params(self, respx_mock: respx.MockRouter): upstream: Final = respx_mock.post( diff --git a/tests/unit/responses/test_responses_api_request_body.py b/tests/unit/responses/test_responses_api_request_body.py index 7a1e12a7beb..1581ba33e12 100644 --- a/tests/unit/responses/test_responses_api_request_body.py +++ b/tests/unit/responses/test_responses_api_request_body.py @@ -17,6 +17,7 @@ import pytest import respx import litellm +from litellm.integrations.anthropic_cache_control_hook import MAX_CACHE_CONTROL_BLOCKS from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler @@ -1015,11 +1016,11 @@ async def test_the_ordinal_point_listed_first_takes_the_last_breakpoint_slot(ind tools=[ { "type": "function", - "name": name, + "name": f"t{i}", "parameters": {"type": "object", "properties": {}}, "cache_control": {"type": "ephemeral"}, } - for name in ("t0", "t1", "t2") + for i in range(MAX_CACHE_CONTROL_BLOCKS - 1) ], )