Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions litellm/litellm_core_utils/reasoning_effort_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Literal

from litellm.constants import (
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
)

OpenAIStyleReasoningEffort = Literal["minimal", "low", "medium", "high"]


def reasoning_effort_from_thinking_budget(
budget_tokens: int,
) -> OpenAIStyleReasoningEffort:
"""Bucket an Anthropic ``thinking.budget_tokens`` into an OpenAI-style
``reasoning_effort`` using the shared ``DEFAULT_REASONING_EFFORT_*_THINKING_BUDGET``
thresholds, so every backend that translates a budget into an effort label
reads the same numbers.
"""
if budget_tokens >= DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET:
return "high"
if budget_tokens >= DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET:
return "medium"
if budget_tokens >= DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET:
return "low"
return "minimal"
14 changes: 4 additions & 10 deletions litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,7 @@ def _build_anthropic_tool_name_maps(
"Sonnet 4.6+, and Mythos Preview."
)

DROP_UNSUPPORTED_SPEED_WARNING = (
"Dropping unsupported `speed` for model=%s "
"(drop_params=True). Fast mode is only supported on select Opus models."
)
DROP_UNSUPPORTED_SPEED_WARNING = "Dropping unsupported `speed` for model=%s (drop_params=True). Fast mode is only supported on select Opus models."


class AnthropicConfig(AnthropicModelInfo, BaseConfig):
Expand Down Expand Up @@ -352,8 +349,7 @@ def _supports_effort_level(model: str, level: str) -> bool:
def _validate_effort_for_model(model: str, effort: Optional[str]) -> Optional[str]:
"""Return ``None`` if ``effort`` is allowed on ``model``, else an error message."""
if effort == "max" and not (
AnthropicConfig._is_claude_4_6_model(model)
or AnthropicConfig._is_claude_4_7_model(model)
AnthropicConfig._is_adaptive_thinking_model(model)
or AnthropicConfig._supports_effort_level(model, "max")
):
return f"effort='max' is not supported by this model. Got model: {model}"
Expand Down Expand Up @@ -471,8 +467,7 @@ def get_supported_openai_params(self, model: str):

if (
"claude-3-7-sonnet" in model
or AnthropicConfig._is_claude_4_6_model(model)
or AnthropicConfig._is_claude_4_7_model(model)
or AnthropicConfig._is_adaptive_thinking_model(model)
or supports_reasoning(
model=model,
custom_llm_provider=self.custom_llm_provider,
Expand Down Expand Up @@ -2093,8 +2088,7 @@ def _apply_output_config(
if effort is not None and effort not in valid_efforts:
raise litellm.exceptions.BadRequestError(
message=(
f"Invalid effort value: {effort!r}. Must be one of: "
f"'high', 'medium', 'low', 'xhigh', 'max'"
f"Invalid effort value: {effort!r}. Must be one of: 'high', 'medium', 'low', 'xhigh', 'max'"
),
model=model,
llm_provider=self.custom_llm_provider or "anthropic",
Expand Down
105 changes: 51 additions & 54 deletions litellm/llms/anthropic/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@
)
from litellm.types.llms.openai import AllMessageValues

_BEDROCK_VERSION_SUFFIX_RE = re.compile(r"-v\d+(?::\d+)?$")
_INFERENCE_PROFILE_MINOR_RE = re.compile(r":\d+$")
_DATED_RELEASE_SUFFIX_RE = re.compile(r"-\d{8}$")
_DOTTED_VERSION_RE = re.compile(r"(\d)\.(\d)")


def _strip_bedrock_id_suffixes(model: str) -> str:
"""Reduce a full Bedrock model id to its base cost-map key by rewriting a
dotted family version then peeling a trailing ``-vN:rev`` and ``-YYYYMMDD``
in that order, so the real ``-<date>-v1:0`` shape (e.g.
``us.anthropic.claude-sonnet-4-6-20251101-v1:0``) resolves rather than only
the date or version in isolation."""
return _DATED_RELEASE_SUFFIX_RE.sub(
"",
_BEDROCK_VERSION_SUFFIX_RE.sub("", _DOTTED_VERSION_RE.sub(r"\1-\2", model)),
)


def is_anthropic_oauth_key(value: Optional[str]) -> bool:
"""Check if a value contains an Anthropic OAuth token (sk-ant-oat*)."""
Expand Down Expand Up @@ -243,38 +260,6 @@ def is_input_examples_used(self, tools: Optional[List]) -> bool:

return False

@staticmethod
def _is_claude_4_6_model(model: str) -> bool:
"""Check if the model is a Claude 4.6 model (Opus 4.6 or Sonnet 4.6)."""
model_lower = model.lower()
return any(
v in model_lower
for v in (
"opus-4-6",
"opus_4_6",
"opus-4.6",
"opus_4.6",
"sonnet-4-6",
"sonnet_4_6",
"sonnet-4.6",
"sonnet_4.6",
)
)

@staticmethod
def _is_claude_4_7_model(model: str) -> bool:
"""Check if the model is a Claude 4.7 model (Opus 4.7)."""
model_lower = model.lower()
return any(
v in model_lower
for v in (
"opus-4-7",
"opus_4_7",
"opus-4.7",
"opus_4.7",
)
)

@staticmethod
def _supports_sampling_params(model: str) -> bool:
"""Claude 4.7+ (Opus 4.7/4.8, Fable 5) removed sampling params: the API
Expand Down Expand Up @@ -336,27 +321,42 @@ def _apply_sampling_param(

@staticmethod
def _model_map_lookup_candidates(model: str) -> List[str]:
"""Model-map keys to try for ``model``, stripping bedrock/vertex
prefixes so a provider-routed Claude still resolves to its entry."""
candidates = [model]
for prefix in (
"""Model-map keys to try for ``model``: the id itself, the same id with a
bedrock/vertex routing prefix removed, the Bedrock base model, and each of
those normalized by stripping a Bedrock version suffix (``-v1:0`` fully or
just the ``:0`` inference-profile minor), stripping a dated-release suffix
(``-20260205``), or rewriting a dotted family version to hyphens
(``4.6`` -> ``4-6``). Lets any reasonable alias (e.g.
``bedrock/invoke/global.anthropic.claude-opus-4-7-v1:0``,
``claude-sonnet-4-6-20260219`` or ``claude-sonnet-4.6``) resolve to its base
cost-map entry so the capability flag on that entry stays authoritative."""
prefixes = (
"bedrock/converse/",
"bedrock/invoke/",
"bedrock/",
"vertex_ai/",
):
if model.startswith(prefix):
candidates.append(model[len(prefix) :])
)
deprefixed = tuple(model[len(p) :] for p in prefixes if model.startswith(p))
try:
from litellm.llms.bedrock.common_utils import BedrockModelInfo

base = BedrockModelInfo.get_base_model(model)
if base:
candidates.append(base)
candidates.append(f"bedrock/{base}")
except Exception:
pass
return candidates
base = None
bedrock_base = (base, f"bedrock/{base}") if base else ()
primary = (model, *deprefixed, *bedrock_base)
normalized = tuple(
stripped
for cand in primary
for stripped in (
_BEDROCK_VERSION_SUFFIX_RE.sub("", cand),
_INFERENCE_PROFILE_MINOR_RE.sub("", cand),
_DATED_RELEASE_SUFFIX_RE.sub("", cand),
_DOTTED_VERSION_RE.sub(r"\1-\2", cand),
_strip_bedrock_id_suffixes(cand),
)
)
return list(dict.fromkeys((*primary, *normalized)))
Comment thread
greptile-apps[bot] marked this conversation as resolved.

@staticmethod
def _get_model_capability(model: str, key: str) -> Optional[bool]:
Expand Down Expand Up @@ -403,19 +403,16 @@ def _supports_model_capability(model: str, key: str) -> bool:

@staticmethod
def _is_adaptive_thinking_model(model: str) -> bool:
"""Claude 4.6+ models use adaptive thinking with ``output_config.effort``.
"""Whether ``model`` uses adaptive thinking (``output_config.effort``).

Driven by the ``supports_adaptive_thinking`` flag in the model map; the
4.6/4.7 name checks remain only as a fallback for provider-routed ids
whose map entries predate the flag.
Sourced solely from the model cost map's ``supports_adaptive_thinking`` flag,
resolved through provider prefixes. A model that resolves to no mapped entry
(an unmapped alias or a future release not yet in the map) is treated as
non-adaptive until a ``fallback_generalizations`` rule covers it.
"""
if AnthropicModelInfo._supports_model_capability(
return AnthropicModelInfo._supports_model_capability(
model, "supports_adaptive_thinking"
):
return True
return AnthropicModelInfo._is_claude_4_6_model(
model
) or AnthropicModelInfo._is_claude_4_7_model(model)
)

def is_effort_used(
self, optional_params: Optional[dict], model: Optional[str] = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ def create_tool_name_mapping(
from litellm.litellm_core_utils.prompt_templates.factory import (
THOUGHT_SIGNATURE_SEPARATOR,
)
from litellm.litellm_core_utils.reasoning_effort_utils import (
reasoning_effort_from_thinking_budget,
)
from litellm.llms.anthropic.common_utils import normalize_anthropic_tool_use_id
from litellm.llms.anthropic.experimental_pass_through.context_management import (
PolyfillResult,
Expand Down Expand Up @@ -716,11 +719,8 @@ def translate_anthropic_thinking_to_reasoning_effort(
Anthropic thinking format: {'type': 'enabled'|'disabled', 'budget_tokens': int}
OpenAI reasoning_effort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'default'

Mapping:
- budget_tokens >= 10000 -> 'high'
- budget_tokens >= 5000 -> 'medium'
- budget_tokens >= 2000 -> 'low'
- budget_tokens < 2000 -> 'minimal'
``budget_tokens`` is bucketed via the shared
``reasoning_effort_from_thinking_budget`` thresholds.
"""
if not isinstance(thinking, dict):
return None
Expand All @@ -730,15 +730,9 @@ def translate_anthropic_thinking_to_reasoning_effort(
if thinking_type == "disabled":
return None
elif thinking_type == "enabled":
budget_tokens = thinking.get("budget_tokens", 0)
if budget_tokens >= 10000:
return "high"
elif budget_tokens >= 5000:
return "medium"
elif budget_tokens >= 2000:
return "low"
else:
return "minimal"
return reasoning_effort_from_thinking_budget(
thinking.get("budget_tokens", 0)
)
elif thinking_type == "adaptive":
# Adaptive thinking: effort is controlled by output_config.effort,
# not budget_tokens. Return a default; caller should override with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import httpx

from litellm.constants import (
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET,
)
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.litellm_logging import verbose_logger
from litellm.llms.base_llm.anthropic_messages.transformation import (
Expand Down Expand Up @@ -248,11 +253,13 @@ def _translate_legacy_thinking_for_adaptive_model(
return

budget = int(thinking.get("budget_tokens") or 0)
if budget >= 24000 and AnthropicConfig._supports_effort_level(model, "xhigh"):
if budget >= DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET and (
AnthropicConfig._supports_effort_level(model, "xhigh")
):
effort = "xhigh"
elif budget >= 10000:
elif budget >= DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET:
effort = "high"
elif budget >= 5000:
elif budget >= DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET:
effort = "medium"
else:
effort = "low"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import json
from typing import Any, Dict, List, Optional, Union, cast

from litellm.litellm_core_utils.reasoning_effort_utils import (
reasoning_effort_from_thinking_budget,
)
from litellm.llms.anthropic.experimental_pass_through.utils import (
is_reasoning_auto_summary_enabled,
)
Expand Down Expand Up @@ -257,11 +260,10 @@ def translate_thinking_to_reasoning(
"""
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.
``thinking.budget_tokens`` is bucketed via the shared
``reasoning_effort_from_thinking_budget`` thresholds. For adaptive
thinking, uses ``output_config.effort`` if available, otherwise defaults
to medium.
"""
if not isinstance(thinking, dict):
return None
Expand All @@ -274,15 +276,9 @@ def translate_thinking_to_reasoning(
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"
effort = reasoning_effort_from_thinking_budget(
thinking.get("budget_tokens", 0)
)
else:
return None

Expand Down
Loading
Loading