mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-07 07:58:09 +00:00
fix(anthropic): send the thinking betas for block_binding and display updates, strip Databricks chat thinking
Every Claude leg now adds the thinking-binding-controls beta when thinking carries block_binding and the display-updates beta for display updates, so those fields go through instead of a 400. Databricks chat rejects both fields with or without the beta, so it drops them under drop_params. A system turn the strip empties stays when it still carries output_config, and the drop warning names both causes.
This commit is contained in:
parent
e2552fe052
commit
b42590dc20
21 changed files with 259 additions and 76 deletions
|
|
@ -23,10 +23,10 @@ from litellm.types.llms.anthropic import (
|
|||
ANTHROPIC_BETA_HEADER_VALUES,
|
||||
ANTHROPIC_MID_CONVERSATION_OUTPUT_CONFIG_BETA_HEADER,
|
||||
ANTHROPIC_MID_CONVERSATION_TOOL_CHANGES_BETA_HEADER,
|
||||
ANTHROPIC_THINKING_BINDING_CONTROLS_BETA_HEADER,
|
||||
ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER,
|
||||
)
|
||||
|
||||
ANTHROPIC_THINKING_BINDING_CONTROLS_BETA_HEADER: Final = "thinking-binding-controls-2026-08-01"
|
||||
ANTHROPIC_MID_CONVERSATION_SYSTEM_CLEAR_AT_BETA_HEADER: Final = "mid-conversation-system-clear-at-2026-08-21"
|
||||
ANTHROPIC_INLINE_TOOLS_BETA_HEADER: Final = "inline-tools-2026-09-15"
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ def strip_beta_gated_thinking(thinking: object, active_betas: Set[str], *, drop_
|
|||
|
||||
def _strip_beta_gated_system_message(message: Mapping[str, object], active_betas: Set[str]) -> StrippedMessages:
|
||||
"""One mid-conversation ``role: system`` message. ``messages`` holds the rewritten message, or nothing
|
||||
when the strip left it with no content, since an empty system turn is itself a 400."""
|
||||
when the strip left it with neither content nor ``output_config``, since such a system turn is itself a 400."""
|
||||
clear_at_gated: Final = "clear_at" in message and field_is_gated(SYSTEM_CLEAR_AT, active_betas)
|
||||
output_config_gated: Final = "output_config" in message and field_is_gated(SYSTEM_OUTPUT_CONFIG, active_betas)
|
||||
content: Final = message.get("content")
|
||||
|
|
@ -184,7 +184,7 @@ def _strip_beta_gated_system_message(message: Mapping[str, object], active_betas
|
|||
*((SYSTEM_OUTPUT_CONFIG,) if output_config_gated else ()),
|
||||
*((SYSTEM_TOOL_CHANGES,) if tool_changes_gated else ()),
|
||||
)
|
||||
if kept_content in (None, "", []):
|
||||
if kept_content in (None, "", []) and "output_config" not in rewritten:
|
||||
return StrippedMessages(messages=(), stripped=(*stripped, SYSTEM_MESSAGE_EMPTIED))
|
||||
return StrippedMessages(messages=(rewritten,), stripped=stripped)
|
||||
|
||||
|
|
@ -292,9 +292,10 @@ def log_beta_gated_strips(stripped: Sequence[str], model: str | None, provider:
|
|||
if not stripped:
|
||||
return
|
||||
verbose_logger.warning(
|
||||
"Stripped %s from the request for model=%s provider=%s because drop_params is on and the request "
|
||||
"does not send the anthropic-beta that gates them on this provider (see anthropic_beta_headers_config.json). "
|
||||
"Send that beta to keep them where the provider accepts it.",
|
||||
"Dropped %s for model=%s provider=%s because drop_params is on and the provider would reject them on "
|
||||
"this request, either since this provider leg does not send the anthropic-beta that unlocks them (see "
|
||||
"anthropic_beta_headers_config.json) or since the model turns mid-conversation system messages into user "
|
||||
"messages.",
|
||||
sorted(set(stripped)),
|
||||
model,
|
||||
provider,
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@
|
|||
"text_editor_20241022": null,
|
||||
"text_editor_20250124": null,
|
||||
"thinking-binding-controls-2026-08-01": "thinking-binding-controls-2026-08-01",
|
||||
"thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18",
|
||||
"token-efficient-tools-2025-02-19": "token-efficient-tools-2025-02-19",
|
||||
"web-fetch-2025-09-10": null,
|
||||
"web-search-2025-03-05": "web-search-2025-03-05"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ from litellm.types.llms.anthropic import (
|
|||
ANTHROPIC_OAUTH_BETA_HEADER,
|
||||
ANTHROPIC_OAUTH_TOKEN_PREFIX,
|
||||
ANTHROPIC_SERVER_TOOL_USE_ID_PREFIX,
|
||||
ANTHROPIC_THINKING_BINDING_CONTROLS_BETA_HEADER,
|
||||
ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER,
|
||||
ANTHROPIC_TOOL_SEARCH_TOOL_TYPES,
|
||||
AllAnthropicToolsValues,
|
||||
|
|
@ -552,6 +553,20 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
return False
|
||||
return thinking.get("type") in ("adaptive", "enabled") and thinking.get("display") == "updates"
|
||||
|
||||
def thinking_betas(self, thinking: AnthropicThinkingParam | None) -> tuple[str, ...]:
|
||||
return (
|
||||
*(
|
||||
(ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER,)
|
||||
if self.is_thinking_display_updates_used(thinking)
|
||||
else ()
|
||||
),
|
||||
*(
|
||||
(ANTHROPIC_THINKING_BINDING_CONTROLS_BETA_HEADER,)
|
||||
if isinstance(thinking, dict) and "block_binding" in thinking
|
||||
else ()
|
||||
),
|
||||
)
|
||||
|
||||
def is_mid_conversation_tool_change_used(self, messages: Sequence[object]) -> bool:
|
||||
for message in messages:
|
||||
if message_field(message, "role") != "system":
|
||||
|
|
@ -1132,7 +1147,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
*,
|
||||
custom_llm_provider: str,
|
||||
is_mid_conversation_output_config_used: bool = False,
|
||||
is_thinking_display_updates_used: bool = False,
|
||||
thinking: AnthropicThinkingParam | None = None,
|
||||
is_mid_conversation_tool_change_used: bool = False,
|
||||
) -> list[str]:
|
||||
"""
|
||||
|
|
@ -1169,13 +1184,10 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
if is_mid_conversation_output_config_used:
|
||||
betas.append(ANTHROPIC_MID_CONVERSATION_OUTPUT_CONFIG_BETA_HEADER)
|
||||
|
||||
thinking_display_betas: Final = (
|
||||
(ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER,) if is_thinking_display_updates_used else ()
|
||||
)
|
||||
tool_change_betas: Final = (
|
||||
(ANTHROPIC_MID_CONVERSATION_TOOL_CHANGES_BETA_HEADER,) if is_mid_conversation_tool_change_used else ()
|
||||
)
|
||||
return list(set(betas).union(thinking_display_betas, tool_change_betas))
|
||||
return list(set(betas).union(self.thinking_betas(thinking), tool_change_betas))
|
||||
|
||||
@staticmethod
|
||||
def _make_api_key_auth_header(
|
||||
|
|
@ -1237,7 +1249,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
api_base: str | None = None,
|
||||
use_bearer_for_custom_base: bool = False,
|
||||
is_mid_conversation_output_config_used: bool = False,
|
||||
is_thinking_display_updates_used: bool = False,
|
||||
thinking: AnthropicThinkingParam | None = None,
|
||||
is_mid_conversation_tool_change_used: bool = False,
|
||||
wif_minted: bool = False,
|
||||
) -> dict:
|
||||
|
|
@ -1298,7 +1310,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
betas.update(user_anthropic_beta_headers)
|
||||
|
||||
all_betas: Final = betas.union(
|
||||
(ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER,) if is_thinking_display_updates_used else (),
|
||||
self.thinking_betas(thinking),
|
||||
(ANTHROPIC_MID_CONVERSATION_TOOL_CHANGES_BETA_HEADER,) if is_mid_conversation_tool_change_used else (),
|
||||
)
|
||||
|
||||
|
|
@ -1385,7 +1397,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
auth_token=auth_token,
|
||||
file_id_used=file_id_used,
|
||||
is_mid_conversation_output_config_used=is_mid_conversation_output_config_used,
|
||||
is_thinking_display_updates_used=self.is_thinking_display_updates_used(optional_params.get("thinking")),
|
||||
thinking=optional_params.get("thinking"),
|
||||
is_mid_conversation_tool_change_used=self.is_mid_conversation_tool_change_used(messages),
|
||||
web_search_tool_used=web_search_tool_used,
|
||||
is_vertex_request=optional_params.get("is_vertex_request", False),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ from litellm.types.llms.anthropic import (
|
|||
ANTHROPIC_ADVISOR_TOOL_TYPE,
|
||||
ANTHROPIC_BETA_HEADER_VALUES,
|
||||
ANTHROPIC_MID_CONVERSATION_TOOL_CHANGES_BETA_HEADER,
|
||||
ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER,
|
||||
AnthropicMessagesRequest,
|
||||
)
|
||||
from litellm.types.llms.anthropic_messages.anthropic_response import (
|
||||
|
|
@ -895,17 +894,13 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
if AnthropicModelInfo().is_tool_search_used(tools):
|
||||
beta_values.add(get_tool_search_beta_header(custom_llm_provider))
|
||||
|
||||
thinking_display_betas: Final = (
|
||||
(ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER,)
|
||||
if AnthropicModelInfo().is_thinking_display_updates_used(optional_params.get("thinking"))
|
||||
else ()
|
||||
)
|
||||
thinking_betas: Final = AnthropicModelInfo().thinking_betas(optional_params.get("thinking"))
|
||||
tool_change_betas: Final = (
|
||||
(ANTHROPIC_MID_CONVERSATION_TOOL_CHANGES_BETA_HEADER,)
|
||||
if AnthropicModelInfo().is_mid_conversation_tool_change_used(messages)
|
||||
else ()
|
||||
)
|
||||
all_beta_values: Final = beta_values.union(thinking_display_betas, tool_change_betas)
|
||||
all_beta_values: Final = beta_values.union(thinking_betas, tool_change_betas)
|
||||
dangerous_tool_use: Final = ANTHROPIC_BETA_HEADER_VALUES.DANGEROUS_TOOL_USE_2026_09_03.value
|
||||
forwarded_beta_values: Final = ( # pyright: ignore[reportUnknownVariableType] # derived from the bare beta set
|
||||
all_beta_values - {dangerous_tool_use}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ class AzureAnthropicConfig(AnthropicConfig):
|
|||
is_vertex_request=optional_params.get("is_vertex_request", False),
|
||||
user_anthropic_beta_headers=user_anthropic_beta_headers,
|
||||
mcp_server_used=mcp_server_used,
|
||||
thinking=optional_params.get("thinking"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # optional_params is the base config's bare dict
|
||||
)
|
||||
# Merge headers - Azure auth (api-key or Authorization) takes precedence
|
||||
headers = {**anthropic_headers, **headers}
|
||||
|
|
|
|||
|
|
@ -1829,7 +1829,15 @@ class AmazonConverseConfig(BaseConfig):
|
|||
filtered_tools.append(tool)
|
||||
|
||||
base_model: Final = BedrockModelInfo.get_base_model(model)
|
||||
client_beta_list: Final = get_anthropic_beta_from_headers(headers or {})
|
||||
header_beta_list: Final = get_anthropic_beta_from_headers(headers or {})
|
||||
body_beta_list: Final = sorted(
|
||||
active_beta_values(
|
||||
{},
|
||||
additional_request_params.get("anthropic_beta"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # additional_request_params is a bare dict
|
||||
)
|
||||
- set(header_beta_list)
|
||||
)
|
||||
client_beta_list: Final = [*header_beta_list, *body_beta_list]
|
||||
eager_beta: Final = (
|
||||
(ANTHROPIC_FINE_GRAINED_TOOL_STREAMING_BETA_HEADER,)
|
||||
if base_model.startswith("anthropic")
|
||||
|
|
@ -1837,7 +1845,15 @@ class AmazonConverseConfig(BaseConfig):
|
|||
and ANTHROPIC_FINE_GRAINED_TOOL_STREAMING_BETA_HEADER not in client_beta_list
|
||||
else ()
|
||||
)
|
||||
anthropic_beta_list: Final = [*client_beta_list, *eager_beta]
|
||||
thinking_betas: Final = AnthropicModelInfo().thinking_betas(
|
||||
additional_request_params.get("thinking"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # additional_request_params is a bare dict
|
||||
)
|
||||
missing_thinking_betas: Final = (
|
||||
tuple(beta for beta in thinking_betas if beta not in client_beta_list)
|
||||
if base_model.startswith("anthropic")
|
||||
else ()
|
||||
)
|
||||
anthropic_beta_list: Final = [*client_beta_list, *eager_beta, *missing_thinking_betas]
|
||||
|
||||
# Only separate tools if computer use tools are actually present
|
||||
if filtered_tools and self.is_computer_use_tool_used(filtered_tools, model):
|
||||
|
|
@ -1907,7 +1923,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
else:
|
||||
computer_use_header = "computer-use-2024-10-22"
|
||||
|
||||
anthropic_beta_list.append(computer_use_header)
|
||||
if computer_use_header not in anthropic_beta_list:
|
||||
anthropic_beta_list.append(computer_use_header)
|
||||
# Transform computer use tools to proper Bedrock format
|
||||
transformed_computer_tools: Final = self._transform_computer_use_tools(computer_use_tools)
|
||||
additional_request_params["tools"] = transformed_computer_tools
|
||||
|
|
|
|||
|
|
@ -303,7 +303,6 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
programmatic_tool_calling_used: Final = self.is_programmatic_tool_calling_used(tools)
|
||||
input_examples_used: Final = self.is_input_examples_used(tools)
|
||||
is_mid_conversation_output_config_used: Final = self.is_mid_conversation_output_config_used(messages)
|
||||
is_thinking_display_updates_used: Final = self.is_thinking_display_updates_used(thinking)
|
||||
|
||||
client_beta_set: Final = set(get_anthropic_beta_from_headers(headers))
|
||||
user_beta_set: Final = (
|
||||
|
|
@ -321,7 +320,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")),
|
||||
custom_llm_provider="bedrock",
|
||||
is_mid_conversation_output_config_used=is_mid_conversation_output_config_used,
|
||||
is_thinking_display_updates_used=is_thinking_display_updates_used,
|
||||
thinking=thinking,
|
||||
)
|
||||
beta_set.update(auto_betas)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ from pydantic import TypeAdapter
|
|||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.anthropic_beta_gated_fields import (
|
||||
ANTHROPIC_THINKING_BINDING_CONTROLS_BETA_HEADER,
|
||||
log_beta_gated_strips,
|
||||
strip_beta_gated_native_request,
|
||||
)
|
||||
from litellm.anthropic_beta_gated_fields import log_beta_gated_strips, strip_beta_gated_native_request
|
||||
from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers
|
||||
from litellm.constants import (
|
||||
BEDROCK_MIN_THINKING_BUDGET_TOKENS,
|
||||
|
|
@ -589,18 +585,13 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
),
|
||||
custom_llm_provider="bedrock",
|
||||
is_mid_conversation_output_config_used=is_mid_conversation_output_config_used,
|
||||
is_thinking_display_updates_used=anthropic_model_info.is_thinking_display_updates_used(
|
||||
anthropic_messages_request.get("thinking")
|
||||
),
|
||||
thinking=anthropic_messages_request.get("thinking"),
|
||||
is_mid_conversation_tool_change_used=anthropic_model_info.is_mid_conversation_tool_change_used(
|
||||
outgoing_messages_typed
|
||||
),
|
||||
)
|
||||
beta_set.update(auto_betas)
|
||||
|
||||
if "block_binding" in (anthropic_messages_request.get("thinking") or {}): # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # the base signature types the request as a bare dict
|
||||
beta_set.add(ANTHROPIC_THINKING_BINDING_CONTROLS_BETA_HEADER)
|
||||
|
||||
if injected_thinking_for_clear_thinking:
|
||||
beta_set.add("interleaved-thinking-2025-05-14")
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal, cast, overload
|
|||
import httpx
|
||||
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
||||
|
||||
from litellm.anthropic_beta_gated_fields import log_beta_gated_strips, strip_beta_gated_thinking
|
||||
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
handle_invalid_parallel_tool_calls,
|
||||
|
|
@ -514,6 +515,14 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=mapped_params
|
||||
)
|
||||
gated_thinking: Final = strip_beta_gated_thinking(
|
||||
mapped_params.get("thinking"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # mapped_params is the base config's bare dict
|
||||
frozenset(),
|
||||
drop_params=drop_params,
|
||||
)
|
||||
if gated_thinking.stripped:
|
||||
log_beta_gated_strips(gated_thinking.stripped, model=model, provider="databricks")
|
||||
mapped_params["thinking"] = gated_thinking.thinking
|
||||
|
||||
return mapped_params
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ class VertexAIAnthropicConfig(AnthropicConfig):
|
|||
file_id_used=self.is_file_id_used(messages),
|
||||
mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")),
|
||||
custom_llm_provider="vertex_ai",
|
||||
thinking=optional_params.get("thinking"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # optional_params is the base config's bare dict
|
||||
)
|
||||
|
||||
beta_set: Final = set(auto_betas)
|
||||
|
|
|
|||
|
|
@ -832,6 +832,7 @@ ANTHROPIC_EFFORT_BETA_HEADER: Final = "effort-2025-11-24"
|
|||
ANTHROPIC_MID_CONVERSATION_OUTPUT_CONFIG_BETA_HEADER: Final = "mid-conversation-output-config-2026-07-01"
|
||||
|
||||
ANTHROPIC_THINKING_DISPLAY_UPDATES_BETA_HEADER: Final = "thinking-display-updates-2026-08-18"
|
||||
ANTHROPIC_THINKING_BINDING_CONTROLS_BETA_HEADER: Final = "thinking-binding-controls-2026-08-01"
|
||||
ANTHROPIC_MID_CONVERSATION_TOOL_CHANGES_BETA_HEADER: Final = "mid-conversation-tool-changes-2026-07-01"
|
||||
|
||||
ANTHROPIC_FINE_GRAINED_TOOL_STREAMING_BETA_HEADER: Final = "fine-grained-tool-streaming-2025-05-14"
|
||||
|
|
|
|||
|
|
@ -3617,9 +3617,9 @@ def test_streamed_end_turn_carries_no_stop_details():
|
|||
assert "stop_reason" not in provider_fields
|
||||
|
||||
|
||||
def _anthropic_chat_body_for_bound_thinking(
|
||||
def _anthropic_chat_request_for_bound_thinking(
|
||||
extra_headers: dict[str, str] | None, drop_params: bool | None = None
|
||||
) -> dict[str, object]:
|
||||
) -> httpx.Request:
|
||||
captured_requests: Final[list[httpx.Request]] = []
|
||||
|
||||
def respond(request: httpx.Request) -> httpx.Response:
|
||||
|
|
@ -3653,7 +3653,13 @@ def _anthropic_chat_body_for_bound_thinking(
|
|||
finally:
|
||||
client.close()
|
||||
assert thinking == {"type": "adaptive", "block_binding": {"prefix_mismatch_behavior": "error"}}
|
||||
return json.loads(captured_requests[0].content)
|
||||
return captured_requests[0]
|
||||
|
||||
|
||||
def _anthropic_chat_body_for_bound_thinking(
|
||||
extra_headers: dict[str, str] | None, drop_params: bool | None = None
|
||||
) -> dict[str, object]:
|
||||
return json.loads(_anthropic_chat_request_for_bound_thinking(extra_headers, drop_params).content)
|
||||
|
||||
|
||||
def test_anthropic_chat_keeps_block_binding_when_the_final_header_carries_the_binding_beta(
|
||||
|
|
@ -3666,12 +3672,18 @@ def test_anthropic_chat_keeps_block_binding_when_the_final_header_carries_the_bi
|
|||
assert body["thinking"] == {"type": "adaptive", "block_binding": {"prefix_mismatch_behavior": "error"}}
|
||||
|
||||
|
||||
def test_anthropic_chat_drops_block_binding_when_no_header_carries_the_binding_beta(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@pytest.mark.parametrize("drop_params", [True, False])
|
||||
def test_anthropic_chat_adds_the_binding_beta_so_block_binding_is_kept(
|
||||
drop_params: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "true")
|
||||
body: Final = _anthropic_chat_body_for_bound_thinking(None, drop_params=True)
|
||||
assert body["thinking"] == {"type": "adaptive"}
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
request: Final = _anthropic_chat_request_for_bound_thinking(None, drop_params=drop_params)
|
||||
assert json.loads(request.content)["thinking"] == {
|
||||
"type": "adaptive",
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
}
|
||||
assert "thinking-binding-controls-2026-08-01" in request.headers["anthropic-beta"].split(",")
|
||||
|
||||
|
||||
def test_anthropic_chat_sends_block_binding_as_is_without_drop_params(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
|
|
|||
|
|
@ -422,6 +422,22 @@ def test_drop_params_forwards_a_non_object_message_item_as_sent_on_the_native_re
|
|||
BINDING_AND_DISPLAY_BETAS: Final = "thinking-binding-controls-2026-08-01,thinking-display-updates-2026-08-18"
|
||||
|
||||
|
||||
def test_messages_headers_add_the_binding_beta_when_thinking_carries_block_binding() -> None:
|
||||
headers: Final = AnthropicMessagesConfig()._update_headers_with_anthropic_beta(
|
||||
headers={},
|
||||
optional_params={"thinking": {"type": "adaptive", "block_binding": {"prefix_mismatch_behavior": "error"}}},
|
||||
messages=[],
|
||||
)
|
||||
assert "thinking-binding-controls-2026-08-01" in headers["anthropic-beta"].split(",")
|
||||
|
||||
|
||||
def test_messages_headers_add_no_binding_beta_without_block_binding() -> None:
|
||||
headers: Final = AnthropicMessagesConfig()._update_headers_with_anthropic_beta(
|
||||
headers={}, optional_params={"thinking": {"type": "adaptive"}}, messages=[]
|
||||
)
|
||||
assert "thinking-binding-controls-2026-08-01" not in headers.get("anthropic-beta", "")
|
||||
|
||||
|
||||
def _bound_adaptive_request(headers: dict[str, str], drop_params: bool | None = True) -> dict[str, object]:
|
||||
thinking: Final = {
|
||||
"type": "adaptive",
|
||||
|
|
|
|||
|
|
@ -4294,7 +4294,14 @@ def test_shared_legacy_thinking_translation_carries_block_binding(
|
|||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
@pytest.mark.parametrize(
|
||||
("custom_llm_provider", "model", "requested_thinking", "rejected_keys", "expected_thinking"),
|
||||
(
|
||||
"custom_llm_provider",
|
||||
"model",
|
||||
"requested_thinking",
|
||||
"rejected_keys",
|
||||
"expected_thinking",
|
||||
"provider_rejects_block_binding",
|
||||
),
|
||||
(
|
||||
(
|
||||
"anthropic",
|
||||
|
|
@ -4302,6 +4309,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding(
|
|||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
False,
|
||||
),
|
||||
(
|
||||
"bedrock",
|
||||
|
|
@ -4309,6 +4317,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding(
|
|||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
False,
|
||||
),
|
||||
(
|
||||
"bedrock",
|
||||
|
|
@ -4316,6 +4325,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding(
|
|||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
False,
|
||||
),
|
||||
(
|
||||
"bedrock",
|
||||
|
|
@ -4323,6 +4333,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding(
|
|||
{"type": "adaptive", "display": "updates"},
|
||||
{"not_a_thinking_key": True, "budget_tokens": 2048},
|
||||
{"type": "adaptive", "display": "updates"},
|
||||
False,
|
||||
),
|
||||
(
|
||||
"vertex_ai",
|
||||
|
|
@ -4330,6 +4341,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding(
|
|||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
False,
|
||||
),
|
||||
(
|
||||
"databricks",
|
||||
|
|
@ -4337,6 +4349,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding(
|
|||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
True,
|
||||
),
|
||||
),
|
||||
ids=(
|
||||
|
|
@ -4355,6 +4368,7 @@ def test_chat_thinking_drops_unknown_keys_only_under_drop_params(
|
|||
requested_thinking: dict[str, object],
|
||||
rejected_keys: dict[str, object],
|
||||
expected_thinking: dict[str, object],
|
||||
provider_rejects_block_binding: bool,
|
||||
drop_params_source: str | None,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
@ -4369,7 +4383,10 @@ def test_chat_thinking_drops_unknown_keys_only_under_drop_params(
|
|||
)
|
||||
|
||||
kept_rejected_keys: Final = {} if drop_params_source else rejected_keys
|
||||
assert optional_params["thinking"] == {**expected_thinking, "block_binding": block_binding, **kept_rejected_keys}
|
||||
kept_block_binding: Final = (
|
||||
{} if drop_params_source and provider_rejects_block_binding else {"block_binding": block_binding}
|
||||
)
|
||||
assert optional_params["thinking"] == {**expected_thinking, **kept_block_binding, **kept_rejected_keys}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ def test_completion_surfaces_the_status_and_headers_of_a_rejected_request():
|
|||
|
||||
@respx.mock
|
||||
@pytest.mark.parametrize("drop_params", [True, False])
|
||||
def test_completion_drops_block_binding_without_its_beta_only_under_drop_params(
|
||||
def test_completion_adds_the_binding_beta_so_block_binding_is_kept(
|
||||
drop_params: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
|
|
@ -308,5 +308,5 @@ def test_completion_drops_block_binding_without_its_beta_only_under_drop_params(
|
|||
)
|
||||
|
||||
sent: Final = json.loads(route.calls.last.request.content)
|
||||
assert sent["thinking"]["budget_tokens"] == 1024
|
||||
assert ("block_binding" in sent["thinking"]) is not drop_params
|
||||
assert sent["thinking"] == {"type": "enabled", "budget_tokens": 1024, "block_binding": block_binding}
|
||||
assert "thinking-binding-controls-2026-08-01" in route.calls.last.request.headers["anthropic-beta"].split(",")
|
||||
|
|
|
|||
|
|
@ -2011,29 +2011,22 @@ def test_invoke_uses_opt_in_native_output_is_false_when_native_output_did_not_co
|
|||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
|
||||
@pytest.mark.parametrize(
|
||||
("drop_params", "headers", "keeps_block_binding"),
|
||||
(
|
||||
(True, {}, False),
|
||||
(False, {}, True),
|
||||
(True, {"anthropic-beta": "thinking-binding-controls-2026-08-01"}, True),
|
||||
),
|
||||
ids=("drop-params-no-beta", "no-drop-params", "drop-params-with-beta"),
|
||||
)
|
||||
def test_bedrock_chat_invoke_drops_block_binding_only_when_drop_params_and_no_beta_gates_it(
|
||||
drop_params: bool, headers: dict[str, str], keeps_block_binding: bool, monkeypatch: pytest.MonkeyPatch
|
||||
@pytest.mark.parametrize("drop_params", [True, False])
|
||||
def test_bedrock_chat_invoke_adds_the_binding_beta_so_block_binding_is_kept(
|
||||
drop_params: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
block_binding: Final = {"prefix_mismatch_behavior": "error"}
|
||||
result: Final = AmazonAnthropicClaudeConfig().transform_request(
|
||||
model="us.anthropic.claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
optional_params={
|
||||
"max_tokens": 2048,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 1024, "block_binding": {"prefix_mismatch_behavior": "error"}},
|
||||
"thinking": {"type": "enabled", "budget_tokens": 1024, "block_binding": block_binding},
|
||||
},
|
||||
litellm_params={"drop_params": drop_params},
|
||||
headers=headers,
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert result["thinking"]["budget_tokens"] == 1024
|
||||
assert ("block_binding" in result["thinking"]) is keeps_block_binding
|
||||
assert result["thinking"] == {"type": "enabled", "budget_tokens": 1024, "block_binding": block_binding}
|
||||
assert "thinking-binding-controls-2026-08-01" in result["anthropic_beta"]
|
||||
|
|
|
|||
|
|
@ -10832,17 +10832,32 @@ def test_converse_keeps_beta_gated_thinking_fields_the_bedrock_converse_row_carr
|
|||
}
|
||||
|
||||
|
||||
def test_converse_drops_beta_gated_thinking_fields_when_the_request_carries_no_beta(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
def test_converse_adds_the_thinking_betas_so_gated_thinking_fields_are_kept(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "true")
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
fields: Final = _converse_bound_thinking_request(None)
|
||||
assert fields["thinking"] == {"type": "adaptive"}
|
||||
assert any(
|
||||
"thinking.block_binding" in record.getMessage() and "provider=bedrock_converse" in record.getMessage()
|
||||
for record in caplog.records
|
||||
)
|
||||
fields: Final = _converse_bound_thinking_request(None)
|
||||
assert fields["thinking"] == {
|
||||
"type": "adaptive",
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
"display": "updates",
|
||||
}
|
||||
assert sorted(fields["anthropic_beta"]) == [
|
||||
"thinking-binding-controls-2026-08-01",
|
||||
"thinking-display-updates-2026-08-18",
|
||||
]
|
||||
|
||||
|
||||
def test_converse_keeps_the_callers_body_betas_next_to_the_added_thinking_betas(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "true")
|
||||
fields: Final = _converse_bound_thinking_request(None, body_betas=("context-1m-2025-08-07",))
|
||||
assert sorted(fields["anthropic_beta"]) == [
|
||||
"context-1m-2025-08-07",
|
||||
"thinking-binding-controls-2026-08-01",
|
||||
"thinking-display-updates-2026-08-18",
|
||||
]
|
||||
|
||||
|
||||
def test_converse_sends_beta_gated_thinking_fields_as_is_without_drop_params(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
|
|
|||
|
|
@ -2465,6 +2465,41 @@ def test_databricks_claude_drops_the_openai_tool_search_tool_under_drop_params(m
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("drop_params", [True, False])
|
||||
def test_databricks_chat_drops_beta_gated_thinking_fields_only_under_drop_params(
|
||||
drop_params: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
thinking: Final = {
|
||||
"type": "adaptive",
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
"display": "updates",
|
||||
}
|
||||
mapped: Final = DatabricksConfig().map_openai_params(
|
||||
non_default_params={"thinking": thinking},
|
||||
optional_params={},
|
||||
model="databricks-claude-opus-5-5",
|
||||
drop_params=drop_params,
|
||||
)
|
||||
assert mapped["thinking"] == ({"type": "adaptive"} if drop_params else thinking)
|
||||
assert thinking == {
|
||||
"type": "adaptive",
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
"display": "updates",
|
||||
}
|
||||
|
||||
|
||||
def test_databricks_chat_keeps_ungated_thinking_display_under_drop_params(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
mapped: Final = DatabricksConfig().map_openai_params(
|
||||
non_default_params={"thinking": {"type": "adaptive", "display": "summarized"}},
|
||||
optional_params={},
|
||||
model="databricks-claude-opus-5-5",
|
||||
drop_params=True,
|
||||
)
|
||||
assert mapped["thinking"] == {"type": "adaptive", "display": "summarized"}
|
||||
|
||||
|
||||
_JSON_TOOL_ARGUMENT_FRAGMENTS: Final = ("", "", '{"name": ', '"Ada"', ', "met', 'a": ', "{}", ', "tags": []}')
|
||||
_RECORD_RESPONSE_FORMAT: Final = {
|
||||
"type": "json_schema",
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ SERVER_TOOLS: Final = [
|
|||
]
|
||||
|
||||
|
||||
async def _workspace_request_body(**request_params: object) -> dict[str, object]:
|
||||
async def _workspace_request(max_tokens: int = 64, **request_params: object) -> httpx.Request:
|
||||
received: Final[list[httpx.Request]] = [] # mutable-ok: the fake workspace records what litellm sent
|
||||
|
||||
def workspace_answers(request: httpx.Request) -> httpx.Response:
|
||||
|
|
@ -303,7 +303,7 @@ async def _workspace_request_body(**request_params: object) -> dict[str, object]
|
|||
upstream: Final = AsyncHTTPHandler(transport=httpx.MockTransport(workspace_answers))
|
||||
try:
|
||||
await litellm.anthropic.messages.acreate(
|
||||
max_tokens=64,
|
||||
max_tokens=max_tokens,
|
||||
messages=USER_TURN,
|
||||
model=f"databricks/{CLAUDE_MODEL}",
|
||||
api_key=DATABRICKS_API_KEY,
|
||||
|
|
@ -314,7 +314,31 @@ async def _workspace_request_body(**request_params: object) -> dict[str, object]
|
|||
finally:
|
||||
await upstream.close()
|
||||
assert len(received) == 1
|
||||
return json.loads(received[0].content)
|
||||
return received[0]
|
||||
|
||||
|
||||
async def _workspace_request_body(**request_params: object) -> dict[str, object]:
|
||||
return json.loads((await _workspace_request(**request_params)).content)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("drop_params", [True, False])
|
||||
async def test_bound_thinking_with_display_updates_reaches_the_workspace_with_both_betas(
|
||||
drop_params: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "true")
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
thinking: Final = {
|
||||
"type": "enabled",
|
||||
"budget_tokens": 1024,
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
"display": "updates",
|
||||
}
|
||||
request: Final = await _workspace_request(max_tokens=4096, thinking=thinking, drop_params=drop_params)
|
||||
assert json.loads(request.content)["thinking"] == thinking
|
||||
assert {"thinking-binding-controls-2026-08-01", "thinking-display-updates-2026-08-18"} <= set(
|
||||
request.headers["anthropic-beta"].split(",")
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -127,6 +127,25 @@ def test_vertex_ai_anthropic_web_search_header_in_completion():
|
|||
), "anthropic-beta with web-search should not be present for non-Vertex requests"
|
||||
|
||||
|
||||
def test_vertex_ai_anthropic_chat_adds_the_betas_for_bound_thinking_with_display_updates() -> None:
|
||||
thinking: Final = {
|
||||
"type": "adaptive",
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
"display": "updates",
|
||||
}
|
||||
result: Final = VertexAIAnthropicConfig().transform_request(
|
||||
model="claude-opus-4-8",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
optional_params={"thinking": thinking, "max_tokens": 100, "is_vertex_request": True},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
assert result["thinking"] == thinking
|
||||
assert {"thinking-binding-controls-2026-08-01", "thinking-display-updates-2026-08-18"} <= set(
|
||||
result["anthropic_beta"]
|
||||
)
|
||||
|
||||
|
||||
def test_vertex_ai_anthropic_context_management_compact_beta_header():
|
||||
"""Test that context_management with compact adds the correct beta header for Vertex AI"""
|
||||
config = VertexAIAnthropicConfig()
|
||||
|
|
|
|||
|
|
@ -168,6 +168,30 @@ def test_system_message_emptied_by_the_strip_is_dropped() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_system_turn_emptied_by_the_strip_stays_as_a_directive_when_its_output_config_survives() -> None:
|
||||
directive: Final = {
|
||||
"role": "system",
|
||||
"output_config": {"effort": "low"},
|
||||
"content": [{"type": "tool_removal", "tool_names": ["lookup"]}],
|
||||
}
|
||||
result: Final = strip_beta_gated_system_messages(
|
||||
[directive], frozenset({ANTHROPIC_MID_CONVERSATION_OUTPUT_CONFIG_BETA_HEADER}), drop_params=True
|
||||
)
|
||||
assert result.messages == [{"role": "system", "output_config": {"effort": "low"}, "content": []}]
|
||||
assert result.stripped == ("messages[].system.content[].tool_addition|tool_removal",)
|
||||
|
||||
|
||||
def test_system_turn_emptied_by_the_strip_is_dropped_when_its_output_config_is_gated_too() -> None:
|
||||
directive: Final = {
|
||||
"role": "system",
|
||||
"output_config": {"effort": "low"},
|
||||
"content": [{"type": "tool_removal", "tool_names": ["lookup"]}],
|
||||
}
|
||||
result: Final = strip_beta_gated_system_messages([directive], frozenset(), drop_params=True)
|
||||
assert result.messages == []
|
||||
assert result.stripped[-1] == "messages[].system (emptied by the strip)"
|
||||
|
||||
|
||||
def test_request_strip_replaces_only_thinking_and_messages_and_keeps_the_dict_otherwise() -> None:
|
||||
request: Final = {
|
||||
"model": "claude",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue