Merge remote-tracking branch 'origin/main' into fix-anthropic-server-tool-use-chat

# Conflicts:
#	tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py
This commit is contained in:
mateo-berri 2026-10-30 09:39:26 -07:00
commit 0ed5afb3d7
57 changed files with 1607 additions and 153 deletions

View file

@ -1,6 +1,12 @@
import reprlib
from collections.abc import Collection, Mapping
from dataclasses import dataclass
from typing import Final, Literal
from pydantic import TypeAdapter
import litellm
from litellm._logging import verbose_logger
from litellm.constants import (
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
@ -11,6 +17,54 @@ from litellm.litellm_core_utils.core_helpers import as_str_mapping
OpenAIStyleReasoningEffort = Literal["minimal", "low", "medium", "high"]
_DROP_WARNING: Final = "Dropping %s=%s for model=%r because %s"
_DROP_PARAMS_DROP_WARNING: Final = "Dropping %s=%s for model=%r (drop_params=True) because %s"
_NON_NUMERIC_BUDGET_REASON: Final = "it is not a number. Send budget_tokens as an integer to set a reasoning level."
_NON_STRING_EFFORT_REASON: Final = (
"it is not a string. Send reasoning_effort as a level name such as 'high' to set a reasoning level."
)
_REASONING_PARAMS: Final = ("reasoning", "reasoning_effort", "thinking")
OPAQUE_VALUE: Final = TypeAdapter(object)
def warn_dropped_reasoning(
param: str, value: object, model: str | None, reason: str, *, drop_params_gated: bool
) -> None:
verbose_logger.warning(
_DROP_PARAMS_DROP_WARNING if drop_params_gated else _DROP_WARNING, param, reprlib.repr(value), model, reason
)
def warn_non_string_reasoning_effort(value: object, model: str | None) -> None:
effort: Final = as_str_mapping(value)
requested_level: Final = effort.get("effort") if effort is not None else value
if requested_level is None or isinstance(value, str):
return
warn_dropped_reasoning("reasoning_effort", value, model, _NON_STRING_EFFORT_REASON, drop_params_gated=False)
def warn_dropped_reasoning_params(
dropped: Collection[object],
request: Mapping[str, object],
model: str | None,
reason: str,
*,
drop_params_gated: bool,
) -> None:
for param in _REASONING_PARAMS:
if param in dropped and param in request:
warn_dropped_reasoning(param, request[param], model, reason, drop_params_gated=drop_params_gated)
@dataclass(frozen=True, slots=True)
class DroppedThinkingBudget:
budget_tokens: object
def warn(self, model: str) -> None:
warn_dropped_reasoning(
"thinking.budget_tokens", self.budget_tokens, model, _NON_NUMERIC_BUDGET_REASON, drop_params_gated=True
)
def reasoning_effort_from_thinking_budget(
budget_tokens: float,
@ -29,7 +83,9 @@ def reasoning_effort_from_thinking_budget(
return "minimal"
def reasoning_effort_from_thinking(thinking: object, *, drop_params: bool) -> OpenAIStyleReasoningEffort | None:
def _effort_or_dropped_budget(
thinking: object, *, drop_params: bool
) -> OpenAIStyleReasoningEffort | DroppedThinkingBudget | None:
fields: Final = as_str_mapping(thinking)
if fields is None or fields.get("type") != "enabled":
return None
@ -37,10 +93,27 @@ def reasoning_effort_from_thinking(thinking: object, *, drop_params: bool) -> Op
if isinstance(budget, (int, float)) and not isinstance(budget, bool):
return reasoning_effort_from_thinking_budget(budget)
if drop_params or litellm.drop_params is True:
return None
return DroppedThinkingBudget(budget)
raise non_numeric_thinking_budget_error(budget)
def reasoning_effort_from_thinking(
thinking: object, *, model: str, drop_params: bool
) -> OpenAIStyleReasoningEffort | None:
effort: Final = _effort_or_dropped_budget(thinking, drop_params=drop_params)
if not isinstance(effort, DroppedThinkingBudget):
return effort
effort.warn(model)
return None
def reasoning_effort_from_thinking_silently(
thinking: object, *, drop_params: bool
) -> OpenAIStyleReasoningEffort | None:
effort: Final = _effort_or_dropped_budget(thinking, drop_params=drop_params)
return None if isinstance(effort, DroppedThinkingBudget) else effort
def non_numeric_thinking_budget_error(budget: object) -> UnsupportedParamsError:
return UnsupportedParamsError(
message=(

View file

@ -49,6 +49,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
place_mid_conversation_system,
split_leading_system_run,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_non_string_reasoning_effort
from litellm.llms.base_llm.base_utils import (
ResponseFormatSchema,
as_json_object,
@ -1989,6 +1990,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
if isinstance(effort_value, dict):
effort_value = effort_value.get("effort")
if not isinstance(effort_value, str):
warn_non_string_reasoning_effort(OPAQUE_VALUE.validate_python(value), model)
continue
mapped_thinking = AnthropicConfig._map_reasoning_effort(
reasoning_effort=effort_value,

View file

@ -37,7 +37,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
message_field,
parts_of,
)
from litellm.litellm_core_utils.reasoning_effort_utils import non_numeric_thinking_budget_error
from litellm.litellm_core_utils.reasoning_effort_utils import DroppedThinkingBudget, non_numeric_thinking_budget_error
from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.llms.anthropic.wif import (
aget_anthropic_wif_token,
@ -631,11 +631,11 @@ def _numeric_budget_tokens(budget_tokens: object) -> int | None:
return None
def _legacy_budget_tokens(budget_tokens: object, *, drop_params: bool) -> int | None:
def _legacy_budget_tokens(budget_tokens: object, *, drop_params: bool) -> int | DroppedThinkingBudget:
if (numeric := _numeric_budget_tokens(budget_tokens)) is not None:
return numeric
if drop_params or litellm.drop_params is True:
return None
return DroppedThinkingBudget(budget_tokens)
raise non_numeric_thinking_budget_error(budget_tokens)
@ -1118,6 +1118,22 @@ class AnthropicModelInfo(BaseLLMModelInfo):
custom_llm_provider: str,
drop_params: bool,
) -> None:
dropped: Final = AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model_silently(
model=model,
optional_params=optional_params,
custom_llm_provider=custom_llm_provider,
drop_params=drop_params,
)
if dropped is not None:
dropped.warn(model)
@staticmethod
def translate_legacy_thinking_for_adaptive_model_silently(
model: str,
optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param like the sibling helpers
custom_llm_provider: str,
drop_params: bool,
) -> DroppedThinkingBudget | None:
"""Translate legacy ``thinking.type=enabled`` to adaptive for the
adaptive-thinking models that reject it (4.7+ and the 5 families).
Models flagged ``supports_legacy_thinking`` (the 4.6 family) accept the
@ -1126,12 +1142,12 @@ class AnthropicModelInfo(BaseLLMModelInfo):
``output_config.effort`` is never overridden.
"""
if not AnthropicModelInfo._is_adaptive_thinking_model(model, custom_llm_provider):
return
return None
if AnthropicModelInfo._supports_legacy_thinking(model, custom_llm_provider):
return
return None
thinking: Final = validated_object_mapping(optional_params.get("thinking"))
if thinking is None or thinking.get("type") != "enabled":
return
return None
budget_tokens: Final = _legacy_budget_tokens(thinking.get("budget_tokens", 0), drop_params=drop_params)
display: Final = thinking.get("display")
@ -1140,8 +1156,8 @@ class AnthropicModelInfo(BaseLLMModelInfo):
thinking, rebuilt_keys=frozenset(("type", "budget_tokens", "display")), drop_params=drop_params
)
optional_params["thinking"] = {**carried_keys, "type": "adaptive", **supported_display}
if budget_tokens is None:
return
if isinstance(budget_tokens, DroppedThinkingBudget):
return budget_tokens
effort: Final = AnthropicModelInfo._legacy_budget_to_effort(
model=model, budget_tokens=budget_tokens, custom_llm_provider=custom_llm_provider
)
@ -1150,6 +1166,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
"effort": effort,
**(existing_output_config if isinstance(existing_output_config, dict) else MappingProxyType({})),
}
return None
@staticmethod
def _legacy_budget_to_effort(model: str, budget_tokens: int, custom_llm_provider: str) -> str:

View file

@ -154,6 +154,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
)
from litellm.litellm_core_utils.reasoning_effort_utils import (
reasoning_effort_from_thinking,
reasoning_effort_from_thinking_silently,
)
from litellm.llms.anthropic.common_utils import (
anthropic_server_tool_call_params,
@ -318,6 +319,7 @@ class AnthropicAdapter:
custom_llm_provider=custom_llm_provider,
api_base=api_base,
drop_params=drop_params,
report_reasoning_drops=True,
)
return translated_body, tool_name_mapping
@ -687,7 +689,9 @@ class LiteLLMAnthropicMessagesAdapter:
def translate_anthropic_thinking_to_reasoning_effort(
thinking: AnthropicThinkingParam,
*,
model: str,
drop_params: bool = True,
report_reasoning_drops: bool = False,
) -> str | None:
"""
Translate Anthropic's thinking parameter to OpenAI's reasoning_effort.
@ -706,7 +710,9 @@ class LiteLLMAnthropicMessagesAdapter:
if thinking_type == "disabled":
return "none"
elif thinking_type == "enabled":
return reasoning_effort_from_thinking(thinking, drop_params=drop_params)
if report_reasoning_drops:
return reasoning_effort_from_thinking(thinking, model=model, drop_params=drop_params)
return reasoning_effort_from_thinking_silently(thinking, drop_params=drop_params)
elif thinking_type == "adaptive":
# Adaptive thinking: effort is controlled by output_config.effort,
# not budget_tokens. Return a default; caller should override with
@ -773,7 +779,7 @@ class LiteLLMAnthropicMessagesAdapter:
return {"thinking": thinking}
else:
reasoning_effort: Final = LiteLLMAnthropicMessagesAdapter.translate_anthropic_thinking_to_reasoning_effort(
thinking
thinking, model=model
)
if reasoning_effort:
return {
@ -1168,6 +1174,7 @@ class LiteLLMAnthropicMessagesAdapter:
*,
custom_llm_provider: str | None = None,
drop_params: bool = True,
report_reasoning_drops: bool = False,
) -> None:
"""Translate Anthropic thinking to either thinking or reasoning_effort.
@ -1228,7 +1235,10 @@ class LiteLLMAnthropicMessagesAdapter:
return
reasoning_effort: Final = declared_effort or self.translate_anthropic_thinking_to_reasoning_effort(
cast(AnthropicThinkingParam, thinking), drop_params=drop_params
cast(AnthropicThinkingParam, thinking),
model=model,
drop_params=drop_params,
report_reasoning_drops=report_reasoning_drops,
)
if not reasoning_effort:
return
@ -1288,6 +1298,7 @@ class LiteLLMAnthropicMessagesAdapter:
api_base: str | None = None,
preserve_midturn_system: bool = False,
drop_params: bool = True,
report_reasoning_drops: bool = False,
) -> tuple[ChatCompletionRequest, dict[str, str]]:
"""
This is used by the beta Anthropic Adapter, for translating anthropic `/v1/messages` requests to the openai format.
@ -1342,6 +1353,7 @@ class LiteLLMAnthropicMessagesAdapter:
new_kwargs=new_kwargs,
custom_llm_provider=custom_llm_provider,
drop_params=drop_params,
report_reasoning_drops=report_reasoning_drops,
)
## CONVERT STOP_SEQUENCES
self._translate_stop_sequences_to_openai(

View file

@ -20,6 +20,7 @@ from litellm.litellm_core_utils.core_helpers import as_str_mapping
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.anthropic.common_utils import (
AnthropicModelInfo,
strip_provider_specific_fields_from_anthropic_messages,
tools_marked_for_a_tool_config_point,
)
@ -36,7 +37,6 @@ from litellm.router_utils.reasoning_effort_capability import (
keeps_effort,
model_group_from_metadata,
resolve_deployment_reasoning_effort,
with_adaptive_thinking_as_effort,
)
from litellm.types.llms.anthropic import AllAnthropicToolsValues
from litellm.types.llms.anthropic_messages.anthropic_request import AnthropicMetadata
@ -185,9 +185,11 @@ def _with_deployment_output_effort(
).value
def translate(source: Mapping[str, object]) -> Mapping[str, object]:
return with_adaptive_thinking_as_effort(
source, model=model, custom_llm_provider=custom_llm_provider, drop_params=drop_params
translated: Final = dict(source)
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
model=model, optional_params=translated, custom_llm_provider=custom_llm_provider, drop_params=drop_params
)
return translated
requested_output: Final = as_str_mapping(params.get("output_config"))
caller_output: Final = None if requested_output is None else resolve(requested_output)

View file

@ -21,6 +21,7 @@ from litellm.constants import ANTHROPIC_BILLING_METADATA_PREFIX
from litellm.exceptions import AuthenticationError
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_non_string_reasoning_effort
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.anthropic.messages.convertible_blocks import drop_non_string_tool_result_texts
from litellm.llms.anthropic.messages.safeguards import proxy_serves_safeguards
@ -103,6 +104,7 @@ _MESSAGES_ADAPTER: Final = TypeAdapter(list[dict[str, object]])
class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
_workload_identity_eligible: ClassVar[bool] = True
_report_reasoning_drops: ClassVar[bool] = True
@property
def custom_llm_provider(self) -> str | None:
@ -695,12 +697,17 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
):
anthropic_messages_optional_request_params.pop("output_format") # pyright: ignore[reportUnknownMemberType] # the request params arrive as a bare dict
requested_reasoning_effort: Final = OPAQUE_VALUE.validate_python(
anthropic_messages_optional_request_params.get("reasoning_effort") # pyright: ignore[reportUnknownMemberType] # the request params arrive as a bare dict
)
self._translate_reasoning_effort_to_anthropic(
model=model,
optional_params=anthropic_messages_optional_request_params,
max_tokens=max_tokens,
custom_llm_provider=self._resolved_provider,
)
if self._report_reasoning_drops:
warn_non_string_reasoning_effort(requested_reasoning_effort, model)
AnthropicModelInfo.maybe_drop_disabled_thinking(
model=model,
@ -708,12 +715,14 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
custom_llm_provider=self._resolved_provider,
)
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
dropped_budget: Final = AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model_silently(
model=model,
optional_params=anthropic_messages_optional_request_params,
custom_llm_provider=self._resolved_provider,
drop_params=getattr(litellm_params, "drop_params", None) is True,
)
if dropped_budget is not None and self._report_reasoning_drops:
dropped_budget.warn(model)
self._translate_adaptive_effort_for_non_adaptive_model(
model=model,

View file

@ -534,6 +534,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
output_config: dict[str, object] | None = None,
auto_summary_level: str = "detailed",
*,
model: str,
drop_params: bool = True,
) -> dict[str, object] | None:
"""
@ -555,7 +556,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
if isinstance(output_config, dict) and output_config.get("effort"):
effort = output_config["effort"]
elif thinking_type == "enabled":
budget_effort: Final = reasoning_effort_from_thinking(thinking, drop_params=drop_params)
budget_effort: Final = reasoning_effort_from_thinking(thinking, model=model, drop_params=drop_params)
if budget_effort is None:
return None
effort = budget_effort
@ -667,6 +668,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
thinking,
output_config=cast(dict[str, object] | None, output_config),
auto_summary_level=auto_summary_level,
model=model,
drop_params=drop_params,
)
if reasoning:

View file

@ -9,7 +9,7 @@ from dataclasses import dataclass, field
from functools import reduce
from itertools import accumulate, groupby
from types import MappingProxyType
from typing import Annotated, Final, Literal, Protocol, TypeAlias
from typing import Annotated, ClassVar, Final, Literal, Protocol, TypeAlias
import httpx
from pydantic import ConfigDict, Field, JsonValue, StrictInt, TypeAdapter, ValidationError
@ -81,6 +81,10 @@ _DEPLOYMENT_OPTIONS: Final = frozenset(
)
class _BaselineAnthropicMessagesConfig(AnthropicMessagesConfig):
_report_reasoning_drops: ClassVar[bool] = False
class _StrictModel(LiteLLMBaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
@ -661,7 +665,7 @@ def prepare_native_baseline_body(request: Mapping[str, object], model: str) -> M
TypeAdapter(tuple[str, ...]).validate_python(owned.get("additional_drop_params") or ()),
dict(options),
)
config: Final = AnthropicMessagesConfig()
config: Final = _BaselineAnthropicMessagesConfig()
body: Final = config.transform_anthropic_messages_request(
model=resolved_model,
messages=strip_provider_specific_fields_from_anthropic_messages(messages),

View file

@ -20,6 +20,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
prompt_cache_breakpoint_for_wire,
with_prompt_cache_breakpoint,
)
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning_params
from litellm.llms.azure.common_utils import BaseAzureLLM
from litellm.llms.azure_ai.common_utils import (
api_key_header_for_base,
@ -40,6 +41,14 @@ if TYPE_CHECKING:
from litellm.litellm_core_utils.tokenizer import Encoding as Tokenizer
_REQUEST_BODY: Final = TypeAdapter(dict[str, object])
def _request_model(request: Mapping[str, object]) -> str | None:
model: Final = request.get("model")
return model if isinstance(model, str) else None
class AzureFoundryErrorStrings(str, enum.Enum):
SET_EXTRA_PARAMETERS_TO_PASS_THROUGH = "Set extra-parameters to 'pass-through'"
@ -510,7 +519,13 @@ class AzureAIStudioConfig(OpenAIConfig):
request_data = self._drop_extra_params_from_request_data(request_data, error_text)
if "Extra inputs are not permitted" in error_text and self._error_has_tool_level_extra_fields(error_text):
request_data = self._drop_tool_level_extra_fields(request_data, error_text)
data: Final = drop_params_from_unprocessable_entity_error(e=e, data=request_data)
data: Final = drop_params_from_unprocessable_entity_error(
e=e,
data=request_data,
model=_request_model(_REQUEST_BODY.validate_python(request_data)),
drop_params_gated="Extra inputs are not permitted" in error_text
and not self._error_has_tool_level_extra_fields(error_text),
)
return data
def _drop_tool_level_extra_fields(self, request_data: dict, error_text: str) -> dict:
@ -526,6 +541,14 @@ class AzureAIStudioConfig(OpenAIConfig):
def _drop_extra_params_from_request_data(self, request_data: dict, error_text: str) -> dict:
params_to_drop: Final = self._extract_params_to_drop_from_error_text(error_text)
if params_to_drop:
request: Final = _REQUEST_BODY.validate_python(request_data)
warn_dropped_reasoning_params(
params_to_drop,
request,
_request_model(request),
"Azure AI rejected it as an extra parameter its deployment does not pass through.",
drop_params_gated=False,
)
for param in params_to_drop:
if param in request_data:
request_data.pop(param, None)

View file

@ -33,6 +33,7 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
inline_remote_image_urls,
inline_remote_media,
)
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import ensure_api_route, join_api_base
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM, bedrock_bearer_token
@ -42,6 +43,7 @@ from litellm.llms.bedrock.common_utils import (
bedrock_rejects_stop_sequences,
bedrock_runtime_chat_completions_serves_reasoning_inline,
split_bedrock_region_path,
strip_bedrock_routing_prefix,
)
from litellm.llms.openai.chat.gpt_transformation import OpenAIChatCompletionStreamingHandler
from litellm.llms.openai_like.chat.transformation import OpenAILikeChatConfig
@ -121,6 +123,13 @@ def without_refused_reasoning_effort(model: str, params: Mapping[str, object]) -
effort: Final = params.get("reasoning_effort")
if not isinstance(effort, str) or effort not in chat_completions_reasoning_efforts_refused_for(model):
return params
warn_dropped_reasoning(
"reasoning_effort",
effort,
strip_bedrock_routing_prefix(model),
"AWS's Chat Completions endpoint rejects that level for this model family, so AWS applies its default effort.",
drop_params_gated=False,
)
return _without_params(params, frozenset(("reasoning_effort",)))
@ -445,6 +454,14 @@ class AmazonBedrockRuntimeChatCompletionsConfig(OpenAILikeChatConfig):
),
status_code=400,
)
if malformed_effort:
warn_dropped_reasoning(
"reasoning_effort",
raw_params["reasoning_effort"],
strip_bedrock_routing_prefix(model),
"Bedrock's Chat Completions endpoint takes it only as a string, so AWS applies its default effort.",
drop_params_gated=True,
)
rejected_stop: Final = frozenset(("stop",)) if bedrock_rejects_stop_sequences(model) else frozenset[str]()
return dict(
without_refused_reasoning_effort(

View file

@ -50,6 +50,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
message_field,
parts_of,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_dropped_reasoning
from litellm.llms.anthropic.chat.transformation import (
DROP_UNSUPPORTED_ADAPTIVE_THINKING_WARNING,
DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING,
@ -187,6 +188,8 @@ def _tools_the_model_accepts(
_OUTPUT_CONFIG_BLOCK: Final = TypeAdapter(OutputConfigBlock)
_DEEPSEEK_THINKING_REASON: Final = "DeepSeek on Bedrock reasons natively and takes no thinking field."
_DEEPSEEK_R1_EFFORT_REASON: Final = "DeepSeek R1 on Bedrock always reasons and rejects reasoning_effort."
def _has_native_text_format(output_config: object) -> bool:
@ -640,6 +643,10 @@ class AmazonConverseConfig(BaseConfig):
or supports_reasoning(model=base_model, custom_llm_provider=self.custom_llm_provider)
)
def _warn_native_reasoning_drop(self, param: str, value: object, model: str, reason: str) -> None:
if param in self.get_supported_openai_params(model):
warn_dropped_reasoning(param, value, model, reason, drop_params_gated=False)
def _model_rejects_reasoning_effort_param(self, model: str, base_model: str) -> bool:
"""Whether the model returns a 400 for every ``reasoning_effort`` shape on Converse.
@ -1143,9 +1150,8 @@ class AmazonConverseConfig(BaseConfig):
"tool_choice": {"type": "auto", "disable_parallel_tool_use": not value}
}
if param == "thinking" and drop_thinking_param:
verbose_logger.debug(
"Dropping unsupported `thinking` param for Bedrock model=%s; it reasons natively.",
model,
self._warn_native_reasoning_drop(
param, OPAQUE_VALUE.validate_python(value), model, _DEEPSEEK_THINKING_REASON
)
elif param == "thinking" and not self._is_openai_gpt_reasoning_model(model):
if (
@ -1179,10 +1185,7 @@ class AmazonConverseConfig(BaseConfig):
drop_params=drop_params,
)
elif param == "reasoning_effort" and isinstance(value, str) and drop_reasoning_effort_param:
verbose_logger.debug(
"Dropping unsupported `reasoning_effort` param for Bedrock model=%s; it always reasons and rejects it.",
model,
)
self._warn_native_reasoning_drop(param, value, model, _DEEPSEEK_R1_EFFORT_REASON)
elif (
param == "reasoning_effort"
and isinstance(value, str)

View file

@ -6,10 +6,12 @@ from collections.abc import Coroutine, Mapping, Sequence
from typing import Final, Literal, cast, overload
import litellm
from litellm.litellm_core_utils.core_helpers import as_str_mapping
from litellm.litellm_core_utils.prompt_templates.common_utils import (
convert_content_list_to_str,
extract_search_results_text,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues
@ -18,6 +20,16 @@ from litellm.utils import supports_reasoning, supports_vision
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
SUPPORTED_REASONING_EFFORTS: Final = ("minimal", "low", "medium", "high", "xhigh", "max", "ultra")
_NO_BUDGET_REASON: Final = "DeepSeek takes thinking only as enabled or disabled, with no budget."
def _warn_on_dropped_budget(thinking: object, model: str) -> None:
fields: Final = as_str_mapping(thinking)
if fields is None or fields.get("type") != "enabled" or "budget_tokens" not in fields:
return
warn_dropped_reasoning(
"thinking.budget_tokens", fields["budget_tokens"], model, _NO_BUDGET_REASON, drop_params_gated=False
)
class DeepSeekChatConfig(OpenAIGPTConfig):
@ -53,6 +65,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
# Then re-add only if valid for DeepSeek
thinking_value: Final = optional_params.pop("thinking", None)
reasoning_effort: Final = optional_params.pop("reasoning_effort", None)
_warn_on_dropped_budget(OPAQUE_VALUE.validate_python(thinking_value), model)
# Handle thinking parameter - accept both enabled and disabled, ignore budget_tokens
if isinstance(thinking_value, dict) and thinking_value.get("type") in ("enabled", "disabled"):

View file

@ -12,7 +12,9 @@ from typing import TYPE_CHECKING, Final
import httpx
from pydantic import ConfigDict, TypeAdapter
import litellm
from litellm.litellm_core_utils.core_helpers import map_finish_reason
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.base import LiteLLMBaseModel
@ -29,6 +31,10 @@ PROVIDER_PREFIX: Final[str] = "fal_ai/"
PASSTHROUGH_PARAMS: Final[frozenset[str]] = frozenset(("reasoning", "temperature", "top_p"))
REASONING_DISABLED_EFFORTS: Final[frozenset[str]] = frozenset(("none", "minimal"))
REASONING_ENABLED_EFFORTS: Final[frozenset[str]] = frozenset(("low", "medium", "high"))
_UNSUPPORTED_EFFORT_REASON: Final = (
f"fal only turns reasoning off for {sorted(REASONING_DISABLED_EFFORTS)} "
f"and on for {sorted(REASONING_ENABLED_EFFORTS)}."
)
class _FalUsage(LiteLLMBaseModel):
@ -120,9 +126,10 @@ class FalAIChatConfig(BaseConfig):
return False
if isinstance(value, str) and value in REASONING_ENABLED_EFFORTS:
return True
if drop_params:
return None
raise FalAIError(status_code=400, message=f"Unsupported reasoning_effort {value!r} for {model}")
if not (drop_params or litellm.drop_params is True):
raise FalAIError(status_code=400, message=f"Unsupported reasoning_effort {value!r} for {model}")
warn_dropped_reasoning("reasoning_effort", value, model, _UNSUPPORTED_EFFORT_REASON, drop_params_gated=True)
return None
def _translate_param(self, param: str, value: object, model: str, drop_params: bool) -> tuple[str, object] | None:
if param in ("temperature", "top_p"):

View file

@ -17,6 +17,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
strip_litellm_response_only_fields,
unpack_legacy_defs,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
@ -97,6 +98,7 @@ def _json_schema_response_format(schema: object, name: str) -> Mapping[str, obje
EFFORT_KWARG_KEYS: Final = frozenset({"enable_thinking", "thinking", "reasoning_budget", "low_effort"})
_NOT_A_REASONING_MODEL_REASON: Final = "litellm's model map does not list it as a reasoning model."
def _bool_from_kwargs(kwargs: Mapping[str, object], keys: tuple[str, ...]) -> bool | None:
@ -107,6 +109,13 @@ def _bool_from_kwargs(kwargs: Mapping[str, object], keys: tuple[str, ...]) -> bo
return None
def warn_dropped_effort_kwargs(chat_template_kwargs: object, effort: object, model: str) -> None:
if effort != "none":
warn_dropped_reasoning(
"chat_template_kwargs", chat_template_kwargs, model, _NOT_A_REASONING_MODEL_REASON, drop_params_gated=False
)
def effort_from_chat_template_kwargs(kwargs: Mapping[str, object]) -> object:
enable_thinking: Final = _bool_from_kwargs(kwargs, ("enable_thinking", "thinking"))
if enable_thinking is False:
@ -438,10 +447,7 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig):
if effort is None:
return ()
if not supports_reasoning(model=model, custom_llm_provider="fireworks_ai"):
verbose_logger.debug(
"fireworks_ai model %r does not support reasoning; dropping chat_template_kwargs effort keys.",
model,
)
warn_dropped_effort_kwargs(OPAQUE_VALUE.validate_python(chat_template_kwargs), effort, model)
return ()
return (("reasoning_effort", effort),)

View file

@ -2,6 +2,7 @@ from collections.abc import Mapping
from typing import Final
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE
from litellm.types.llms.openai import AllMessageValues, OpenAITextCompletionUserMessage
from litellm.utils import supports_reasoning
@ -12,6 +13,7 @@ from ..chat.transformation import (
NIM_VLLM_STRIP_PARAMS,
FireworksAIConfig,
effort_from_chat_template_kwargs,
warn_dropped_effort_kwargs,
)
from ..common_utils import FireworksAIMixin, resolve_fireworks_resource_name
@ -129,10 +131,7 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig
)
return result
if not supports_reasoning(model=model, custom_llm_provider="fireworks_ai"):
verbose_logger.debug(
"fireworks_ai model %r does not support reasoning; dropping chat_template_kwargs effort keys.",
model,
)
warn_dropped_effort_kwargs(OPAQUE_VALUE.validate_python(chat_template_kwargs), effort, model)
return result
return {**result, "reasoning_effort": effort}

View file

@ -99,7 +99,7 @@ class HostedVLLMChatConfig(OpenAIGPTConfig):
thinking_effort: Final = (
None
if "reasoning_effort" in non_default_params
else reasoning_effort_from_thinking(thinking, drop_params=drop_params)
else reasoning_effort_from_thinking(thinking, model=model, drop_params=drop_params)
)
if thinking_effort is not None:
non_default_params["reasoning_effort"] = thinking_effort

View file

@ -25,6 +25,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
strip_litellm_internal_message_fields,
strip_none_values_from_message,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.openai.chat.gpt_transformation import (
OpenAIChatCompletionStreamingHandler,
@ -74,6 +75,12 @@ _MISTRAL_REQUEST_ROUTING: Final = TypeAdapter(MistralRequestRouting)
_MISTRAL_CACHE_KEY_PARAMS: Final = TypeAdapter(MistralCacheKeyParams)
_MISTRAL_REASONING_EFFORT_PARAMS: Final = TypeAdapter(MistralReasoningEffortParams)
_MISTRAL_FILE_PART: Final = TypeAdapter(MistralFilePart)
_REASONING_PROMPT_REASON: Final = "litellm sends Magistral a reasoning system prompt instead of the requested level."
_NON_LEVEL_EFFORT_REASON: Final = (
"the Mistral API takes it only as a level such as 'high', so Mistral applies its default effort."
)
_DOCUMENTED_MAGISTRAL_EFFORTS: Final = frozenset({"low", "medium", "high"})
_REASONING_ON_THINKING_TYPES: Final = frozenset({"enabled", "adaptive"})
def _reasoning_level_and_summary(reasoning_effort: object) -> tuple[object, object]:
@ -83,6 +90,16 @@ def _reasoning_level_and_summary(reasoning_effort: object) -> tuple[object, obje
return requested.get("effort"), requested.get("summary")
def _warn_unless_reasoning_prompt_honors(param: str, value: object, model: str) -> None:
if param == "reasoning_effort" and isinstance(value, str) and value in _DOCUMENTED_MAGISTRAL_EFFORTS:
return
thinking: Final = as_str_mapping(value)
thinking_type: Final = thinking.get("type") if thinking is not None else None
if param == "thinking" and isinstance(thinking_type, str) and thinking_type in _REASONING_ON_THINKING_TYPES:
return
warn_dropped_reasoning(param, value, model, _REASONING_PROMPT_REASON, drop_params_gated=False)
def _is_json_array(value: object) -> TypeIs[Sequence[object]]: # guard-ok: a decoded JSON array is a list
return isinstance(value, list)
@ -253,6 +270,7 @@ class MistralConfig(OpenAIGPTConfig):
if param in ("reasoning_effort", "thinking") and "magistral" in model.lower():
# Flag that we need to add reasoning system prompt
optional_params["_add_reasoning_prompt"] = True
_warn_unless_reasoning_prompt_honors(param, OPAQUE_VALUE.validate_python(value), model)
if param in ("parallel_tool_calls", "prompt_cache_key"):
optional_params[param] = value
return optional_params
@ -260,13 +278,18 @@ class MistralConfig(OpenAIGPTConfig):
def _reasoning_level_to_send(
self, model: str, requested: MistralReasoningEffortParams, drop_params: bool
) -> str | None:
level, summary = _reasoning_level_and_summary(requested.get("reasoning_effort"))
requested_effort: Final = requested.get("reasoning_effort")
level, summary = _reasoning_level_and_summary(requested_effort)
sendable: Final = (
_accepted_reasoning_effort(model, level, self.custom_llm_provider) if isinstance(level, str) else None
)
if sendable is not None and summary is None:
return sendable
if drop_params or litellm.drop_params is True:
if sendable is None and level is not None:
warn_dropped_reasoning(
"reasoning_effort", requested_effort, model, _NON_LEVEL_EFFORT_REASON, drop_params_gated=True
)
return sendable
raise UnsupportedParamsError(
message=(

View file

@ -21,6 +21,7 @@ from litellm._logging import verbose_logger
from litellm.constants import DEFAULT_OCI_CHAT_MAX_TOKENS
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
from litellm.litellm_core_utils.prompt_templates.common_utils import messages_with_file_parts_as_image_urls
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
@ -428,6 +429,13 @@ class OCIChatConfig(BaseConfig):
alias = param_map.get(key)
if key == "reasoning_effort" and alias and not _model_supports_reasoning_effort(model):
if drop_params or litellm.drop_params:
warn_dropped_reasoning(
key,
value,
model,
"the model map lists no reasoning_effort levels for it, so OCI would reject reasoningEffort.",
drop_params_gated=True,
)
continue
raise OCIError(
status_code=400,
@ -451,6 +459,14 @@ class OCIChatConfig(BaseConfig):
if key == "n" and (value is None or value == 1):
continue
if drop_params or litellm.drop_params:
if key == "reasoning_effort":
warn_dropped_reasoning(
key,
value,
model,
"OCI's Cohere chat API has no reasoning effort field.",
drop_params_gated=True,
)
continue
raise OCIError(
status_code=400,

View file

@ -14,6 +14,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
extract_images_from_message,
extract_reasoning_content,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
@ -29,7 +30,7 @@ from litellm.types.llms.openai import (
)
from litellm.types.utils import ModelResponse, ModelResponseStream
from ..common_utils import OllamaError
from ..common_utils import OllamaError, ollama_think
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
@ -172,10 +173,7 @@ class OllamaChatConfig(BaseConfig):
if value.get("json_schema") and value["json_schema"].get("schema"):
optional_params["format"] = value["json_schema"]["schema"]
if param == "reasoning_effort" and value is not None:
if model.startswith("gpt-oss"):
optional_params["think"] = value
else:
optional_params["think"] = value in {"low", "medium", "high"}
optional_params["think"] = ollama_think(model, OPAQUE_VALUE.validate_python(value))
### FUNCTION CALLING LOGIC ###
# Ollama 0.4+ supports native tool calling - pass tools directly
# and let Ollama handle model capability detection

View file

@ -8,11 +8,14 @@ from pydantic import BaseModel, TypeAdapter
from litellm import verbose_logger
from litellm._uuid import uuid
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.types.utils import ModelResponse, ModelResponseStream
_PROVIDER_FIELDS: Final = TypeAdapter(dict[str, object])
_THINK_ON_EFFORTS: Final = frozenset({"low", "medium", "high"})
_THINK_OFF_REASON: Final = "Ollama takes think only as on or off, and litellm maps this level to off."
_DECODED_JSON: Final = TypeAdapter(object)
@ -296,3 +299,12 @@ class OllamaModelInfo(BaseLLMModelInfo):
Return the base model name for Ollama (no-op).
"""
return model
def ollama_think(model: str, reasoning_effort: object) -> object:
if model.startswith("gpt-oss"):
return reasoning_effort
think: Final = reasoning_effort in _THINK_ON_EFFORTS
if isinstance(reasoning_effort, str) and not think and reasoning_effort != "none":
warn_dropped_reasoning("reasoning_effort", reasoning_effort, model, _THINK_OFF_REASON, drop_params_gated=False)
return think

View file

@ -23,6 +23,7 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
async_inline_remote_media,
inline_remote_image_urls,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
@ -43,7 +44,7 @@ from litellm.types.utils import (
generate_id,
)
from ..common_utils import OllamaError, OllamaModelInfo, convert_image
from ..common_utils import OllamaError, OllamaModelInfo, convert_image, ollama_think
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
@ -249,10 +250,7 @@ class OllamaConfig(BaseConfig):
elif param == "stop":
optional_params["stop"] = value
elif param == "reasoning_effort" and value is not None:
if model.startswith("gpt-oss"):
optional_params["think"] = value
else:
optional_params["think"] = value in {"low", "medium", "high"}
optional_params["think"] = ollama_think(model, OPAQUE_VALUE.validate_python(value))
elif param == "response_format" and isinstance(value, dict):
if value["type"] == "json_object":
optional_params["format"] = "json"

View file

@ -32,6 +32,7 @@ import litellm
from litellm.exceptions import error_response_without_credentials
from litellm.litellm_core_utils.completion_timeout import CompletionTimeout
from litellm.litellm_core_utils.core_helpers import as_str_mapping, decoded_json_object
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning_params
from litellm.litellm_core_utils.token_counter import token_counter, usage_tokenizer_model
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.custom_httpx.http_handler import (
@ -165,17 +166,11 @@ _DECODED_JSON: Final = TypeAdapter(object)
def drop_params_from_unprocessable_entity_error(
e: openai.UnprocessableEntityError | httpx.HTTPStatusError,
data: Mapping[str, object],
*,
model: str | None,
drop_params_gated: bool,
) -> dict[str, object]:
"""
Helper function to read OpenAI UnprocessableEntityError and drop the params that raised an error from the error message.
Args:
e (UnprocessableEntityError): The UnprocessableEntityError exception
data (Dict[str, Any]): The original data dictionary containing all parameters
Returns:
Dict[str, Any]: A new dictionary with invalid parameters removed
"""
"""Return a copy of ``data`` without the params a 422 response names as invalid."""
invalid_params: Final[list[str]] = []
if isinstance(e, httpx.HTTPStatusError):
error_json: Final = decoded_json_object(_DECODED_JSON.validate_python(e.response.json()))
@ -203,6 +198,13 @@ def drop_params_from_unprocessable_entity_error(
invalid_params.append(error_dict["loc"][1])
new_data: Final = {k: v for k, v in data.items() if k not in invalid_params}
warn_dropped_reasoning_params(
invalid_params,
data,
model,
"the provider answered 422 naming it an invalid param.",
drop_params_gated=drop_params_gated,
)
return new_data

View file

@ -940,7 +940,9 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
except openai.UnprocessableEntityError as e:
## check if body contains unprocessable params - related issue https://github.com/BerriAI/litellm/issues/4800
if litellm.drop_params is True or drop_params is True:
inference_params = drop_params_from_unprocessable_entity_error(e, inference_params)
inference_params = drop_params_from_unprocessable_entity_error(
e, inference_params, model=model, drop_params_gated=True
)
else:
raise e
# e.message
@ -1097,7 +1099,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
except openai.UnprocessableEntityError as e:
## check if body contains unprocessable params - related issue https://github.com/BerriAI/litellm/issues/4800
if litellm.drop_params is True or drop_params is True:
data = drop_params_from_unprocessable_entity_error(e, data)
data = drop_params_from_unprocessable_entity_error(e, data, model=model, drop_params_gated=True)
else:
raise e
# e.message
@ -1252,7 +1254,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
except openai.UnprocessableEntityError as e:
## check if body contains unprocessable params - related issue https://github.com/BerriAI/litellm/issues/4800
if litellm.drop_params is True or drop_params is True:
data = drop_params_from_unprocessable_entity_error(e, data)
data = drop_params_from_unprocessable_entity_error(e, data, model=model, drop_params_gated=True)
else:
raise e
except (

View file

@ -9,12 +9,15 @@ from typing import Final, TypedDict
from typing_extensions import ReadOnly
import litellm
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import ensure_api_route, join_api_base
from litellm.secret_managers.main import get_secret_str
from litellm.utils import supports_reasoning
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
_ADAPTIVE_BUDGET_REASON: Final = "it is not an integer, so litellm sends adaptive thinking with no budget."
class ThinkingPayload(TypedDict, total=False):
"""Tencent TokenHub `thinking` object.
@ -102,6 +105,10 @@ class TencentChatConfig(OpenAIGPTConfig):
if isinstance(budget, int):
coerced_with_budget: Final[ThinkingPayload] = {"type": "adaptive", "budget_tokens": budget}
return coerced_with_budget
if budget is not None:
warn_dropped_reasoning(
"thinking.budget_tokens", budget, model, _ADAPTIVE_BUDGET_REASON, drop_params_gated=False
)
coerced: Final[ThinkingPayload] = {"type": "adaptive"}
return coerced

View file

@ -34,6 +34,7 @@ from litellm.litellm_core_utils.json_fragment_accumulator import JSONFragmentAcc
from litellm.litellm_core_utils.prompt_templates.factory import (
encode_tool_call_id_with_signature,
)
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
@ -201,6 +202,13 @@ def _unsupported_reasoning_effort(reasoning_effort: str) -> UnsupportedParamsErr
_SAMPLING_DEPRECATION_UNMAPPED_MODELS: Final = "models outside litellm's model map"
_NON_INTEGER_BUDGET_REASON: Final = (
"Gemini takes budget_tokens only as an integer, so it uses its default thinking budget."
)
_LEVEL_ONLY_BUDGET_REASON: Final = (
"Gemini 3 and later set reasoning by thinking level, not budget, so litellm does not send the budget. "
"Send reasoning_effort to pick a level."
)
_sampling_deprecation_lock: Final = threading.Lock()
@ -1466,6 +1474,14 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
params["includeThoughts"] = False
else:
params["includeThoughts"] = sends_thoughts
if thinking_budget is not None:
warn_dropped_reasoning(
"thinking.budget_tokens",
thinking_budget,
model,
_LEVEL_ONLY_BUDGET_REASON,
drop_params_gated=False,
)
# Follow provider defaults unless explicitly opted into legacy behavior.
if litellm.enable_gemini_default_thinking_level_low is True:
params["thinkingLevel"] = (
@ -1478,8 +1494,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
# For older Gemini models, use thinkingBudget
if thinking_enabled and not VertexGeminiConfig._is_thinking_budget_zero(thinking_budget):
params["includeThoughts"] = sends_thoughts
if thinking_budget is not None and isinstance(thinking_budget, int):
if isinstance(thinking_budget, int):
params["thinkingBudget"] = thinking_budget
elif thinking_budget is not None:
warn_dropped_reasoning(
"thinking.budget_tokens",
thinking_budget,
model,
_NON_INTEGER_BUDGET_REASON,
drop_params_gated=False,
)
return params

View file

@ -1,3 +1,4 @@
from collections.abc import Mapping
from typing import Final
import httpx
@ -10,6 +11,14 @@ from litellm.types.llms.openai import AllMessageValues
from litellm.utils import add_path_to_api_base
def endpoint_under_api_base(endpoint: str, api_base: str | None) -> str:
return (
endpoint.lstrip("/").removeprefix("v1/")
if api_base is not None and api_base.rstrip("/").endswith("/v1")
else endpoint
)
class VLLMError(BaseLLMException):
def __init__(
self,
@ -39,9 +48,14 @@ class VLLMModelInfo(BaseLLMModelInfo):
api_key: str | None = None,
api_base: str | None = None,
) -> dict:
if api_key is not None:
headers["x-api-key"] = api_key
return headers
upstream_key: Final = self.upstream_api_key(api_key, api_base)
return {**headers, **self.upstream_auth_headers(upstream_key)} if upstream_key else headers
def upstream_api_key(self, api_key: str | None, api_base: str | None) -> str | None:
return api_key
def upstream_auth_headers(self, api_key: str) -> Mapping[str, str]:
return {"Authorization": f"Bearer {api_key}", "x-api-key": api_key}
@staticmethod
def get_api_base(api_base: str | None = None) -> str | None:

View file

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Final
from httpx import Response
from litellm.constants import PLACEHOLDER_API_KEY
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig
from litellm.llms.openai_like.passthrough.transformation import (
@ -12,8 +13,14 @@ from litellm.llms.openai_like.passthrough.transformation import (
logged_text_completion_stream,
relays_text_completion,
)
from litellm.secret_managers.main import (
GOOGLE_OIDC_SECRET_PREFIX,
UnresolvableSecretReference,
get_secret_str,
resolve_local_oidc_reference,
)
from ..common_utils import VLLMModelInfo
from ..common_utils import VLLMModelInfo, endpoint_under_api_base
if TYPE_CHECKING:
from httpx import URL
@ -22,6 +29,30 @@ if TYPE_CHECKING:
class VLLMPassthroughConfig(VLLMModelInfo, BasePassthroughConfig):
@staticmethod
def get_api_key(api_key: str | None = None) -> str | None:
return None if api_key == PLACEHOLDER_API_KEY else api_key
def upstream_api_key(self, api_key: str | None, api_base: str | None) -> str | None:
if api_key is not None or not self._is_vllm_api_base(api_base):
return api_key
vllm_api_key: Final = get_secret_str("VLLM_API_KEY")
if vllm_api_key is None or vllm_api_key.startswith(GOOGLE_OIDC_SECRET_PREFIX):
return None
match resolve_local_oidc_reference(vllm_api_key):
case UnresolvableSecretReference():
return None
case str() as resolved:
return resolved
@staticmethod
def _is_vllm_api_base(api_base: str | None) -> bool:
vllm_api_base: Final = get_secret_str("VLLM_API_BASE")
return api_base is not None and vllm_api_base is not None and api_base.rstrip("/") == vllm_api_base.rstrip("/")
def upstream_auth_headers(self, api_key: str) -> Mapping[str, str]:
return {"Authorization": f"Bearer {api_key}"}
def is_streaming_request(self, endpoint: str, request_data: dict) -> bool:
return "stream" in request_data
@ -40,7 +71,7 @@ class VLLMPassthroughConfig(VLLMModelInfo, BasePassthroughConfig):
raise Exception("VLLM api base not found")
return (
self.format_url(endpoint, base_target_url, request_query_params),
self.format_url(endpoint_under_api_base(endpoint, base_target_url), base_target_url, request_query_params),
base_target_url,
)

View file

@ -72,6 +72,7 @@ from litellm.llms.openai.common_utils import is_openai_agents_api_path, openai_a
from litellm.llms.openai.workload_identity import get_workload_identity_bearer_token_for_api_base
from litellm.llms.oss_decision import OssDecisionProvider, oss_connection, validate_oss_request
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
from litellm.llms.vllm.common_utils import endpoint_under_api_base
from litellm.passthrough.main import AsyncPassthroughStreamingResponse, is_provider_error
from litellm.proxy._types import *
from litellm.proxy.auth.auth_checks import enforced_model_allowlists
@ -600,7 +601,7 @@ async def vllm_proxy_route(
)
return await llm_passthrough_factory_proxy_route(
endpoint=endpoint,
endpoint=endpoint_under_api_base(endpoint, get_secret_str("VLLM_API_BASE")),
request=request,
fastapi_response=fastapi_response,
user_api_key_dict=user_api_key_dict,

View file

@ -38,7 +38,7 @@ from litellm.litellm_core_utils.reasoning_effort_strength import (
ReasoningEffortRounding,
nearest_declared_reasoning_effort,
)
from litellm.litellm_core_utils.reasoning_effort_utils import reasoning_effort_from_thinking
from litellm.litellm_core_utils.reasoning_effort_utils import reasoning_effort_from_thinking_silently
from litellm.llms.anthropic.common_utils import AnthropicModelInfo, validated_object_mapping
from litellm.types.llms.anthropic import AnthropicThinkingParam
from litellm.types.llms.openai import REASONING_EFFORT
@ -143,7 +143,7 @@ def with_adaptive_thinking_as_effort(
params: Mapping[str, object], *, model: str, custom_llm_provider: str, drop_params: bool
) -> Mapping[str, object]:
translated: Final = dict(params)
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model_silently(
model=model, optional_params=translated, custom_llm_provider=custom_llm_provider, drop_params=drop_params
)
return translated
@ -151,7 +151,7 @@ def with_adaptive_thinking_as_effort(
def thinking_budget_effort(thinking: object, *, model: str, custom_llm_provider: str, drop_params: bool) -> str | None:
if custom_llm_provider in _PROVIDERS_SENDING_THINKING_AS_EFFORT:
return reasoning_effort_from_thinking(thinking, drop_params=drop_params)
return reasoning_effort_from_thinking_silently(thinking, drop_params=drop_params)
adaptive_config: Final = as_str_mapping(
with_adaptive_thinking_as_effort(
{"thinking": thinking}, model=model, custom_llm_provider=custom_llm_provider, drop_params=drop_params
@ -162,7 +162,7 @@ def thinking_budget_effort(thinking: object, *, model: str, custom_llm_provider:
return adaptive_effort if isinstance(adaptive_effort, str) else None
if "thinking" in (get_supported_openai_params(model=model, custom_llm_provider=custom_llm_provider) or ()):
return None
return reasoning_effort_from_thinking(thinking, drop_params=drop_params)
return reasoning_effort_from_thinking_silently(thinking, drop_params=drop_params)
def provider_reads_thinking(thinking: object, *, model: str, custom_llm_provider: str, drop_params: bool) -> bool:

View file

@ -6,6 +6,7 @@ with guardrail transformations, specifically testing edge cases with empty choic
"""
import json
import logging
import re
from typing import Any, Literal, Optional, Final
from unittest.mock import MagicMock, patch
@ -3391,3 +3392,18 @@ class TestAnthropicMessagesHandlerPostCallHookResponse:
native = {"type": "message", "role": "assistant", "content": [{"type": "text", "text": "hi"}]}
assert AnthropicMessagesHandler().post_call_hook_response(native) is native
def test_reading_a_dropped_budget_for_a_guardrail_logs_no_drop_warning(caplog: pytest.LogCaptureFixture) -> None:
data: Final = {
"model": "gemini-2.5-flash",
"max_tokens": 64,
"thinking": {"type": "enabled", "budget_tokens": "high"},
"messages": [{"role": "user", "content": "hi"}],
}
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
messages: Final = AnthropicMessagesHandler().get_structured_messages(data)
assert messages == [{"role": "user", "content": "hi"}]
assert not [record for record in caplog.records if "Dropping" in record.getMessage()]

View file

@ -1,5 +1,6 @@
import copy
import json
import logging
from collections.abc import Mapping
from itertools import chain
from typing import Final, cast
@ -3195,6 +3196,47 @@ def test_reasoning_effort_accepts_dict_shape_for_non_adaptive_model(
)
def _reasoning_effort_drop_warnings(caplog: pytest.LogCaptureFixture) -> tuple[str, ...]:
return tuple(
record.getMessage() for record in caplog.records if "Dropping reasoning_effort=" in record.getMessage()
)
@pytest.mark.parametrize("reasoning_effort_value", [5, {"effort": 5, "summary": "concise"}])
def test_reasoning_effort_that_is_not_a_level_name_is_dropped_with_one_warning(
reasoning_effort_value: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
result: Final = AnthropicConfig().map_openai_params(
non_default_params={"reasoning_effort": reasoning_effort_value},
optional_params={},
model="claude-sonnet-4-6-20260219",
drop_params=False,
)
warnings: Final = _reasoning_effort_drop_warnings(caplog)
assert "thinking" not in result
assert "output_config" not in result
assert len(warnings) == 1
assert f"reasoning_effort={reasoning_effort_value!r}" in warnings[0]
assert "claude-sonnet-4-6-20260219" in warnings[0]
@pytest.mark.parametrize("reasoning_effort_value", [{"summary": "concise"}, {"effort": "low", "summary": "concise"}])
def test_reasoning_effort_with_a_level_name_or_no_level_logs_no_drop(
reasoning_effort_value: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
AnthropicConfig().map_openai_params(
non_default_params={"reasoning_effort": reasoning_effort_value},
optional_params={},
model="claude-sonnet-4-6-20260219",
drop_params=False,
)
assert _reasoning_effort_drop_warnings(caplog) == ()
@pytest.mark.parametrize(
"model,budget_tokens,expected",
[

View file

@ -1,6 +1,7 @@
import base64
import functools
import json
import logging
from collections.abc import Mapping
from types import MappingProxyType
from typing import Any, Final, cast
@ -2924,14 +2925,21 @@ def test_translate_completion_input_params_rejects_a_non_numeric_budget_without_
@pytest.mark.parametrize("budget", ["2048", True])
def test_translate_completion_input_params_drops_a_non_numeric_budget_under_drop_params(
budget: object, monkeypatch: pytest.MonkeyPatch
budget: object, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
):
monkeypatch.setattr(litellm, "drop_params", False)
translated: Final = AnthropicAdapter().translate_completion_input_params(
_gemini_request_with_budget(budget, drop_params=True)
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
translated: Final = AnthropicAdapter().translate_completion_input_params(
_gemini_request_with_budget(budget, drop_params=True)
)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert translated is not None
assert not {"reasoning_effort", "thinking"} & translated.keys()
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0]
for fragment in (f"thinking.budget_tokens={budget!r}", "gemini-2.5-flash", "(drop_params=True)")
)
def test_translate_openai_content_to_anthropic_reasoning_content_without_thinking_blocks():

View file

@ -8,6 +8,7 @@ leaves the adapter, in the shape the target expects.
import functools
import json
import logging
from collections.abc import Mapping
from typing import Final
@ -510,26 +511,33 @@ class TestADeclaredDeploymentRoundsTheMessagesTierOntoItsLevels:
@pytest.mark.asyncio
@pytest.mark.parametrize("budget", ["2048", True])
@pytest.mark.parametrize("litellm_params, model_info, upstream_url, upstream_body", _MALFORMED_BUDGET_TARGETS)
async def test_drop_params_drops_a_non_numeric_budget(
async def test_drop_params_drops_a_non_numeric_budget_with_one_warning(
self,
local_model_cost_map: None,
respx_mock: respx.MockRouter,
caplog: pytest.LogCaptureFixture,
litellm_params: Mapping[str, object],
model_info: Mapping[str, object],
upstream_url: str,
upstream_body: Mapping[str, object],
budget: object,
) -> None:
sent: Final = await _body_sent_upstream(
respx_mock,
{**litellm_params, "drop_params": True},
model_info,
upstream_url,
upstream_body,
thinking={"type": "enabled", "budget_tokens": budget},
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
sent: Final = await _body_sent_upstream(
respx_mock,
{**litellm_params, "drop_params": True},
model_info,
upstream_url,
upstream_body,
thinking={"type": "enabled", "budget_tokens": budget},
)
assert not {"reasoning_effort", "reasoning", "thinking"} & sent.keys()
dropped: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert len(dropped) == 1
assert f"thinking.budget_tokens={budget!r}" in dropped[0]
assert "(drop_params=True)" in dropped[0]
assert str(litellm_params["model"]).split("/", 1)[1] in dropped[0]
@pytest.mark.asyncio
@pytest.mark.parametrize(

View file

@ -159,7 +159,7 @@ async def test_openai_model_does_not_forward_stream_options_to_responses_api():
await litellm.anthropic.messages.acreate(
max_tokens=100,
messages=[{"role": "user", "content": "Hello, how are you?"}],
model="openai/gpt-5.5",
model="openai/gpt-6.1-sol",
api_key="test-api-key",
stream_options={"include_usage": True},
)
@ -669,7 +669,9 @@ class TestThinkingSummaryPreservation:
)
thinking = {"type": "enabled", "budget_tokens": 5000, "summary": "concise"}
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking)
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(
thinking, model="openai/gpt-6.1-sol"
)
assert result == {"effort": "high", "summary": "concise"}
def test_responses_adapter_no_summary_by_default(self):
@ -683,7 +685,9 @@ class TestThinkingSummaryPreservation:
try:
litellm.reasoning_auto_summary = False
thinking = {"type": "enabled", "budget_tokens": 5000}
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking)
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(
thinking, model="openai/gpt-6.1-sol"
)
assert result == {"effort": "high"}
assert result is not None and "summary" not in result
finally:

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import base64
import json
import logging
import struct
import time
import uuid
@ -891,24 +892,32 @@ async def test_native_messages_send_no_effort_for_a_default_effort_beside_a_thin
@pytest.mark.asyncio
@pytest.mark.parametrize("model_info", [None, _DECLARING_LOW_AND_HIGH])
@pytest.mark.parametrize("drop_globally", [False, True])
async def test_native_messages_drop_params_drops_a_non_numeric_thinking_budget_on_adaptive_claude(
async def test_native_messages_drop_params_drops_a_non_numeric_thinking_budget_on_adaptive_claude_with_one_warning(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
model_info: Mapping[str, object] | None,
drop_globally: bool,
) -> None:
monkeypatch.setattr(litellm, "drop_params", drop_globally)
request_drop: Final = {} if drop_globally else {"drop_params": True}
dropped: Final = await _native_opus_body_sent(
respx_mock, model_info, thinking={"type": "enabled", "budget_tokens": "lots"}, **request_drop
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
dropped: Final = await _native_opus_body_sent(
respx_mock, model_info, thinking={"type": "enabled", "budget_tokens": "lots"}, **request_drop
)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
adaptive_alone: Final = await _native_opus_body_sent(
respx_mock, model_info, thinking={"type": "adaptive"}, **request_drop
)
assert "output_config" not in adaptive_alone
assert dropped == adaptive_alone
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0]
for fragment in ("thinking.budget_tokens='lots'", "claude-opus-5-5", "(drop_params=True)")
)
_MIXED_GROUP: Final = "gpt-5.6-mixed"

View file

@ -1,5 +1,8 @@
"""Tests for ``reasoning_effort`` translation on the Anthropic /v1/messages route."""
import logging
from typing import Final
import pytest
from litellm.constants import (
@ -46,6 +49,49 @@ def test_reasoning_effort_maps_to_output_config_for_adaptive_model(
assert result.get("output_config") == {"effort": expected_effort}
def _reasoning_effort_drop_warnings(caplog: pytest.LogCaptureFixture) -> tuple[str, ...]:
return tuple(
record.getMessage() for record in caplog.records if "Dropping reasoning_effort=" in record.getMessage()
)
@pytest.mark.parametrize("reasoning_effort", [5, {"effort": "high"}])
def test_reasoning_effort_that_is_not_a_string_is_dropped_with_one_warning(
reasoning_effort: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
result: Final = AnthropicMessagesConfig().transform_anthropic_messages_request(
model="claude-opus-4-7",
messages=[{"role": "user", "content": "Hello"}],
anthropic_messages_optional_request_params={"max_tokens": 1024, "reasoning_effort": reasoning_effort},
litellm_params={},
headers={},
)
warnings: Final = _reasoning_effort_drop_warnings(caplog)
assert "reasoning_effort" not in result
assert "output_config" not in result
assert len(warnings) == 1
assert f"reasoning_effort={reasoning_effort!r}" in warnings[0]
assert "claude-opus-4-7" in warnings[0]
@pytest.mark.parametrize("reasoning_effort", ["high", {"summary": "concise"}])
def test_reasoning_effort_that_is_a_string_or_names_no_level_logs_no_drop(
reasoning_effort: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
AnthropicMessagesConfig().transform_anthropic_messages_request(
model="claude-opus-4-7",
messages=[{"role": "user", "content": "Hello"}],
anthropic_messages_optional_request_params={"max_tokens": 1024, "reasoning_effort": reasoning_effort},
litellm_params={},
headers={},
)
assert _reasoning_effort_drop_warnings(caplog) == ()
def test_reasoning_effort_none_clears_thinking_and_output_config():
config = AnthropicMessagesConfig()
optional_params = {

View file

@ -44,6 +44,7 @@ def _make_request(**overrides) -> AnthropicMessagesRequest:
_ADAPTER = LiteLLMAnthropicToResponsesAPIAdapter()
_MODEL: Final = "openai/gpt-6.1-sol"
# ---------------------------------------------------------------------------
@ -957,13 +958,13 @@ class TestTranslateThinkingToReasoning:
"""Anthropic thinking param -> Responses API reasoning param."""
def test_budget_high_effort(self):
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 10000})
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 10000}, model=_MODEL)
# Default (reasoning_auto_summary=False): only effort, no summary
assert result == {"effort": "high"}
assert result is not None and "summary" not in result
def test_budget_above_threshold_high_effort(self):
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 50000})
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 50000}, model=_MODEL)
assert result is not None
assert result["effort"] == "high"
assert "summary" not in result
@ -973,7 +974,8 @@ class TestTranslateThinkingToReasoning:
{
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
}
},
model=_MODEL,
)
assert result == {"effort": "medium"}
assert result is not None and "summary" not in result
@ -983,13 +985,14 @@ class TestTranslateThinkingToReasoning:
{
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
}
},
model=_MODEL,
)
assert result == {"effort": "low"}
assert result is not None and "summary" not in result
def test_budget_minimal_effort(self):
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 500})
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 500}, model=_MODEL)
assert result == {"effort": "minimal"}
assert result is not None and "summary" not in result
@ -998,7 +1001,8 @@ class TestTranslateThinkingToReasoning:
{
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
}
},
model=_MODEL,
)
assert result_high is not None
assert result_high["effort"] == "high"
@ -1006,7 +1010,8 @@ class TestTranslateThinkingToReasoning:
{
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
}
},
model=_MODEL,
)
assert result_medium is not None
assert result_medium["effort"] == "medium"
@ -1015,23 +1020,24 @@ class TestTranslateThinkingToReasoning:
{
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
}
},
model=_MODEL,
)
assert result_low is not None
assert result_low["effort"] == "low"
assert "summary" not in result_low
def test_disabled_type_returns_none(self):
result = _ADAPTER.translate_thinking_to_reasoning({"type": "disabled"})
result = _ADAPTER.translate_thinking_to_reasoning({"type": "disabled"}, model=_MODEL)
assert result is None
def test_non_dict_returns_none(self):
result = _ADAPTER.translate_thinking_to_reasoning("enabled") # type: ignore
result = _ADAPTER.translate_thinking_to_reasoning("enabled", model=_MODEL) # type: ignore
assert result is None
def test_missing_budget_defaults_to_minimal(self):
"""Missing budget_tokens defaults to 0, below the low threshold -> minimal."""
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled"})
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled"}, model=_MODEL)
assert result == {"effort": "minimal"}
assert result is not None and "summary" not in result
@ -1042,7 +1048,7 @@ class TestTranslateThinkingToReasoning:
original = litellm.reasoning_auto_summary
try:
litellm.reasoning_auto_summary = True
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 10000})
result = _ADAPTER.translate_thinking_to_reasoning({"type": "enabled", "budget_tokens": 10000}, model=_MODEL)
assert result == {"effort": "high", "summary": "detailed"}
finally:
litellm.reasoning_auto_summary = original
@ -1059,7 +1065,8 @@ class TestTranslateThinkingToReasoning:
{
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
}
},
model=_MODEL,
)
assert result == {"effort": "medium", "summary": "detailed"}
finally:

View file

@ -162,7 +162,8 @@ class TestAdapterAdaptiveThinking:
adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_thinking_to_reasoning_effort(
{"type": "adaptive"}
{"type": "adaptive"},
model="openai/gpt-6.1-sol",
)
assert result == "medium"
@ -198,6 +199,7 @@ class TestAdapterAdaptiveThinking:
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(
thinking={"type": "adaptive"},
output_config={"effort": "xhigh"},
model="openai/gpt-6.1-sol",
)
assert result is not None
assert result["effort"] == "xhigh"
@ -210,6 +212,7 @@ class TestAdapterAdaptiveThinking:
result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(
thinking={"type": "adaptive"},
model="openai/gpt-6.1-sol",
)
assert result is not None
assert result["effort"] == "medium"

View file

@ -11,6 +11,7 @@ Verifies that:
"""
import json
import logging
import os
import sys
import threading
@ -4530,16 +4531,25 @@ def test_shared_legacy_thinking_translation_rejects_a_non_numeric_budget(budget_
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize("budget_tokens", _NON_NUMERIC_BUDGETS)
def test_shared_legacy_thinking_translation_drops_a_non_numeric_budget_under_drop_params(budget_tokens: object) -> None:
def test_shared_legacy_thinking_translation_drops_a_non_numeric_budget_under_drop_params(
budget_tokens: object, caplog: pytest.LogCaptureFixture
) -> None:
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
optional_params: Final = {"thinking": {"type": "enabled", "budget_tokens": budget_tokens}}
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
model="claude-opus-5", optional_params=optional_params, custom_llm_provider="azure_ai", drop_params=True
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
model="claude-opus-5", optional_params=optional_params, custom_llm_provider="azure_ai", drop_params=True
)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert optional_params == {"thinking": {"type": "adaptive"}}
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0]
for fragment in (f"thinking.budget_tokens={budget_tokens!r}", "claude-opus-5", "(drop_params=True)")
)
@pytest.mark.parametrize(

View file

@ -1,4 +1,5 @@
import json
import logging
from collections.abc import Mapping
from datetime import datetime
from types import SimpleNamespace
@ -597,6 +598,45 @@ async def test_native_count_preserves_settings_and_requires_every_prefix(
assert unexpected.call_count == 0
@pytest.mark.usefixtures("local_model_cost_map")
def test_native_baseline_body_drops_a_non_numeric_budget_without_logging_a_drop(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
monkeypatch.setattr(litellm, "drop_params", True)
request: Final = {
"model": "claude-opus-5",
"max_tokens": 64,
"messages": [{"role": "user", "content": "hi"}],
"thinking": {"type": "enabled", "budget_tokens": "high"},
}
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
body: Final = prepare_native_baseline_body(request, "anthropic/claude-opus-5")
assert body is not None
assert body["thinking"] == {"type": "adaptive"}
assert [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()] == []
@pytest.mark.usefixtures("local_model_cost_map")
def test_native_baseline_body_drops_a_non_string_reasoning_effort_without_logging_a_drop(
caplog: pytest.LogCaptureFixture,
) -> None:
request: Final = {
"model": "claude-opus-5",
"max_tokens": 64,
"messages": [{"role": "user", "content": "hi"}],
"reasoning_effort": 5,
}
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
body: Final = prepare_native_baseline_body(request, "anthropic/claude-opus-5")
assert body is not None
assert "reasoning_effort" not in body
assert [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()] == []
def test_native_baseline_body_matches_the_sent_body_when_drop_params_meets_an_auto_added_beta(
monkeypatch: pytest.MonkeyPatch,
) -> None:

View file

@ -1,4 +1,5 @@
import json
import logging
import traceback
from collections.abc import Callable
from types import MappingProxyType
@ -758,6 +759,66 @@ def test_drop_tool_level_extra_fields_strips_copilot_mcp_server_name():
assert result["tools"][1]["function"]["name"] == "read_file"
def test_extra_parameters_error_drops_reasoning_effort_with_one_warning(caplog: pytest.LogCaptureFixture) -> None:
import httpx
error_text: Final = json.dumps(
{
"error": {
"message": "Extra parameters ['reasoning_effort'] are not allowed when extra-parameters is not set "
"or set to be 'error'. Set extra-parameters to 'pass-through' to pass to the model."
}
}
)
error: Final = httpx.HTTPStatusError(
message="422",
request=httpx.Request("POST", "https://example.services.ai.azure.com/models/chat/completions"),
response=httpx.Response(422, text=error_text),
)
request_data: Final = {
"model": "Kimi-K2.6",
"messages": [{"role": "user", "content": "Say hi."}],
"reasoning_effort": "high",
}
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
retried: Final = AzureAIStudioConfig().transform_request_on_unprocessable_entity_error(error, request_data)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert retried == {"model": "Kimi-K2.6", "messages": [{"role": "user", "content": "Say hi."}]}
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("reasoning_effort='high'", "Kimi-K2.6"))
assert "drop_params" not in drop_warnings[0]
def test_extra_inputs_error_names_drop_params_in_the_reasoning_drop_warning(caplog: pytest.LogCaptureFixture) -> None:
import httpx
detail: Final = [
{"type": "extra_forbidden", "loc": ["body", "reasoning_effort"], "msg": "Extra inputs are not permitted"}
]
error: Final = httpx.HTTPStatusError(
message="422",
request=httpx.Request("POST", "https://example.services.ai.azure.com/models/chat/completions"),
response=httpx.Response(422, text=json.dumps({"error": {"message": json.dumps({"detail": detail})}})),
)
request_data: Final = {
"model": "Mistral-Large-3",
"messages": [{"role": "user", "content": "Say hi."}],
"reasoning_effort": "high",
}
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
retried: Final = AzureAIStudioConfig().transform_request_on_unprocessable_entity_error(error, request_data)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert retried == {"model": "Mistral-Large-3", "messages": [{"role": "user", "content": "Say hi."}]}
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0] for fragment in ("reasoning_effort='high'", "Mistral-Large-3", "(drop_params=True)")
)
def _find_key_anywhere(obj, key: str) -> bool:
if isinstance(obj, dict):
if key in obj:

View file

@ -1,6 +1,7 @@
"""Bedrock Runtime Chat Completions: the default for GPT 5.6 and newer, ``bedrock/chat_completions/<model>`` for the rest."""
import json
import logging
import re
import httpx
@ -706,18 +707,30 @@ def test_with_max_completion_tokens_leaves_other_params_alone():
@pytest.mark.parametrize(
"model",
["us.xai.grok-4.6", "bedrock/us-gov-west-1/us.xai.grok-4.6"],
("model", "logged_model"),
[
("us.xai.grok-4.6", "us.xai.grok-4.6"),
("chat_completions/us.xai.grok-4.6", "us.xai.grok-4.6"),
("bedrock/us-gov-west-1/us.xai.grok-4.6", "us-gov-west-1/us.xai.grok-4.6"),
],
)
def test_map_openai_params_drops_reasoning_effort_none_for_grok(model):
def test_map_openai_params_drops_reasoning_effort_none_for_grok_with_one_warning(
model: str, logged_model: str, caplog: pytest.LogCaptureFixture
) -> None:
cfg = AmazonBedrockRuntimeChatCompletionsConfig()
mapped = cfg.map_openai_params(
non_default_params={"reasoning_effort": "none", "max_tokens": 64},
optional_params={},
model=model,
drop_params=False,
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped = cfg.map_openai_params(
non_default_params={"reasoning_effort": "none", "max_tokens": 64},
optional_params={},
model=model,
drop_params=False,
)
drop_warnings = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert "reasoning_effort" not in mapped
assert len(drop_warnings) == 1
assert "reasoning_effort='none'" in drop_warnings[0]
assert f"model={logged_model!r}" in drop_warnings[0]
assert "drop_params" not in drop_warnings[0]
def test_map_openai_params_keeps_reasoning_effort_low_for_grok():
@ -750,17 +763,24 @@ def test_map_openai_params_refuses_a_non_string_reasoning_effort_without_drop_pa
@pytest.mark.parametrize("reasoning_effort", [["low"], {"effort": "low"}, 5], ids=["list", "object", "int"])
@pytest.mark.parametrize("drop_params_via", ["request", "litellm.drop_params"])
def test_map_openai_params_drops_a_non_string_reasoning_effort_under_drop_params(
monkeypatch, model, reasoning_effort, drop_params_via
monkeypatch, caplog, model, reasoning_effort, drop_params_via
):
monkeypatch.setattr(litellm, "drop_params", drop_params_via == "litellm.drop_params")
mapped = AmazonBedrockRuntimeChatCompletionsConfig().map_openai_params(
non_default_params={"reasoning_effort": reasoning_effort, "max_tokens": 64},
optional_params={},
model=model,
drop_params=drop_params_via == "request",
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped = AmazonBedrockRuntimeChatCompletionsConfig().map_openai_params(
non_default_params={"reasoning_effort": reasoning_effort, "max_tokens": 64},
optional_params={},
model=model,
drop_params=drop_params_via == "request",
)
drop_warnings = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert "reasoning_effort" not in mapped
assert mapped["max_completion_tokens"] == 64
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0]
for fragment in (f"reasoning_effort={reasoning_effort!r}", repr(model), "(drop_params=True)")
)
def test_map_openai_params_keeps_reasoning_effort_none_for_gpt56():

View file

@ -13,6 +13,7 @@ import pytest
import respx
import litellm
from litellm._logging import verbose_logger
from litellm import ModelResponse, Router, completion
from litellm.litellm_core_utils.prompt_templates.factory import (
_bedrock_converse_messages_pt,
@ -11096,6 +11097,52 @@ def test_a_deployment_with_a_batch_control_plane_endpoint_keeps_it_out_of_chat_r
assert "aws_bedrock_control_plane_endpoint" not in route.calls[0].request.content.decode()
def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str, ...]:
return tuple(
r.getMessage() for r in caplog.records if r.levelno == logging.WARNING and f"Dropping {param}=" in r.getMessage()
)
_DEEPSEEK_R1_PROFILE_ARN: Final = "arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.deepseek.r1-v1:0"
@pytest.mark.parametrize(
"param, value", (("thinking", {"type": "enabled", "budget_tokens": 1024}), ("reasoning_effort", "high"))
)
def test_bedrock_deepseek_arn_reasoning_drop_warns_once(
param: str, value: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
optional_params: Final = AmazonConverseConfig().map_openai_params(
non_default_params={param: value, "max_tokens": 100},
optional_params={},
model=_DEEPSEEK_R1_PROFILE_ARN,
drop_params=False,
)
warnings: Final = _warnings_naming(caplog, param)
assert param not in optional_params
assert len(warnings) == 1
assert _DEEPSEEK_R1_PROFILE_ARN in warnings[0]
@pytest.mark.parametrize(
"param, value", (("thinking", {"type": "enabled", "budget_tokens": 1024}), ("reasoning_effort", "high"))
)
def test_bedrock_deepseek_r1_drop_of_an_unadvertised_param_does_not_warn(
param: str, value: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
AmazonConverseConfig().map_openai_params(
non_default_params={param: value, "max_tokens": 100},
optional_params={},
model="us.deepseek.r1-v1:0",
drop_params=False,
)
assert _warnings_naming(caplog, param) == ()
def _converse_bound_thinking_request(
headers: dict[str, str] | None, body_betas: tuple[str, ...] = (), drop_params: bool = True
) -> dict[str, object]:

View file

@ -1,4 +1,5 @@
import json
import logging
from collections.abc import Iterator
from typing import Final
@ -6,6 +7,7 @@ import httpx
import litellm
import pytest
import respx
from litellm._logging import verbose_logger
from litellm.caching.llm_caching_handler import LLMClientCache
from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig
@ -951,3 +953,42 @@ def test_get_complete_url_ignores_a_trailing_slash_on_api_base(api_base: str):
)
== "https://gateway.example.com/deepseek/chat/completions"
)
def _budget_warnings(caplog: pytest.LogCaptureFixture) -> tuple[str, ...]:
return tuple(
r.getMessage()
for r in caplog.records
if r.levelno == logging.WARNING and "Dropping thinking.budget_tokens=" in r.getMessage()
)
def test_enabled_thinking_budget_dropped_for_deepseek_warns_once(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = DeepSeekChatConfig().map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4000}},
optional_params={},
model="deepseek-reasoner",
drop_params=False,
)
warnings: Final = _budget_warnings(caplog)
assert result["thinking"] == {"type": "enabled"}
assert len(warnings) == 1
assert "budget_tokens=4000" in warnings[0]
assert "deepseek-reasoner" in warnings[0]
@pytest.mark.parametrize(
"thinking", ({"type": "enabled"}, {"type": "disabled", "budget_tokens": 4000}), ids=("no-budget", "disabled")
)
def test_deepseek_thinking_without_a_requested_budget_does_not_warn(
thinking: dict[str, object], caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = DeepSeekChatConfig().map_openai_params(
non_default_params={"thinking": thinking}, optional_params={}, model="deepseek-reasoner", drop_params=False
)
assert result["thinking"] == {"type": thinking["type"]}
assert _budget_warnings(caplog) == ()

View file

@ -1,3 +1,6 @@
import logging
from typing import Final
import httpx
import pytest
@ -242,11 +245,33 @@ def test_map_openai_params_maps_reasoning_effort(effort, expected):
assert mapped["reasoning"] is expected
def test_map_openai_params_drops_unknown_reasoning_effort_when_dropping():
mapped = FalAIChatConfig().map_openai_params(
non_default_params={"reasoning_effort": "extreme"}, optional_params={}, model=MODEL, drop_params=True
)
@pytest.mark.parametrize("drop_globally", [False, True])
def test_map_openai_params_drops_unknown_reasoning_effort_with_one_warning(
drop_globally: bool, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
monkeypatch.setattr(litellm, "drop_params", drop_globally)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
mapped: Final = FalAIChatConfig().map_openai_params(
non_default_params={"reasoning_effort": "xhigh"},
optional_params={},
model=MODEL,
drop_params=not drop_globally,
)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert "reasoning" not in mapped
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("reasoning_effort='xhigh'", MODEL, "(drop_params=True)"))
def test_map_openai_params_rejects_unknown_reasoning_effort_without_drop_params(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(litellm, "drop_params", False)
with pytest.raises(FalAIError) as exc_info:
FalAIChatConfig().map_openai_params(
non_default_params={"reasoning_effort": "xhigh"}, optional_params={}, model=MODEL, drop_params=False
)
assert exc_info.value.status_code == 400
@pytest.mark.parametrize("effort", [{"level": "low"}, ["low"], 1])

View file

@ -1,4 +1,5 @@
import json
import logging
from collections.abc import Callable
from typing import Final
from unittest.mock import MagicMock, patch
@ -9,6 +10,7 @@ import respx
from pydantic import ValidationError
import litellm
from litellm._logging import verbose_logger
from litellm.constants import SESSION_ID_GENERATED_METADATA_KEY
from litellm.litellm_core_utils.get_supported_openai_params import get_supported_openai_params
from litellm.llms.custom_httpx.http_handler import HTTPHandler
@ -2361,6 +2363,39 @@ def test_global_disable_flag_with_transform_messages_helper(
assert json.loads(route.calls.last.request.content)["messages"] == [{"role": "user", "content": content}]
def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str, ...]:
return tuple(
r.getMessage() for r in caplog.records if r.levelno == logging.WARNING and f"Dropping {param}=" in r.getMessage()
)
def test_chat_template_kwargs_effort_dropped_for_non_reasoning_model_warns_once(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
FireworksAIConfig().map_extra_body_params(
{"extra_body": {"chat_template_kwargs": {"reasoning_budget": 512}}},
_NON_REASONING_MODEL,
)
warnings: Final = _warnings_naming(caplog, "chat_template_kwargs")
assert len(warnings) == 1
assert "'reasoning_budget': 512" in warnings[0]
assert "llama-v3-70b-instruct" in warnings[0]
def test_chat_template_kwargs_thinking_off_for_non_reasoning_model_does_not_warn(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
FireworksAIConfig().map_extra_body_params(
{"extra_body": {"chat_template_kwargs": {"enable_thinking": False}}},
_NON_REASONING_MODEL,
)
assert _warnings_naming(caplog, "chat_template_kwargs") == ()
_CODEX_DEVELOPER_MESSAGE_AFTER_A_USER_TURN: Final = (
{"role": "system", "content": "You are Codex"},
{"role": "user", "content": "Hi there"},

View file

@ -1,7 +1,11 @@
import logging
from typing import Final
import pytest
import litellm
from litellm._logging import verbose_logger
from litellm.llms.fireworks_ai.completion.transformation import (
@ -204,3 +208,36 @@ def test_transform_text_completion_request_keeps_sdk_rejected_keys_in_extra_body
assert "chat_template_kwargs" not in data["extra_body"]
assert "best_of" not in data["extra_body"]
assert "response_format" not in data
def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str, ...]:
return tuple(
r.getMessage() for r in caplog.records if r.levelno == logging.WARNING and f"Dropping {param}=" in r.getMessage()
)
def test_chat_template_kwargs_effort_dropped_for_non_reasoning_model_warns_once(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
FireworksAITextCompletionConfig().map_extra_body_params(
{"extra_body": {"chat_template_kwargs": {"reasoning_budget": 512}}},
_NON_REASONING_MODEL,
)
warnings: Final = _warnings_naming(caplog, "chat_template_kwargs")
assert len(warnings) == 1
assert "'reasoning_budget': 512" in warnings[0]
assert "llama-v3-70b-instruct" in warnings[0]
def test_chat_template_kwargs_thinking_off_for_non_reasoning_model_does_not_warn(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
FireworksAITextCompletionConfig().map_extra_body_params(
{"extra_body": {"chat_template_kwargs": {"enable_thinking": False}}},
_NON_REASONING_MODEL,
)
assert _warnings_naming(caplog, "chat_template_kwargs") == ()

View file

@ -1,4 +1,5 @@
import json
import logging
from collections.abc import Mapping
from typing import Final, List, cast
from unittest.mock import MagicMock, patch
@ -9,6 +10,7 @@ import pytest
import respx
from openai import AsyncOpenAI
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.prompt_templates.common_utils import TOOL_RESULT_IMAGE_BOUNDARY
from litellm.llms.mistral.chat.transformation import (
MistralChatResponseIterator,
@ -1670,6 +1672,109 @@ def test_mistral_transform_request_drops_an_empty_tool_message_whose_call_id_is_
assert [m.get("role") for m in request["messages"]] == ["user", "assistant"]
def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str, ...]:
return tuple(
r.getMessage() for r in caplog.records if r.levelno == logging.WARNING and f"Dropping {param}=" in r.getMessage()
)
@pytest.mark.parametrize(
"param, value",
(
("reasoning_effort", "none"),
("reasoning_effort", "xhigh"),
("thinking", {"type": "disabled"}),
("thinking", {"type": ["enabled"]}),
),
)
def test_magistral_reasoning_request_replaced_by_prompt_warns_once(
param: str, value: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = MistralConfig().map_openai_params(
non_default_params={param: value},
optional_params={},
model="magistral-medium-latest",
drop_params=False,
)
warnings: Final = _warnings_naming(caplog, param)
assert result["_add_reasoning_prompt"] is True
assert len(warnings) == 1
assert "magistral-medium-latest" in warnings[0]
@pytest.mark.parametrize(
"param, value",
(
("reasoning_effort", "low"),
("reasoning_effort", "medium"),
("reasoning_effort", "high"),
("thinking", {"type": "enabled", "budget_tokens": 1024}),
),
)
def test_magistral_reasoning_request_the_prompt_honors_does_not_warn(
param: str, value: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = MistralConfig().map_openai_params(
non_default_params={param: value},
optional_params={},
model="magistral-medium-latest",
drop_params=False,
)
assert result["_add_reasoning_prompt"] is True
assert _warnings_naming(caplog, param) == ()
def test_non_magistral_reasoning_effort_is_sent_without_warning(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = MistralConfig().map_openai_params(
non_default_params={"reasoning_effort": "high"},
optional_params={},
model="mistral-medium-latest",
drop_params=False,
)
assert result["reasoning_effort"] == "high"
assert _warnings_naming(caplog, "reasoning_effort") == ()
@pytest.mark.parametrize("reasoning_effort", [5, {"effort": 5}, {"effort": 5, "summary": "auto"}])
def test_mistral_warns_once_when_drop_params_drops_a_level_it_cannot_send(
caplog: pytest.LogCaptureFixture, reasoning_effort: object
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
result: Final = MistralConfig().map_openai_params(
non_default_params={"reasoning_effort": reasoning_effort},
optional_params={},
model="mistral-medium-latest",
drop_params=True,
)
assert "reasoning_effort" not in result
warnings: Final = _warnings_naming(caplog, "reasoning_effort")
assert len(warnings) == 1
assert "model='mistral-medium-latest'" in warnings[0]
assert "(drop_params=True)" in warnings[0]
@pytest.mark.parametrize("reasoning_effort", [{"summary": "auto"}, {"effort": "high", "summary": "auto"}])
def test_mistral_stays_quiet_under_drop_params_when_no_level_is_lost(
caplog: pytest.LogCaptureFixture, reasoning_effort: object
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
MistralConfig().map_openai_params(
non_default_params={"reasoning_effort": reasoning_effort},
optional_params={},
model="mistral-medium-latest",
drop_params=True,
)
assert _warnings_naming(caplog, "reasoning_effort") == ()
@pytest.mark.parametrize(
("model", "custom_llm_provider"),
[

View file

@ -1,4 +1,5 @@
import datetime
import logging
from typing import Final
import httpx
import pytest
@ -1103,19 +1104,25 @@ class TestOCIReasoningEffort:
)
assert chat_request.get("reasoningEffort") == "HIGH"
def test_reasoning_effort_unsupported_on_cohere_dropped(self):
"""drop_params=True → silently drop reasoning_effort for Cohere."""
def test_reasoning_effort_unsupported_on_cohere_dropped_with_one_warning(self, caplog):
from litellm.llms.oci.chat.transformation import OCIChatConfig
config = OCIChatConfig()
result = config.map_openai_params(
non_default_params={"reasoning_effort": "low"},
optional_params={},
model="cohere.command-latest",
drop_params=True,
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
result = config.map_openai_params(
non_default_params={"reasoning_effort": "low"},
optional_params={},
model="cohere.command-latest",
drop_params=True,
)
drop_warnings = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert "reasoning_effort" not in result
assert "reasoningEffort" not in result
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0]
for fragment in ("reasoning_effort='low'", "cohere.command-latest", "(drop_params=True)")
)
def test_reasoning_effort_unsupported_on_cohere_raises(self):
"""drop_params=False → raise rather than ship a payload Cohere will reject."""
@ -2110,16 +2117,20 @@ class TestReasoningEffortGating:
assert "reasoning_effort" not in OCIChatConfig().get_supported_openai_params("meta.llama-3.3-70b-instruct")
@pytest.mark.parametrize("model", SERVED_GROK_4_20_MODELS)
def test_reasoning_effort_dropped_under_drop_params(self, local_model_cost_map, model):
result = OCIChatConfig().map_openai_params(
non_default_params={"reasoning_effort": "low", "temperature": 0.1},
optional_params={},
model=model,
drop_params=True,
)
def test_reasoning_effort_dropped_under_drop_params_with_one_warning(self, local_model_cost_map, model, caplog):
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
result = OCIChatConfig().map_openai_params(
non_default_params={"reasoning_effort": "low", "temperature": 0.1},
optional_params={},
model=model,
drop_params=True,
)
drop_warnings = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert "reasoningEffort" not in result
assert "reasoning_effort" not in result
assert result["temperature"] == 0.1
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("reasoning_effort='low'", model, "(drop_params=True)"))
def test_reasoning_effort_raises_without_drop_params_for_gated_grok(self, local_model_cost_map, monkeypatch):
monkeypatch.setattr(litellm, "drop_params", False)

View file

@ -1,5 +1,6 @@
import asyncio
import inspect
import logging
import os
import sys
from typing import Final, cast
@ -23,6 +24,7 @@ import json
from unittest.mock import ANY, MagicMock, patch
import litellm
from litellm._logging import verbose_logger
from litellm.types.utils import Choices, EmbeddingResponse, Message, ModelResponse, ModelResponseStream
from unittest import mock
@ -1114,3 +1116,36 @@ def test_get_complete_url_ignores_a_trailing_slash_on_api_base(api_base: str):
)
== "http://gateway.example.com/ollama/api/chat"
)
def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str, ...]:
return tuple(
r.getMessage() for r in caplog.records if r.levelno == logging.WARNING and f"Dropping {param}=" in r.getMessage()
)
@pytest.mark.parametrize("effort", ("minimal", "xhigh"))
def test_reasoning_effort_mapped_to_think_off_warns_once(effort: str, caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = OllamaChatConfig().map_openai_params(
non_default_params={"reasoning_effort": effort}, optional_params={}, model="qwen3", drop_params=False
)
warnings: Final = _warnings_naming(caplog, "reasoning_effort")
assert result["think"] is False
assert len(warnings) == 1
assert f"reasoning_effort={effort!r}" in warnings[0]
assert "qwen3" in warnings[0]
@pytest.mark.parametrize("effort, think", (("none", False), ("low", True), ("high", True)))
def test_reasoning_effort_with_a_matching_think_value_does_not_warn(
effort: str, think: bool, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = OllamaChatConfig().map_openai_params(
non_default_params={"reasoning_effort": effort}, optional_params={}, model="qwen3", drop_params=False
)
assert result["think"] is think
assert _warnings_naming(caplog, "reasoning_effort") == ()

View file

@ -1,6 +1,7 @@
import base64
import io
import json
import logging
import sys
from typing import Final
from unittest.mock import MagicMock, patch
@ -10,6 +11,7 @@ import pytest
import respx
import litellm
from litellm._logging import verbose_logger
from litellm._uuid import uuid
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.llms.ollama.completion.transformation import (
@ -990,3 +992,40 @@ def test_get_complete_url_ignores_a_trailing_slash_on_api_base(api_base: str):
)
== "http://gateway.example.com/ollama/api/generate"
)
def _reasoning_effort_warnings(caplog: pytest.LogCaptureFixture) -> tuple[str, ...]:
return tuple(
r.getMessage()
for r in caplog.records
if r.levelno == logging.WARNING and "Dropping reasoning_effort=" in r.getMessage()
)
@pytest.mark.parametrize("effort", ("minimal", "xhigh"))
def test_reasoning_effort_mapped_to_think_off_warns_once(effort: str, caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = OllamaConfig().map_openai_params(
non_default_params={"reasoning_effort": effort}, optional_params={}, model="qwen3:0.6b", drop_params=False
)
warnings: Final = _reasoning_effort_warnings(caplog)
assert result["think"] is False
assert len(warnings) == 1
assert f"reasoning_effort={effort!r}" in warnings[0]
assert "qwen3:0.6b" in warnings[0]
@pytest.mark.parametrize(
"model, effort, think",
(("qwen3:0.6b", "none", False), ("qwen3:0.6b", "high", True), ("gpt-oss:20b", "xhigh", "xhigh")),
)
def test_reasoning_effort_ollama_honors_does_not_warn(
model: str, effort: str, think: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = OllamaConfig().map_openai_params(
non_default_params={"reasoning_effort": effort}, optional_params={}, model=model, drop_params=False
)
assert result["think"] == think
assert _reasoning_effort_warnings(caplog) == ()

View file

@ -1,4 +1,6 @@
import asyncio
import json
import logging
from typing import Final
from unittest.mock import MagicMock, patch
@ -9,7 +11,11 @@ import respx
import litellm
from litellm.litellm_core_utils.token_counter import token_counter
from litellm.llms.openai.common_utils import BaseOpenAILLM, is_openai_backed_api_base
from litellm.llms.openai.common_utils import (
BaseOpenAILLM,
drop_params_from_unprocessable_entity_error,
is_openai_backed_api_base,
)
# Test parameters for different API functions
API_FUNCTION_PARAMS = [
@ -319,6 +325,104 @@ def _completion_kwargs(provider: str, client, **overrides) -> dict:
}
_REASONING_EFFORT_422: Final = {
"error": {
"message": json.dumps(
{"detail": [{"loc": ["body", "reasoning_effort"], "msg": "Extra inputs are not permitted"}]}
)
}
}
_CHAT_COMPLETION: Final = {
"id": "chatcmpl-1",
"object": "chat.completion",
"created": 1,
"model": "gpt-6.1-sol",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
_CHAT_COMPLETION_STREAM: Final = (
'data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1,"model":"gpt-6.1-sol",'
'"choices":[{"index":0,"delta":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}\n\n'
"data: [DONE]\n\n"
)
def _answer_422_while_reasoning_effort_is_sent(request: httpx.Request) -> httpx.Response:
body: Final = json.loads(request.content)
if "reasoning_effort" in body:
return httpx.Response(422, json=_REASONING_EFFORT_422)
if body.get("stream"):
return httpx.Response(200, text=_CHAT_COMPLETION_STREAM, headers={"content-type": "text/event-stream"})
return httpx.Response(200, json=_CHAT_COMPLETION)
async def _complete_through_the_422_retry(call_shape: str) -> str:
transport: Final = httpx.MockTransport(_answer_422_while_reasoning_effort_is_sent)
kwargs: Final = {
"model": "gpt-6.1-sol",
"messages": [{"role": "user", "content": "hi"}],
"reasoning_effort": "high",
}
if call_shape == "sync":
sync_client: Final = openai.OpenAI(api_key="sk-not-a-real-key", http_client=httpx.Client(transport=transport))
return litellm.completion(**kwargs, client=sync_client).choices[0].message.content
async_client: Final = openai.AsyncOpenAI(
api_key="sk-not-a-real-key", http_client=httpx.AsyncClient(transport=transport)
)
if call_shape == "async":
return (await litellm.acompletion(**kwargs, client=async_client)).choices[0].message.content
stream: Final = await litellm.acompletion(**kwargs, client=async_client, stream=True)
return "".join([chunk.choices[0].delta.content or "" async for chunk in stream])
@pytest.mark.parametrize("call_shape", ["sync", "async", "async_stream"])
def test_a_422_naming_reasoning_effort_drops_it_with_one_warning(
call_shape: str, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
monkeypatch.setattr(litellm, "drop_params", True)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
content: Final = asyncio.run(_complete_through_the_422_retry(call_shape))
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert content == "ok"
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0]
for fragment in ("reasoning_effort='high'", "gpt-6.1-sol", "(drop_params=True)", "422")
)
def test_a_422_with_an_unhashable_loc_still_drops_reasoning_effort_with_one_warning(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
detail: Final = [
{"loc": ["body", ["messages", 0]], "msg": "Invalid"},
{"loc": ["body", "reasoning_effort"], "msg": "Extra inputs are not permitted"},
]
def answer(request: httpx.Request) -> httpx.Response:
if "reasoning_effort" in json.loads(request.content):
return httpx.Response(422, json={"error": {"message": json.dumps({"detail": detail})}})
return httpx.Response(200, json=_CHAT_COMPLETION)
monkeypatch.setattr(litellm, "drop_params", True)
client: Final = openai.OpenAI(
api_key="sk-not-a-real-key", http_client=httpx.Client(transport=httpx.MockTransport(answer))
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
response: Final = litellm.completion(
model="gpt-6.1-sol",
messages=[{"role": "user", "content": "hi"}],
reasoning_effort="high",
client=client,
)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert response.choices[0].message.content == "ok"
assert len(drop_warnings) == 1
assert "reasoning_effort='high'" in drop_warnings[0]
@pytest.mark.parametrize("provider", ["openai", "azure"])
def test_sync_output_limit_400_maps_to_length_truncated_response(provider):
response = litellm.completion(
@ -588,3 +692,24 @@ async def test_azure_gateway_and_image_variations_use_the_callers_async_session(
)
assert response.data[0].url == "https://example.com/image.png"
assert not session.is_closed
def test_reasoning_drop_warning_caps_a_large_caller_value(caplog: pytest.LogCaptureFixture) -> None:
error: Final = httpx.HTTPStatusError(
message="422",
request=httpx.Request("POST", "https://example.invalid/v1/chat/completions"),
response=httpx.Response(422, json=_REASONING_EFFORT_422),
)
data: Final[dict[str, object]] = {"model": "gpt-6.1-sol", "messages": [], "reasoning_effort": "x" * 5000}
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
retried: Final = drop_params_from_unprocessable_entity_error(
error, data, model="gpt-6.1-sol", drop_params_gated=True
)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert "reasoning_effort" not in retried
assert len(drop_warnings) == 1
assert len(drop_warnings[0]) < 300
assert all(fragment in drop_warnings[0] for fragment in ("reasoning_effort='xxx", "'gpt-6.1-sol'", "422"))

View file

@ -1,5 +1,10 @@
import logging
from typing import Final
from unittest.mock import patch
import pytest
from litellm._logging import verbose_logger
from litellm.llms.tencent.chat.transformation import TencentChatConfig
@ -208,6 +213,27 @@ class TestAdaptiveThinkingCoercion:
assert result["extra_body"]["thinking"] == {"type": "adaptive", "budget_tokens": 4096}
@pytest.mark.parametrize("budget, warned", (("high", 1), (4096, 0)), ids=("non-integer", "integer"))
def test_enabled_thinking_on_adaptive_only_model_warns_only_when_the_budget_is_dropped(
self, local_model_cost_map, budget: object, warned: int, caplog: pytest.LogCaptureFixture
):
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = TencentChatConfig().map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": budget}},
optional_params={},
model="tencent/minimax-m3",
drop_params=False,
)
warnings: Final = tuple(
r.getMessage()
for r in caplog.records
if r.levelno == logging.WARNING and f"Dropping thinking.budget_tokens={budget!r}" in r.getMessage()
)
assert result["extra_body"]["thinking"]["type"] == "adaptive"
assert result["extra_body"]["thinking"].get("budget_tokens") == (None if warned else budget)
assert len(warnings) == warned
def test_disabled_thinking_kept_for_adaptive_only_model(self, local_model_cost_map):
config = TencentChatConfig()
result = config.map_openai_params(

View file

@ -1693,6 +1693,78 @@ def test_vertex_ai_map_thinking_param_without_budget_tokens_for_gemini_3():
assert result["thinkingConfig"] == {"includeThoughts": True}
def _budget_drop_warnings(caplog: pytest.LogCaptureFixture) -> tuple[str, ...]:
return tuple(
r.getMessage()
for r in caplog.records
if r.levelno == logging.WARNING and "thinking.budget_tokens" in r.getMessage()
)
@pytest.mark.parametrize("budget", ("high", 1024.5, {"tokens": 1024}))
def test_gemini_non_integer_thinking_budget_drop_warns_once(budget: object, caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = VertexGeminiConfig().map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": budget}},
optional_params={},
model="gemini-2.5-flash",
drop_params=False,
)
warnings: Final = _budget_drop_warnings(caplog)
assert "thinkingBudget" not in result["thinkingConfig"]
assert len(warnings) == 1
assert f"thinking.budget_tokens={budget!r}" in warnings[0]
assert "gemini-2.5-flash" in warnings[0]
def test_gemini_integer_thinking_budget_is_sent_without_warning(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = VertexGeminiConfig().map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 2048}},
optional_params={},
model="gemini-2.5-flash",
drop_params=False,
)
assert result["thinkingConfig"]["thinkingBudget"] == 2048
assert _budget_drop_warnings(caplog) == ()
def test_gemini_3_thinking_budget_drop_warns_once(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = VertexGeminiConfig().map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 2048}},
optional_params={},
model="gemini-3-pro-preview",
drop_params=False,
)
warnings: Final = _budget_drop_warnings(caplog)
assert "thinkingBudget" not in result["thinkingConfig"]
assert len(warnings) == 1
assert "thinking.budget_tokens=2048" in warnings[0]
assert "gemini-3-pro-preview" in warnings[0]
@pytest.mark.parametrize(
"thinking",
({"type": "enabled"}, {"type": "enabled", "budget_tokens": 0}, {"type": "disabled", "budget_tokens": 2048}),
)
def test_gemini_3_thinking_without_a_dropped_budget_does_not_warn(
thinking: dict[str, object], caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
VertexGeminiConfig().map_openai_params(
non_default_params={"thinking": thinking},
optional_params={},
model="gemini-3-pro-preview",
drop_params=False,
)
assert _budget_drop_warnings(caplog) == ()
def test_vertex_ai_map_tools():
v = VertexGeminiConfig()
optional_params = {}

View file

@ -51,3 +51,178 @@ async def test_allm_passthrough_route_hosted_vllm_sends_normalized_model(respx_m
outbound: Final = json.loads(route.calls[0].request.content)
assert outbound["model"] == "my-deployment"
assert outbound["messages"] == [{"role": "user", "content": "Hello"}]
@pytest.mark.asyncio
async def test_allm_passthrough_route_hosted_vllm_sends_deployment_api_key(respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(f"{_API_BASE}/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"ok": True})
)
response: Final = await allm_passthrough_route(
method="POST",
endpoint="v1/chat/completions",
model="hosted_vllm/my-deployment",
api_base=_API_BASE,
api_key="sk-upstream",
json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]},
client=AsyncHTTPHandler(),
)
assert response.status_code == 200
assert route.calls[0].request.headers["authorization"] == "Bearer sk-upstream"
assert "x-api-key" not in route.calls[0].request.headers
@pytest.mark.parametrize("endpoint", ["v1/chat/completions", "chat/completions"])
@pytest.mark.asyncio
async def test_allm_passthrough_route_hosted_vllm_reaches_v1_paths_under_an_api_base_ending_in_v1(
respx_mock: respx.MockRouter, endpoint: str
) -> None:
route: Final = respx_mock.post(f"{_API_BASE}/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"ok": True})
)
response: Final = await allm_passthrough_route(
method="POST",
endpoint=endpoint,
model="hosted_vllm/my-deployment",
api_base=f"{_API_BASE}/v1",
api_key="sk-upstream",
json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]},
client=AsyncHTTPHandler(),
)
assert response.status_code == 200
assert route.call_count == 1
@pytest.mark.asyncio
async def test_allm_passthrough_route_hosted_vllm_sends_no_auth_for_a_keyless_deployment(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VLLM_API_KEY", "sk-vllm-route-only")
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
route: Final = respx_mock.post(f"{_API_BASE}/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"ok": True})
)
response: Final = await allm_passthrough_route(
method="POST",
endpoint="v1/chat/completions",
model="hosted_vllm/my-deployment",
api_base=_API_BASE,
json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]},
client=AsyncHTTPHandler(),
)
assert response.status_code == 200
assert "authorization" not in route.calls[0].request.headers
assert "x-api-key" not in route.calls[0].request.headers
@pytest.mark.parametrize(
("vllm_api_key", "expected_authorization"),
[("sk-vllm-env", "Bearer sk-vllm-env"), ("oidc/env/VLLM_OIDC_TOKEN", "Bearer sk-vllm-oidc")],
)
@pytest.mark.asyncio
async def test_allm_passthrough_route_keyless_hosted_vllm_without_api_base_sends_the_vllm_api_base_key(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, vllm_api_key: str, expected_authorization: str
) -> None:
monkeypatch.setenv("VLLM_API_BASE", _API_BASE)
monkeypatch.setenv("VLLM_API_KEY", vllm_api_key)
monkeypatch.setenv("VLLM_OIDC_TOKEN", "sk-vllm-oidc")
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
route: Final = respx_mock.post(f"{_API_BASE}/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"ok": True})
)
response: Final = await allm_passthrough_route(
method="POST",
endpoint="v1/chat/completions",
model="hosted_vllm/my-deployment",
json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]},
client=AsyncHTTPHandler(),
)
assert response.status_code == 200
assert route.calls[0].request.headers["authorization"] == expected_authorization
@pytest.mark.parametrize(
("model_api_base", "vllm_api_base"), [(f"{_API_BASE}/", _API_BASE), (_API_BASE, f"{_API_BASE}/")]
)
@pytest.mark.asyncio
async def test_allm_passthrough_route_keyless_hosted_vllm_matches_the_vllm_api_base_across_a_trailing_slash(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, model_api_base: str, vllm_api_base: str
) -> None:
monkeypatch.setenv("VLLM_API_BASE", vllm_api_base)
monkeypatch.setenv("VLLM_API_KEY", "sk-vllm-env")
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
route: Final = respx_mock.post(url__regex=rf"^{_API_BASE}/+v1/chat/completions$").mock(
return_value=httpx.Response(200, json={"ok": True})
)
response: Final = await allm_passthrough_route(
method="POST",
endpoint="v1/chat/completions",
model="hosted_vllm/my-deployment",
api_base=model_api_base,
json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]},
client=AsyncHTTPHandler(),
)
assert response.status_code == 200
assert route.calls[0].request.headers["authorization"] == "Bearer sk-vllm-env"
@pytest.mark.parametrize("vllm_api_key", ["", "oidc/google/audience"])
@pytest.mark.asyncio
async def test_allm_passthrough_route_unusable_vllm_api_key_sends_no_authorization_header(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, vllm_api_key: str
) -> None:
monkeypatch.setenv("VLLM_API_BASE", _API_BASE)
monkeypatch.setenv("VLLM_API_KEY", vllm_api_key)
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.delenv("HOSTED_VLLM_API_KEY", raising=False)
route: Final = respx_mock.post(f"{_API_BASE}/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"ok": True})
)
response: Final = await allm_passthrough_route(
method="POST",
endpoint="v1/chat/completions",
model="hosted_vllm/my-deployment",
json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]},
client=AsyncHTTPHandler(),
)
assert response.status_code == 200
assert "authorization" not in route.calls[0].request.headers
@pytest.mark.parametrize(
("api_base", "api_key", "expected_authorization"),
[
(None, "sk-upstream", "Bearer sk-upstream"),
(None, "sk-hosted-env", "Bearer sk-hosted-env"),
(_API_BASE, "sk-hosted-env", "Bearer sk-hosted-env"),
(None, None, "Bearer sk-hosted-env"),
],
)
@pytest.mark.asyncio
async def test_allm_passthrough_route_hosted_vllm_on_the_vllm_api_base_sends_its_own_resolved_key(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
api_base: str | None,
api_key: str | None,
expected_authorization: str,
) -> None:
monkeypatch.setenv("VLLM_API_BASE", _API_BASE)
monkeypatch.setenv("VLLM_API_KEY", "sk-vllm-env")
monkeypatch.delenv("HOSTED_VLLM_API_BASE", raising=False)
monkeypatch.setenv("HOSTED_VLLM_API_KEY", "sk-hosted-env")
route: Final = respx_mock.post(f"{_API_BASE}/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"ok": True})
)
response: Final = await allm_passthrough_route(
method="POST",
endpoint="v1/chat/completions",
model="hosted_vllm/my-deployment",
api_base=api_base,
api_key=api_key,
json={"model": "anything", "messages": [{"role": "user", "content": "Hello"}]},
client=AsyncHTTPHandler(),
)
assert response.status_code == 200
assert route.calls[0].request.headers["authorization"] == expected_authorization

View file

@ -2759,6 +2759,68 @@ class TestVLLMProxyRoute:
mock_factory_route.assert_awaited_once()
@pytest.mark.parametrize(
("vllm_api_base", "request_path"),
[
("http://vllm-upstream.test:8090", "/vllm/v1/chat/completions"),
("http://vllm-upstream.test:8090/v1", "/vllm/v1/chat/completions"),
("http://vllm-upstream.test:8090/v1", "/vllm/chat/completions"),
],
)
def test_vllm_provider_route_reaches_v1_paths_with_its_key_as_a_bearer(
monkeypatch: pytest.MonkeyPatch, vllm_api_base: str, request_path: str
) -> None:
from litellm.proxy import proxy_server
monkeypatch.setenv("VLLM_API_BASE", vllm_api_base)
monkeypatch.setenv("VLLM_API_KEY", "sk-vllm-upstream")
monkeypatch.setattr(proxy_server, "llm_router", None)
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()
monkeypatch.setitem(
proxy_server.app.dependency_overrides, user_api_key_auth, lambda: UserAPIKeyAuth(api_key="sk-virtual")
)
with respx.mock(assert_all_called=True) as upstream:
route: Final = upstream.post("http://vllm-upstream.test:8090/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"id": "chatcmpl-1"})
)
response: Final = TestClient(proxy_server.app).post(
request_path,
json={"model": "Qwen/Qwen3-0.6B", "messages": [{"role": "user", "content": "hi"}]},
)
assert (response.status_code, response.json()) == (200, {"id": "chatcmpl-1"})
assert route.calls.last.request.headers["authorization"] == "Bearer sk-vllm-upstream"
assert route.calls.last.request.headers["x-api-key"] == "sk-vllm-upstream"
def test_vllm_provider_route_with_an_empty_key_sends_no_credential_headers(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.proxy import proxy_server
monkeypatch.setenv("VLLM_API_BASE", "http://vllm-upstream.test:8090")
monkeypatch.setenv("VLLM_API_KEY", "")
monkeypatch.setattr(proxy_server, "llm_router", None)
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()
monkeypatch.setitem(
proxy_server.app.dependency_overrides, user_api_key_auth, lambda: UserAPIKeyAuth(api_key="sk-virtual")
)
with respx.mock(assert_all_called=True) as upstream:
route: Final = upstream.post("http://vllm-upstream.test:8090/v1/chat/completions").mock(
return_value=httpx.Response(200, json={"id": "chatcmpl-1"})
)
response: Final = TestClient(proxy_server.app).post(
"/vllm/v1/chat/completions",
json={"model": "Qwen/Qwen3-0.6B", "messages": [{"role": "user", "content": "hi"}]},
)
assert (response.status_code, response.json()) == (200, {"id": "chatcmpl-1"})
assert "authorization" not in route.calls.last.request.headers
assert "x-api-key" not in route.calls.last.request.headers
class TestGigachatProxyRoute:
@pytest.mark.asyncio
@patch( # test-quality-ok: patching litellm internal for unit test isolation

View file

@ -10262,9 +10262,10 @@ def test_a_non_numeric_thinking_budget_on_adaptive_claude_is_rejected_with_a_400
@pytest.mark.parametrize("drop_globally", [False, True])
@pytest.mark.parametrize("model_info", [None, _OPUS_DECLARING_LOW_AND_HIGH])
@pytest.mark.parametrize("body_sent", [_anthropic_chat_body_sent, _bedrock_converse_request_fields_sent])
def test_drop_params_drops_a_non_numeric_thinking_budget_on_adaptive_claude(
def test_drop_params_drops_a_non_numeric_thinking_budget_on_adaptive_claude_with_one_warning(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
body_sent: Callable[..., Mapping[str, object]],
model_info: Mapping[str, object] | None,
drop_globally: bool,
@ -10272,13 +10273,40 @@ def test_drop_params_drops_a_non_numeric_thinking_budget_on_adaptive_claude(
monkeypatch.setattr(litellm, "drop_params", drop_globally)
request_drop: Final = {} if drop_globally else {"drop_params": True}
dropped: Final = body_sent(
respx_mock, model_info, thinking={"type": "enabled", "budget_tokens": "lots"}, **request_drop
)
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
dropped: Final = body_sent(
respx_mock, model_info, thinking={"type": "enabled", "budget_tokens": "lots"}, **request_drop
)
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
adaptive_alone: Final = body_sent(respx_mock, model_info, thinking={"type": "adaptive"}, **request_drop)
assert "output_config" not in adaptive_alone
assert dropped == adaptive_alone
assert len(drop_warnings) == 1
assert all(
fragment in drop_warnings[0]
for fragment in ("thinking.budget_tokens='lots'", "claude-opus-5-5", "(drop_params=True)")
)
@pytest.mark.parametrize("model_info", [None, _KIMI_DEPLOYMENT])
def test_drop_params_drops_a_non_numeric_thinking_budget_on_hosted_vllm_with_one_warning(
respx_mock: respx.MockRouter, caplog: pytest.LogCaptureFixture, model_info: Mapping[str, object] | None
) -> None:
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
sent: Final = _chat_body_sent(
respx_mock,
"hosted_vllm/my-kimi",
None,
model_info,
thinking={"type": "enabled", "budget_tokens": "lots"},
drop_params=True,
)
assert not {"reasoning_effort", "thinking"} & sent.keys()
drop_warnings: Final = [record.getMessage() for record in caplog.records if "Dropping" in record.getMessage()]
assert len(drop_warnings) == 1
assert all(fragment in drop_warnings[0] for fragment in ("thinking.budget_tokens='lots'", "my-kimi"))
@pytest.mark.parametrize("model", ["azure_ai/my-kimi", "hosted_vllm/my-kimi"])