fix(anthropic,bedrock): reject unmapped reasoning_effort at mapping site

Both the chat completion path (AnthropicConfig.map_openai_params) and the
Bedrock Converse path (_handle_reasoning_effort_parameter) used
REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(value, value) which falls
back to the raw input on unmapped keys. Combined with _map_reasoning_effort
returning type='adaptive' for any string on Claude 4.6/4.7, garbage values
(e.g. 'disabled') could leak into optional_params['output_config']['effort']
unvalidated if map_openai_params ran without the downstream transform_request
or _validate_anthropic_adaptive_effort check.

Mirror the /v1/messages pattern: use .get(value) (no fallback) and raise
BadRequestError immediately when the value is unmapped, co-locating
validation with the mapping for defense in depth.
This commit is contained in:
Cursor Agent 2026-05-03 08:42:22 +00:00 committed by mateo-berri
parent e400de4654
commit 039b26626f
2 changed files with 38 additions and 2 deletions

View file

@ -1133,9 +1133,28 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
if AnthropicConfig._is_claude_4_6_model(
model
) or AnthropicConfig._is_claude_4_7_model(model):
# ``_map_reasoning_effort`` returns ``type=adaptive``
# for any string on adaptive models without checking
# the value, so reject unmapped efforts here (matching
# the /v1/messages path) instead of relying on the
# downstream ``_apply_output_config`` check. Co-locating
# validation with the mapping prevents garbage from
# leaking into ``optional_params`` if ``map_openai_params``
# is ever called without a subsequent ``transform_request``.
mapped_effort = AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(
value, value
value
)
if mapped_effort is None:
raise litellm.exceptions.BadRequestError(
message=(
f"Invalid reasoning_effort: {value!r}. "
f"Must be one of: 'minimal', 'low', "
f"'medium', 'high', 'xhigh', 'max', 'none'"
),
model=model,
llm_provider=self.custom_llm_provider
or "anthropic",
)
optional_params["output_config"] = {"effort": mapped_effort}
elif param == "web_search_options" and isinstance(value, dict):
hosted_web_search_tool = self.map_web_search_tool(

View file

@ -485,11 +485,28 @@ class AmazonConverseConfig(BaseConfig):
if AnthropicConfig._is_claude_4_6_model(
model
) or AnthropicConfig._is_claude_4_7_model(model):
# Use ``.get()`` without a fallback so unmapped efforts
# (e.g. ``"disabled"``) surface as a clean 400 here
# rather than leaking the raw garbage string through to
# ``_validate_anthropic_adaptive_effort`` (which does
# catch it, but only because validation happens to run).
# Matches the /v1/messages pattern where validation is
# co-located with the mapping.
mapped_effort = (
AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(
reasoning_effort, reasoning_effort
reasoning_effort
)
)
if mapped_effort is None:
raise litellm.exceptions.BadRequestError(
message=(
f"Invalid reasoning_effort: {reasoning_effort!r}. "
f"Must be one of: 'minimal', 'low', 'medium', "
f"'high', 'xhigh', 'max', 'none'"
),
model=model,
llm_provider="bedrock_converse",
)
self._validate_anthropic_adaptive_effort(
model=model, effort=mapped_effort
)