Merge pull request #30148 from BerriAI/litellm_fable5_stable_1_86_x

chore(release): backport #30064, #29991, #30009 to stable/1.86.x and cut 1.86.5
This commit is contained in:
yuneng-jiang
2026-06-10 19:46:17 -07:00
committed by GitHub
19 changed files with 1559 additions and 40 deletions
+1
View File
@@ -1147,6 +1147,7 @@ BEDROCK_CONVERSE_MODELS = [
"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-7",
"anthropic.claude-opus-4-6-v1:0",
"anthropic.claude-opus-4-6-v1",
+23 -4
View File
@@ -1451,10 +1451,15 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
_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
@@ -1947,6 +1952,20 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
# 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,
+118 -4
View File
@@ -272,19 +272,133 @@ class AnthropicModelInfo(BaseLLMModelInfo):
)
@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",
)
)
@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)
@@ -902,10 +902,15 @@ class AmazonConverseConfig(BaseConfig):
continue
value = [value]
optional_params["stopSequences"] = value
if param == "temperature":
optional_params["temperature"] = value
if param == "top_p":
optional_params["topP"] = value
if 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="topP" if param == "top_p" else param,
)
if param == "tools" and isinstance(value, list):
self._apply_tool_call_transformation(
tools=cast(List[OpenAIChatCompletionToolParam], value),
@@ -1177,7 +1182,9 @@ class AmazonConverseConfig(BaseConfig):
inference_params["topK"] = inference_params.pop("top_k")
return InferenceConfig(**inference_params)
def _handle_top_k_value(self, model: str, inference_params: dict) -> dict:
def _handle_top_k_value(
self, model: str, inference_params: dict, drop_params: bool = False
) -> dict:
base_model = BedrockModelInfo.get_base_model(model)
val_top_k = None
@@ -1186,16 +1193,25 @@ class AmazonConverseConfig(BaseConfig):
elif "top_k" in inference_params:
val_top_k = inference_params.pop("top_k")
if val_top_k:
if val_top_k is not None:
if base_model.startswith("anthropic"):
return {"top_k": val_top_k}
top_k_params: dict = {}
AnthropicConfig._apply_sampling_param(
optional_params=top_k_params,
model=model,
param="top_k",
value=val_top_k,
drop_params=drop_params,
output_key="top_k",
)
return top_k_params
if base_model.startswith("amazon.nova"):
return {"inferenceConfig": {"topK": val_top_k}}
return {}
def _prepare_request_params(
self, optional_params: dict, model: str
self, optional_params: dict, model: str, drop_params: bool = False
) -> Tuple[dict, dict, dict, Optional[OutputConfigBlock]]:
"""Prepare and separate request parameters."""
# Filter out exception objects before deepcopy to prevent deepcopy failures
@@ -1255,7 +1271,7 @@ class AmazonConverseConfig(BaseConfig):
# Only set the topK value in for models that support it
additional_request_params.update(
self._handle_top_k_value(model, inference_params)
self._handle_top_k_value(model, inference_params, drop_params)
)
# Filter out internal/MCP-related parameters that shouldn't be sent to the API
@@ -1444,6 +1460,7 @@ class AmazonConverseConfig(BaseConfig):
optional_params: dict,
messages: Optional[List[AllMessageValues]] = None,
headers: Optional[dict] = None,
drop_params: bool = False,
) -> CommonRequestObject:
## VALIDATE REQUEST
"""
@@ -1490,7 +1507,7 @@ class AmazonConverseConfig(BaseConfig):
additional_request_params,
request_metadata,
output_config,
) = self._prepare_request_params(optional_params, model)
) = self._prepare_request_params(optional_params, model, drop_params)
original_tools = inference_params.pop("tools", [])
@@ -1571,6 +1588,7 @@ class AmazonConverseConfig(BaseConfig):
optional_params=optional_params,
messages=messages,
headers=headers,
drop_params=litellm_params.get("drop_params") is True,
)
bedrock_messages = (
@@ -1628,6 +1646,7 @@ class AmazonConverseConfig(BaseConfig):
optional_params=optional_params,
messages=messages,
headers=headers,
drop_params=litellm_params.get("drop_params") is True,
)
## TRANSFORMATION ##
@@ -1155,6 +1155,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1201,6 +1202,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1232,6 +1234,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1262,6 +1265,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1292,6 +1296,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1300,6 +1305,138 @@
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.375e-05,
"cache_creation_input_token_cost_above_1hr": 2.2e-05,
"cache_read_input_token_cost": 1.1e-06,
"input_cost_per_token": 1.1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5.5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"us.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.375e-05,
"cache_creation_input_token_cost_above_1hr": 2.2e-05,
"cache_read_input_token_cost": 1.1e-06,
"input_cost_per_token": 1.1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5.5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"global.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 3.75e-06,
"cache_creation_input_token_cost_above_1hr": 6e-06,
@@ -1993,6 +2130,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -2000,6 +2138,36 @@
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"azure_ai/claude-fable-5": {
"input_cost_per_token": 1e-05,
"output_cost_per_token": 5e-05,
"litellm_provider": "azure_ai",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true
},
"azure_ai/claude-opus-4-1": {
"cache_creation_input_token_cost": 1.875e-05,
"cache_creation_input_token_cost_above_1hr": 3e-05,
@@ -9868,6 +10036,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -9902,6 +10071,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -9913,6 +10083,40 @@
},
"supports_minimal_reasoning_effort": true
},
"claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "anthropic",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"provider_specific_entry": {
"us": 1.1
},
"supports_output_config": true
},
"claude-sonnet-4-20250514": {
"deprecation_date": "2026-05-14",
"cache_creation_input_token_cost": 3.75e-06,
@@ -33135,6 +33339,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -33164,6 +33369,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -33171,6 +33377,66 @@
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-fable-5@default": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "vertex_ai-anthropic_models",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true
},
"vertex_ai/claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "vertex_ai-anthropic_models",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true
},
"vertex_ai/claude-sonnet-4-5": {
"cache_creation_input_token_cost": 3.75e-06,
"cache_read_input_token_cost": 3e-07,
@@ -105,6 +105,16 @@ def _extract_text_from_content(content: object) -> str:
return ""
def _merge_metadata_bags(request_data: Mapping[str, Any]) -> Optional[dict[str, Any]]:
merged: dict[str, Any] = {}
present = False
for bag in (request_data.get("metadata"), request_data.get("litellm_metadata")):
if isinstance(bag, Mapping):
present = True
merged.update(bag)
return merged if present else None
class CrowdStrikeAIDRHandler(CustomGuardrail):
"""
CrowdStrike AIDR AI Guardrail handler to interact with the CrowdStrike AIDR
@@ -312,11 +322,27 @@ class CrowdStrikeAIDRHandler(CustomGuardrail):
event_type = "output"
hook_name = "apply_guardrail (response)"
ai_guard_payload = {
ai_guard_payload: dict[str, Any] = {
"guard_input": guard_input.model_dump(mode="json"),
"event_type": event_type,
}
model = inputs.get("model")
if model:
ai_guard_payload["model"] = model
metadata = _merge_metadata_bags(request_data)
if metadata is not None:
user_id = metadata.get("user_api_key_user_id")
if user_id:
ai_guard_payload["user_id"] = user_id
extra_info: dict[str, str] = {}
user_email = metadata.get("user_api_key_user_email")
if user_email:
extra_info["user_name"] = user_email
ai_guard_payload["extra_info"] = extra_info
ai_guard_response = await self._call_crowdstrike_aidr_guard(
ai_guard_payload, hook_name
)
+19 -10
View File
@@ -227,11 +227,17 @@ class _PROXY_BatchRateLimiter(CustomLogger):
# Check if this is a managed file (base64 encoded unified file ID)
from litellm.proxy.openai_files_endpoints.common_utils import (
_is_base64_encoded_unified_file_id,
get_models_from_unified_file_id,
)
# Managed files require bypassing the HTTP endpoint (which runs access-check hooks)
# and calling the managed files hook directly with the user's credentials.
is_managed_file = _is_base64_encoded_unified_file_id(file_id)
target_model_names = (
get_models_from_unified_file_id(is_managed_file)
if is_managed_file
else []
)
if is_managed_file and user_api_key_dict is not None:
file_content = await self._fetch_managed_file_content(
file_id=file_id,
@@ -256,6 +262,7 @@ class _PROXY_BatchRateLimiter(CustomLogger):
await self._enforce_batch_file_model_access(
user_api_key_dict=user_api_key_dict,
file_content_as_dict=file_content_as_dict,
target_model_names=target_model_names or None,
)
input_file_usage = _get_batch_job_input_file_usage(
@@ -291,9 +298,13 @@ class _PROXY_BatchRateLimiter(CustomLogger):
self,
user_api_key_dict: UserAPIKeyAuth,
file_content_as_dict: List[dict],
target_model_names: Optional[List[str]] = None,
) -> None:
"""Reject the batch if the caller is not authorized for every
``body.model`` named inside the JSONL.
"""Reject the batch if the caller is not authorized for the upload target.
For managed files, ``target_model_names`` (from the unified file id) is
the proxy alias the file was uploaded for and is used directly for auth.
For legacy/non-managed files, falls back to ``body.model`` values in the JSONL.
Reuses ``can_key_call_model`` so the same allowlist semantics
(wildcards, access groups, ``all-proxy-models``, team aliases)
@@ -302,18 +313,16 @@ class _PROXY_BatchRateLimiter(CustomLogger):
from litellm.proxy.auth.auth_checks import can_key_call_model
from litellm.proxy.proxy_server import llm_router
models = _get_models_from_batch_input_file_content(file_content_as_dict)
if not models:
return
if target_model_names:
models = target_model_names
else:
models = _get_models_from_batch_input_file_content(file_content_as_dict)
if not models:
return
llm_model_list = llm_router.model_list if llm_router is not None else None
for model in models:
# body.model may be the provider id after replace_model_in_jsonl; map to proxy model_name for auth.
model_to_check = model
if llm_router is not None:
proxy_model_name = llm_router.resolve_model_name_from_model_id(model)
if proxy_model_name is not None:
model_to_check = proxy_model_name
try:
await can_key_call_model(
model=model_to_check,
+2 -1
View File
@@ -52,11 +52,12 @@ PROVIDERS: List[Dict] = [
{
"id": "anthropic",
"name": "Anthropic",
"description": "Claude Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5",
"description": "Claude Fable 5, Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5",
"env_key": "ANTHROPIC_API_KEY",
"key_hint": "sk-ant-...",
"test_model": "claude-haiku-4-5-20251001",
"models": [
"claude-fable-5",
"claude-opus-4-7",
"claude-opus-4-6",
"claude-sonnet-4-6",
+266
View File
@@ -1155,6 +1155,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1201,6 +1202,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1232,6 +1234,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1262,6 +1265,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1292,6 +1296,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -1300,6 +1305,138 @@
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"eu.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.375e-05,
"cache_creation_input_token_cost_above_1hr": 2.2e-05,
"cache_read_input_token_cost": 1.1e-06,
"input_cost_per_token": 1.1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5.5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"us.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.375e-05,
"cache_creation_input_token_cost_above_1hr": 2.2e-05,
"cache_read_input_token_cost": 1.1e-06,
"input_cost_per_token": 1.1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5.5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"global.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": true,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh"
},
"anthropic.claude-sonnet-4-6": {
"cache_creation_input_token_cost": 3.75e-06,
"cache_creation_input_token_cost_above_1hr": 6e-06,
@@ -2022,6 +2159,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -2029,6 +2167,36 @@
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"azure_ai/claude-fable-5": {
"input_cost_per_token": 1e-05,
"output_cost_per_token": 5e-05,
"litellm_provider": "azure_ai",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true
},
"azure_ai/claude-opus-4-1": {
"cache_creation_input_token_cost": 1.875e-05,
"cache_creation_input_token_cost_above_1hr": 3e-05,
@@ -9901,6 +10069,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -9936,6 +10105,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -9947,6 +10117,40 @@
},
"supports_minimal_reasoning_effort": true
},
"claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "anthropic",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true,
"provider_specific_entry": {
"us": 1.1
},
"supports_output_config": true
},
"claude-sonnet-4-20250514": {
"deprecation_date": "2026-05-14",
"cache_creation_input_token_cost": 3.75e-06,
@@ -33169,6 +33373,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -33198,6 +33403,7 @@
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
@@ -33205,6 +33411,66 @@
"supports_max_reasoning_effort": true,
"supports_minimal_reasoning_effort": true
},
"vertex_ai/claude-fable-5@default": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "vertex_ai-anthropic_models",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true
},
"vertex_ai/claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_above_1hr": 2e-05,
"cache_read_input_token_cost": 1e-06,
"input_cost_per_token": 1e-05,
"litellm_provider": "vertex_ai-anthropic_models",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 5e-05,
"search_context_cost_per_query": {
"search_context_size_high": 0.01,
"search_context_size_low": 0.01,
"search_context_size_medium": 0.01
},
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_max_reasoning_effort": true
},
"vertex_ai/claude-sonnet-4-5": {
"cache_creation_input_token_cost": 3.75e-06,
"cache_read_input_token_cost": 3e-07,
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "litellm"
version = "1.86.4"
version = "1.86.5"
description = "Library to easily interface with LLM API providers"
readme = "README.md"
requires-python = ">=3.10, <3.14"
@@ -251,7 +251,7 @@ source-exclude = [
profile = "black"
[tool.commitizen]
version = "1.86.4"
version = "1.86.5"
version_files = [
"pyproject.toml:^version",
]
@@ -105,6 +105,13 @@ _CAPS_NONE: FrozenSet[str] = frozenset()
ANTHROPIC_DIRECT_MODELS: Tuple[ModelEntry, ...] = (
ModelEntry(
alias="claude-fable-5",
model="anthropic/claude-fable-5",
mode="adaptive",
required_env=_ANTHROPIC_REQ,
caps=_CAPS_OPUS_4_7,
),
ModelEntry(
alias="claude-opus-4-7",
model="anthropic/claude-opus-4-7",
@@ -130,6 +137,19 @@ ANTHROPIC_DIRECT_MODELS: Tuple[ModelEntry, ...] = (
AZURE_AI_MODELS: Tuple[ModelEntry, ...] = (
ModelEntry(
alias="azure-claude-fable-5",
model="azure_ai/claude-fable-5",
mode="adaptive",
required_env=_AZURE_FOUNDRY_REQ,
caps=_CAPS_OPUS_4_7,
fail_reason=(
"claude-fable-5 has no deployment on the CI Microsoft Foundry "
"resource yet; Foundry returns DeploymentNotFound until someone "
"creates the fable-5 deployment, so this cell stays loud in CI. "
"Remove this fail_reason once the deployment exists."
),
),
ModelEntry(
alias="azure-claude-opus-4-7",
model="azure_ai/claude-opus-4-7",
@@ -162,6 +182,20 @@ AZURE_AI_MODELS: Tuple[ModelEntry, ...] = (
VERTEX_AI_MODELS: Tuple[ModelEntry, ...] = (
ModelEntry(
alias="vertex-claude-fable-5",
model="vertex_ai/claude-fable-5",
mode="adaptive",
extra_params=(("vertex_location", "global"),),
required_env=_VERTEX_REQ,
caps=_CAPS_OPUS_4_7,
fail_reason=(
"claude-fable-5 availability on the CI Vertex project is not yet "
"confirmed for this brand-new release, so this cell stays loud in "
"CI until verified. Remove this fail_reason once the model is "
"confirmed available on the global Vertex endpoint."
),
),
ModelEntry(
alias="vertex-claude-opus-4-7",
model="vertex_ai/claude-opus-4-7",
@@ -198,6 +232,21 @@ VERTEX_AI_MODELS: Tuple[ModelEntry, ...] = (
BEDROCK_CONVERSE_MODELS: Tuple[ModelEntry, ...] = (
ModelEntry(
alias="bedrock-claude-fable-5",
model="bedrock/converse/us.anthropic.claude-fable-5",
mode="adaptive",
extra_params=(("aws_region_name", "us-east-1"),),
required_env=_BEDROCK_REQ,
caps=_CAPS_OPUS_4_7,
fail_reason=(
"claude-fable-5 on Bedrock requires the account to opt in to "
"provider data sharing (data retention mode "
"'provider_data_sharing' via the Data Retention API); the CI "
"account has not opted in yet, so this cell stays loud in CI. "
"Remove this fail_reason once the opt-in is done."
),
),
ModelEntry(
alias="bedrock-claude-opus-4-7",
model="bedrock/converse/us.anthropic.claude-opus-4-7",
@@ -191,8 +191,8 @@ async def test_reasoning_effort_grid(
def test_grid_cell_count() -> None:
assert len(_PARAMS) == 21 * 11, (
f"expected 231 cells (21 provider x model combos x 11 efforts), "
assert len(_PARAMS) == 25 * 11, (
f"expected 275 cells (25 provider x model combos x 11 efforts), "
f"got {len(_PARAMS)}"
)
@@ -4750,3 +4750,140 @@ def test_sanitize_tool_names_in_request_no_tools_is_noop():
forward, reverse = AnthropicConfig._sanitize_tool_names_in_request({"tools": []})
assert forward == {}
assert reverse == {}
@pytest.mark.parametrize(
"model",
["claude-fable-5", "claude-opus-4-7", "claude-opus-4-8-20260120"],
)
def test_sampling_params_dropped_for_models_that_removed_them(model):
"""Fable 5 / Opus 4.7 / 4.8 reject temperature != 1 and any top_p with a
400; with drop_params set they must be dropped, not forwarded (#30064)."""
config = AnthropicConfig()
result = config.map_openai_params(
non_default_params={"temperature": 0.5, "top_p": 0.9},
optional_params={},
model=model,
drop_params=True,
)
assert "temperature" not in result
assert "top_p" not in result
@pytest.mark.parametrize("params", [{"temperature": 0.5}, {"top_p": 0.9}, {"top_p": 1}])
def test_sampling_params_raise_clean_error_without_drop_params(params, monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)
config = AnthropicConfig()
with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"):
config.map_openai_params(
non_default_params=params,
optional_params={},
model="claude-fable-5",
drop_params=False,
)
def test_temperature_1_forwarded_on_models_that_removed_sampling_params():
"""temperature=1 (the API default) is still accepted and must pass through."""
config = AnthropicConfig()
result = config.map_openai_params(
non_default_params={"temperature": 1},
optional_params={},
model="claude-fable-5",
drop_params=False,
)
assert result["temperature"] == 1
@pytest.mark.parametrize("model", ["claude-opus-4-6", "claude-sonnet-4-6"])
def test_sampling_params_forwarded_on_models_that_accept_them(model):
config = AnthropicConfig()
result = config.map_openai_params(
non_default_params={"temperature": 0.5, "top_p": 0.9},
optional_params={},
model=model,
drop_params=True,
)
assert result["temperature"] == 0.5
assert result["top_p"] == 0.9
def test_sampling_param_gating_driven_by_model_map_flag(monkeypatch):
"""The drop/raise decision must come from ``supports_sampling_params`` in
the model map, not just name matching: a flagged entry gates a model whose
name says nothing, and an explicit ``true`` overrides the name fallback."""
monkeypatch.setitem(
litellm.model_cost, "claude-zeta-9", {"supports_sampling_params": False}
)
monkeypatch.setitem(
litellm.model_cost, "claude-fable-5-test", {"supports_sampling_params": True}
)
config = AnthropicConfig()
flagged_off = config.map_openai_params(
non_default_params={"top_p": 0.9},
optional_params={},
model="claude-zeta-9",
drop_params=True,
)
assert "top_p" not in flagged_off
flagged_on = config.map_openai_params(
non_default_params={"top_p": 0.9},
optional_params={},
model="claude-fable-5-test",
drop_params=True,
)
assert flagged_on["top_p"] == 0.9
def test_top_k_dropped_at_transform_for_models_that_removed_it():
"""``top_k`` is a provider-specific kwarg that bypasses
``map_openai_params``, so it must be stripped at the transform_request
boundary shared by the direct, invoke, Vertex, and Azure paths (#30064)."""
config = AnthropicConfig()
result = config.transform_request(
model="claude-fable-5",
messages=[{"role": "user", "content": "hello"}],
optional_params={"max_tokens": 10, "top_k": 40},
litellm_params={"drop_params": True},
headers={},
)
assert "top_k" not in result
def test_top_k_raises_at_transform_without_drop_params(monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)
config = AnthropicConfig()
with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"):
config.transform_request(
model="claude-fable-5",
messages=[{"role": "user", "content": "hello"}],
optional_params={"max_tokens": 10, "top_k": 40},
litellm_params={},
headers={},
)
def test_top_k_forwarded_at_transform_on_models_that_accept_it():
config = AnthropicConfig()
result = config.transform_request(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "hello"}],
optional_params={"max_tokens": 10, "top_k": 40},
litellm_params={"drop_params": True},
headers={},
)
assert result["top_k"] == 40
@@ -4578,3 +4578,122 @@ def test_transform_response_does_not_leak_body_on_parse_failure():
msg = str(exc_info.value)
assert "secret content" not in msg
assert "Error converting to valid response block" in msg
def test_converse_drops_sampling_params_for_models_that_removed_them():
"""Fable 5 / Opus 4.7 / 4.8 reject temperature != 1 and any top_p; with
drop_params set, converse must drop them instead of forwarding (#30064)."""
config = AmazonConverseConfig()
result = config.map_openai_params(
non_default_params={"temperature": 0.5, "top_p": 0.9},
optional_params={},
model="us.anthropic.claude-fable-5",
drop_params=True,
)
assert "temperature" not in result
assert "topP" not in result
def test_converse_sampling_params_raise_without_drop_params(monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)
config = AmazonConverseConfig()
with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"):
config.map_openai_params(
non_default_params={"temperature": 0.5},
optional_params={},
model="global.anthropic.claude-opus-4-8-v1:0",
drop_params=False,
)
def test_converse_sampling_params_forwarded_on_models_that_accept_them():
config = AmazonConverseConfig()
result = config.map_openai_params(
non_default_params={"temperature": 0.5, "top_p": 0.9},
optional_params={},
model="us.anthropic.claude-sonnet-4-6",
drop_params=True,
)
assert result["temperature"] == 0.5
assert result["topP"] == 0.9
def test_converse_top_k_dropped_for_models_that_removed_it():
"""``top_k`` reaches converse as a provider-specific kwarg destined for
``additionalModelRequestFields``, bypassing ``map_openai_params``; the
transform must strip it for models that removed sampling params (#30064)."""
config = AmazonConverseConfig()
result = config.transform_request(
model="us.anthropic.claude-fable-5",
messages=[{"role": "user", "content": "hello"}],
optional_params={"top_k": 40},
litellm_params={"drop_params": True},
headers={},
)
assert "top_k" not in result.get("additionalModelRequestFields", {})
def test_converse_top_k_raises_without_drop_params(monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)
config = AmazonConverseConfig()
with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"):
config.transform_request(
model="us.anthropic.claude-fable-5",
messages=[{"role": "user", "content": "hello"}],
optional_params={"top_k": 40},
litellm_params={},
headers={},
)
def test_converse_top_k_forwarded_on_models_that_accept_it():
config = AmazonConverseConfig()
result = config.transform_request(
model="us.anthropic.claude-sonnet-4-6",
messages=[{"role": "user", "content": "hello"}],
optional_params={"top_k": 40},
litellm_params={"drop_params": True},
headers={},
)
assert result["additionalModelRequestFields"]["top_k"] == 40
def test_converse_top_k_zero_raises_without_drop_params(monkeypatch):
"""``top_k=0`` must hit the same gating as any other value; previously the
truthiness check let it silently disappear on models that removed sampling
params, diverging from the Anthropic boundary that treats ``0`` as present."""
monkeypatch.setattr(litellm, "drop_params", False)
config = AmazonConverseConfig()
with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"):
config.transform_request(
model="us.anthropic.claude-fable-5",
messages=[{"role": "user", "content": "hello"}],
optional_params={"top_k": 0},
litellm_params={},
headers={},
)
def test_converse_top_k_zero_forwarded_on_models_that_accept_it():
config = AmazonConverseConfig()
result = config.transform_request(
model="us.anthropic.claude-sonnet-4-6",
messages=[{"role": "user", "content": "hello"}],
optional_params={"top_k": 0},
litellm_params={"drop_params": True},
headers={},
)
assert result["additionalModelRequestFields"]["top_k"] == 0
@@ -41,7 +41,8 @@ def test_crowdstrike_aidr_guardrail_config() -> None:
)
def test_crowdstrike_aidr_guardrail_config_no_api_key() -> None:
def test_crowdstrike_aidr_guardrail_config_no_api_key(monkeypatch) -> None:
monkeypatch.delenv("CS_AIDR_TOKEN", raising=False)
with pytest.raises(CrowdStrikeAIDRGuardrailMissingSecrets):
init_guardrails_v2(
all_guardrails=[
@@ -59,7 +60,8 @@ def test_crowdstrike_aidr_guardrail_config_no_api_key() -> None:
)
def test_crowdstrike_aidr_guardrail_config_no_api_base() -> None:
def test_crowdstrike_aidr_guardrail_config_no_api_base(monkeypatch) -> None:
monkeypatch.delenv("CS_AIDR_BASE_URL", raising=False)
with pytest.raises(CrowdStrikeAIDRGuardrailMissingSecrets):
init_guardrails_v2(
all_guardrails=[
@@ -478,3 +480,168 @@ async def test_apply_guardrail_request_skipped_messages_stay_aligned(
assert result["texts"][1] == ""
assert result["texts"][2] == "Here is my SSN: <US_SSN>"
assert result["structured_messages"] == inputs["structured_messages"]
@pytest.mark.asyncio
async def test_apply_guardrail_sends_user_id_model_and_extra_info(
crowdstrike_aidr_guardrail: CrowdStrikeAIDRHandler,
) -> None:
inputs: GenericGuardrailAPIInputs = {
"texts": ["Hello"],
"structured_messages": [{"role": "user", "content": "Hello"}],
"model": "gpt-4o",
}
request_data = {
"messages": inputs["structured_messages"],
"model": "gpt-4o",
"litellm_metadata": {
"user_api_key_user_id": "uid-abc",
"user_api_key_user_email": "alice@example.com",
},
}
guardrail_endpoint = (
f"{crowdstrike_aidr_guardrail.api_base}/v1/guard_chat_completions"
)
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
return_value=httpx.Response(
status_code=200,
json={"result": {"blocked": False, "transformed": False}},
request=httpx.Request(method="POST", url=guardrail_endpoint),
),
) as mock_method:
await crowdstrike_aidr_guardrail.apply_guardrail(
inputs=inputs,
request_data=request_data,
input_type="request",
)
payload = mock_method.call_args.kwargs["json"]
assert payload["user_id"] == "uid-abc"
assert payload["model"] == "gpt-4o"
assert payload["extra_info"] == {"user_name": "alice@example.com"}
@pytest.mark.asyncio
async def test_apply_guardrail_empty_extra_info_when_no_email(
crowdstrike_aidr_guardrail: CrowdStrikeAIDRHandler,
) -> None:
inputs: GenericGuardrailAPIInputs = {
"texts": ["Hello"],
"structured_messages": [{"role": "user", "content": "Hello"}],
"model": "gemini-flash",
}
request_data = {
"messages": inputs["structured_messages"],
"model": "gemini-flash",
"litellm_metadata": {
"user_api_key_user_id": "uid-no-email",
"user_api_key_user_email": None,
},
}
guardrail_endpoint = (
f"{crowdstrike_aidr_guardrail.api_base}/v1/guard_chat_completions"
)
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
return_value=httpx.Response(
status_code=200,
json={"result": {"blocked": False, "transformed": False}},
request=httpx.Request(method="POST", url=guardrail_endpoint),
),
) as mock_method:
await crowdstrike_aidr_guardrail.apply_guardrail(
inputs=inputs,
request_data=request_data,
input_type="request",
)
payload = mock_method.call_args.kwargs["json"]
assert payload["user_id"] == "uid-no-email"
assert payload["model"] == "gemini-flash"
assert payload["extra_info"] == {}
@pytest.mark.asyncio
async def test_apply_guardrail_no_metadata_skips_user_fields(
crowdstrike_aidr_guardrail: CrowdStrikeAIDRHandler,
) -> None:
inputs: GenericGuardrailAPIInputs = {
"texts": ["Hello"],
"structured_messages": [{"role": "user", "content": "Hello"}],
}
request_data = {"messages": inputs["structured_messages"]}
guardrail_endpoint = (
f"{crowdstrike_aidr_guardrail.api_base}/v1/guard_chat_completions"
)
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
return_value=httpx.Response(
status_code=200,
json={"result": {"blocked": False, "transformed": False}},
request=httpx.Request(method="POST", url=guardrail_endpoint),
),
) as mock_method:
await crowdstrike_aidr_guardrail.apply_guardrail(
inputs=inputs,
request_data=request_data,
input_type="request",
)
payload = mock_method.call_args.kwargs["json"]
assert "user_id" not in payload
assert "model" not in payload
assert "extra_info" not in payload
@pytest.mark.asyncio
@pytest.mark.parametrize(
"litellm_metadata, metadata",
[
(None, {"user_api_key_user_id": "uid-abc", "user_api_key_user_email": "alice@example.com"}),
({"trace_id": "t1"}, {"user_api_key_user_id": "uid-abc", "user_api_key_user_email": "alice@example.com"}),
(["unexpected"], {"user_api_key_user_id": "uid-abc", "user_api_key_user_email": "alice@example.com"}),
({"user_api_key_user_id": "uid-abc", "user_api_key_user_email": "alice@example.com"}, {"trace_id": "t1"}),
],
ids=["identity_in_metadata_llm_none", "identity_in_metadata_llm_user_dict", "identity_in_metadata_llm_non_mapping", "identity_in_litellm_metadata"],
)
async def test_apply_guardrail_reads_identity_from_either_metadata_bag(
crowdstrike_aidr_guardrail: CrowdStrikeAIDRHandler,
litellm_metadata,
metadata,
) -> None:
inputs: GenericGuardrailAPIInputs = {
"texts": ["Hello"],
"structured_messages": [{"role": "user", "content": "Hello"}],
"model": "gpt-4o",
}
request_data = {
"messages": inputs["structured_messages"],
"model": "gpt-4o",
"litellm_metadata": litellm_metadata,
"metadata": metadata,
}
guardrail_endpoint = (
f"{crowdstrike_aidr_guardrail.api_base}/v1/guard_chat_completions"
)
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
return_value=httpx.Response(
status_code=200,
json={"result": {"blocked": False, "transformed": False}},
request=httpx.Request(method="POST", url=guardrail_endpoint),
),
) as mock_method:
await crowdstrike_aidr_guardrail.apply_guardrail(
inputs=inputs,
request_data=request_data,
input_type="request",
)
payload = mock_method.call_args.kwargs["json"]
assert payload["user_id"] == "uid-abc"
assert payload["extra_info"] == {"user_name": "alice@example.com"}
@@ -262,7 +262,8 @@ async def test_pre_call_allows_authorized_model_in_batch_file():
@pytest.mark.asyncio
async def test_pre_call_allows_stripped_provider_model_when_key_has_proxy_alias():
"""After replace_model_in_jsonl, body.model is the provider id (e.g. gpt-5.5).
Auth must check the proxy model_name the key was granted, not the stripped id."""
Auth must check target_model_names from the unified file id, not reverse-map
the stripped id."""
from litellm.proxy.hooks.batch_rate_limiter import _PROXY_BatchRateLimiter
rate_limiter = _PROXY_BatchRateLimiter(
@@ -281,7 +282,6 @@ async def test_pre_call_allows_stripped_provider_model_when_key_has_proxy_alias(
)
mock_router = MagicMock()
mock_router.model_list = []
mock_router.resolve_model_name_from_model_id.return_value = proxy_alias
can_key_call_model = AsyncMock(return_value=True)
with (
@@ -294,10 +294,105 @@ async def test_pre_call_allows_stripped_provider_model_when_key_has_proxy_alias(
await rate_limiter._enforce_batch_file_model_access(
user_api_key_dict=user,
file_content_as_dict=file_dict,
target_model_names=[proxy_alias],
)
can_key_call_model.assert_awaited_once()
assert can_key_call_model.await_args.kwargs["model"] == proxy_alias
mock_router.resolve_model_name_from_model_id.assert_not_called()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"model_list_order",
[
[
"openai/openai/gpt-5.5",
"openai/openai/gpt-5.5-batch",
"us/azure/openai/gpt-5.5",
],
[
"us/azure/openai/gpt-5.5",
"openai/openai/gpt-5.5",
"openai/openai/gpt-5.5-batch",
],
[
"openai/openai/gpt-5.5-batch",
"us/azure/openai/gpt-5.5",
"openai/openai/gpt-5.5",
],
],
)
async def test_pre_call_uses_target_model_names_not_stripped_reverse_lookup(
model_list_order,
):
"""LIT-3593: three deployments strip to gpt-5.5; auth must use the upload
target alias from target_model_names, not first-match reverse lookup."""
from litellm.proxy.hooks.batch_rate_limiter import _PROXY_BatchRateLimiter
rate_limiter = _PROXY_BatchRateLimiter(
internal_usage_cache=MagicMock(),
parallel_request_limiter=MagicMock(),
)
batch_alias = "openai/openai/gpt-5.5-batch"
deployment_templates = {
"openai/openai/gpt-5.5": {
"model_name": "openai/openai/gpt-5.5",
"litellm_params": {"model": "openai/gpt-5.5"},
"model_info": {"id": "openai/openai/gpt-5.5", "mode": "chat"},
},
"openai/openai/gpt-5.5-batch": {
"model_name": "openai/openai/gpt-5.5-batch",
"litellm_params": {"model": "openai/gpt-5.5"},
"model_info": {"id": "openai/openai/gpt-5.5-batch", "mode": "batch"},
},
"us/azure/openai/gpt-5.5": {
"model_name": "us/azure/openai/gpt-5.5",
"litellm_params": {"model": "azure/gpt-5.5"},
"model_info": {"id": "openai/openai/gpt-5.5", "mode": "chat"},
},
}
mock_router = MagicMock()
mock_router.model_list = [deployment_templates[name] for name in model_list_order]
def _resolve(model_id):
for deployment in mock_router.model_list:
actual_model = deployment.get("litellm_params", {}).get("model")
if actual_model == model_id or (
actual_model and actual_model.endswith(f"/{model_id}")
):
return deployment.get("model_name")
return None
mock_router.resolve_model_name_from_model_id.side_effect = _resolve
file_dict = [
{"body": {"model": "gpt-5.5", "messages": [{"role": "user", "content": "x"}]}}
]
user = UserAPIKeyAuth(
api_key="sk-ok",
user_id="alice",
models=[batch_alias],
user_role=LitellmUserRoles.INTERNAL_USER.value,
)
can_key_call_model = AsyncMock(return_value=True)
with (
patch(
"litellm.proxy.auth.auth_checks.can_key_call_model",
new=can_key_call_model,
),
patch("litellm.proxy.proxy_server.llm_router", mock_router),
):
await rate_limiter._enforce_batch_file_model_access(
user_api_key_dict=user,
file_content_as_dict=file_dict,
target_model_names=[batch_alias],
)
can_key_call_model.assert_awaited_once()
assert can_key_call_model.await_args.kwargs["model"] == batch_alias
mock_router.resolve_model_name_from_model_id.assert_not_called()
@pytest.mark.asyncio
@@ -0,0 +1,230 @@
"""
Validate Claude Fable 5 model configuration entries.
Fable 5 is a new tier above Opus ($10/$50 per MTok) with the same adaptive-only
API surface as Opus 4.7/4.8. The cost-map entries below are what make the model
resolvable across Anthropic, Bedrock, Vertex AI, and Azure AI (Microsoft
Foundry), and the ``supports_adaptive_thinking`` flag is what makes LiteLLM send
``thinking.type='adaptive'`` instead of the legacy ``enabled``/``budget_tokens``
shape, which Fable 5 rejects with a 400.
"""
import json
import os
import pytest
import litellm
from litellm.constants import BEDROCK_CONVERSE_MODELS
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
REPO_ROOT = os.path.join(os.path.dirname(__file__), "../..")
def _load_root_cost_map() -> dict:
json_path = os.path.join(REPO_ROOT, "model_prices_and_context_window.json")
with open(json_path) as f:
return json.load(f)
@pytest.fixture
def local_model_cost_map(monkeypatch):
"""Force the bundled backup cost map so assertions don't depend on the
network-fetched ``main`` copy (which lags this branch until merge)."""
original_model_cost = litellm.model_cost
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
litellm.model_cost = litellm.get_model_cost_map(url="")
litellm.get_model_info.cache_clear()
try:
yield
finally:
litellm.model_cost = original_model_cost
litellm.get_model_info.cache_clear()
def test_fable_5_model_pricing_and_capabilities():
model_data = _load_root_cost_map()
expected_models = [
("claude-fable-5", "anthropic"),
("anthropic.claude-fable-5", "bedrock_converse"),
("vertex_ai/claude-fable-5", "vertex_ai-anthropic_models"),
# Unlike Opus 4.8 (200k on Foundry), Fable 5 has the full 1M context
# window on Microsoft Foundry.
("azure_ai/claude-fable-5", "azure_ai"),
]
for model_name, provider in expected_models:
assert model_name in model_data, f"Missing model entry: {model_name}"
info = model_data[model_name]
assert info["litellm_provider"] == provider
assert info["mode"] == "chat"
assert info["max_input_tokens"] == 1000000
assert info["max_output_tokens"] == 128000
assert info["max_tokens"] == 128000
# $10 / $50 per MTok (2x Opus 4.8), with the standard 1.25x 5m
# cache-write, 2x 1h cache-write, and 0.1x cache-read multipliers.
assert info["input_cost_per_token"] == 1e-05
assert info["output_cost_per_token"] == 5e-05
assert info["cache_creation_input_token_cost"] == 1.25e-05
assert info["cache_creation_input_token_cost_above_1hr"] == 2e-05
assert info["cache_read_input_token_cost"] == 1e-06
# Flat-rate across the full 1M context window.
assert "input_cost_per_token_above_200k_tokens" not in info
assert "output_cost_per_token_above_200k_tokens" not in info
assert info["supports_assistant_prefill"] is False
assert info["supports_function_calling"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_reasoning"] is True
assert info["supports_tool_choice"] is True
assert info["supports_vision"] is True
assert info["supports_xhigh_reasoning_effort"] is True
assert info["supports_max_reasoning_effort"] is True
def test_fable_5_bedrock_regional_model_pricing():
model_data = _load_root_cost_map()
# Fable 5 launched with us/eu geo inference profiles plus a global profile
# (no au/apac/jp). Global uses base pricing; geo profiles carry the
# standard 10% regional premium.
expected_models = {
"global.anthropic.claude-fable-5": {
"input_cost_per_token": 1e-05,
"output_cost_per_token": 5e-05,
"cache_creation_input_token_cost": 1.25e-05,
"cache_read_input_token_cost": 1e-06,
},
"us.anthropic.claude-fable-5": {
"input_cost_per_token": 1.1e-05,
"output_cost_per_token": 5.5e-05,
"cache_creation_input_token_cost": 1.375e-05,
"cache_read_input_token_cost": 1.1e-06,
},
"eu.anthropic.claude-fable-5": {
"input_cost_per_token": 1.1e-05,
"output_cost_per_token": 5.5e-05,
"cache_creation_input_token_cost": 1.375e-05,
"cache_read_input_token_cost": 1.1e-06,
},
}
for model_name, expected in expected_models.items():
assert model_name in model_data, f"Missing model entry: {model_name}"
info = model_data[model_name]
assert info["litellm_provider"] == "bedrock_converse"
assert info["max_input_tokens"] == 1000000
assert info["max_output_tokens"] == 128000
assert info["bedrock_output_config_effort_ceiling"] == "xhigh"
for key, value in expected.items():
assert info[key] == value
def test_fable_5_geo_multiplier_without_fast_mode():
"""First-party ``inference_geo='us'`` carries the 1.1x premium, but unlike
the Opus line there is no fast-mode variant for Fable 5; a ``fast`` key
here would silently misprice ``speed='fast'`` requests."""
model_data = _load_root_cost_map()
entry = model_data["claude-fable-5"]["provider_specific_entry"]
assert entry == {"us": 1.1}
def test_fable_5_present_in_bundled_backup():
"""The bundled backup is the runtime fallback (and what tests load with
``LITELLM_LOCAL_MODEL_COST_MAP=True``) it must carry the same entries as
the root cost map, otherwise the model resolves on one path but not the
other."""
backup = GetModelCostMap.load_local_model_cost_map()
root = _load_root_cost_map()
for model_name in (
"claude-fable-5",
"anthropic.claude-fable-5",
"global.anthropic.claude-fable-5",
"us.anthropic.claude-fable-5",
"eu.anthropic.claude-fable-5",
"vertex_ai/claude-fable-5",
"vertex_ai/claude-fable-5@default",
"azure_ai/claude-fable-5",
):
assert model_name in backup, f"Missing from backup cost map: {model_name}"
assert backup[model_name] == root[model_name], model_name
def test_fable_5_registered_for_bedrock_converse():
assert "anthropic.claude-fable-5" in BEDROCK_CONVERSE_MODELS
def test_fable_5_provider_resolves_via_model_info(local_model_cost_map):
info = litellm.get_model_info(model="claude-fable-5")
assert info["litellm_provider"] == "anthropic"
assert info["max_input_tokens"] == 1000000
assert info["max_output_tokens"] == 128000
@pytest.mark.parametrize(
"cost_map",
[_load_root_cost_map(), GetModelCostMap.load_local_model_cost_map()],
ids=["root", "bundled_backup"],
)
def test_fable_5_all_variants_carry_adaptive_thinking_flag(cost_map):
"""Every Fable 5 entry must advertise ``supports_adaptive_thinking``.
Adaptive-thinking detection is cost-map driven, so a single variant missing
the flag silently sends the legacy ``thinking.type='enabled'`` shape and the
provider 400s (issue #29188 for the Opus 4.8 equivalent). Fable 5 is even
stricter than Opus 4.8: an explicit ``thinking.type='disabled'`` also 400s,
so adaptive is the only valid thinking shape LiteLLM can emit for it."""
variants = [k for k in cost_map if "claude-fable-5" in k]
assert variants, "no claude-fable-5 entries found in cost map"
missing = [
k for k in variants if cost_map[k].get("supports_adaptive_thinking") is not True
]
assert not missing, f"missing supports_adaptive_thinking: {missing}"
@pytest.mark.parametrize(
"model",
[
"claude-fable-5",
"anthropic/claude-fable-5",
"anthropic.claude-fable-5",
"bedrock/us.anthropic.claude-fable-5",
"bedrock/invoke/eu.anthropic.claude-fable-5",
"bedrock/global.anthropic.claude-fable-5",
"vertex_ai/claude-fable-5",
"azure_ai/claude-fable-5",
],
)
def test_adaptive_thinking_detected_for_fable_5(local_model_cost_map, model):
"""Provider-routed ids must resolve to a flagged entry so ``reasoning_effort``
maps to ``thinking.type='adaptive'`` + ``output_config.effort``."""
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True
@pytest.mark.parametrize(
"cost_map",
[_load_root_cost_map(), GetModelCostMap.load_local_model_cost_map()],
ids=["root", "bundled_backup"],
)
def test_sampling_params_flag_on_all_models_that_removed_them(cost_map):
"""Fable 5 and Opus 4.7/4.8 reject ``top_p``/``top_k``/``temperature != 1``;
the drop/raise gating is cost-map driven, so every variant must carry an
explicit ``supports_sampling_params: false``. The perplexity route is
exempt: it is OpenAI-compatible and maps sampling params upstream."""
variants = [
k
for k in cost_map
if any(v in k for v in ("claude-fable-5", "claude-opus-4-7", "claude-opus-4-8"))
and not k.startswith("perplexity/")
]
assert variants, "no matching entries found in cost map"
missing = [
k for k in variants if cost_map[k].get("supports_sampling_params") is not False
]
assert not missing, f"missing supports_sampling_params=false: {missing}"
+1
View File
@@ -853,6 +853,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
"supports_xhigh_reasoning_effort": {"type": "boolean"},
"supports_max_reasoning_effort": {"type": "boolean"},
"supports_adaptive_thinking": {"type": "boolean"},
"supports_sampling_params": {"type": "boolean"},
"supports_service_tier": {"type": "boolean"},
"supports_preset": {"type": "boolean"},
"tool_use_system_prompt_tokens": {"type": "number"},
Generated
+2 -2
View File
@@ -9,7 +9,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-05-30T23:40:12.713734Z"
exclude-newer = "2026-06-08T02:16:23.24531Z"
exclude-newer-span = "P3D"
[manifest]
@@ -3189,7 +3189,7 @@ wheels = [
[[package]]
name = "litellm"
version = "1.86.4"
version = "1.86.5"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },