mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
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:
commit
0ed5afb3d7
57 changed files with 1607 additions and 153 deletions
|
|
@ -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=(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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),)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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) == ()
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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") == ()
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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") == ()
|
||||
|
|
|
|||
|
|
@ -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) == ()
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue