mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(bedrock): keep an application inference profile's effort with thinking and refuse a malformed effort
This commit is contained in:
parent
da86002981
commit
b76bbd3c92
5 changed files with 76 additions and 25 deletions
|
|
@ -16,6 +16,7 @@ from litellm.llms.anthropic.messages.utils import (
|
|||
json_copy_or_original,
|
||||
prompt_cache_key_from_user_id,
|
||||
)
|
||||
from litellm.llms.bedrock.common_utils import is_bedrock_application_inference_profile_arn
|
||||
from litellm.responses.utils import auto_reasoning_summary
|
||||
|
||||
# OpenAI has a 64-character limit for function/tool names
|
||||
|
|
@ -366,6 +367,11 @@ _BlockT: Final = TypeVar("_BlockT", bound=Mapping[str, object])
|
|||
|
||||
|
||||
_CLAUDE_OVER_REASONING_EFFORT_PROVIDERS: Final = frozenset({LlmProviders.KYMA.value})
|
||||
UNRESOLVED_ARN_EFFORT_WITHOUT_THINKING_WARNING: Final = (
|
||||
"Dropping `output_config` for model=%s under drop_params: an application inference profile ARN does not name "
|
||||
"its model, so LiteLLM cannot tell whether it takes an effort without a `thinking` block. Turn off drop_params "
|
||||
"on the deployment to send it."
|
||||
)
|
||||
|
||||
|
||||
class LiteLLMAnthropicMessagesAdapter:
|
||||
|
|
@ -1203,6 +1209,7 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
self,
|
||||
anthropic_message_request: AnthropicMessagesRequest,
|
||||
new_kwargs: ChatCompletionRequest,
|
||||
drop_params: bool,
|
||||
) -> None:
|
||||
"""Forward the effort-bearing part of ``output_config`` to a Bedrock Claude target.
|
||||
|
||||
|
|
@ -1218,8 +1225,17 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
return
|
||||
|
||||
effort_config: Final = {k: v for k, v in output_config.items() if k != "format"}
|
||||
if effort_config:
|
||||
new_kwargs["output_config"] = effort_config # rebind-ok: out-param store like its sibling translators
|
||||
if not effort_config:
|
||||
return
|
||||
model: Final = new_kwargs["model"]
|
||||
if (
|
||||
drop_params
|
||||
and not anthropic_message_request.get("thinking")
|
||||
and is_bedrock_application_inference_profile_arn(model)
|
||||
):
|
||||
litellm.verbose_logger.warning(UNRESOLVED_ARN_EFFORT_WITHOUT_THINKING_WARNING, model)
|
||||
return
|
||||
new_kwargs["output_config"] = effort_config # rebind-ok: out-param store like its sibling translators
|
||||
|
||||
def _translate_output_format_to_openai(
|
||||
self,
|
||||
|
|
@ -1324,6 +1340,7 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
self._translate_output_config_to_openai(
|
||||
anthropic_message_request=anthropic_message_request,
|
||||
new_kwargs=new_kwargs,
|
||||
drop_params=drop_params,
|
||||
)
|
||||
## CONVERT STOP_SEQUENCES
|
||||
self._translate_stop_sequences_to_openai(
|
||||
|
|
|
|||
|
|
@ -142,11 +142,6 @@ ADAPTIVE_THINKING_ON_UNRESOLVED_ARN_WARNING: Final = (
|
|||
"does not name its model, so LiteLLM cannot tell whether it supports adaptive thinking. Set `base_model` on "
|
||||
"the deployment (for example `us.anthropic.claude-opus-4-8`) to send adaptive thinking as requested."
|
||||
)
|
||||
UNRESOLVED_ARN_OUTPUT_CONFIG_WARNING: Final = (
|
||||
"Dropping `output_config` for model=%s under drop_params: an application inference profile ARN does not name "
|
||||
"its model, so LiteLLM cannot tell whether it takes an effort. Set `base_model` on the deployment (for example "
|
||||
"`us.anthropic.claude-opus-4-8`) to send it."
|
||||
)
|
||||
DROP_UNSUPPORTED_EFFORT_WARNING: Final = "Dropping `output_config.effort` for model=%s under drop_params: %s"
|
||||
|
||||
# Computer use tool prefixes supported by Bedrock
|
||||
|
|
@ -583,7 +578,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
optional_params["_output_config_normalized"] = True
|
||||
|
||||
@staticmethod
|
||||
def _anthropic_adaptive_effort_error(model: str, effort: object) -> str | None:
|
||||
def _anthropic_adaptive_effort_error(model: str, effort: object, checks_tier: bool) -> str | None:
|
||||
"""Why ``output_config.effort`` is not valid on ``model``, or None when it is."""
|
||||
valid_efforts: Final = {"high", "medium", "low", "xhigh", "max"}
|
||||
if not isinstance(effort, str) or effort not in valid_efforts:
|
||||
|
|
@ -592,12 +587,14 @@ class AmazonConverseConfig(BaseConfig):
|
|||
f"{effort!r}. Must be one of: 'low', 'medium', 'high', "
|
||||
f"'xhigh', or 'max'."
|
||||
)
|
||||
if not checks_tier:
|
||||
return None
|
||||
return AnthropicConfig.validate_effort_for_model(model=model, effort=effort, custom_llm_provider="bedrock")
|
||||
|
||||
@staticmethod
|
||||
def _validate_anthropic_adaptive_effort(model: str, effort: str) -> None:
|
||||
"""Validate ``output_config.effort`` for adaptive-thinking Claude 4.6/4.7."""
|
||||
error: Final = AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort)
|
||||
error: Final = AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort, checks_tier=True)
|
||||
if error is not None:
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=error,
|
||||
|
|
@ -614,15 +611,11 @@ class AmazonConverseConfig(BaseConfig):
|
|||
drops_unsupported_effort: bool,
|
||||
) -> Mapping[str, object] | None:
|
||||
if is_bedrock_application_inference_profile_arn(model):
|
||||
if not drops_unsupported_effort:
|
||||
return output_config
|
||||
litellm.verbose_logger.warning(UNRESOLVED_ARN_OUTPUT_CONFIG_WARNING, model)
|
||||
return None
|
||||
return output_config
|
||||
if not base_model.startswith("anthropic"):
|
||||
return None
|
||||
model_takes_effort: Final = AnthropicConfig.model_supports_effort_param(
|
||||
model, "bedrock"
|
||||
) or AnthropicConfig.is_adaptive_thinking_model(model, "bedrock")
|
||||
model_lists_effort: Final = AnthropicConfig.model_supports_effort_param(model, "bedrock")
|
||||
model_takes_effort: Final = model_lists_effort or AnthropicConfig.is_adaptive_thinking_model(model, "bedrock")
|
||||
if drops_unsupported_effort and not model_takes_effort:
|
||||
litellm.verbose_logger.warning(DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, model)
|
||||
return None
|
||||
|
|
@ -631,7 +624,9 @@ class AmazonConverseConfig(BaseConfig):
|
|||
normalize_bedrock_opus_output_config_effort(model=model, output_config=normalized_output_config)
|
||||
effort: Final = normalized_output_config.get("effort")
|
||||
effort_error: Final = (
|
||||
None if effort is None else AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort)
|
||||
None
|
||||
if effort is None
|
||||
else AmazonConverseConfig._anthropic_adaptive_effort_error(model, effort, checks_tier=model_lists_effort)
|
||||
)
|
||||
if effort_error is None:
|
||||
return normalized_output_config
|
||||
|
|
|
|||
|
|
@ -1634,7 +1634,7 @@ def normalize_bedrock_opus_output_config_effort(model: str, output_config: objec
|
|||
return
|
||||
|
||||
effort: Final = output_config.get("effort")
|
||||
if effort not in _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER:
|
||||
if not isinstance(effort, str) or effort not in _BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER:
|
||||
return
|
||||
|
||||
ceiling: Final = _get_bedrock_output_config_effort_ceiling(model)
|
||||
|
|
|
|||
|
|
@ -5553,6 +5553,39 @@ async def test_bedrock_converse_claude_without_effort_support_drops_the_effort_o
|
|||
assert ("output_config" in _sent_converse_fields(_converse_route)) is forwards_effort
|
||||
|
||||
|
||||
_APPLICATION_INFERENCE_PROFILE: Final = (
|
||||
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abcdef123456"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("thinking", "drop_params", "forwards_effort"),
|
||||
[
|
||||
pytest.param(None, False, True, id="no_thinking"),
|
||||
pytest.param(None, True, False, id="no_thinking_drop_params"),
|
||||
pytest.param({"type": "enabled", "budget_tokens": 1024}, True, True, id="thinking_drop_params"),
|
||||
],
|
||||
)
|
||||
async def test_bedrock_application_inference_profile_loses_the_effort_only_without_thinking_under_drop_params(
|
||||
_converse_route: respx.Route,
|
||||
thinking: Mapping[str, object] | None,
|
||||
drop_params: bool,
|
||||
forwards_effort: bool,
|
||||
) -> None:
|
||||
await litellm.anthropic.messages.acreate(
|
||||
model=f"bedrock/converse/{_APPLICATION_INFERENCE_PROFILE}",
|
||||
max_tokens=2048,
|
||||
messages=[{"role": "user", "content": "Name the capital of France."}],
|
||||
output_config={"effort": "high", "format": _CITY_FORMAT},
|
||||
drop_params=drop_params,
|
||||
**({"thinking": thinking} if thinking else {}),
|
||||
)
|
||||
|
||||
sent: Final = _sent_converse_fields(_converse_route)
|
||||
assert sent.get("output_config") == ({"effort": "high"} if forwards_effort else None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"output_config",
|
||||
[pytest.param(None, id="null"), pytest.param({"format": _CITY_FORMAT}, id="format_only")],
|
||||
|
|
|
|||
|
|
@ -5581,13 +5581,13 @@ def _converse_output_config(model: str, output_config: dict, litellm_params: dic
|
|||
return result.get("additionalModelRequestFields", {}).get("output_config")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("effort", ["xhigh", "turbo"])
|
||||
@pytest.mark.parametrize("effort", ["xhigh", "turbo", ["high"]])
|
||||
def test_converse_refuses_an_effort_the_model_cannot_take_without_drop_params(local_model_cost_map, effort):
|
||||
with pytest.raises(litellm.BadRequestError, match="effort"):
|
||||
_converse_output_config("us.anthropic.claude-sonnet-4-6", {"effort": effort}, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("effort", ["xhigh", "turbo"])
|
||||
@pytest.mark.parametrize("effort", ["xhigh", "turbo", ["high"]])
|
||||
def test_converse_drops_an_effort_the_model_cannot_take_under_deployment_drop_params(
|
||||
local_model_cost_map, caplog, effort
|
||||
):
|
||||
|
|
@ -5600,9 +5600,11 @@ def test_converse_drops_an_effort_the_model_cannot_take_under_deployment_drop_pa
|
|||
assert any("output_config.effort" in record.getMessage() for record in caplog.records)
|
||||
|
||||
|
||||
def test_converse_forwards_an_unresolved_arn_effort_only_without_drop_params(local_model_cost_map):
|
||||
assert _converse_output_config(_APPLICATION_INFERENCE_PROFILE_ARN, {"effort": "high"}, {}) == {"effort": "high"}
|
||||
assert _converse_output_config(_APPLICATION_INFERENCE_PROFILE_ARN, {"effort": "high"}, {"drop_params": True}) is None
|
||||
@pytest.mark.parametrize("litellm_params", [{}, {"drop_params": True}])
|
||||
def test_converse_forwards_an_unresolved_arn_effort_verbatim(local_model_cost_map, litellm_params):
|
||||
sent: Final = _converse_output_config(_APPLICATION_INFERENCE_PROFILE_ARN, {"effort": "high"}, litellm_params)
|
||||
|
||||
assert sent == {"effort": "high"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -5625,11 +5627,15 @@ def test_converse_refuses_a_reasoning_effort_tier_the_model_cannot_take(local_mo
|
|||
)
|
||||
|
||||
|
||||
def test_converse_keeps_the_effort_for_a_claude_id_the_cost_map_does_not_list_yet(local_model_cost_map):
|
||||
@pytest.mark.parametrize("effort", ["high", "xhigh", "max"])
|
||||
@pytest.mark.parametrize("litellm_params", [{}, {"drop_params": True}])
|
||||
def test_converse_keeps_the_effort_for_a_claude_id_the_cost_map_does_not_list_yet(
|
||||
local_model_cost_map, effort, litellm_params
|
||||
):
|
||||
model: Final = "us.anthropic.claude-opus-9-9-v1:0"
|
||||
assert model not in litellm.model_cost
|
||||
|
||||
assert _converse_output_config(model, {"effort": "high"}, {"drop_params": True}) == {"effort": "high"}
|
||||
assert _converse_output_config(model, {"effort": effort}, litellm_params) == {"effort": effort}
|
||||
|
||||
|
||||
def test_transform_response_native_structured_output():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue