fix(adapter): normalize reasoning effort with graceful degradation (#26111)

* fix(model-info): include reasoning effort support fields in get_model_info

_get_model_info_helper constructs ModelInfoBase explicitly but never
reads supports_xhigh/minimal/none_reasoning_effort from the cost map
JSON. Add the three fields so get_model_info() returns them correctly.

Also add supports_minimal_reasoning_effort to the ModelInfo TypedDict
(xhigh and none were already declared, minimal was missing).

* fix(model-registry): add missing reasoning effort fields for claude 4.6/4.7

Claude Opus 4.7 supports max reasoning effort (above xhigh).
The field was present for Opus 4.6 but missing for all Opus 4.7
entries (base, dated, Bedrock, Vertex AI, Azure AI).

All Claude 4.6/4.7 models (Opus 4.6, Sonnet 4.6, Opus 4.7) support
minimal reasoning effort via adaptive thinking. Add the field to all
provider variants.

* fix(adapter): map output_config.effort to reasoning_effort (#25079)

Anthropic's adaptive thinking (thinking.type="adaptive") and
output_config.effort were silently dropped when translating to
OpenAI format, resulting in no reasoning_effort on the outgoing
request.

Adapter changes (format translation):
- adapters/transformation.py: add "adaptive" branch to
  translate_anthropic_thinking_to_reasoning_effort(); pass through
  output_config.effort as-is in _translate_thinking_to_openai();
  add "output_config" to translatable_anthropic_params
- adapters/handler.py: extract output_config from extra_kwargs into
  request_data so it reaches the translation layer
- responses_adapters/transformation.py: add "adaptive" branch and
  output_config param to translate_thinking_to_reasoning()

Handler changes (model-aware normalization):
- utils.py: add normalize_reasoning_effort_value() that uses
  get_model_info() to map "max" → "xhigh"/"high" and
  "minimal" → "minimal"/"low" based on model capabilities
- adapters/handler.py: call normalization before responses routing
- responses_adapters/handler.py: call normalization after translation

Relates to BerriAI/litellm#25079

* test(reasoning-effort): add tests for effort capability fields and normalize logic

Test coverage for:
- get_model_info returning supports_minimal/max_reasoning_effort fields
- JSON registry entries for claude 4.6/4.7 across all providers
- normalize_reasoning_effort_value degradation chains and exception fallback
- Adapter translation of adaptive thinking + output_config.effort

* fix: forward custom_llm_provider to normalize_reasoning_effort_value in responses adapter
This commit is contained in:
Vigilans 2026-04-23 10:19:54 +08:00 • committed by GitHub
parent 034f4fdef2
commit b42b86df7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 598 additions and 74 deletions

View file

@ -106,6 +106,44 @@ class LiteLLMMessagesToCompletionTransformationHandler:
updated_reasoning_effort["summary"] = effective_summary
completion_kwargs["reasoning_effort"] = updated_reasoning_effort
@staticmethod
def _normalize_reasoning_effort(
completion_kwargs: Dict[str, Any],
) -> None:
"""
Normalize reasoning_effort values based on target model capabilities.
Handles both string ("max") and dict ({"effort": "max", "summary": ...})
formats. Uses model registry to check supports_xhigh/supports_minimal.
"""
from litellm.llms.anthropic.experimental_pass_through.utils import (
normalize_reasoning_effort_value,
)
reasoning_effort = completion_kwargs.get("reasoning_effort")
if reasoning_effort is None:
return
model = cast(str, completion_kwargs.get("model", ""))
custom_llm_provider = completion_kwargs.get("custom_llm_provider")
if isinstance(reasoning_effort, str):
normalized = normalize_reasoning_effort_value(
reasoning_effort, model=model, custom_llm_provider=custom_llm_provider
)
if normalized != reasoning_effort:
completion_kwargs["reasoning_effort"] = normalized
elif isinstance(reasoning_effort, dict) and "effort" in reasoning_effort:
effort = reasoning_effort["effort"]
normalized = normalize_reasoning_effort_value(
effort, model=model, custom_llm_provider=custom_llm_provider
)
if normalized != effort:
completion_kwargs["reasoning_effort"] = {
**reasoning_effort,
"effort": normalized,
}
@staticmethod
def _prepare_completion_kwargs(
*,
@ -163,6 +201,12 @@ class LiteLLMMessagesToCompletionTransformationHandler:
if output_format:
request_data["output_format"] = output_format
# Extract output_config from extra_kwargs so the translator can use it
# (e.g. output_config.effort for adaptive thinking → reasoning_effort)
extra_kwargs = extra_kwargs or {}
if "output_config" in extra_kwargs:
request_data["output_config"] = extra_kwargs["output_config"]
(
openai_request,
tool_name_mapping,
@ -202,6 +246,14 @@ class LiteLLMMessagesToCompletionTransformationHandler:
):
completion_kwargs[key] = value
# Normalize reasoning_effort based on model capabilities
# (e.g. "max" → "xhigh"/"high", "minimal" → "low" if unsupported)
# Must run BEFORE _route_openai_thinking, which prepends "responses/"
# to the model name and would break get_model_info() lookups.
LiteLLMMessagesToCompletionTransformationHandler._normalize_reasoning_effort(
completion_kwargs
)
LiteLLMMessagesToCompletionTransformationHandler._route_openai_thinking_to_responses_api_if_needed(
completion_kwargs,
thinking=thinking,

View file

@ -317,6 +317,7 @@ class LiteLLMAnthropicMessagesAdapter:
"tools",
"thinking",
"output_format",
"output_config",
]
def _is_web_search_tool(self, tool: Dict[str, Any]) -> bool:
@ -694,6 +695,11 @@ class LiteLLMAnthropicMessagesAdapter:
return "low"
else:
return "minimal"
elif thinking_type == "adaptive":
# Adaptive thinking: effort is controlled by output_config.effort,
# not budget_tokens. Return a default; caller should override with
# output_config.effort when available.
return "medium"
return None
@ -1041,6 +1047,12 @@ class LiteLLMAnthropicMessagesAdapter:
if not reasoning_effort:
return
# For adaptive thinking, override with output_config.effort if available
if isinstance(thinking, dict) and thinking.get("type") == "adaptive":
output_config = anthropic_message_request.get("output_config")
if isinstance(output_config, dict) and output_config.get("effort"):
reasoning_effort = output_config["effort"]
summary = thinking.get("summary") if isinstance(thinking, dict) else None
auto_summary = is_reasoning_auto_summary_enabled()
if summary:

View file

@ -72,6 +72,23 @@ def _build_responses_kwargs(
anthropic_request = AnthropicMessagesRequest(**request_data) # type: ignore[typeddict-item]
responses_kwargs = _ADAPTER.translate_request(anthropic_request)
# Normalize reasoning effort based on model capabilities
# (e.g. "max" → "xhigh"/"high", "minimal" → "low" if unsupported)
reasoning = responses_kwargs.get("reasoning")
if isinstance(reasoning, dict) and "effort" in reasoning:
from litellm.llms.anthropic.experimental_pass_through.utils import (
normalize_reasoning_effort_value,
)
effort = reasoning["effort"]
normalized = normalize_reasoning_effort_value(
effort,
model=model,
custom_llm_provider=(extra_kwargs or {}).get("custom_llm_provider"),
)
if normalized != effort:
responses_kwargs["reasoning"] = {**reasoning, "effort": normalized}
if stream:
responses_kwargs["stream"] = True

View file

@ -251,25 +251,41 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
@staticmethod
def translate_thinking_to_reasoning(
thinking: Dict[str, Any]
thinking: Dict[str, Any],
output_config: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
"""
Convert Anthropic thinking param to Responses API reasoning param.
thinking.budget_tokens maps to reasoning effort:
>= 10000 -> high, >= 5000 -> medium, >= 2000 -> low, < 2000 -> minimal
For adaptive thinking, uses output_config.effort if available,
otherwise defaults to medium.
"""
if not isinstance(thinking, dict) or thinking.get("type") != "enabled":
if not isinstance(thinking, dict):
return None
budget = thinking.get("budget_tokens", 0)
if budget >= 10000:
effort = "high"
elif budget >= 5000:
thinking_type = thinking.get("type")
if thinking_type == "adaptive":
# Use output_config.effort if available
effort = "medium"
elif budget >= 2000:
effort = "low"
if isinstance(output_config, dict) and output_config.get("effort"):
effort = output_config["effort"]
elif thinking_type == "enabled":
budget = thinking.get("budget_tokens", 0)
if budget >= 10000:
effort = "high"
elif budget >= 5000:
effort = "medium"
elif budget >= 2000:
effort = "low"
else:
effort = "minimal"
else:
effort = "minimal"
return None
auto_summary = is_reasoning_auto_summary_enabled()
result: Dict[str, Any] = {"effort": effort}
summary = thinking.get("summary")
@ -346,7 +362,11 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
# thinking -> reasoning
thinking = anthropic_request.get("thinking")
if isinstance(thinking, dict):
reasoning = self.translate_thinking_to_reasoning(thinking)
output_config = anthropic_request.get("output_config")
reasoning = self.translate_thinking_to_reasoning(
thinking,
output_config=cast(Optional[Dict[str, Any]], output_config),
)
if reasoning:
responses_kwargs["reasoning"] = reasoning

View file

@ -1,4 +1,5 @@
import os
from typing import Optional
import litellm
@ -9,3 +10,46 @@ def is_reasoning_auto_summary_enabled() -> bool:
litellm.reasoning_auto_summary
or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
)
def normalize_reasoning_effort_value(
effort: str,
model: str,
custom_llm_provider: Optional[str] = None,
) -> str:
"""
Normalize a reasoning effort value based on model capabilities.
Degradation chains:
- "max" → max / xhigh / high
- "xhigh" → xhigh / high
- "minimal" → minimal / low
- other values pass through unchanged
"""
if effort not in ("max", "xhigh", "minimal"):
return effort
from litellm.utils import get_model_info
try:
model_info = get_model_info(
model=model, custom_llm_provider=custom_llm_provider
)
except Exception:
model_info = {}
if effort == "max":
if model_info.get("supports_max_reasoning_effort"):
return "max"
if model_info.get("supports_xhigh_reasoning_effort"):
return "xhigh"
return "high"
elif effort == "xhigh":
if model_info.get("supports_xhigh_reasoning_effort"):
return "xhigh"
return "high"
elif effort == "minimal":
if model_info.get("supports_minimal_reasoning_effort"):
return "minimal"
return "low"
return "medium"

View file

@ -1006,7 +1006,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"global.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.25e-06,
@ -1034,7 +1035,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"us.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1062,7 +1064,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1090,7 +1093,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"au.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1118,7 +1122,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@ -1146,7 +1151,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"global.anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@ -1174,7 +1181,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"us.anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1202,7 +1211,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1230,7 +1241,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"au.anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1258,7 +1271,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 3.75e-06,
@ -1285,7 +1300,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"global.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 3.75e-06,
@ -1312,7 +1328,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"us.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 4.125e-06,
@ -1339,7 +1356,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 4.125e-06,
@ -1366,7 +1384,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"au.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 4.125e-06,
@ -1393,7 +1412,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-sonnet-4-20250514-v1:0": {
"cache_creation_input_token_cost": 3.75e-06,
@ -1911,7 +1931,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 159,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"azure_ai/claude-opus-4-7": {
"input_cost_per_token": 5e-06,
@ -1939,7 +1960,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 159
"tool_use_system_prompt_tokens": 159,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"azure_ai/claude-opus-4-1": {
"cache_creation_input_token_cost": 1.875e-05,
@ -2003,7 +2026,8 @@
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_minimal_reasoning_effort": true
},
"azure/computer-use-preview": {
"input_cost_per_token": 3e-06,
@ -8909,7 +8933,8 @@
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_minimal_reasoning_effort": true
},
"claude-sonnet-4-5-20250929-v1:0": {
"cache_creation_input_token_cost": 3.75e-06,
@ -9103,7 +9128,8 @@
"us": 1.1,
"fast": 6.0
},
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-opus-4-6-20260205": {
"cache_creation_input_token_cost": 6.25e-06,
@ -9135,7 +9161,8 @@
"us": 1.1,
"fast": 6.0
},
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@ -9167,7 +9194,9 @@
"provider_specific_entry": {
"us": 1.1,
"fast": 6.0
}
},
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-opus-4-7-20260416": {
"cache_creation_input_token_cost": 6.25e-06,
@ -9199,7 +9228,9 @@
"provider_specific_entry": {
"us": 1.1,
"fast": 6.0
}
},
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-sonnet-4-20250514": {
"deprecation_date": "2026-05-14",
@ -25052,7 +25083,8 @@
"supports_reasoning": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 159
"tool_use_system_prompt_tokens": 159,
"supports_minimal_reasoning_effort": true
},
"openrouter/anthropic/claude-opus-4.5": {
"cache_creation_input_token_cost": 6.25e-06,
@ -25090,7 +25122,8 @@
"supports_reasoning": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_minimal_reasoning_effort": true
},
"openrouter/anthropic/claude-sonnet-4.5": {
"input_cost_per_image": 0.0048,
@ -30118,7 +30151,8 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_minimal_reasoning_effort": true
},
"vercel_ai_gateway/anthropic/claude-sonnet-4": {
"cache_creation_input_token_cost": 3.75e-06,
@ -31345,7 +31379,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-opus-4-6@default": {
"cache_creation_input_token_cost": 6.25e-06,
@ -31372,7 +31407,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@ -31399,7 +31435,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-opus-4-7@default": {
"cache_creation_input_token_cost": 6.25e-06,
@ -31426,7 +31464,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-sonnet-4-5": {
"cache_creation_input_token_cost": 3.75e-06,
@ -31478,7 +31518,8 @@
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
}
},
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-sonnet-4-5@20250929": {
"cache_creation_input_token_cost": 3.75e-06,
@ -38345,7 +38386,8 @@
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
}
},
"supports_minimal_reasoning_effort": true
},
"duckduckgo/search": {
"litellm_provider": "duckduckgo",

View file

@ -139,7 +139,9 @@ class ProviderSpecificModelInfo(TypedDict, total=False):
supports_reasoning: Optional[bool]
supports_url_context: Optional[bool]
supports_none_reasoning_effort: Optional[bool]
supports_minimal_reasoning_effort: Optional[bool]
supports_xhigh_reasoning_effort: Optional[bool]
supports_max_reasoning_effort: Optional[bool]
class SearchContextCostPerQuery(TypedDict, total=False):

View file

@ -5893,9 +5893,15 @@ def _get_model_info_helper( # noqa: PLR0915
supports_none_reasoning_effort=_model_info.get(
"supports_none_reasoning_effort", None
),
supports_minimal_reasoning_effort=_model_info.get(
"supports_minimal_reasoning_effort", None
),
supports_xhigh_reasoning_effort=_model_info.get(
"supports_xhigh_reasoning_effort", None
),
supports_max_reasoning_effort=_model_info.get(
"supports_max_reasoning_effort", None
),
supports_computer_use=_model_info.get("supports_computer_use", None),
search_context_cost_per_query=_model_info.get(
"search_context_cost_per_query", None

View file

@ -1006,7 +1006,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"global.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.25e-06,
@ -1034,7 +1035,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"us.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1062,7 +1064,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1090,7 +1093,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"au.anthropic.claude-opus-4-6-v1": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1118,7 +1122,8 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@ -1146,7 +1151,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-mythos-preview": {
"input_cost_per_token": 0,
@ -1188,7 +1195,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"us.anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1216,7 +1225,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1244,7 +1255,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"au.anthropic.claude-opus-4-7": {
"cache_creation_input_token_cost": 6.875e-06,
@ -1272,7 +1285,9 @@
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 3.75e-06,
@ -1299,7 +1314,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"global.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 3.75e-06,
@ -1326,7 +1342,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"us.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 4.125e-06,
@ -1353,7 +1370,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 4.125e-06,
@ -1380,7 +1398,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"au.anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 4.125e-06,
@ -1407,7 +1426,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_native_structured_output": true
"supports_native_structured_output": true,
"supports_minimal_reasoning_effort": true
},
"anthropic.claude-sonnet-4-20250514-v1:0": {
"cache_creation_input_token_cost": 3.75e-06,
@ -1925,7 +1945,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 159,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"azure_ai/claude-opus-4-7": {
"input_cost_per_token": 5e-06,
@ -1953,7 +1974,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 159
"tool_use_system_prompt_tokens": 159,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"azure_ai/claude-opus-4-1": {
"cache_creation_input_token_cost": 1.875e-05,
@ -2017,7 +2040,8 @@
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_minimal_reasoning_effort": true
},
"azure/computer-use-preview": {
"input_cost_per_token": 3e-06,
@ -8923,7 +8947,8 @@
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_minimal_reasoning_effort": true
},
"claude-sonnet-4-5-20250929-v1:0": {
"cache_creation_input_token_cost": 3.75e-06,
@ -9117,7 +9142,8 @@
"us": 1.1,
"fast": 6.0
},
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-opus-4-6-20260205": {
"cache_creation_input_token_cost": 6.25e-06,
@ -9149,7 +9175,8 @@
"us": 1.1,
"fast": 6.0
},
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@ -9181,7 +9208,9 @@
"provider_specific_entry": {
"us": 1.1,
"fast": 6.0
}
},
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-opus-4-7-20260416": {
"cache_creation_input_token_cost": 6.25e-06,
@ -9213,7 +9242,9 @@
"provider_specific_entry": {
"us": 1.1,
"fast": 6.0
}
},
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"claude-sonnet-4-20250514": {
"deprecation_date": "2026-05-14",
@ -25066,7 +25097,8 @@
"supports_reasoning": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 159
"tool_use_system_prompt_tokens": 159,
"supports_minimal_reasoning_effort": true
},
"openrouter/anthropic/claude-opus-4.5": {
"cache_creation_input_token_cost": 6.25e-06,
@ -25104,7 +25136,8 @@
"supports_reasoning": true,
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_minimal_reasoning_effort": true
},
"openrouter/anthropic/claude-sonnet-4.5": {
"input_cost_per_image": 0.0048,
@ -30132,7 +30165,8 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
"supports_vision": true,
"supports_minimal_reasoning_effort": true
},
"vercel_ai_gateway/anthropic/claude-sonnet-4": {
"cache_creation_input_token_cost": 3.75e-06,
@ -31359,7 +31393,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-opus-4-6@default": {
"cache_creation_input_token_cost": 6.25e-06,
@ -31386,7 +31421,8 @@
"supports_tool_choice": true,
"supports_vision": true,
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@ -31413,7 +31449,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-opus-4-7@default": {
"cache_creation_input_token_cost": 6.25e-06,
@ -31440,7 +31478,9 @@
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346
"tool_use_system_prompt_tokens": 346,
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-sonnet-4-5": {
"cache_creation_input_token_cost": 3.75e-06,
@ -31492,7 +31532,8 @@
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
}
},
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-sonnet-4-5@20250929": {
"cache_creation_input_token_cost": 3.75e-06,
@ -38386,7 +38427,8 @@
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
}
},
"supports_minimal_reasoning_effort": true
},
"duckduckgo/search": {
"litellm_provider": "duckduckgo",

View file

@ -0,0 +1,287 @@
"""
Tests for reasoning effort capability fields and normalize_reasoning_effort_value.
Covers:
- Commit 1: get_model_info returns supports_minimal/supports_max fields
- Commit 2: Model registry entries have correct reasoning effort fields
- Commit 3: normalize_reasoning_effort_value degradation chains + adapter translation
"""
import json
import os
from typing import Any, Dict, Optional
from unittest.mock import patch
import pytest
from litellm.llms.anthropic.experimental_pass_through.utils import (
normalize_reasoning_effort_value,
)
from litellm.utils import get_model_info
def _load_model_registry() -> Dict[str, Any]:
"""Load the root model_prices_and_context_window.json."""
json_path = os.path.join(
os.path.dirname(__file__),
"../../../../../model_prices_and_context_window.json",
)
with open(json_path) as f:
return json.load(f)
# ---------------------------------------------------------------------------
# Commit 1: get_model_info returns supports_minimal and supports_max fields
# ---------------------------------------------------------------------------
class TestGetModelInfoReasoningEffortFields:
"""get_model_info should expose supports_minimal_reasoning_effort and
supports_max_reasoning_effort from the model registry."""
def test_opus_4_6_has_supports_minimal(self):
info = get_model_info("claude-opus-4-6")
assert "supports_minimal_reasoning_effort" in info
def test_opus_4_6_has_supports_max(self):
info = get_model_info("claude-opus-4-6")
assert "supports_max_reasoning_effort" in info
def test_opus_4_7_has_supports_minimal(self):
info = get_model_info("claude-opus-4-7")
assert "supports_minimal_reasoning_effort" in info
def test_opus_4_7_has_supports_max(self):
info = get_model_info("claude-opus-4-7")
assert "supports_max_reasoning_effort" in info
# ---------------------------------------------------------------------------
# Commit 2: JSON registry has correct reasoning effort fields
# ---------------------------------------------------------------------------
class TestModelRegistryReasoningEffortFields:
"""Verify specific models have the expected reasoning effort capability
values in the JSON registry file."""
@pytest.fixture(autouse=True)
def _load_registry(self):
self.registry = _load_model_registry()
def test_opus_4_7_supports_max(self):
entry = self.registry["claude-opus-4-7"]
assert entry.get("supports_max_reasoning_effort") is True
def test_opus_4_6_supports_max(self):
entry = self.registry["claude-opus-4-6"]
assert entry.get("supports_max_reasoning_effort") is True
def test_opus_4_7_supports_minimal(self):
entry = self.registry["claude-opus-4-7"]
assert entry.get("supports_minimal_reasoning_effort") is True
def test_opus_4_6_supports_minimal(self):
entry = self.registry["claude-opus-4-6"]
assert entry.get("supports_minimal_reasoning_effort") is True
def test_sonnet_4_6_supports_minimal(self):
entry = self.registry["anthropic.claude-sonnet-4-6"]
assert entry.get("supports_minimal_reasoning_effort") is True
def test_bedrock_opus_4_7_supports_max(self):
entry = self.registry["anthropic.claude-opus-4-7"]
assert entry.get("supports_max_reasoning_effort") is True
assert entry.get("supports_minimal_reasoning_effort") is True
def test_vertex_opus_4_7_supports_max(self):
entry = self.registry["vertex_ai/claude-opus-4-7"]
assert entry.get("supports_max_reasoning_effort") is True
assert entry.get("supports_minimal_reasoning_effort") is True
def test_vertex_opus_4_6_supports_max(self):
entry = self.registry["vertex_ai/claude-opus-4-6"]
assert entry.get("supports_max_reasoning_effort") is True
assert entry.get("supports_minimal_reasoning_effort") is True
def test_azure_ai_opus_4_6_supports_minimal(self):
entry = self.registry["azure_ai/claude-opus-4-6"]
assert entry.get("supports_minimal_reasoning_effort") is True
def test_azure_ai_opus_4_7_supports_max(self):
entry = self.registry["azure_ai/claude-opus-4-7"]
assert entry.get("supports_max_reasoning_effort") is True
assert entry.get("supports_minimal_reasoning_effort") is True
# ---------------------------------------------------------------------------
# Commit 3: normalize_reasoning_effort_value
# ---------------------------------------------------------------------------
def _mock_model_info(**flags):
"""Return a mock model_info dict with given capability flags."""
return flags
class TestNormalizeReasoningEffortValue:
"""Test degradation chains for normalize_reasoning_effort_value."""
# --- "max" degradation chain ---
def test_max_stays_max_when_supported(self):
with patch(
"litellm.utils.get_model_info",
return_value=_mock_model_info(
supports_max_reasoning_effort=True,
supports_xhigh_reasoning_effort=True,
),
):
assert normalize_reasoning_effort_value("max", model="test") == "max"
def test_max_degrades_to_xhigh(self):
with patch(
"litellm.utils.get_model_info",
return_value=_mock_model_info(
supports_max_reasoning_effort=False,
supports_xhigh_reasoning_effort=True,
),
):
assert normalize_reasoning_effort_value("max", model="test") == "xhigh"
def test_max_degrades_to_high(self):
with patch(
"litellm.utils.get_model_info",
return_value=_mock_model_info(
supports_max_reasoning_effort=False,
supports_xhigh_reasoning_effort=False,
),
):
assert normalize_reasoning_effort_value("max", model="test") == "high"
# --- "xhigh" degradation chain ---
def test_xhigh_stays_xhigh_when_supported(self):
with patch(
"litellm.utils.get_model_info",
return_value=_mock_model_info(supports_xhigh_reasoning_effort=True),
):
assert normalize_reasoning_effort_value("xhigh", model="test") == "xhigh"
def test_xhigh_degrades_to_high(self):
with patch(
"litellm.utils.get_model_info",
return_value=_mock_model_info(supports_xhigh_reasoning_effort=False),
):
assert normalize_reasoning_effort_value("xhigh", model="test") == "high"
# --- "minimal" degradation chain ---
def test_minimal_stays_minimal_when_supported(self):
with patch(
"litellm.utils.get_model_info",
return_value=_mock_model_info(supports_minimal_reasoning_effort=True),
):
assert (
normalize_reasoning_effort_value("minimal", model="test") == "minimal"
)
def test_minimal_degrades_to_low(self):
with patch(
"litellm.utils.get_model_info",
return_value=_mock_model_info(supports_minimal_reasoning_effort=False),
):
assert normalize_reasoning_effort_value("minimal", model="test") == "low"
# --- passthrough values ---
def test_high_passes_through(self):
assert normalize_reasoning_effort_value("high", model="test") == "high"
def test_medium_passes_through(self):
assert normalize_reasoning_effort_value("medium", model="test") == "medium"
def test_low_passes_through(self):
assert normalize_reasoning_effort_value("low", model="test") == "low"
# --- exception fallback ---
def test_exception_fallback_uses_empty_model_info(self):
"""When get_model_info raises, treat model_info as {} (no capabilities)."""
with patch(
"litellm.utils.get_model_info",
side_effect=Exception("model not found"),
):
# "max" with no capabilities -> "high"
assert normalize_reasoning_effort_value("max", model="unknown") == "high"
# "minimal" with no capabilities -> "low"
assert normalize_reasoning_effort_value("minimal", model="unknown") == "low"
# ---------------------------------------------------------------------------
# Commit 3: Adapter translation — adaptive thinking + output_config.effort
# ---------------------------------------------------------------------------
class TestAdapterAdaptiveThinking:
"""Test that adaptive thinking type maps correctly through the adapters."""
def test_messages_adapter_adaptive_returns_medium_default(self):
"""Adaptive thinking returns 'medium' as default reasoning_effort."""
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
LiteLLMAnthropicMessagesAdapter,
)
adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_thinking_to_reasoning_effort(
{"type": "adaptive"}
)
assert result == "medium"
def test_messages_adapter_adaptive_overridden_by_output_config(self):
"""For adaptive thinking, output_config.effort overrides reasoning_effort."""
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
LiteLLMAnthropicMessagesAdapter,
)
from litellm.types.llms.anthropic import AnthropicMessagesRequest
adapter = LiteLLMAnthropicMessagesAdapter()
request = AnthropicMessagesRequest(
model="test-model",
messages=[{"role": "user", "content": "hello"}],
max_tokens=1024,
thinking={"type": "adaptive"},
output_config={"effort": "high"},
)
openai_kwargs, _ = adapter.translate_anthropic_to_openai(request)
# reasoning_effort should be set (either as string or dict with effort)
re = openai_kwargs.get("reasoning_effort")
if isinstance(re, dict):
assert re["effort"] == "high"
else:
assert re == "high"
def test_responses_adapter_adaptive_with_output_config(self):
"""Responses adapter: adaptive thinking + output_config.effort."""
from litellm.llms.anthropic.experimental_pass_through.responses_adapters.transformation import (
LiteLLMAnthropicToResponsesAPIAdapter,
)
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(
thinking={"type": "adaptive"},
output_config={"effort": "xhigh"},
)
assert result is not None
assert result["effort"] == "xhigh"
def test_responses_adapter_adaptive_default_medium(self):
"""Responses adapter: adaptive thinking without output_config defaults to medium."""
from litellm.llms.anthropic.experimental_pass_through.responses_adapters.transformation import (
LiteLLMAnthropicToResponsesAPIAdapter,
)
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(
thinking={"type": "adaptive"},
)
assert result is not None
assert result["effort"] == "medium"