mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-17 04:34:25 +00:00
fix(anthropic): drop unknown legacy thinking keys under drop_params when rebuilding adaptive thinking (#46741)
* fix(anthropic): drop unknown legacy thinking keys under drop_params when rebuilding adaptive thinking * fix(bedrock): drop unknown thinking keys under drop_params on display updates rebuilds Bedrock Invoke copied every caller thinking key when adding display updates, so an unknown key still reached the provider with drop_params on. Filter that copy the same way, and cover each chat call site plus the global litellm.drop_params path in tests * fix(anthropic): drop budget_tokens from rebuilt adaptive thinking under drop_params * test(anthropic): cover drop_params sources for rebuilt adaptive thinking on /v1/messages and Bedrock Invoke
This commit is contained in:
parent
8007773ec5
commit
d489c04554
12 changed files with 335 additions and 32 deletions
|
|
@ -1878,7 +1878,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
else:
|
||||
optional_params["thinking"] = value
|
||||
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=optional_params, custom_llm_provider=self._resolved_provider
|
||||
model=model,
|
||||
optional_params=optional_params,
|
||||
custom_llm_provider=self._resolved_provider,
|
||||
drop_params=drop_params,
|
||||
)
|
||||
elif param == "reasoning_effort":
|
||||
# Accept both string ("low") and dict ({"effort": "low",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ _INFERENCE_PROFILE_MINOR_RE: Final = re.compile(r":\d+$")
|
|||
_DATED_RELEASE_SUFFIX_RE: Final = re.compile(r"-\d{8}$")
|
||||
_DOTTED_VERSION_RE: Final = re.compile(r"(\d)\.(\d)")
|
||||
_CLAUDE_CODE_BILLING_HEADER_PREFIX: Final = "x-anthropic-billing-header:"
|
||||
_OBJECT_MAPPING_ADAPTER: Final = TypeAdapter(dict[object, object])
|
||||
OBJECT_MAPPING_ADAPTER: Final = TypeAdapter(dict[object, object])
|
||||
_CLAUDE_CODE_OBJECT_LIST_ADAPTER: Final = TypeAdapter(list[object])
|
||||
_DECODED_JSON: Final = TypeAdapter(object)
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ def is_claude_code_user_agent(user_agent: str) -> bool:
|
|||
|
||||
def validated_object_mapping(value: object) -> dict[object, object] | None:
|
||||
try:
|
||||
return _OBJECT_MAPPING_ADAPTER.validate_python(value)
|
||||
return OBJECT_MAPPING_ADAPTER.validate_python(value)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
|
|
@ -925,6 +925,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
model: str,
|
||||
optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param like the sibling helpers
|
||||
custom_llm_provider: str,
|
||||
drop_params: bool,
|
||||
) -> None:
|
||||
"""Translate legacy ``thinking.type=enabled`` to adaptive for the
|
||||
adaptive-thinking models that reject it (4.7+ and the 5 families).
|
||||
|
|
@ -950,10 +951,10 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
existing_output_config: Final = optional_params.get("output_config")
|
||||
display: Final = thinking.get("display")
|
||||
supported_display: Final = {"display": display} if display in ("summarized", "omitted") else {}
|
||||
carried_thinking_keys: Final = {
|
||||
key: value for key, value in thinking.items() if key not in ("type", "budget_tokens", "display")
|
||||
}
|
||||
optional_params["thinking"] = {**carried_thinking_keys, "type": "adaptive", **supported_display}
|
||||
carried_keys: Final = carried_thinking_keys(
|
||||
thinking, rebuilt_keys=frozenset(("type", "budget_tokens", "display")), drop_params=drop_params
|
||||
)
|
||||
optional_params["thinking"] = {**carried_keys, "type": "adaptive", **supported_display}
|
||||
optional_params["output_config"] = {
|
||||
"effort": effort,
|
||||
**(existing_output_config if isinstance(existing_output_config, dict) else MappingProxyType({})),
|
||||
|
|
@ -1621,6 +1622,22 @@ def is_anthropic_invalid_thinking_block_error(error_text: str) -> bool:
|
|||
return "must contain thinking" in lower
|
||||
|
||||
|
||||
_ADAPTIVE_THINKING_KEYS: Final = (
|
||||
AnthropicThinkingParam.__required_keys__ | AnthropicThinkingParam.__optional_keys__
|
||||
) - {"budget_tokens"}
|
||||
|
||||
|
||||
def carried_thinking_keys(
|
||||
thinking: Mapping[object, object], rebuilt_keys: frozenset[str], drop_params: bool
|
||||
) -> Mapping[object, object]:
|
||||
drops_unknown_keys: Final = drop_params or litellm.drop_params is True
|
||||
return {
|
||||
key: value
|
||||
for key, value in thinking.items()
|
||||
if key not in rebuilt_keys and (key in _ADAPTIVE_THINKING_KEYS or not drops_unknown_keys)
|
||||
}
|
||||
|
||||
|
||||
def is_anthropic_thinking_binding_mismatch_error(error_text: str) -> bool:
|
||||
"""messages.N.content.M: Invalid `signature` in `thinking` block. The block is bound to a different conversation."""
|
||||
return "bound to a different conversation" in error_text.lower()
|
||||
|
|
|
|||
|
|
@ -608,6 +608,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
model=model,
|
||||
optional_params=anthropic_messages_optional_request_params,
|
||||
custom_llm_provider=self._resolved_provider,
|
||||
drop_params=getattr(litellm_params, "drop_params", None) is True,
|
||||
)
|
||||
|
||||
self._translate_adaptive_effort_for_non_adaptive_model(
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,10 @@ class AmazonConverseConfig(BaseConfig):
|
|||
else:
|
||||
optional_params["thinking"] = value
|
||||
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=optional_params, custom_llm_provider="bedrock"
|
||||
model=model,
|
||||
optional_params=optional_params,
|
||||
custom_llm_provider="bedrock",
|
||||
drop_params=drop_params,
|
||||
)
|
||||
elif param == "reasoning_effort" and isinstance(value, str) and drop_reasoning_effort_param:
|
||||
verbose_logger.debug(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
|
|||
convert_url_to_base64,
|
||||
)
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
|
||||
from litellm.llms.anthropic.common_utils import (
|
||||
OBJECT_MAPPING_ADAPTER,
|
||||
AnthropicModelInfo,
|
||||
carried_thinking_keys,
|
||||
)
|
||||
from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import (
|
||||
AmazonInvokeConfig,
|
||||
)
|
||||
|
|
@ -114,7 +118,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
)
|
||||
|
||||
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="bedrock"
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="bedrock", drop_params=drop_params
|
||||
)
|
||||
translated_thinking: Final = mapped_params.get("thinking")
|
||||
if (
|
||||
|
|
@ -122,7 +126,14 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
and isinstance(translated_thinking, dict)
|
||||
and translated_thinking.get("type") == "adaptive"
|
||||
):
|
||||
mapped_params["thinking"] = {**translated_thinking, "display": "updates"}
|
||||
mapped_params["thinking"] = {
|
||||
**carried_thinking_keys(
|
||||
OBJECT_MAPPING_ADAPTER.validate_python(translated_thinking),
|
||||
rebuilt_keys=frozenset(("display",)),
|
||||
drop_params=drop_params,
|
||||
),
|
||||
"display": "updates",
|
||||
}
|
||||
|
||||
return mapped_params
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@ from litellm.llms.anthropic.chat.transformation import (
|
|||
DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING,
|
||||
AnthropicConfig,
|
||||
)
|
||||
from litellm.llms.anthropic.common_utils import AnthropicModelInfo, validated_object_mapping
|
||||
from litellm.llms.anthropic.common_utils import (
|
||||
OBJECT_MAPPING_ADAPTER,
|
||||
AnthropicModelInfo,
|
||||
carried_thinking_keys,
|
||||
validated_object_mapping,
|
||||
)
|
||||
from litellm.llms.anthropic.pass_through.messages.transformation import (
|
||||
AnthropicMessagesConfig,
|
||||
)
|
||||
|
|
@ -250,6 +255,7 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
self,
|
||||
anthropic_messages_request: dict,
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
) -> bool:
|
||||
"""
|
||||
Bedrock rejects ``clear_thinking_20251015`` context-management edits unless
|
||||
|
|
@ -286,7 +292,9 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
return False
|
||||
if t == "enabled":
|
||||
budget_tokens: Final = self._resolve_clear_thinking_budget_tokens(thinking.get("budget_tokens"))
|
||||
self._inject_adaptive_thinking_for_clear_thinking(anthropic_messages_request, budget_tokens, model)
|
||||
self._inject_adaptive_thinking_for_clear_thinking(
|
||||
anthropic_messages_request, budget_tokens, model, drop_params
|
||||
)
|
||||
return True
|
||||
verbose_logger.debug(
|
||||
"Bedrock clear_thinking_20251015: replacing thinking=%s with minimal thinking config",
|
||||
|
|
@ -305,7 +313,7 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
return False
|
||||
|
||||
if is_adaptive_thinking_model:
|
||||
self._inject_adaptive_thinking_for_clear_thinking(anthropic_messages_request, budget, model)
|
||||
self._inject_adaptive_thinking_for_clear_thinking(anthropic_messages_request, budget, model, drop_params)
|
||||
return True
|
||||
|
||||
anthropic_messages_request["thinking"] = {
|
||||
|
|
@ -338,7 +346,7 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
return "low"
|
||||
|
||||
def _inject_adaptive_thinking_for_clear_thinking(
|
||||
self, anthropic_messages_request: dict, budget_tokens: int, model: str
|
||||
self, anthropic_messages_request: dict, budget_tokens: int, model: str, drop_params: bool
|
||||
) -> None:
|
||||
"""Adaptive-thinking models (Opus 4.7/4.8, Fable 5) reject
|
||||
``thinking.type=enabled`` on Bedrock. Use ``thinking.type=adaptive`` plus
|
||||
|
|
@ -349,7 +357,9 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
output_config = {}
|
||||
output_config.setdefault("effort", self._effort_from_thinking_budget(budget_tokens))
|
||||
thinking: Final = validated_object_mapping(anthropic_messages_request.get("thinking"))
|
||||
carried: Final = {key: value for key, value in (thinking or {}).items() if key not in ("type", "budget_tokens")}
|
||||
carried: Final = carried_thinking_keys(
|
||||
thinking or {}, rebuilt_keys=frozenset(("type", "budget_tokens")), drop_params=drop_params
|
||||
)
|
||||
anthropic_messages_request["output_config"] = output_config
|
||||
anthropic_messages_request["thinking"] = {**carried, "type": "adaptive"}
|
||||
verbose_logger.debug(
|
||||
|
|
@ -664,6 +674,7 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
) -> dict:
|
||||
requested_thinking: Final = anthropic_messages_optional_request_params.get("thinking")
|
||||
requested_display_updates: Final = AnthropicModelInfo().is_thinking_display_updates_used(requested_thinking)
|
||||
drop_params: Final = getattr(litellm_params, "drop_params", None) is True
|
||||
self._clamp_adaptive_reasoning_effort_for_bedrock(
|
||||
model=model,
|
||||
optional_params=anthropic_messages_optional_request_params,
|
||||
|
|
@ -682,7 +693,14 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
and isinstance(translated_thinking, dict)
|
||||
and translated_thinking.get("type") == "adaptive"
|
||||
):
|
||||
anthropic_messages_request["thinking"] = {**translated_thinking, "display": "updates"}
|
||||
anthropic_messages_request["thinking"] = {
|
||||
**carried_thinking_keys(
|
||||
OBJECT_MAPPING_ADAPTER.validate_python(translated_thinking),
|
||||
rebuilt_keys=frozenset(("display",)),
|
||||
drop_params=drop_params,
|
||||
),
|
||||
"display": "updates",
|
||||
}
|
||||
self._normalize_system_role_messages(anthropic_messages_request, model=model)
|
||||
#########################################################
|
||||
############## BEDROCK Invoke SPECIFIC TRANSFORMATION ###
|
||||
|
|
@ -705,6 +723,7 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
injected_thinking_for_clear_thinking: Final = self._ensure_thinking_for_clear_thinking_context_management(
|
||||
anthropic_messages_request=anthropic_messages_request,
|
||||
model=model,
|
||||
drop_params=drop_params,
|
||||
)
|
||||
|
||||
# 4. Remove `ttl` field from cache_control in messages (Bedrock doesn't support it for older models)
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
mapped_params: Final = super().map_openai_params(non_default_params, optional_params, model, drop_params)
|
||||
if "claude" in model:
|
||||
AnthropicConfig.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="databricks"
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="databricks", drop_params=drop_params
|
||||
)
|
||||
if "tools" in mapped_params:
|
||||
mapped_params["tools"] = self._map_openai_to_dbrx_tool(
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class VertexAIAnthropicConfig(AnthropicConfig):
|
|||
)
|
||||
|
||||
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="vertex_ai"
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="vertex_ai", drop_params=drop_params
|
||||
)
|
||||
|
||||
return mapped_params
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
|
@ -15,7 +16,15 @@ def _claude_code_turn(sent: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
|||
return {**without_reasoning, "stream": False, **sent}
|
||||
|
||||
|
||||
def _forward(gateway: Gateway, upstream_model: str, sent: Mapping[str, JsonValue]) -> cc.Forwarded:
|
||||
def _forward(
|
||||
gateway: Gateway,
|
||||
upstream_model: str,
|
||||
sent: Mapping[str, JsonValue],
|
||||
*,
|
||||
deployment: Mapping[str, JsonValue] = MappingProxyType({}),
|
||||
gateway_only: Mapping[str, JsonValue] = MappingProxyType({}),
|
||||
from_claude_code: bool = True,
|
||||
) -> cc.Forwarded:
|
||||
client_body: Final = _claude_code_turn(sent)
|
||||
|
||||
def respond(request: Request) -> Reply:
|
||||
|
|
@ -30,13 +39,13 @@ def _forward(gateway: Gateway, upstream_model: str, sent: Mapping[str, JsonValue
|
|||
|
||||
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
||||
model: Final = scenario.model(
|
||||
model=f"anthropic/{upstream_model}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY
|
||||
model=f"anthropic/{upstream_model}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY, **deployment
|
||||
)
|
||||
response: Final = gateway.request(
|
||||
"POST",
|
||||
"/v1/messages",
|
||||
{**client_body, "model": model},
|
||||
headers=cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA),
|
||||
{**client_body, **gateway_only, "model": model},
|
||||
headers=cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA) if from_claude_code else None,
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
received: Final = wire.drain()
|
||||
|
|
@ -194,6 +203,51 @@ def test_legacy_thinking_budget_becomes_adaptive_effort_only_on_models_that_reje
|
|||
assert forwarded.reasoning_betas == cc.CLAUDE_CODE_REASONING_BETAS, forwarded.reasoning_betas
|
||||
|
||||
|
||||
UNKNOWN_THINKING_KEY: Final = MappingProxyType({"not_a_thinking_key": True})
|
||||
BLOCK_BINDING: Final = MappingProxyType({"prefix_mismatch_behavior": "error"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("from_claude_code", "deployment", "gateway_only", "carried_unknown_key"),
|
||||
(
|
||||
pytest.param(False, {}, {"drop_params": True}, {}, id="request-drop-params-drops-the-unknown-key"),
|
||||
pytest.param(False, {"drop_params": True}, {}, {}, id="deployment-drop-params-drops-the-unknown-key"),
|
||||
pytest.param(
|
||||
False, {}, {}, dict(UNKNOWN_THINKING_KEY), id="without-drop-params-the-unknown-key-reaches-anthropic"
|
||||
),
|
||||
pytest.param(True, {}, {}, {}, id="claude-code-gets-drop-params-by-default-and-drops-the-unknown-key"),
|
||||
),
|
||||
)
|
||||
def test_rebuilt_legacy_thinking_drops_unknown_keys_only_under_drop_params(
|
||||
gateway: Gateway,
|
||||
from_claude_code: bool,
|
||||
deployment: dict[str, JsonValue],
|
||||
gateway_only: dict[str, JsonValue],
|
||||
carried_unknown_key: dict[str, JsonValue],
|
||||
) -> None:
|
||||
sent: Final = {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"budget_tokens": 2048,
|
||||
"block_binding": dict(BLOCK_BINDING),
|
||||
**UNKNOWN_THINKING_KEY,
|
||||
}
|
||||
}
|
||||
forwarded: Final = _forward(
|
||||
gateway,
|
||||
"claude-opus-4-7",
|
||||
sent,
|
||||
deployment=deployment,
|
||||
gateway_only=gateway_only,
|
||||
from_claude_code=from_claude_code,
|
||||
)
|
||||
assert forwarded.reasoning == {
|
||||
"thinking": {"type": "adaptive", "block_binding": dict(BLOCK_BINDING), **carried_unknown_key},
|
||||
"output_config": {"effort": "medium"},
|
||||
}, forwarded
|
||||
assert forwarded.other_changes == {}, forwarded.other_changes
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("upstream_model", "sent", "received"),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import Final
|
|||
import pytest
|
||||
from integration._support.client import Gateway
|
||||
from integration._support.wire import Reply, Request, wire_server
|
||||
from pydantic import JsonValue
|
||||
|
||||
MODEL: Final = "bedrock/invoke/us.anthropic.claude-opus-4-8"
|
||||
TOKEN: Final = "synthetic-bedrock-bearer"
|
||||
|
|
@ -58,3 +59,49 @@ def test_prefixed_opus_4_8_reasoning_effort_reaches_bedrock_as_adaptive_thinking
|
|||
assert response.json()["choices"][0]["message"]["content"] == "adaptive thinking control"
|
||||
assert response.json()["usage"]["prompt_tokens"] == 12 and response.json()["usage"]["completion_tokens"] == 5
|
||||
assert len(wire.drain()) == 1
|
||||
|
||||
|
||||
BLOCK_BINDING: Final = {"prefix_mismatch_behavior": "error"}
|
||||
DISPLAY_UPDATES_THINKING: Final = {"type": "adaptive", "display": "updates", "block_binding": BLOCK_BINDING}
|
||||
NON_ADAPTIVE_THINKING_KEYS: Final = {"budget_tokens": 2048, "not_a_thinking_key": True}
|
||||
|
||||
|
||||
def recorded_reply(_request: Request) -> Reply:
|
||||
return Reply(body=RESPONSE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("route", ("/v1/chat/completions", "/v1/messages"))
|
||||
@pytest.mark.parametrize(
|
||||
("client_drop_params", "carried_keys"),
|
||||
(
|
||||
pytest.param({"drop_params": True}, {}, id="drop-params-sends-only-adaptive-fields"),
|
||||
pytest.param({}, NON_ADAPTIVE_THINKING_KEYS, id="without-drop-params-forwards-every-key"),
|
||||
),
|
||||
)
|
||||
def test_bedrock_display_updates_rebuild_drops_non_adaptive_thinking_keys_only_under_drop_params(
|
||||
gateway: Gateway, route: str, client_drop_params: dict[str, JsonValue], carried_keys: dict[str, JsonValue]
|
||||
) -> None:
|
||||
with wire_server(recorded_reply) as wire, gateway.scenario() as scenario:
|
||||
model: Final = scenario.model(
|
||||
model=MODEL,
|
||||
api_key=TOKEN,
|
||||
aws_region_name="us-east-1",
|
||||
api_base=wire.url,
|
||||
aws_bedrock_runtime_endpoint=wire.url,
|
||||
)
|
||||
response: Final = gateway.request(
|
||||
"POST",
|
||||
route,
|
||||
{
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": "synthetic display updates request"}],
|
||||
"max_tokens": 4096,
|
||||
"thinking": {**DISPLAY_UPDATES_THINKING, **NON_ADAPTIVE_THINKING_KEYS},
|
||||
**client_drop_params,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
received: Final = wire.drain()
|
||||
assert len(received) == 1, received
|
||||
assert received[0].target == "/model/us.anthropic.claude-opus-4-8/invoke", received[0].target
|
||||
assert json.loads(received[0].body)["thinking"] == {**DISPLAY_UPDATES_THINKING, **carried_keys}
|
||||
|
|
|
|||
|
|
@ -2700,7 +2700,9 @@ class TestWifZeroBehaviorChange:
|
|||
assert "ANTHROPIC_SERVICE_ACCOUNT_ID" in exc_info.value.message
|
||||
assert "ANTHROPIC_IDENTITY_TOKEN_FILE" in exc_info.value.message
|
||||
|
||||
def test_a_token_file_credential_missing_an_id_names_the_id_not_the_key(self, clean_anthropic_env: None, tmp_path: Path):
|
||||
def test_a_token_file_credential_missing_an_id_names_the_id_not_the_key(
|
||||
self, clean_anthropic_env: None, tmp_path: Path
|
||||
):
|
||||
import litellm
|
||||
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
|
||||
|
||||
|
|
@ -4254,13 +4256,17 @@ def test_shared_legacy_thinking_translation_preserves_supported_display(
|
|||
model="claude-opus-5",
|
||||
optional_params=optional_params,
|
||||
custom_llm_provider="azure_ai",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert optional_params["thinking"] == expected_thinking
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
def test_shared_legacy_thinking_translation_carries_block_binding() -> None:
|
||||
@pytest.mark.parametrize(("drop_params", "expected_unknown_keys"), ((True, {}), (False, {"not_a_thinking_key": True})))
|
||||
def test_shared_legacy_thinking_translation_carries_block_binding(
|
||||
drop_params: bool, expected_unknown_keys: dict[str, object]
|
||||
) -> None:
|
||||
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
|
||||
|
||||
optional_params: Final = {
|
||||
|
|
@ -4268,6 +4274,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding() -> None:
|
|||
"type": "enabled",
|
||||
"budget_tokens": 2048,
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
"not_a_thinking_key": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -4275,14 +4282,96 @@ def test_shared_legacy_thinking_translation_carries_block_binding() -> None:
|
|||
model="claude-opus-5",
|
||||
optional_params=optional_params,
|
||||
custom_llm_provider="azure_ai",
|
||||
drop_params=drop_params,
|
||||
)
|
||||
|
||||
assert optional_params["thinking"] == {
|
||||
"type": "adaptive",
|
||||
"block_binding": {"prefix_mismatch_behavior": "error"},
|
||||
**expected_unknown_keys,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
@pytest.mark.parametrize(
|
||||
("custom_llm_provider", "model", "requested_thinking", "rejected_keys", "expected_thinking"),
|
||||
(
|
||||
(
|
||||
"anthropic",
|
||||
"claude-opus-5-5",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
),
|
||||
(
|
||||
"bedrock",
|
||||
"converse/us.anthropic.claude-opus-5-5",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
),
|
||||
(
|
||||
"bedrock",
|
||||
"invoke/us.anthropic.claude-opus-5-5",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
),
|
||||
(
|
||||
"bedrock",
|
||||
"invoke/us.anthropic.claude-opus-5-5",
|
||||
{"type": "adaptive", "display": "updates"},
|
||||
{"not_a_thinking_key": True, "budget_tokens": 2048},
|
||||
{"type": "adaptive", "display": "updates"},
|
||||
),
|
||||
(
|
||||
"vertex_ai",
|
||||
"claude-opus-5-5",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
),
|
||||
(
|
||||
"databricks",
|
||||
"databricks-claude-opus-5-5",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"type": "adaptive"},
|
||||
),
|
||||
),
|
||||
ids=(
|
||||
"anthropic",
|
||||
"bedrock_converse",
|
||||
"bedrock_invoke",
|
||||
"bedrock_invoke_display_updates",
|
||||
"vertex_ai",
|
||||
"databricks",
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("drop_params_source", ("request", "global", None))
|
||||
def test_chat_thinking_drops_unknown_keys_only_under_drop_params(
|
||||
custom_llm_provider: str,
|
||||
model: str,
|
||||
requested_thinking: dict[str, object],
|
||||
rejected_keys: dict[str, object],
|
||||
expected_thinking: dict[str, object],
|
||||
drop_params_source: str | None,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
block_binding: Final = {"prefix_mismatch_behavior": "error"}
|
||||
monkeypatch.setattr(litellm, "drop_params", drop_params_source == "global")
|
||||
|
||||
optional_params: Final = litellm.utils.get_optional_params(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
thinking={**requested_thinking, "block_binding": block_binding, **rejected_keys},
|
||||
drop_params=drop_params_source == "request",
|
||||
)
|
||||
|
||||
kept_rejected_keys: Final = {} if drop_params_source else rejected_keys
|
||||
assert optional_params["thinking"] == {**expected_thinking, "block_binding": block_binding, **kept_rejected_keys}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
|
||||
@pytest.mark.parametrize("action", (None, "tool_addition", "tool_removal"))
|
||||
@pytest.mark.parametrize("explicit_beta", (False, True))
|
||||
|
|
|
|||
|
|
@ -2047,7 +2047,7 @@ def test_bedrock_clear_thinking_injects_adaptive_with_effort_for_adaptive_models
|
|||
}
|
||||
|
||||
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
|
||||
anthropic_messages_request=request, model=model
|
||||
anthropic_messages_request=request, model=model, drop_params=False
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
|
|
@ -2070,7 +2070,7 @@ def test_bedrock_clear_thinking_converts_legacy_enabled_budget_to_effort():
|
|||
}
|
||||
|
||||
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
|
||||
anthropic_messages_request=request, model="us.anthropic.claude-opus-4-8"
|
||||
anthropic_messages_request=request, model="us.anthropic.claude-opus-4-8", drop_params=False
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
|
|
@ -2098,7 +2098,7 @@ def test_bedrock_clear_thinking_keeps_enabled_for_non_adaptive_models():
|
|||
}
|
||||
|
||||
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
|
||||
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0"
|
||||
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0", drop_params=False
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
|
|
@ -2605,7 +2605,9 @@ def test_inject_adaptive_thinking_preserves_existing_effort():
|
|||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
request = {"output_config": {"effort": "max", "other": "keep"}}
|
||||
|
||||
cfg._inject_adaptive_thinking_for_clear_thinking(request, budget_tokens=24000, model="us.anthropic.claude-fable-5")
|
||||
cfg._inject_adaptive_thinking_for_clear_thinking(
|
||||
request, budget_tokens=24000, model="us.anthropic.claude-fable-5", drop_params=False
|
||||
)
|
||||
|
||||
assert request["thinking"] == {"type": "adaptive"}
|
||||
assert request["output_config"] == {"effort": "max", "other": "keep"}
|
||||
|
|
@ -2622,7 +2624,7 @@ def test_bedrock_clear_thinking_noops_when_thinking_already_adaptive():
|
|||
}
|
||||
|
||||
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
|
||||
anthropic_messages_request=request, model="us.anthropic.claude-fable-5"
|
||||
anthropic_messages_request=request, model="us.anthropic.claude-fable-5", drop_params=False
|
||||
)
|
||||
|
||||
assert changed is False
|
||||
|
|
@ -2642,7 +2644,7 @@ def test_bedrock_clear_thinking_replaces_disabled_thinking_on_adaptive_model():
|
|||
}
|
||||
|
||||
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
|
||||
anthropic_messages_request=request, model="us.anthropic.claude-fable-5"
|
||||
anthropic_messages_request=request, model="us.anthropic.claude-fable-5", drop_params=False
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
|
|
@ -2662,7 +2664,7 @@ def test_bedrock_clear_thinking_leaves_enabled_thinking_on_non_adaptive_model():
|
|||
}
|
||||
|
||||
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
|
||||
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0"
|
||||
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0", drop_params=False
|
||||
)
|
||||
|
||||
assert changed is False
|
||||
|
|
@ -3727,6 +3729,63 @@ def test_bedrock_messages_keeps_block_binding_with_display_updates(
|
|||
assert result.get("thinking") == {"type": "adaptive", "display": "updates", "block_binding": BLOCK_BINDING}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
|
||||
@pytest.mark.parametrize(
|
||||
("model", "requested_thinking", "rejected_keys", "extra_params", "expected_thinking"),
|
||||
(
|
||||
(
|
||||
"eu.anthropic.claude-opus-5",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{},
|
||||
{"type": "adaptive"},
|
||||
),
|
||||
(
|
||||
"us.anthropic.claude-opus-4-6",
|
||||
{"type": "enabled", "budget_tokens": 2048},
|
||||
{"not_a_thinking_key": True},
|
||||
{"context_management": {"edits": [{"type": "clear_thinking_20251015"}]}},
|
||||
{"type": "adaptive"},
|
||||
),
|
||||
(
|
||||
"eu.anthropic.claude-opus-5",
|
||||
{"type": "adaptive", "display": "updates"},
|
||||
{"not_a_thinking_key": True, "budget_tokens": 2048},
|
||||
{},
|
||||
{"type": "adaptive", "display": "updates"},
|
||||
),
|
||||
),
|
||||
ids=("legacy", "legacy_clear_thinking", "adaptive_display_updates"),
|
||||
)
|
||||
@pytest.mark.parametrize("drop_params_source", ("request", "global", None))
|
||||
def test_bedrock_messages_thinking_drops_unknown_keys_only_under_drop_params(
|
||||
model: str,
|
||||
requested_thinking: dict[str, object],
|
||||
rejected_keys: dict[str, object],
|
||||
extra_params: dict[str, object],
|
||||
expected_thinking: dict[str, object],
|
||||
drop_params_source: str | None,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
monkeypatch.setattr(litellm, "drop_params", drop_params_source == "global")
|
||||
result: Final = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": "Reply with OK"}],
|
||||
anthropic_messages_optional_request_params={
|
||||
"max_tokens": 4096,
|
||||
"thinking": {**requested_thinking, "block_binding": BLOCK_BINDING, **rejected_keys},
|
||||
**extra_params,
|
||||
},
|
||||
litellm_params=GenericLiteLLMParams(drop_params=drop_params_source == "request"),
|
||||
headers={},
|
||||
)
|
||||
|
||||
kept_rejected_keys: Final = {} if drop_params_source else rejected_keys
|
||||
assert result.get("thinking") == {**expected_thinking, "block_binding": BLOCK_BINDING, **kept_rejected_keys}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
|
||||
@pytest.mark.parametrize("action", (None, "tool_addition", "tool_removal"))
|
||||
@pytest.mark.parametrize("explicit_beta", (False, True))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue