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
3 changes: 3 additions & 0 deletions litellm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,9 @@ def set_global_gitlab_config(config: Dict[str, Any]) -> None:
from .llms.openrouter.responses.transformation import (
OpenRouterResponsesAPIConfig as OpenRouterResponsesAPIConfig,
)
from .llms.bedrock_mantle.responses.transformation import (
BedrockMantleResponsesAPIConfig as BedrockMantleResponsesAPIConfig,
)
from .llms.gemini.interactions.transformation import (
GoogleAIStudioInteractionsConfig as GoogleAIStudioInteractionsConfig,
)
Expand Down
5 changes: 5 additions & 0 deletions litellm/_lazy_imports_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@
"PerplexityResponsesConfig",
"DatabricksResponsesAPIConfig",
"OpenRouterResponsesAPIConfig",
"BedrockMantleResponsesAPIConfig",
"GoogleAIStudioInteractionsConfig",
"OpenAIOSeriesConfig",
"AnthropicSkillsConfig",
Expand Down Expand Up @@ -956,6 +957,10 @@
".llms.openrouter.responses.transformation",
"OpenRouterResponsesAPIConfig",
),
"BedrockMantleResponsesAPIConfig": (
".llms.bedrock_mantle.responses.transformation",
"BedrockMantleResponsesAPIConfig",
),
"GoogleAIStudioInteractionsConfig": (
".llms.gemini.interactions.transformation",
"GoogleAIStudioInteractionsConfig",
Expand Down
1 change: 1 addition & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,7 @@
"openai.gpt-oss-120b-1:0",
"anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-fable-5",
"anthropic.claude-opus-4-8",
"anthropic.claude-opus-4-7",
"anthropic.claude-opus-4-6-v1:0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import litellm
from litellm.constants import OPENAI_FILE_SEARCH_COST_PER_1K_CALLS
from litellm.litellm_core_utils.llm_cost_calc.utils import _get_web_search_requests
from litellm.types.llms.openai import (
FileSearchTool,
ResponsesAPIResponse,
Expand Down Expand Up @@ -339,8 +340,7 @@ def response_object_includes_web_search_call(
# and _handle_web_search_cost() is never called.
if (
hasattr(usage, "server_tool_use")
and usage.server_tool_use is not None
and usage.server_tool_use.web_search_requests is not None
and _get_web_search_requests(usage.server_tool_use) is not None
):
return True
return False
Expand All @@ -352,8 +352,7 @@ def response_object_includes_web_search_call(
elif usage is not None:
if (
hasattr(usage, "server_tool_use")
and usage.server_tool_use is not None
and usage.server_tool_use.web_search_requests is not None
and _get_web_search_requests(usage.server_tool_use) is not None
):
return True
elif (
Expand Down
30 changes: 29 additions & 1 deletion litellm/litellm_core_utils/llm_cost_calc/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# What is this?
## Helper utilities for cost_per_token()

from typing import Literal, Optional, Tuple, TypedDict, cast
from typing import Any, Literal, Optional, Tuple, TypedDict, cast

import litellm
from litellm._logging import verbose_logger
Expand Down Expand Up @@ -34,6 +34,34 @@
_VALID_DATA_RESIDENCIES = frozenset(r.value for r in DataResidency)


def _get_token_detail_value(details: object, key: str) -> Optional[int]:
if isinstance(details, dict):
value = details.get(key)
else:
value = getattr(details, key, None)
return value if isinstance(value, int) else None


def _get_web_search_requests(server_tool_use: Any) -> Optional[int]:
"""
Tolerantly read ``web_search_requests`` from a ``server_tool_use`` value
that may be ``None``, a ``dict``, a ``ServerToolUse`` pydantic instance,
or any other object supporting attribute access.

Returns ``None`` when the value cannot be resolved — callers can
distinguish "absent" from "zero" using ``is None``.

See https://github.com/BerriAI/litellm/issues/26153 — ``stream_chunk_builder``
historically left this as a plain ``dict``, which broke direct attribute
access in cost calculation.
"""
if server_tool_use is None:
return None
if isinstance(server_tool_use, dict):
return server_tool_use.get("web_search_requests")
return getattr(server_tool_use, "web_search_requests", None)


def _is_above_128k(tokens: float) -> bool:
if tokens > 128000:
return True
Expand Down
13 changes: 12 additions & 1 deletion litellm/litellm_core_utils/streaming_chunk_builder_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,18 @@ def _calculate_usage_per_chunk(
hasattr(usage_chunk, "server_tool_use")
and usage_chunk.server_tool_use is not None
):
server_tool_use = usage_chunk.server_tool_use
# Coerce dict to ServerToolUse so downstream cost-calc code
# (which accesses .web_search_requests as an attribute)
# doesn't raise AttributeError. Some providers / streaming
# paths leave server_tool_use as a plain dict on the chunk.
if isinstance(usage_chunk.server_tool_use, dict):
server_tool_use = ServerToolUse(**usage_chunk.server_tool_use)
elif isinstance(usage_chunk.server_tool_use, ServerToolUse):
server_tool_use = usage_chunk.server_tool_use
else:
server_tool_use = ServerToolUse.model_validate(
usage_chunk.server_tool_use
)
if (
usage_chunk_dict["prompt_tokens_details"] is not None
and getattr(
Expand Down
27 changes: 23 additions & 4 deletions litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1463,10 +1463,15 @@ def map_openai_params( # noqa: PLR0915
_value = self._map_stop_sequences(value)
if _value is not None:
optional_params["stop_sequences"] = _value
elif param == "temperature":
optional_params["temperature"] = value
elif param == "top_p":
optional_params["top_p"] = value
elif param == "temperature" or param == "top_p":
AnthropicConfig._apply_sampling_param(
optional_params=optional_params,
model=model,
param=param,
value=value,
drop_params=drop_params,
output_key=param,
)
elif param == "response_format" and isinstance(value, dict):
if any(
substring in model
Expand Down Expand Up @@ -1974,6 +1979,20 @@ def transform_request(
# Remove internal LiteLLM parameters that should not be sent to Anthropic API
optional_params.pop("is_vertex_request", None)

# ``top_k`` is a provider-specific kwarg that bypasses
# ``map_openai_params``; gate it here, the single boundary shared by
# the direct Anthropic, Bedrock invoke, Vertex, and Azure paths.
top_k = optional_params.pop("top_k", None)
if top_k is not None:
AnthropicConfig._apply_sampling_param(
optional_params=optional_params,
model=model,
param="top_k",
value=top_k,
drop_params=litellm_params.get("drop_params") is True,
output_key="top_k",
)

data = {
"model": model,
"messages": anthropic_messages,
Expand Down
122 changes: 118 additions & 4 deletions litellm/llms/anthropic/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,19 +272,133 @@ def _is_claude_4_7_model(model: str) -> bool:
)

@staticmethod
def _is_adaptive_thinking_model(model: str) -> bool:
"""Claude 4.6+ models use adaptive thinking with ``output_config.effort``."""
def _supports_sampling_params(model: str) -> bool:
"""Claude 4.7+ (Opus 4.7/4.8, Fable 5) removed sampling params: the API
rejects ``top_p``, ``top_k``, and any ``temperature`` other than 1 with
a 400 ("`temperature` is deprecated for this model").

Driven by the ``supports_sampling_params`` flag in the model map; the
name check remains only as a fallback for provider-routed ids whose
map entries predate the flag."""
flag = AnthropicModelInfo._get_model_capability(
model, "supports_sampling_params"
)
if flag is not None:
return flag
model_lower = model.lower()
return not any(
v in model_lower
for v in (
"fable",
"opus-4-7",
"opus_4_7",
"opus-4.7",
"opus_4.7",
"opus-4-8",
"opus_4_8",
"opus-4.8",
"opus_4.8",
)
)
Comment on lines +288 to +302

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded model-name fallback violates team rule

The name-based fallback ("fable", "opus-4-7", …) hardcodes which models reject sampling params directly in Python, which the team rule explicitly prohibits. All currently affected models now carry supports_sampling_params: false in the cost map, and _model_map_lookup_candidates already strips provider prefixes (bedrock/, bedrock/converse/, vertex_ai/, etc.), so every valid provider-routed ID resolves to a flagged entry without this fallback. The residual risk is that a future model whose name contains "fable" or "opus-4-7" but does support sampling params would silently be gated incorrectly. Remove the string-matching block and let an absent flag default to True (supports sampling params), which is the safe assumption.

Rule Used: What: Do not hardcode model-specific flags in the ... (source)


@staticmethod
def _apply_sampling_param(
optional_params: dict,
model: str,
param: str,
value: Any,
drop_params: bool,
output_key: str,
) -> None:
"""Forward ``temperature``/``top_p``/``top_k`` to
``optional_params[output_key]`` unless the model removed sampling
params, in which case drop the param (with drop_params) or raise a
clean client-side 400."""
if AnthropicModelInfo._supports_sampling_params(model) or (
param == "temperature" and value == 1
):
optional_params[output_key] = value
elif not (litellm.drop_params or drop_params):
supported_hint = (
"Only temperature=1 is supported. " if param == "temperature" else ""
)
raise litellm.utils.UnsupportedParamsError(
message=(
f"{model} does not support {param}={value}. {supported_hint}"
"To drop unsupported params, set `litellm.drop_params = True`."
),
status_code=400,
)

@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 (
"bedrock/converse/",
"bedrock/invoke/",
"bedrock/",
"vertex_ai/",
):
if model.startswith(prefix):
candidates.append(model[len(prefix) :])
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

@staticmethod
def _get_model_capability(model: str, key: str) -> Optional[bool]:
"""Read boolean capability ``key`` from the model map, or None when
no entry declares it."""
try:
for cand in AnthropicModelInfo._model_map_lookup_candidates(model):
value = litellm.model_cost.get(cand, {}).get(key)
if isinstance(value, bool):
return value
except Exception:
pass
return None

@staticmethod
def _supports_model_capability(model: str, key: str) -> bool:
"""Check a boolean capability ``key`` in the model map.

Strips bedrock/vertex prefixes so a provider-routed Claude still
resolves to the Anthropic model-map entry.
"""
from litellm.utils import _supports_factory

try:
if _supports_factory(
model=model,
custom_llm_provider=None,
key="supports_adaptive_thinking",
custom_llm_provider="anthropic",
key=key,
):
return True
except Exception:
pass
return AnthropicModelInfo._get_model_capability(model, key) is True

@staticmethod
def _is_adaptive_thinking_model(model: str) -> bool:
"""Claude 4.6+ models use adaptive thinking with ``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.
"""
if 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)
Expand Down
14 changes: 8 additions & 6 deletions litellm/llms/anthropic/cost_calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from litellm.litellm_core_utils.llm_cost_calc.utils import (
_get_token_base_cost,
_get_web_search_requests,
_parse_prompt_tokens_details,
calculate_cache_writing_cost,
generic_cost_per_token,
Expand Down Expand Up @@ -110,11 +111,12 @@ def get_cost_for_anthropic_web_search(
if model_info is None:
return 0.0

if (
usage is None
or usage.server_tool_use is None
or usage.server_tool_use.web_search_requests is None
):
if usage is None:
return 0.0
web_search_requests = _get_web_search_requests(
getattr(usage, "server_tool_use", None)
)
if web_search_requests is None:
return 0.0

## Get the cost per web search request
Expand All @@ -128,5 +130,5 @@ def get_cost_for_anthropic_web_search(
return 0.0

## Calculate the total cost
total_cost = cost_per_web_search_request * usage.server_tool_use.web_search_requests
total_cost = cost_per_web_search_request * web_search_requests
return total_cost
20 changes: 20 additions & 0 deletions litellm/llms/base_llm/responses/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ def supports_native_file_search(self) -> bool:
"""
return False

def sign_request(
self,
headers: dict,
optional_params: dict,
request_data: dict,
api_base: str,
api_key: Optional[str] = None,
model: Optional[str] = None,
stream: Optional[bool] = None,
fake_stream: Optional[bool] = None,
) -> Tuple[dict, Optional[bytes]]:
"""Sign the request after the body is finalized.

Default is a no-op (returns headers unchanged, no signed body). Providers
whose endpoint requires request signing (e.g. Bedrock Mantle SigV4)
override this and return the signed body bytes so the handler sends those
exact bytes.
"""
return headers, None

@abstractmethod
def get_supported_openai_params(self, model: str) -> list:
pass
Expand Down
Loading
Loading