feat(anthropic): map reasoning_effort to adaptive thinking with budget_tokens for Claude 4.6 models

- Claude 4.6 models now receive `budget_tokens` in the adaptive thinking param (e.g. reasoning_effort="low" → {"type": "adaptive", "budget_tokens": 1024})
- Add xhigh effort level support: maps to 10000 budget_tokens for all models, and "max" in output_config for Claude 4.6
- Add DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET constant (default 10000, configurable via env)
- Refactor effort-to-budget mapping into a single shared dict to reduce duplication
- Update and expand tests to assert budget_tokens are set correctly for all effort levels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sameer Kankute 2026-03-02 10:27:41 +05:30
parent b518c24ff4
commit ac98f311e1
4 changed files with 57 additions and 21 deletions

View file

@ -177,6 +177,9 @@ DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET = int(
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET = int(
os.getenv("DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET", 4096)
)
DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET = int(
os.getenv("DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET", 10000)
)
MAX_TOKEN_TRIMMING_ATTEMPTS = int(
os.getenv("MAX_TOKEN_TRIMMING_ATTEMPTS", 10)
) # Maximum number of attempts to trim the message

View file

@ -13,6 +13,7 @@ from litellm.constants import (
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET,
RESPONSE_FORMAT_TOOL_NAME,
)
from litellm.litellm_core_utils.core_helpers import map_finish_reason
@ -55,7 +56,10 @@ from litellm.types.utils import (
CompletionTokensDetailsWrapper,
)
from litellm.types.utils import Message as LitellmMessage
from litellm.types.utils import PromptTokensDetailsWrapper, ServerToolUse
from litellm.types.utils import (
PromptTokensDetailsWrapper,
ServerToolUse,
)
from litellm.utils import (
ModelResponse,
Usage,
@ -729,32 +733,30 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
) -> Optional[AnthropicThinkingParam]:
if reasoning_effort is None or reasoning_effort == "none":
return None
effort_to_budget = {
"minimal": DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET,
"low": DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
"medium": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
"high": DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
"xhigh": DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET,
}
if AnthropicConfig._is_claude_4_6_model(model):
budget_tokens = effort_to_budget.get(reasoning_effort)
if budget_tokens is not None:
return AnthropicThinkingParam(
type="adaptive",
budget_tokens=budget_tokens,
)
return AnthropicThinkingParam(
type="adaptive",
)
elif reasoning_effort == "low":
budget_tokens = effort_to_budget.get(reasoning_effort)
if budget_tokens is not None:
return AnthropicThinkingParam(
type="enabled",
budget_tokens=DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
budget_tokens=budget_tokens,
)
elif reasoning_effort == "medium":
return AnthropicThinkingParam(
type="enabled",
budget_tokens=DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
)
elif reasoning_effort == "high":
return AnthropicThinkingParam(
type="enabled",
budget_tokens=DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
)
elif reasoning_effort == "minimal":
return AnthropicThinkingParam(
type="enabled",
budget_tokens=DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET,
)
else:
raise ValueError(f"Unmapped reasoning effort: {reasoning_effort}")
raise ValueError(f"Unmapped reasoning effort: {reasoning_effort}")
def _extract_json_schema_from_response_format(
self, value: Optional[dict]
@ -1014,6 +1016,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"minimal": "low",
"medium": "medium",
"high": "high",
"xhigh": "max",
"max": "max",
}
mapped_effort = effort_map.get(value, value)

View file

@ -16289,7 +16289,7 @@
"cache_read_input_token_cost": 3e-08,
"input_cost_per_audio_token": 1e-06,
"input_cost_per_token": 3e-07,
"litellm_provider": "vertex_ai-language-models",
"litellm_provider": "gemini",
"max_audio_length_hours": 8.4,
"max_audio_per_prompt": 1,
"supports_reasoning": false,

View file

@ -28,12 +28,28 @@ class TestMapReasoningEffort:
reasoning_effort="low", model="claude-opus-4-6"
)
assert result["type"] == "adaptive"
assert result["budget_tokens"] == 1024
def test_opus_4_6_returns_adaptive_for_high(self):
result = AnthropicConfig._map_reasoning_effort(
reasoning_effort="high", model="claude-opus-4-6"
)
assert result["type"] == "adaptive"
assert result["budget_tokens"] == 4096
def test_opus_4_6_returns_adaptive_for_medium(self):
result = AnthropicConfig._map_reasoning_effort(
reasoning_effort="medium", model="claude-opus-4-6"
)
assert result["type"] == "adaptive"
assert result["budget_tokens"] == 2048
def test_opus_4_6_returns_adaptive_for_minimal(self):
result = AnthropicConfig._map_reasoning_effort(
reasoning_effort="minimal", model="claude-opus-4-6"
)
assert result["type"] == "adaptive"
assert result["budget_tokens"] == 128
def test_other_model_low_returns_enabled_with_budget(self):
result = AnthropicConfig._map_reasoning_effort(
@ -62,3 +78,17 @@ class TestMapReasoningEffort:
reasoning_effort="none", model="claude-4-sonnet-20250514"
)
assert result is None
def test_opus_4_6_returns_adaptive_with_budget_for_xhigh(self):
result = AnthropicConfig._map_reasoning_effort(
reasoning_effort="xhigh", model="claude-opus-4-6"
)
assert result["type"] == "adaptive"
assert result["budget_tokens"] == 10000
def test_other_model_xhigh_returns_enabled_with_budget(self):
result = AnthropicConfig._map_reasoning_effort(
reasoning_effort="xhigh", model="claude-4-sonnet-20250514"
)
assert result["type"] == "enabled"
assert result["budget_tokens"] == 10000