fix(responses): keep the caller's request stripped of cache_control as before when a marker is copied

This commit is contained in:
mateo-berri 2026-10-28 13:53:09 -07:00
parent a6a1336dd1
commit 6a0d30bcab
2 changed files with 71 additions and 12 deletions

View file

@ -586,12 +586,8 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
marker_safe_input: Final = self._input_with_wire_safe_prompt_cache_breakpoints(
model=model, input=validated_input, litellm_params=litellm_params
)
marked_input: Final = (
self._input_with_prompt_cache_breakpoints(marker_safe_input)
if self._reads_prompt_cache_breakpoints(
model=model, litellm_params=litellm_params, prompt_cache_options=prompt_cache_options
)
else marker_safe_input
reads_breakpoints: Final = self._reads_prompt_cache_breakpoints(
model=model, litellm_params=litellm_params, prompt_cache_options=prompt_cache_options
)
maps_anthropic_tool_search: Final = self._targets_openai_api(litellm_params) and any(
is_anthropic_tool_search_chat_tool(tool) for tool in tools or ()
@ -601,20 +597,43 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
if maps_anthropic_tool_search
else tools
)
replayable_input: Final = (
input_without_unanswered_anthropic_tool_search_calls(marked_input)
if maps_anthropic_tool_search
else marked_input
)
stripped_input, stripped_tools = self.remove_cache_control_flag_from_input_and_tools(
model=model, input=replayable_input, tools=mapped_tools
model=model,
input=self._replayable_input(marker_safe_input, reads_breakpoints, maps_anthropic_tool_search),
tools=mapped_tools,
)
if marker_safe_input is not validated_input:
self._strip_caller_input_as_if_unmarked(
model=model,
caller_input=validated_input,
reads_breakpoints=reads_breakpoints,
maps_anthropic_tool_search=maps_anthropic_tool_search,
)
object_schema_tools: Final = self._tools_with_object_parameters(model=model, tools=stripped_tools)
sanitized_tools: Final = self._sanitized_tool_schemas_for_openai(
model=model, tools=object_schema_tools, litellm_params=litellm_params
)
return self._drop_foreign_tool_call_item_ids(stripped_input), sanitized_tools
def _replayable_input(
self, input: str | ResponseInputParam, reads_breakpoints: bool, maps_anthropic_tool_search: bool
) -> str | ResponseInputParam:
marked_input: Final = self._input_with_prompt_cache_breakpoints(input) if reads_breakpoints else input
if not maps_anthropic_tool_search:
return marked_input
return input_without_unanswered_anthropic_tool_search_calls(marked_input)
def _strip_caller_input_as_if_unmarked(
self,
model: str,
caller_input: str | ResponseInputParam,
reads_breakpoints: bool,
maps_anthropic_tool_search: bool,
) -> None:
self.remove_cache_control_flag_from_input_and_tools(
model=model, input=self._replayable_input(caller_input, reads_breakpoints, maps_anthropic_tool_search)
)
def _input_with_wire_safe_prompt_cache_breakpoints(
self, model: str, input: str | ResponseInputParam, litellm_params: GenericLiteLLMParams
) -> str | ResponseInputParam:

View file

@ -4382,6 +4382,46 @@ def test_cache_control_still_marks_a_part_whose_own_marker_is_malformed_under_dr
}
def _caller_input_after_transform(model: str, marker: object) -> str:
hint: Final = {"type": "ephemeral"}
marked: Final = {} if marker is None else {"prompt_cache_breakpoint": marker}
input: Final = [
{
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "Say ok", "cache_control": hint, **marked},
{"type": "input_text", "text": "Thanks", "cache_control": hint},
],
"cache_control": hint,
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "ok", "cache_control": hint, **marked}],
},
]
_CONFIGS[0].transform_responses_api_request(
model=model,
input=cast("ResponseInputParam", input), # cast-ok: plain dicts shaped like the SDK's input items
response_api_optional_request_params={},
litellm_params=GenericLiteLLMParams(drop_params=True),
headers={},
)
return json.dumps(input)
@pytest.mark.parametrize("model", ("gpt-4.1", "gpt-6.1-sol"))
@pytest.mark.parametrize("marker", (_VALID_MARKERS[0], "yes"), ids=("explicit", "malformed"))
def test_a_marker_leaves_the_caller_request_that_fallbacks_and_logs_read_as_an_unmarked_one_would(
monkeypatch: pytest.MonkeyPatch, model: str, marker: object
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
marker_json: Final = f', "prompt_cache_breakpoint": {json.dumps(marker)}'
unmarked: Final = _caller_input_after_transform(model, None)
assert _caller_input_after_transform(model, marker).replace(marker_json, "") == unmarked
@pytest.mark.asyncio
@pytest.mark.usefixtures(httpx_transport.__name__)
async def test_aresponses_forwards_previous_response_id_to_openai() -> None: