fix(anthropic): keep breakpoint translation off other providers and settle marks litellm itself adds
Some checks failed
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

A string additional_drop_params no longer fails every completion() call. It is
validated only on a Bedrock model, as it was before, and the Converse checks run
only for a Claude target.

A cache_control without a ttl that the request already carries, such as an
operator's injection point, now takes 1h when a later 1h breakpoint needs it,
so the request stays valid for Claude instead of getting its 400.

A cache_control under a tool's function no longer counts on Converse, which
never sends it, so it no longer holds a breakpoint at 5m ahead of a 1h one.

A breakpoint on a server tool call now counts in the cache-control hook's census,
and a breakpoint on a trailing tool_search call counts toward the four when
modify_params answers it with a placeholder, so injection points no longer push
a request to five marks.
This commit is contained in:
mateo-berri 2026-10-16 04:00:56 +00:00
parent efc5e1e8d3
commit 80675d89b3
6 changed files with 202 additions and 62 deletions

View file

@ -309,7 +309,7 @@ def _is_marked_server_tool_call(tool_call: object) -> bool:
return ( return (
isinstance(tool_call_id, str) isinstance(tool_call_id, str)
and tool_call_id.startswith(ANTHROPIC_SERVER_TOOL_USE_ID_PREFIX) and tool_call_id.startswith(ANTHROPIC_SERVER_TOOL_USE_ID_PREFIX)
and _attribute_or_key(tool_call, "cache_control") is not None and _carries_cache_breakpoint(tool_call)
) )

View file

@ -50,6 +50,7 @@ class _Placed:
path: _Path path: _Path
ttl: Literal["5m", "1h"] ttl: Literal["5m", "1h"]
kind: _Kind kind: _Kind
translated: bool
first_read: tuple[int, ...] first_read: tuple[int, ...]
last_read: tuple[int, ...] last_read: tuple[int, ...]
@ -447,13 +448,16 @@ def _on_wire(
tools: Sequence[object], tools: Sequence[object],
unsent_call_ids: frozenset[str], unsent_call_ids: frozenset[str],
tool_history_as_text: bool, tool_history_as_text: bool,
function_marks_on_wire: bool,
) -> bool: ) -> bool:
"""Claude sends a message-level mark on string content, or on list content only for a tool result, and none on """Claude sends a message-level mark on string content, or on list content only for a tool result, and none on
a developer message, which reaches it as a system message without one; a mark under ``function`` only when the a developer message, which reaches it as a system message without one; a mark under ``function`` only on a route
tool carries none of its own; no tool_search call nobody answered; and no mark on tool history the route sends that sends one and when the tool carries none of its own; no tool_search call nobody answered; and no mark on
as text.""" tool history the route sends as text."""
if path[0] == "tools": if path[0] == "tools":
return len(path) == 2 or (_fields(tools[int(path[1])]) or {}).get("cache_control") is None return len(path) == 2 or (
function_marks_on_wire and (_fields(tools[int(path[1])]) or {}).get("cache_control") is None
)
message: Final = _fields(messages[int(path[1])]) or {} message: Final = _fields(messages[int(path[1])]) or {}
if tool_history_as_text and _is_tool_history(message): if tool_history_as_text and _is_tool_history(message):
return False return False
@ -467,10 +471,10 @@ def _ttl(cache_control: object) -> Literal["5m", "1h"]:
return "1h" if (_fields(cache_control) or {}).get("ttl") == "1h" else "5m" return "1h" if (_fields(cache_control) or {}).get("ttl") == "1h" else "5m"
def _kind(path: _Path, cache_control: object, translated: frozenset[_Path]) -> _Kind: def _kind(cache_control: object, from_breakpoint: bool) -> _Kind:
if path not in translated: if (_fields(cache_control) or {}).get("ttl") is None:
return "written" return "free"
return "free" if (_fields(cache_control) or {}).get("ttl") is None else "explicit" return "explicit" if from_breakpoint else "written"
def _chosen(mark: _Placed, ttl: Literal["5m", "1h"]) -> bool: def _chosen(mark: _Placed, ttl: Literal["5m", "1h"]) -> bool:
@ -514,9 +518,9 @@ def _loosened(marks: Sequence[_Placed], drop_params: bool) -> tuple[_Placed, ...
def _settled(marks: Sequence[_Placed], drop_params: bool) -> tuple[_Placed, ...]: def _settled(marks: Sequence[_Placed], drop_params: bool) -> tuple[_Placed, ...]:
"""A breakpoint without a ttl takes 1h when Claude may read a 1h mark after it and no 5m mark before it, so the """A mark without a ttl, a breakpoint's or a ``cache_control`` the request already carries, takes 1h when Claude
request stays valid for Claude; under ``drop_params`` a ttl the client set on a breakpoint that breaks that order may read a 1h mark after it and no 5m mark before it, so the request stays valid for Claude; under
is dropped first.""" ``drop_params`` a ttl the client set on a breakpoint that breaks that order is dropped first."""
loosened: Final = _loosened(marks, drop_params) loosened: Final = _loosened(marks, drop_params)
first_five: Final = _first_five(loosened) first_five: Final = _first_five(loosened)
last_hour: Final = max((mark.last_read for mark in loosened if _chosen(mark, "1h")), default=(-1,)) last_hour: Final = max((mark.last_read for mark in loosened if _chosen(mark, "1h")), default=(-1,))
@ -530,22 +534,14 @@ def _settled(marks: Sequence[_Placed], drop_params: bool) -> tuple[_Placed, ...]
def _route_marks( def _route_marks(
tool_config_cache_control: ChatCompletionCachedContent | None, request_mark: object tool_config_cache_control: ChatCompletionCachedContent | None, request_mark: object
) -> tuple[tuple[_Path, object, _Kind], ...]: ) -> tuple[tuple[_Path, object, bool], ...]:
"""The cachePoint a route appends after the tools takes 1h like a breakpoint when it sets no ttl. Neither it nor """Neither the cachePoint a route appends after the tools nor the request-level mark is ever dropped."""
the request-level mark is ever dropped."""
tool_config_kind: Final[_Kind] = (
"free" if tool_config_cache_control is None or tool_config_cache_control.get("ttl") is None else "written"
)
return ( return (
*(() if tool_config_cache_control is None else ((_TOOL_CONFIG, tool_config_cache_control, tool_config_kind),)), *(() if tool_config_cache_control is None else ((_TOOL_CONFIG, tool_config_cache_control, False),)),
*(() if request_mark is None else ((_REQUEST, request_mark, "written"),)), *(() if request_mark is None else ((_REQUEST, request_mark, False),)),
) )
def _droppable(mark: _Placed) -> bool:
return mark.kind != "written" and mark.path != _TOOL_CONFIG
def claude_prompt_cache( def claude_prompt_cache(
messages: Sequence[object], messages: Sequence[object],
tools: Sequence[object] | None, tools: Sequence[object] | None,
@ -554,13 +550,17 @@ def claude_prompt_cache(
drop_params: bool, drop_params: bool,
tool_history_as_text: bool, tool_history_as_text: bool,
tool_config_cache_control: ChatCompletionCachedContent | None, tool_config_cache_control: ChatCompletionCachedContent | None,
function_marks_on_wire: bool,
pairs_unanswered_tool_calls: bool,
) -> ClaudePromptCache | UnsupportedPromptCacheParam: ) -> ClaudePromptCache | UnsupportedPromptCacheParam:
"""``prompt_cache_breakpoint`` marks renamed to ``cache_control``. A mark Claude cannot honor is reported unless """``prompt_cache_breakpoint`` marks renamed to ``cache_control``. A mark Claude cannot honor is reported unless
``drop_params``, which keeps what Claude can use and, when the request ends up with more than Claude's four ``drop_params``, which keeps what Claude can use and, when the request ends up with more than Claude's four
breakpoints, drops the earliest translated ones. A request-level ``cache_control`` in ``params`` and the breakpoints, drops the earliest translated ones. A request-level ``cache_control`` in ``params`` and the
``tool_config_cache_control`` cachePoint a route appends after the tools each take one of the four, and marks on ``tool_config_cache_control`` cachePoint a route appends after the tools each take one of the four, and marks on
tool history a ``tool_history_as_text`` route flattens take none. A request carrying no breakpoint is returned as tool history a ``tool_history_as_text`` route flattens, marks under a tool's ``function`` on a route without
written.""" ``function_marks_on_wire``, and marks on a tool_search call nobody answered, unless the route
``pairs_unanswered_tool_calls`` with a placeholder result, take none. A request carrying no breakpoint is returned
as written."""
listed_tools: Final = tools or () listed_tools: Final = tools or ()
if not any(map(_message_carries_breakpoint, messages)) and not any(map(_tool_carries_breakpoint, listed_tools)): if not any(map(_message_carries_breakpoint, messages)) and not any(map(_tool_carries_breakpoint, listed_tools)):
return ClaudePromptCache(tuple(messages), None if tools is None else tuple(tools)) return ClaudePromptCache(tuple(messages), None if tools is None else tuple(tools))
@ -588,14 +588,16 @@ def claude_prompt_cache(
translated: Final = _translated(hoisted_messages, hoisted_tools) translated: Final = _translated(hoisted_messages, hoisted_tools)
leading_system_count: Final = _leading_system_count(messages) leading_system_count: Final = _leading_system_count(messages)
run_ends: Final = _run_ends(messages) run_ends: Final = _run_ends(messages)
unsent_call_ids: Final = unanswered_tool_search_call_ids(messages) unsent_call_ids: Final = frozenset() if pairs_unanswered_tool_calls else unanswered_tool_search_call_ids(messages)
request_mark: Final = params.get("cache_control") request_mark: Final = params.get("cache_control")
placements: Final[tuple[tuple[_Path, object, _Kind], ...]] = ( placements: Final[tuple[tuple[_Path, object, bool], ...]] = (
*( *(
(path, block.get("cache_control"), _kind(path, block.get("cache_control"), translated)) (path, block.get("cache_control"), path in translated)
for path, block in _markable(renamed_messages, renamed_tools) for path, block in _markable(renamed_messages, renamed_tools)
if block.get("cache_control") is not None if block.get("cache_control") is not None
and _on_wire(path, renamed_messages, renamed_tools, unsent_call_ids, tool_history_as_text) and _on_wire(
path, renamed_messages, renamed_tools, unsent_call_ids, tool_history_as_text, function_marks_on_wire
)
), ),
*_route_marks(tool_config_cache_control, request_mark), *_route_marks(tool_config_cache_control, request_mark),
) )
@ -604,17 +606,18 @@ def claude_prompt_cache(
_Placed( _Placed(
path, path,
_ttl(cache_control), _ttl(cache_control),
kind, _kind(cache_control, from_breakpoint),
from_breakpoint,
_wire_position(path, leading_system_count), _wire_position(path, leading_system_count),
_last_read(path, messages, leading_system_count, run_ends), _last_read(path, messages, leading_system_count, run_ends),
) )
for path, cache_control, kind in placements for path, cache_control, from_breakpoint in placements
), ),
key=lambda mark: mark.first_read, key=lambda mark: mark.first_read,
) )
excess: Final = len(marks) - MAX_CACHE_CONTROL_BLOCKS excess: Final = len(marks) - MAX_CACHE_CONTROL_BLOCKS
dropped: Final[frozenset[_Path]] = ( dropped: Final[frozenset[_Path]] = (
frozenset(tuple(mark.path for mark in marks if _droppable(mark))[:excess]) frozenset(tuple(mark.path for mark in marks if mark.translated)[:excess])
if excess > 0 and drop_params if excess > 0 and drop_params
else frozenset() else frozenset()
) )

View file

@ -1110,14 +1110,22 @@ def _reaches_converse(
custom_llm_provider: str | None, custom_llm_provider: str | None,
model: str, model: str,
route_params: Mapping[str, object], route_params: Mapping[str, object],
additional_drop_params: Sequence[str] | None, additional_drop_params: object,
) -> bool: ) -> bool:
"""A Bedrock model on the native Chat Completions route still reaches Converse when the request needs it.""" """A Bedrock model on the native Chat Completions route still reaches Converse when the request needs it."""
if isinstance(provider_config, AmazonConverseConfig): if isinstance(provider_config, AmazonConverseConfig):
return True return True
if custom_llm_provider != "bedrock":
return False
return ( return (
custom_llm_provider == "bedrock" bedrock_route_for_request(
and bedrock_route_for_request(model, route_params, additional_drop_params) == "converse" model,
route_params,
None
if additional_drop_params is None
else _ADDITIONAL_DROP_PARAMS_ADAPTER.validate_python(additional_drop_params),
)
== "converse"
) )
@ -5646,14 +5654,12 @@ def completion(
and normalize_drop_params(cast(object, kwargs["drop_params"])) is True # cast-ok: untyped request kwargs and normalize_drop_params(cast(object, kwargs["drop_params"])) is True # cast-ok: untyped request kwargs
) )
request_tools: Final = cast(list[object] | None, tools) # cast-ok: completion types tools as a bare List request_tools: Final = cast(list[object] | None, tools) # cast-ok: completion types tools as a bare List
converse_may_send_tools: Final = AmazonConverseConfig.may_send_tools( claude_cache_target: Final = provider_config is not None and _is_claude_prompt_cache_target(
tools=request_tools, custom_llm_provider=custom_llm_provider,
functions=cast(object, functions), # cast-ok: completion types functions as a bare List model=model,
response_format=cast(object, response_format), # cast-ok: completion types response_format as a bare dict base_model=base_model if isinstance(base_model, str) else None,
web_search_options=web_search_options,
messages=routed_messages,
) )
reaches_converse: Final = _reaches_converse( reaches_converse: Final = claude_cache_target and _reaches_converse(
provider_config, provider_config,
custom_llm_provider, custom_llm_provider,
model, model,
@ -5668,11 +5674,14 @@ def completion(
**non_default_params, **non_default_params,
} }
), ),
( kwargs.get("additional_drop_params"),
_ADDITIONAL_DROP_PARAMS_ADAPTER.validate_python(kwargs["additional_drop_params"]) )
if kwargs.get("additional_drop_params") is not None converse_may_send_tools: Final = reaches_converse and AmazonConverseConfig.may_send_tools(
else None tools=request_tools,
), functions=cast(object, functions), # cast-ok: completion types functions as a bare List
response_format=cast(object, response_format), # cast-ok: completion types response_format as a bare dict
web_search_options=web_search_options,
messages=routed_messages,
) )
claude_cache: Final = ( claude_cache: Final = (
resolved_claude_prompt_cache( resolved_claude_prompt_cache(
@ -5687,19 +5696,16 @@ def completion(
AmazonConverseConfig.tool_config_cache_control( AmazonConverseConfig.tool_config_cache_control(
non_default_params.get("cache_control_injection_points") non_default_params.get("cache_control_injection_points")
) )
if reaches_converse and converse_may_send_tools if converse_may_send_tools
else None else None
), ),
function_marks_on_wire=not reaches_converse,
pairs_unanswered_tool_calls=litellm.modify_params is True,
), ),
model=model, model=model,
llm_provider=custom_llm_provider, llm_provider=custom_llm_provider,
) )
if provider_config is not None if claude_cache_target
and _is_claude_prompt_cache_target(
custom_llm_provider=custom_llm_provider,
model=model,
base_model=base_model if isinstance(base_model, str) else None,
)
else None else None
) )
provider_messages: Final = cast( # cast-ok: completion() only renames marks and drops tool_search calls provider_messages: Final = cast( # cast-ok: completion() only renames marks and drops tool_search calls

View file

@ -28,6 +28,10 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
_convert_to_bedrock_tool_call_invoke, _convert_to_bedrock_tool_call_invoke,
convert_to_anthropic_tool_invoke, convert_to_anthropic_tool_invoke,
) )
from litellm.llms.anthropic.chat.prompt_cache_breakpoints import (
ClaudePromptCache,
claude_prompt_cache,
)
from litellm.llms.anthropic.chat.transformation import AnthropicConfig from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.types.llms.openai import AllMessageValues, ChatCompletionAssistantToolCall from litellm.types.llms.openai import AllMessageValues, ChatCompletionAssistantToolCall
@ -1272,6 +1276,53 @@ def test_cache_control_hook_counts_a_server_tool_call_mark_only_when_anthropic_r
assert (census, _wire_cache_marks(messages, model)) == (expected_breakpoints, expected_breakpoints) assert (census, _wire_cache_marks(messages, model)) == (expected_breakpoints, expected_breakpoints)
def _translated_for_claude(messages: list[AllMessageValues], modify_params: bool) -> list[AllMessageValues]:
outcome: Final = claude_prompt_cache(
messages,
None,
{},
drop_params=False,
tool_history_as_text=False,
tool_config_cache_control=None,
function_marks_on_wire=True,
pairs_unanswered_tool_calls=modify_params,
)
assert isinstance(outcome, ClaudePromptCache), outcome
return cast(list[AllMessageValues], list(outcome.messages))
@pytest.mark.parametrize(
("follow_ups", "modify_params", "expected_breakpoints"),
[
pytest.param((), False, 0, id="trailing_call_rebuilt_from_its_kept_result"),
pytest.param((), True, 1, id="trailing_call_replayed_with_the_modify_params_placeholder"),
pytest.param((_SEARCH_ANSWER,), False, 1, id="replayed_with_the_client_answer"),
pytest.param((_SEARCH_ANSWER, _GO_ON), False, 0, id="client_answer_replaced_by_the_kept_result"),
],
)
def test_cache_control_hook_counts_a_breakpoint_on_a_server_tool_call_only_when_claude_receives_its_mark(
monkeypatch: pytest.MonkeyPatch,
follow_ups: tuple[AllMessageValues, ...],
modify_params: bool,
expected_breakpoints: int,
):
monkeypatch.setattr(litellm, "modify_params", modify_params)
search_call: Final = {
**{key: value for key, value in _SEARCH_CALL.items() if key != "cache_control"},
"prompt_cache_breakpoint": {"mode": "explicit"},
}
messages: Final = [
cast(AllMessageValues, {"role": "user", "content": "Weather in Paris?"}),
_replayed_search_call(search_call),
*follow_ups,
]
census: Final = AnthropicCacheControlHook.count_request_cache_breakpoints(messages)
received: Final = _wire_cache_marks(_translated_for_claude(messages, modify_params))
assert (census, received) == (expected_breakpoints, expected_breakpoints)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"after_the_call", "after_the_call",
[(_SEARCH_ANSWER, _SYSTEM_NOTE), (_SYSTEM_NOTE, _SEARCH_ANSWER)], [(_SEARCH_ANSWER, _SYSTEM_NOTE), (_SYSTEM_NOTE, _SEARCH_ANSWER)],

View file

@ -24,6 +24,8 @@ def _cache(
*, *,
drop_params: bool = False, drop_params: bool = False,
tool_config_cache_control: ChatCompletionCachedContent | None = None, tool_config_cache_control: ChatCompletionCachedContent | None = None,
function_marks_on_wire: bool = True,
pairs_unanswered_tool_calls: bool = False,
) -> ClaudePromptCache | UnsupportedPromptCacheParam: ) -> ClaudePromptCache | UnsupportedPromptCacheParam:
return claude_prompt_cache( return claude_prompt_cache(
messages, messages,
@ -32,6 +34,8 @@ def _cache(
drop_params=drop_params, drop_params=drop_params,
tool_history_as_text=False, tool_history_as_text=False,
tool_config_cache_control=tool_config_cache_control, tool_config_cache_control=tool_config_cache_control,
function_marks_on_wire=function_marks_on_wire,
pairs_unanswered_tool_calls=pairs_unanswered_tool_calls,
) )
@ -42,9 +46,17 @@ def _resolved(
*, *,
drop_params: bool = False, drop_params: bool = False,
tool_config_cache_control: ChatCompletionCachedContent | None = None, tool_config_cache_control: ChatCompletionCachedContent | None = None,
function_marks_on_wire: bool = True,
pairs_unanswered_tool_calls: bool = False,
) -> ClaudePromptCache: ) -> ClaudePromptCache:
outcome: Final = _cache( outcome: Final = _cache(
messages, tools, params, drop_params=drop_params, tool_config_cache_control=tool_config_cache_control messages,
tools,
params,
drop_params=drop_params,
tool_config_cache_control=tool_config_cache_control,
function_marks_on_wire=function_marks_on_wire,
pairs_unanswered_tool_calls=pairs_unanswered_tool_calls,
) )
assert isinstance(outcome, ClaudePromptCache), outcome assert isinstance(outcome, ClaudePromptCache), outcome
return outcome return outcome
@ -191,7 +203,14 @@ def test_a_breakpoint_a_client_cache_control_wins_over_is_still_reported(
assert outcome.value == _THIRTY_MINUTES assert outcome.value == _THIRTY_MINUTES
def test_a_breakpoint_on_a_tool_search_call_nobody_answered_takes_no_slot_from_the_system_mark_under_drop_params(): @pytest.mark.parametrize(
("pairs_unanswered_tool_calls", "system_mark"),
[(False, {"cache_control": _EPHEMERAL}), (True, {})],
ids=["dropped_unanswered", "answered_with_a_placeholder"],
)
def test_a_breakpoint_on_a_tool_search_call_nobody_answered_takes_a_slot_only_when_the_route_answers_it(
pairs_unanswered_tool_calls: bool, system_mark: dict[str, object]
):
search_call: Final = { search_call: Final = {
"id": "srvtoolu_01search", "id": "srvtoolu_01search",
"type": "function", "type": "function",
@ -205,9 +224,47 @@ def test_a_breakpoint_on_a_tool_search_call_nobody_answered_takes_no_slot_from_t
{"role": "user", "content": "Which section mentions watchlists?"}, {"role": "user", "content": "Which section mentions watchlists?"},
] ]
result: Final = _resolved(messages, drop_params=True) result: Final = _resolved(messages, drop_params=True, pairs_unanswered_tool_calls=pairs_unanswered_tool_calls)
assert result.messages[0] == {"role": "system", "content": [_part("sys", cache_control=_EPHEMERAL)]} assert result.messages[0] == {"role": "system", "content": [_part("sys", **system_mark)]}
def test_a_cache_control_without_a_ttl_takes_the_hour_a_later_breakpoint_asks_for():
result: Final = _resolved(
[
{"role": "system", "content": [_part("sys", cache_control=_EPHEMERAL)]},
{"role": "user", "content": [_part("ask", prompt_cache_breakpoint={"mode": "explicit", "ttl": "1h"})]},
]
)
assert list(result.messages) == [
{"role": "system", "content": [_part("sys", cache_control=_ONE_HOUR)]},
{"role": "user", "content": [_part("ask", cache_control=_ONE_HOUR)]},
]
@pytest.mark.parametrize(
("function_marks_on_wire", "system_mark"),
[(True, _EPHEMERAL), (False, _ONE_HOUR)],
ids=["route_sends_function_marks", "route_drops_function_marks"],
)
def test_a_five_minute_mark_under_a_tools_function_holds_back_an_hour_only_on_a_route_that_sends_it(
function_marks_on_wire: bool, system_mark: dict[str, object]
):
five_minutes: Final = {"type": "ephemeral", "ttl": "5m"}
tool: Final = {"type": "function", "function": {"name": "t", "cache_control": five_minutes}}
result: Final = _resolved(
[
{"role": "system", "content": [_part("sys", prompt_cache_breakpoint=_EXPLICIT)]},
{"role": "user", "content": [_part("ask", prompt_cache_breakpoint={"mode": "explicit", "ttl": "1h"})]},
],
[tool],
function_marks_on_wire=function_marks_on_wire,
)
assert result.messages[0] == {"role": "system", "content": [_part("sys", cache_control=system_mark)]}
assert list(result.tools or ()) == [tool]
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -341,10 +398,10 @@ def test_a_breakpoint_read_after_the_last_one_hour_mark_keeps_the_default_ttl():
("system", "ask", "as_written", "under_drop_params"), ("system", "ask", "as_written", "under_drop_params"),
[ [
( (
{"cache_control": _EPHEMERAL}, {"cache_control": {"type": "ephemeral", "ttl": "5m"}},
{"prompt_cache_breakpoint": {"mode": "explicit", "ttl": "1h"}}, {"prompt_cache_breakpoint": {"mode": "explicit", "ttl": "1h"}},
({"cache_control": _EPHEMERAL}, {"cache_control": _ONE_HOUR}), ({"cache_control": {"type": "ephemeral", "ttl": "5m"}}, {"cache_control": _ONE_HOUR}),
({"cache_control": _EPHEMERAL}, {"cache_control": _EPHEMERAL}), ({"cache_control": {"type": "ephemeral", "ttl": "5m"}}, {"cache_control": _EPHEMERAL}),
), ),
( (
{"prompt_cache_breakpoint": {"mode": "explicit", "ttl": "5m"}}, {"prompt_cache_breakpoint": {"mode": "explicit", "ttl": "5m"}},

View file

@ -759,6 +759,29 @@ def test_a_breakpoint_reaches_bedrock_mantle_claude_as_cache_control(respx_mock:
] ]
def test_a_string_additional_drop_params_still_reaches_claude_on_the_anthropic_route(respx_mock: respx.MockRouter):
route: Final = respx_mock.post("https://api.anthropic.com/v1/messages").respond(
json={
"id": "msg_1",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [{"type": "text", "text": "hi"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1},
}
)
response: Final = litellm.completion(
model="anthropic/claude-opus-5-5",
messages=[{"role": "user", "content": "Say hi."}],
api_key="fake",
additional_drop_params="temperature",
)
assert (route.call_count, response.choices[0].message.content) == (1, "hi")
_WEATHER_TOOL: Final = {"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}} _WEATHER_TOOL: Final = {"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}}