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 008e91a5c6, adapted to the line's test imports.
This commit is contained in:
mateo-berri 2026-11-07 11:44:45 -08:00
parent dd52d08259
commit 255ec81c4a
3 changed files with 22 additions and 8 deletions

View file

@ -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"]

View file

@ -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(

View file

@ -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)
],
)