Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.prompt_templates.common_utils import (
responses_reasoning_items_from_thinking_blocks,
with_prompt_cache_breakpoint,
)
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
from litellm.llms.base_llm.bridges.completion_transformation import (
Expand Down Expand Up @@ -1060,16 +1061,22 @@ def _convert_content_to_responses_format(
# Handle multimodal content
original_type = item.get("type")
if original_type == "text":
converted = self._convert_content_str_to_input_text(item.get("text", ""), role)
converted = with_prompt_cache_breakpoint(
self._convert_content_str_to_input_text(item.get("text", ""), role),
item.get("prompt_cache_breakpoint"),
)
Comment thread
sorgfresser marked this conversation as resolved.
result.append(converted)
verbose_logger.debug("Chat provider: text -> %s", converted)
elif original_type == "image_url":
# Map to responses API image format
converted = cast(
dict,
self._convert_content_to_responses_format_image(
cast(ChatCompletionImageObject, item), role
converted = with_prompt_cache_breakpoint(
cast(
dict,
self._convert_content_to_responses_format_image(
cast(ChatCompletionImageObject, item), role
),
),
item.get("prompt_cache_breakpoint"),
)
result.append(converted)
verbose_logger.debug("Chat provider: image_url -> %s", converted)
Expand All @@ -1081,8 +1088,11 @@ def _convert_content_to_responses_format(
result.append(converted)
verbose_logger.debug("Chat provider: image -> %s", converted)
elif item_type == "file":
converted = _input_file_from_file_value(
cast("ChatCompletionFileObject", item).get("file"), # cast-ok: type tag checked
converted = with_prompt_cache_breakpoint(
_input_file_from_file_value(
cast("ChatCompletionFileObject", item).get("file"), # cast-ok: type tag checked
),
item.get("prompt_cache_breakpoint"),
)
result.append(converted)
verbose_logger.debug("Chat provider: file -> %s", converted)
Expand Down
2 changes: 1 addition & 1 deletion litellm/integrations/anthropic_cache_control_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def get_chat_completion_prompt(
openai_dialect
and AnthropicCacheControlHook.count_request_cache_breakpoints(processed_messages) > breakpoints_before
):
non_default_params.setdefault("prompt_cache_options", PromptCacheOptions(mode="explicit"))
non_default_params.setdefault("prompt_cache_options", PromptCacheOptions(mode="implicit"))

# Points this pass did not place: non-message ones for the provider transform, and
# the deferred role-targeted ones. Deferring is what reaches the Responses API's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4185,6 +4185,53 @@ def _system_input_item(text: str) -> dict[str, object]:
return {"type": "message", "role": "system", "content": [{"type": "input_text", "text": text}]}


@pytest.mark.parametrize(
("content_block", "expected_content"),
[
(
{"type": "text", "text": "Stable prefix"},
{"type": "input_text", "text": "Stable prefix"},
),
(
{"type": "image_url", "image_url": "https://example.com/image.png"},
{"type": "input_image", "image_url": "https://example.com/image.png", "detail": "auto"},
),
(
{"type": "file", "file": {"file_id": "file-123"}},
{"type": "input_file", "file_id": "file-123"},
),
],
ids=("text", "image_url", "file"),
)
def test_prompt_cache_breakpoint_survives_chat_to_responses_conversion(
content_block: dict[str, object], expected_content: dict[str, object]
) -> None:
handler: Final = LiteLLMResponsesTransformationHandler()
cache_breakpoint: Final = {"mode": "explicit"}
marked_content: Final = {**content_block, "prompt_cache_breakpoint": cache_breakpoint}

request: Final = handler.transform_request(
model="gpt-5.6-sol",
messages=[
{
"role": "user",
"content": [marked_content],
}
],
optional_params={"prompt_cache_options": cache_breakpoint},
litellm_params={},
headers={},
litellm_logging_obj=Mock(),
)

assert request["input"][0] == {
"type": "message",
"role": "user",
"content": [{**expected_content, "prompt_cache_breakpoint": cache_breakpoint}],
}
assert request["prompt_cache_options"] == cache_breakpoint


def test_mid_conversation_system_string_stays_in_input_after_a_user_turn():
handler: Final = LiteLLMResponsesTransformationHandler()

Expand Down
33 changes: 17 additions & 16 deletions tests/unit/integrations/test_anthropic_cache_control_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -2960,7 +2960,7 @@ def test_chat_system_string_wrapped_with_block_breakpoint(self):
}
assert processed[1] == {"role": "user", "content": "hi"}
assert returned is params
assert returned == {"prompt_cache_options": self.EXPLICIT}
assert returned == {"prompt_cache_options": {"mode": "implicit"}}

def test_chat_list_content_marks_last_block(self):
messages = [
Expand All @@ -2982,21 +2982,22 @@ def test_chat_list_content_marks_last_block(self):
"prompt_cache_breakpoint": self.EXPLICIT,
},
]
assert params["prompt_cache_options"] == self.EXPLICIT
assert params["prompt_cache_options"] == {"mode": "implicit"}

def test_chat_unprefixed_model_resolves_to_openai(self):
params = {"cache_control_injection_points": copy.deepcopy(self.SYSTEM_POINT)}
_, processed, _ = self._chat([{"role": "system", "content": "sys"}], params, model="gpt-5.6")
assert processed[0]["content"] == [{"type": "text", "text": "sys", "prompt_cache_breakpoint": self.EXPLICIT}]
assert params["prompt_cache_options"] == self.EXPLICIT
assert params["prompt_cache_options"] == {"mode": "implicit"}

def test_chat_keeps_caller_prompt_cache_options(self):
@pytest.mark.parametrize("mode", ["explicit", "implicit"])
def test_chat_keeps_caller_prompt_cache_options(self, mode):
params = {
"cache_control_injection_points": copy.deepcopy(self.SYSTEM_POINT),
"prompt_cache_options": {"mode": "implicit"},
"prompt_cache_options": {"mode": mode, "ttl": "24h"},
}
self._chat([{"role": "system", "content": "sys"}], params)
assert params["prompt_cache_options"] == {"mode": "implicit"}
assert params["prompt_cache_options"] == {"mode": mode, "ttl": "24h"}

def test_chat_no_prompt_cache_options_when_nothing_injected(self):
params = {"cache_control_injection_points": copy.deepcopy(self.SYSTEM_POINT)}
Expand Down Expand Up @@ -3030,7 +3031,7 @@ def test_chat_seeded_points_apply_beside_client_breakpoint(self):
_, processed, _ = self._chat(messages, params)
assert processed[0]["content"] == [{"type": "text", "text": "sys", "prompt_cache_breakpoint": self.EXPLICIT}]
assert processed[1] == messages[1]
assert params["prompt_cache_options"] == self.EXPLICIT
assert params["prompt_cache_options"] == {"mode": "implicit"}

def test_cap_counts_client_breakpoints_of_both_kinds(self):
messages = [
Expand Down Expand Up @@ -3082,7 +3083,7 @@ def test_tool_message_text_is_marked_on_chat_path(self):
]
out, params = self._chat(messages, [{"location": "message", "index": -1}])
assert out[2]["content"] == [{"type": "text", "text": "sunny", "prompt_cache_breakpoint": self.EXPLICIT}]
assert params["prompt_cache_options"] == self.EXPLICIT
assert params["prompt_cache_options"] == {"mode": "implicit"}

def test_tool_result_only_turn_is_skipped_on_v1_messages(self):
messages = [
Expand Down Expand Up @@ -3194,12 +3195,12 @@ def test_openai_compatible_provider_keeps_anthropic_style_markers(self):
def test_explicit_openai_provider_uses_openai_dialect(self):
out, params = self._seed_and_run("gpt-5.6", "openai")
assert out[0]["content"] == [{"type": "text", "text": "sys", "prompt_cache_breakpoint": {"mode": "explicit"}}]
assert params["prompt_cache_options"] == {"mode": "explicit"}
assert params["prompt_cache_options"] == {"mode": "implicit"}

def test_bare_gpt_model_without_provider_resolves_to_openai(self):
out, params = self._seed_and_run("gpt-5.6", None)
assert out[0]["content"] == [{"type": "text", "text": "sys", "prompt_cache_breakpoint": {"mode": "explicit"}}]
assert params["prompt_cache_options"] == {"mode": "explicit"}
assert params["prompt_cache_options"] == {"mode": "implicit"}

def test_points_keep_identity_for_models_below_gpt_5_6(self):
points = copy.deepcopy(self.POINTS)
Expand Down Expand Up @@ -3240,7 +3241,7 @@ def test_custom_api_base_opts_in_through_prompt_cache_options(self):
def test_regional_openai_api_base_uses_openai_dialect(self):
out, params = self._seed_and_run("gpt-5.6", None, api_base="https://eu.api.openai.com/v1")
assert out[0]["content"] == self.OPENAI_STYLE
assert params["prompt_cache_options"] == {"mode": "explicit"}
assert params["prompt_cache_options"] == {"mode": "implicit"}

@pytest.mark.parametrize("env_var", ["OPENAI_BASE_URL", "OPENAI_API_BASE"])
def test_env_api_base_override_keeps_anthropic_style_markers(self, monkeypatch, env_var):
Expand All @@ -3259,7 +3260,7 @@ def test_request_api_base_wins_over_env_override(self, monkeypatch):
monkeypatch.setenv("OPENAI_BASE_URL", self.CUSTOM_API_BASE)
out, params = self._seed_and_run("gpt-5.6", None, api_base="https://api.openai.com/v1")
assert out[0]["content"] == self.OPENAI_STYLE
assert params["prompt_cache_options"] == {"mode": "explicit"}
assert params["prompt_cache_options"] == {"mode": "implicit"}

@pytest.mark.parametrize(
"api_base,expected",
Expand Down Expand Up @@ -3350,7 +3351,7 @@ def test_marker_lands_on_last_input_text_part(self):
"text": "second",
"prompt_cache_breakpoint": self.EXPLICIT,
}
assert params["prompt_cache_options"] == self.EXPLICIT
assert params["prompt_cache_options"] == {"mode": "implicit"}

@pytest.mark.parametrize(
"part",
Expand All @@ -3362,7 +3363,7 @@ def test_marker_lands_on_last_input_text_part(self):
def test_input_image_and_input_file_parts_are_eligible(self, part):
out, params = self._chat([{"role": "user", "content": [part]}], [{"location": "message", "index": -1}])
assert out[0]["content"][0] == {**part, "prompt_cache_breakpoint": self.EXPLICIT}
assert params["prompt_cache_options"] == self.EXPLICIT
assert params["prompt_cache_options"] == {"mode": "implicit"}


class TestMessagesPathApiBaseGate:
Expand Down Expand Up @@ -3433,7 +3434,7 @@ def test_chat_path_marks_all_four_messages(self):
dynamic_callback_params={},
)
assert [msg["content"][0].get("prompt_cache_breakpoint") for msg in out] == [self.EXPLICIT] * 4
assert params["prompt_cache_options"] == self.EXPLICIT
assert params["prompt_cache_options"] == {"mode": "implicit"}

def test_messages_path_marks_all_four_messages(self):
out, _, _ = AnthropicCacheControlHook.apply_to_anthropic_messages_request(
Expand Down Expand Up @@ -3496,7 +3497,7 @@ def test_published_map_without_the_flag_still_injects_on_gpt_5_6(self, monkeypat
assert chat_messages[0]["content"] == [
{"type": "text", "text": "sys", "prompt_cache_breakpoint": {"mode": "explicit"}}
]
assert chat_params["prompt_cache_options"] == {"mode": "explicit"}
assert chat_params["prompt_cache_options"] == {"mode": "implicit"}

kwargs = {"cache_control_injection_points": copy.deepcopy(points)}
_, system = AnthropicCacheControlHook.maybe_inject_cache_control(
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/responses/test_responses_api_request_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ async def test_aresponses_injection_point_marks_input_text_on_gpt_5_6():
"prompt_cache_breakpoint": {"mode": "explicit"},
}
assert body["input"][1] == {"role": "user", "content": "hi"}
assert body["prompt_cache_options"] == {"mode": "explicit"}
assert body["prompt_cache_options"] == {"mode": "implicit"}


def test_responses_injection_point_marks_input_text_on_gpt_5_6():
Expand All @@ -683,7 +683,7 @@ def test_responses_injection_point_marks_input_text_on_gpt_5_6():
"prompt_cache_breakpoint": {"mode": "explicit"},
}
assert body["input"][1] == {"role": "user", "content": "hi"}
assert body["prompt_cache_options"] == {"mode": "explicit"}
assert body["prompt_cache_options"] == {"mode": "implicit"}


@pytest.mark.asyncio
Expand Down Expand Up @@ -767,7 +767,7 @@ async def test_aresponses_custom_api_base_opts_in_through_prompt_cache_options()
async def test_aresponses_regional_openai_api_base_marks_input_text():
body = await _aresponses_body_with_system_point(model="gpt-5.6", api_base="https://eu.api.openai.com/v1")
assert body["input"][0]["content"][0]["prompt_cache_breakpoint"] == {"mode": "explicit"}
assert body["prompt_cache_options"] == {"mode": "explicit"}
assert body["prompt_cache_options"] == {"mode": "implicit"}


@pytest.mark.usefixtures("_no_openai_api_base_override")
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3627,7 +3627,7 @@ def test_completion_default_api_base_sends_prompt_cache_breakpoint_for_gpt_5_6()
assert request_body["messages"][0]["content"] == [
{"type": "text", "text": "sys", "prompt_cache_breakpoint": {"mode": "explicit"}}
]
assert request_body["extra_body"]["prompt_cache_options"] == {"mode": "explicit"}
assert request_body["extra_body"]["prompt_cache_options"] == {"mode": "implicit"}


_SUBSCRIPTION_OAUTH_CREDENTIAL = "Bearer sk-ant-oat01-fake-subscription-token-for-testing-0123456789"
Expand Down
Loading