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.
This commit is contained in:
mateo-berri 2026-11-07 11:34:57 -08:00
parent 8af15814f1
commit 008e91a5c6
3 changed files with 22 additions and 8 deletions

View file

@ -100,7 +100,6 @@ from litellm.types.utils import (
all_litellm_params,
)
from litellm.utils import (
LITELLM_ONLY_PARAMS,
ProviderConfigManager,
client,
)
@ -644,12 +643,20 @@ def _chat_completion_request_fields(allowed_openai_params: Sequence[str] | None)
*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)
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 {}
_ADDITIONAL_DROP_PARAMS: Final = TypeAdapter(list[str])
@ -667,7 +674,12 @@ def _bridge_kwargs(
) -> Mapping[str, object] | _UntranslatableResponsesFields:
forwarded_keys: Final = _chat_completion_request_fields(allowed_openai_params)
if responses_api_provider_config is not None:
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),
}
)
candidate_keys: Final = _RESPONSES_REQUEST_FIELDS.difference(forwarded_keys, _explicitly_dropped_fields(kwargs))
untranslatable: Final = tuple(sorted(key for key in candidate_keys if kwargs.get(key) is not None))
if untranslatable and not drops_unsupported_params(kwargs.get("drop_params")):

View file

@ -412,17 +412,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

@ -23,6 +23,7 @@ from pydantic import TypeAdapter
import litellm
from litellm._logging import verbose_logger
from litellm.integrations.anthropic_cache_control_hook import MAX_CACHE_CONTROL_BLOCKS
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.types.llms.openai import ResponsesAPIResponse
from tests.unit.llms.bedrock.slow_upstream import (
@ -1208,11 +1209,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)
],
)