mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(reasoning): log the GPT-5 reasoning_effort drop warning only when the provider gets no effort (#50995)
* fix(reasoning): log the GPT-5 reasoning_effort drop warning only when the provider gets no effort * fix(proxy): count an extra_body effort as sent and pass the drop holder through ChatGPT and Copilot * fix(proxy): pass the drop holder to get_optional_params under the internal _litellm_ prefix * fix(openai): keep the reasoning effort drop warning when extra_body sends another level The held warning now stays quiet only when the request sends the level the gate dropped, so a top-level xhigh dropped while extra_body sends none still logs. The Copilot test case writes the access-token file the current authenticator reads. * fix(openai): keep the reasoning effort drop warning when a request fails before reaching the provider The drop warning holder now lives for the whole completion call, and completion's exception handler prints whatever it still holds, since a request that fails after mapping never reached the provider. Each flush empties the holder, so a provider error after dispatch does not print the warning a second time * fix(openai): warn when extra_body's reasoning_effort carries no effort level on the Responses bridge --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
cb024047ac
commit
054ed65cdc
9 changed files with 457 additions and 70 deletions
|
|
@ -11,7 +11,7 @@ from collections.abc import Iterator, Mapping
|
|||
from datetime import datetime
|
||||
from logging import Formatter
|
||||
from types import FrameType
|
||||
from typing import Final, TextIO
|
||||
from typing import Final, Protocol, TextIO
|
||||
from urllib.parse import unquote
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
|
@ -936,6 +936,11 @@ else:
|
|||
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
|
||||
class WarningLogger(Protocol):
|
||||
def warning(self, msg: str, /, *args: object) -> None: ...
|
||||
|
||||
|
||||
verbose_proxy_logger = logging.getLogger("LiteLLM Proxy")
|
||||
# Malformed virtual key rejections log through this child; LevelRoutingStreamHandler
|
||||
# writes its WARNING records to stdout. It has no handler or level of its own.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from collections.abc import Collection, Mapping
|
|||
from typing import ClassVar, Final
|
||||
|
||||
import litellm
|
||||
from litellm._logging import WarningLogger, verbose_logger
|
||||
from litellm.llms.openai.chat.gpt_reasoning_transformation import (
|
||||
OpenAIGPT5Config,
|
||||
is_gpt_reasoning_series_name,
|
||||
|
|
@ -90,6 +91,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
|||
api_version: str = "",
|
||||
*,
|
||||
declared_reasoning_efforts: Collection[str] | None = None,
|
||||
reasoning_drop_logger: WarningLogger = verbose_logger,
|
||||
) -> dict:
|
||||
return OpenAIGPT5Config.map_openai_params(
|
||||
self,
|
||||
|
|
@ -98,6 +100,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
|||
model=model,
|
||||
drop_params=drop_params,
|
||||
declared_reasoning_efforts=declared_reasoning_efforts,
|
||||
reasoning_drop_logger=reasoning_drop_logger,
|
||||
)
|
||||
|
||||
def transform_request(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from httpx import Response
|
|||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm._logging import WarningLogger, verbose_logger
|
||||
from litellm.integrations.anthropic_cache_control_hook import (
|
||||
AnthropicCacheControlHook,
|
||||
declared_prompt_cache_breakpoint,
|
||||
|
|
@ -203,6 +203,7 @@ class AzureAIStudioConfig(OpenAIConfig):
|
|||
drop_params: bool,
|
||||
*,
|
||||
declared_reasoning_efforts: Collection[str] | None = None,
|
||||
reasoning_drop_logger: WarningLogger = verbose_logger,
|
||||
) -> dict[str, object]: # mutable-ok: OpenAIConfig.map_openai_params signature
|
||||
if not azureAIGPT5Config.is_model_gpt_5_model(model):
|
||||
return super().map_openai_params(
|
||||
|
|
@ -217,6 +218,7 @@ class AzureAIStudioConfig(OpenAIConfig):
|
|||
model=model,
|
||||
drop_params=drop_params,
|
||||
declared_reasoning_efforts=declared_reasoning_efforts,
|
||||
reasoning_drop_logger=reasoning_drop_logger,
|
||||
)
|
||||
|
||||
def _supports_stop_reason(self, model: str) -> bool:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
from litellm._logging import WarningLogger, verbose_logger
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.llms.openai.openai import OpenAIConfig
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
|
@ -118,11 +119,15 @@ class ChatGPTConfig(OpenAIConfig):
|
|||
|
||||
def map_openai_params(
|
||||
self,
|
||||
non_default_params: dict,
|
||||
optional_params: dict,
|
||||
non_default_params: dict[str, object],
|
||||
optional_params: dict[str, object],
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
) -> dict:
|
||||
optional_params = super().map_openai_params(non_default_params, optional_params, model, drop_params)
|
||||
*,
|
||||
reasoning_drop_logger: WarningLogger = verbose_logger,
|
||||
) -> dict[str, object]:
|
||||
optional_params = super().map_openai_params(
|
||||
non_default_params, optional_params, model, drop_params, reasoning_drop_logger=reasoning_drop_logger
|
||||
)
|
||||
optional_params.setdefault("stream", False)
|
||||
return optional_params
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
"""Support for OpenAI gpt-5 model family."""
|
||||
|
||||
import re
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Collection, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Final
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm._logging import WarningLogger, verbose_logger
|
||||
from litellm.utils import (
|
||||
declared_value_factory,
|
||||
has_catalog_entry,
|
||||
|
|
@ -37,6 +38,44 @@ def refused_reasoning_effort_warning(*, suggest_declared_levels: bool, suggest_b
|
|||
)
|
||||
|
||||
|
||||
def _effort_level(sent_effort: object) -> str | None:
|
||||
match sent_effort:
|
||||
case str(level) | {"effort": str(level)}:
|
||||
return level
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def _sent_effort_level(sent_params: Mapping[str, object]) -> str | None:
|
||||
match sent_params:
|
||||
case {"extra_body": {"reasoning_effort": sent_effort}}:
|
||||
return _effort_level(sent_effort)
|
||||
case {"reasoning_effort": sent_effort}:
|
||||
return _effort_level(sent_effort)
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DeferredReasoningDropWarnings:
|
||||
logger: WarningLogger
|
||||
held: tuple[tuple[str, tuple[object, ...]], ...] = ()
|
||||
|
||||
def warning(self, msg: str, /, *args: object) -> None:
|
||||
self.held = (*self.held, (msg, args))
|
||||
|
||||
def log_unless_dropped_effort_sent(self, sent_params: Mapping[str, object]) -> None:
|
||||
sent_effort: Final = _sent_effort_level(sent_params)
|
||||
held: Final = self.held
|
||||
self.held = ()
|
||||
for msg, args in held:
|
||||
match args:
|
||||
case ("reasoning_effort", dropped_effort, *_) if dropped_effort == sent_effort:
|
||||
continue
|
||||
case _:
|
||||
self.logger.warning(msg, *args)
|
||||
|
||||
|
||||
def _catalogue_declares_default_effort() -> bool:
|
||||
"""Whether the loaded cost map carries default_reasoning_effort for ANY entry.
|
||||
|
||||
|
|
@ -330,6 +369,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
drop_params: bool,
|
||||
*,
|
||||
declared_reasoning_efforts: Collection[str] | None = None,
|
||||
reasoning_drop_logger: WarningLogger = verbose_logger,
|
||||
) -> dict:
|
||||
if self.is_model_gpt_5_search_model(model):
|
||||
if "max_tokens" in non_default_params:
|
||||
|
|
@ -364,7 +404,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
and self.refuses_reasoning_effort_level(model, requested_effort)
|
||||
):
|
||||
if litellm.drop_params or drop_params:
|
||||
verbose_logger.warning(
|
||||
reasoning_drop_logger.warning(
|
||||
refused_reasoning_effort_warning(
|
||||
suggest_declared_levels=declared_reasoning_efforts is not None,
|
||||
suggest_base_model=self.LOOKS_UP_BASE_MODEL
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from typing_extensions import overload
|
|||
|
||||
import litellm
|
||||
from litellm import LlmProviders
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm._logging import WarningLogger, verbose_logger
|
||||
from litellm.constants import DEFAULT_MAX_RETRIES
|
||||
from litellm.exceptions import UnsupportedParamsError
|
||||
from litellm.files.types import FileContentStreamingResult
|
||||
|
|
@ -260,6 +260,8 @@ class OpenAIConfig(BaseConfig):
|
|||
optional_params: dict,
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
*,
|
||||
reasoning_drop_logger: WarningLogger = verbose_logger,
|
||||
) -> dict:
|
||||
""" """
|
||||
if openaiOSeriesConfig.is_model_o_series_model(model=model):
|
||||
|
|
@ -275,6 +277,7 @@ class OpenAIConfig(BaseConfig):
|
|||
optional_params=optional_params,
|
||||
model=model,
|
||||
drop_params=drop_params,
|
||||
reasoning_drop_logger=reasoning_drop_logger,
|
||||
)
|
||||
elif litellm.openAIGPTAudioConfig.is_model_gpt_audio_model(model=model):
|
||||
return litellm.openAIGPTAudioConfig.map_openai_params(
|
||||
|
|
|
|||
124
litellm/main.py
124
litellm/main.py
|
|
@ -128,6 +128,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
)
|
||||
from litellm.litellm_core_utils.provider_affinity import add_provider_affinity_header
|
||||
from litellm.litellm_core_utils.reasoning_effort_capability import (
|
||||
RequestedReasoningEffort,
|
||||
model_group_from_metadata,
|
||||
reasoning_effort_needs_allowing,
|
||||
resolve_chat_reasoning,
|
||||
|
|
@ -154,7 +155,10 @@ from litellm.llms.bedrock.common_utils import (
|
|||
from litellm.llms.bedrock_mantle.chat.claude_transformation import bedrock_mantle_chat_config
|
||||
from litellm.llms.cohere.common_utils import CohereModelInfo
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler, http2_enabled
|
||||
from litellm.llms.openai.chat.gpt_reasoning_transformation import OpenAIGPT5Config
|
||||
from litellm.llms.openai.chat.gpt_reasoning_transformation import (
|
||||
DeferredReasoningDropWarnings,
|
||||
OpenAIGPT5Config,
|
||||
)
|
||||
from litellm.llms.openai.common_utils import get_openai_credentials
|
||||
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
|
||||
from litellm.llms.vertex_ai.common_utils import (
|
||||
|
|
@ -225,7 +229,7 @@ from litellm.utils import (
|
|||
token_counter as token_counter, # noqa: PLC0414 # public re-export
|
||||
)
|
||||
|
||||
from ._logging import verbose_logger
|
||||
from ._logging import WarningLogger, verbose_logger
|
||||
from .caching.caching import disable_cache, enable_cache, update_cache
|
||||
from .litellm_core_utils.core_helpers import (
|
||||
as_str_mapping,
|
||||
|
|
@ -5478,6 +5482,51 @@ def _complete_langflow(ctx: CompletionDispatchContext) -> _CompletionDispatchRes
|
|||
)
|
||||
|
||||
|
||||
def _responses_bridge_optional_params(
|
||||
optional_params: dict[str, object], # mutable-ok: completion's optional_params, which the Responses bridge takes
|
||||
summary_alias: object,
|
||||
requested: RequestedReasoningEffort,
|
||||
*,
|
||||
custom_llm_provider: str,
|
||||
model: str,
|
||||
base_model: str | None,
|
||||
api_base: str | None,
|
||||
deployment_model_info: Mapping[str, object] | None,
|
||||
drop_params: bool,
|
||||
additional_drop_params: object,
|
||||
) -> dict[str, object]: # mutable-ok: completion's optional_params, which the Responses bridge takes
|
||||
if isinstance(requested, dict):
|
||||
from litellm.responses.utils import (
|
||||
RefusedReasoningFields,
|
||||
bridge_reasoning,
|
||||
raise_refused_reasoning_fields,
|
||||
)
|
||||
|
||||
bridged_reasoning: Final = bridge_reasoning(
|
||||
requested=requested,
|
||||
effort_kept="reasoning_effort" in optional_params,
|
||||
summary_alias=summary_alias,
|
||||
model=model,
|
||||
base_model=base_model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=api_base,
|
||||
deployment_model_info=deployment_model_info,
|
||||
drop_params=drop_params,
|
||||
additional_drop_params=additional_drop_params,
|
||||
)
|
||||
if isinstance(bridged_reasoning, RefusedReasoningFields):
|
||||
raise_refused_reasoning_fields(bridged_reasoning, model, custom_llm_provider)
|
||||
return {**optional_params, "reasoning_effort": bridged_reasoning}
|
||||
if summary_alias is None:
|
||||
return optional_params
|
||||
kept_effort: Final = optional_params.get("reasoning_effort", requested)
|
||||
if isinstance(kept_effort, dict):
|
||||
return {**optional_params, "reasoning_effort": {**kept_effort, "summary": summary_alias}}
|
||||
if kept_effort is None:
|
||||
return {**optional_params, "reasoning_effort": {"summary": summary_alias}}
|
||||
return {**optional_params, "reasoning_effort": {"effort": kept_effort, "summary": summary_alias}}
|
||||
|
||||
|
||||
def _complete_databricks_agent(ctx: CompletionDispatchContext) -> _CompletionDispatchResult:
|
||||
from litellm.llms.databricks.agent.transformation import DatabricksAgentConfig
|
||||
|
||||
|
|
@ -5830,6 +5879,7 @@ def completion(
|
|||
merge_with_first_system=True,
|
||||
)
|
||||
|
||||
reasoning_drop_warnings: Final = DeferredReasoningDropWarnings(logger=verbose_logger)
|
||||
try:
|
||||
if base_url is not None:
|
||||
api_base = base_url
|
||||
|
|
@ -6085,14 +6135,22 @@ def completion(
|
|||
mapping_args: Final = MappingProxyType({**optional_param_args, "reasoning_effort": provider_reasoning_effort})
|
||||
mapping_special_params: Final = MappingProxyType(dict(provider_params))
|
||||
|
||||
def optional_params_for_model_info(deployment_info: Mapping[str, object] | None) -> dict[str, object]:
|
||||
return get_optional_params(**mapping_args, model_info=deployment_info, **mapping_special_params)
|
||||
def optional_params_for_model_info(
|
||||
deployment_info: Mapping[str, object] | None,
|
||||
reasoning_drop_logger: WarningLogger = verbose_logger,
|
||||
) -> dict[str, object]:
|
||||
return get_optional_params(
|
||||
**mapping_args,
|
||||
model_info=deployment_info,
|
||||
_litellm_reasoning_drop_logger=reasoning_drop_logger,
|
||||
**mapping_special_params,
|
||||
)
|
||||
|
||||
validated_model_info: Final = (
|
||||
_MODEL_INFO_ADAPTER.validate_python(model_info) if isinstance(model_info, dict) else None
|
||||
)
|
||||
deployment_base_model: Final = base_model if isinstance(base_model, str) else None
|
||||
optional_params = optional_params_for_model_info(validated_model_info)
|
||||
optional_params = optional_params_for_model_info(validated_model_info, reasoning_drop_warnings)
|
||||
fakes_stream: Final = partial(
|
||||
_completion_fakes_stream,
|
||||
model,
|
||||
|
|
@ -6289,6 +6347,7 @@ def completion(
|
|||
)
|
||||
if mock_response is not None or mock_tool_calls or mock_timeout:
|
||||
kwargs.pop("mock_timeout", None) # remove for any fallbacks triggered
|
||||
reasoning_drop_warnings.log_unless_dropped_effort_sent(optional_params)
|
||||
return mock_completion(
|
||||
model,
|
||||
messages,
|
||||
|
|
@ -6337,41 +6396,19 @@ def completion(
|
|||
from litellm.completion_extras import responses_api_bridge
|
||||
|
||||
optional_params, rs_val = strip_reasoning_summary_aliases_from_optional_params(optional_params)
|
||||
|
||||
if isinstance(resolved_reasoning_effort, dict):
|
||||
from litellm.responses.utils import (
|
||||
RefusedReasoningFields,
|
||||
bridge_reasoning,
|
||||
raise_refused_reasoning_fields,
|
||||
)
|
||||
|
||||
bridged_reasoning: Final = bridge_reasoning(
|
||||
requested=resolved_reasoning_effort,
|
||||
effort_kept="reasoning_effort" in optional_params,
|
||||
summary_alias=rs_val,
|
||||
model=model,
|
||||
base_model=base_model if isinstance(base_model, str) else None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=api_base,
|
||||
deployment_model_info=typed_deployment_model_info,
|
||||
drop_params=drop_unsupported_reasoning,
|
||||
additional_drop_params=routing_kwargs.get("additional_drop_params"),
|
||||
)
|
||||
if isinstance(bridged_reasoning, RefusedReasoningFields):
|
||||
raise_refused_reasoning_fields(bridged_reasoning, model, custom_llm_provider)
|
||||
optional_params["reasoning_effort"] = bridged_reasoning
|
||||
elif rs_val is not None:
|
||||
eff: Final = optional_params.get("reasoning_effort", resolved_reasoning_effort)
|
||||
if isinstance(eff, dict):
|
||||
optional_params["reasoning_effort"] = {**eff, "summary": rs_val}
|
||||
elif eff is not None:
|
||||
optional_params["reasoning_effort"] = {
|
||||
"effort": eff,
|
||||
"summary": rs_val,
|
||||
}
|
||||
else:
|
||||
optional_params["reasoning_effort"] = {"summary": rs_val}
|
||||
|
||||
bridge_optional_params: Final = _responses_bridge_optional_params(
|
||||
optional_params,
|
||||
rs_val,
|
||||
resolved_reasoning_effort,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model=model,
|
||||
base_model=deployment_base_model,
|
||||
api_base=api_base,
|
||||
deployment_model_info=typed_deployment_model_info,
|
||||
drop_params=drop_unsupported_reasoning,
|
||||
additional_drop_params=routing_kwargs.get("additional_drop_params"),
|
||||
)
|
||||
reasoning_drop_warnings.log_unless_dropped_effort_sent(bridge_optional_params)
|
||||
return responses_api_bridge.completion( # pyright: ignore[reportReturnType] # bridge returns a coroutine on the acompletion path; awaited by the async caller
|
||||
model=model,
|
||||
messages=messages,
|
||||
|
|
@ -6381,7 +6418,7 @@ def completion(
|
|||
api_base=api_base,
|
||||
acompletion=acompletion,
|
||||
logging_obj=logging,
|
||||
optional_params=optional_params,
|
||||
optional_params=bridge_optional_params,
|
||||
litellm_params=litellm_params,
|
||||
timeout=timeout,
|
||||
client=client, # pass AsyncOpenAI, OpenAI client
|
||||
|
|
@ -6390,7 +6427,9 @@ def completion(
|
|||
stream=stream,
|
||||
cache=kwargs.get("cache"),
|
||||
)
|
||||
elif (custom_llm_provider == "openai" and OpenAIGPT5Config.is_model_gpt_5_model(model)) or (
|
||||
|
||||
reasoning_drop_warnings.log_unless_dropped_effort_sent(optional_params)
|
||||
if (custom_llm_provider == "openai" and OpenAIGPT5Config.is_model_gpt_5_model(model)) or (
|
||||
custom_llm_provider == "azure"
|
||||
and litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(_azure_detection_model)
|
||||
):
|
||||
|
|
@ -6656,6 +6695,7 @@ def completion(
|
|||
raise LiteLLMUnknownProvider(model=model, custom_llm_provider=custom_llm_provider)
|
||||
return response
|
||||
except Exception as e:
|
||||
reasoning_drop_warnings.log_unless_dropped_effort_sent(MappingProxyType({}))
|
||||
## Map to OpenAI Exception
|
||||
raise exception_type(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -516,7 +516,7 @@ from litellm.secret_managers.main import (
|
|||
resolve_api_key_oidc_reference,
|
||||
)
|
||||
|
||||
from ._logging import is_debugging_on, verbose_logger
|
||||
from ._logging import WarningLogger, is_debugging_on, verbose_logger
|
||||
from .caching.caching import (
|
||||
AzureBlobCache,
|
||||
Cache,
|
||||
|
|
@ -5441,6 +5441,42 @@ def _declared_reasoning_efforts(model_info: Mapping[str, object] | None) -> tupl
|
|||
return deployment_reasoning_effort_levels(model_info) or ()
|
||||
|
||||
|
||||
def _map_params_with_provider_config(
|
||||
provider_config: BaseConfig,
|
||||
*,
|
||||
custom_llm_provider: str,
|
||||
non_default_params: dict[str, object], # mutable-ok: map_openai_params takes the caller's dicts
|
||||
optional_params: dict[str, object], # mutable-ok: map_openai_params fills and returns the caller's dict
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
model_info: Mapping[str, object] | None,
|
||||
reasoning_drop_logger: WarningLogger,
|
||||
) -> dict[str, object]: # mutable-ok: get_optional_params keeps building on the mapped params
|
||||
if custom_llm_provider == "azure_ai" and isinstance(provider_config, litellm.AzureAIStudioConfig):
|
||||
return provider_config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=model,
|
||||
drop_params=drop_params,
|
||||
declared_reasoning_efforts=_declared_reasoning_efforts(model_info),
|
||||
reasoning_drop_logger=reasoning_drop_logger,
|
||||
)
|
||||
if isinstance(provider_config, (litellm.ChatGPTConfig, litellm.GithubCopilotConfig)):
|
||||
return provider_config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=model,
|
||||
drop_params=drop_params,
|
||||
reasoning_drop_logger=reasoning_drop_logger,
|
||||
)
|
||||
return provider_config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=model,
|
||||
drop_params=drop_params,
|
||||
)
|
||||
|
||||
|
||||
_DROPPED_REASONING_PARAMS: Final = ("reasoning_effort", "thinking")
|
||||
|
||||
_DROP_UNREAD_OUTPUT_CONFIG_WARNING: Final = (
|
||||
|
|
@ -5530,6 +5566,7 @@ def get_optional_params(
|
|||
prompt_cache_key: str | None = None,
|
||||
base_model: str | None = None,
|
||||
model_info: Mapping[str, object] | None = None,
|
||||
_litellm_reasoning_drop_logger: WarningLogger = verbose_logger,
|
||||
**kwargs: object,
|
||||
):
|
||||
drop_params = normalize_drop_params(drop_params) # rebind-ok: config and DB deployments pass "true" as a string
|
||||
|
|
@ -5541,6 +5578,7 @@ def get_optional_params(
|
|||
# OpenAI param.
|
||||
passed_params.pop("base_model", None)
|
||||
passed_params.pop("model_info", None)
|
||||
passed_params.pop("_litellm_reasoning_drop_logger", None)
|
||||
provider_config: BaseConfig | None = None
|
||||
if custom_llm_provider is not None and custom_llm_provider in [provider.value for provider in LlmProviders]:
|
||||
provider_config = ProviderConfigManager.get_provider_chat_config(
|
||||
|
|
@ -6035,6 +6073,7 @@ def get_optional_params(
|
|||
model=model,
|
||||
drop_params=bool(drop_params),
|
||||
declared_reasoning_efforts=_declared_reasoning_efforts(model_info),
|
||||
reasoning_drop_logger=_litellm_reasoning_drop_logger,
|
||||
)
|
||||
if litellm.OpenAIGPT5Config.is_model_gpt_5_model(model=model)
|
||||
else litellm.OpenAIConfig().map_openai_params(
|
||||
|
|
@ -6067,6 +6106,7 @@ def get_optional_params(
|
|||
model=_azure_detection_model,
|
||||
drop_params=bool(drop_params),
|
||||
declared_reasoning_efforts=_declared_reasoning_efforts(model_info),
|
||||
reasoning_drop_logger=_litellm_reasoning_drop_logger,
|
||||
)
|
||||
else:
|
||||
verbose_logger.debug(
|
||||
|
|
@ -6088,21 +6128,15 @@ def get_optional_params(
|
|||
drop_params=bool(drop_params),
|
||||
)
|
||||
elif provider_config is not None:
|
||||
optional_params = (
|
||||
provider_config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=model,
|
||||
drop_params=bool(drop_params),
|
||||
declared_reasoning_efforts=_declared_reasoning_efforts(model_info),
|
||||
)
|
||||
if custom_llm_provider == "azure_ai" and isinstance(provider_config, litellm.AzureAIStudioConfig)
|
||||
else provider_config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=model,
|
||||
drop_params=bool(drop_params),
|
||||
)
|
||||
optional_params = _map_params_with_provider_config(
|
||||
provider_config,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=model,
|
||||
drop_params=bool(drop_params),
|
||||
model_info=model_info,
|
||||
reasoning_drop_logger=_litellm_reasoning_drop_logger,
|
||||
)
|
||||
else: # assume passing in params for openai-like api
|
||||
optional_params = litellm.OpenAILikeChatConfig().map_openai_params(
|
||||
|
|
@ -11734,8 +11768,8 @@ def reasoning_effort_with_summary_alias(
|
|||
|
||||
|
||||
def strip_reasoning_summary_aliases_from_optional_params(
|
||||
optional_params: dict,
|
||||
) -> tuple[dict, object | None]:
|
||||
optional_params: Mapping[str, object],
|
||||
) -> tuple[dict[str, object], object | None]:
|
||||
"""Copy optional_params; remove reasoningSummary aliases from top-level and extra_body."""
|
||||
op: Final = dict(optional_params)
|
||||
rs_val = op.pop("reasoningSummary", None)
|
||||
|
|
|
|||
|
|
@ -2451,6 +2451,261 @@ def test_openai_responses_bridge_drops_a_reasoning_effort_dict_for_a_model_witho
|
|||
assert "reasoning" not in request_body
|
||||
|
||||
|
||||
_AZURE_TEST_API_BASE: Final = "https://example.openai.azure.com"
|
||||
_UNLISTED_AZURE_GPT_5_MODEL: Final = "azure/gpt-5.6-litellm-unlisted"
|
||||
_FUNCTION_TOOL: Final = {"type": "function", "function": {"name": "lookup", "parameters": {"type": "object"}}}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AzureUpstreamRoutes:
|
||||
chat: respx.Route
|
||||
responses: respx.Route
|
||||
|
||||
def sent_reasoning_effort(self) -> object:
|
||||
if self.responses.called:
|
||||
return json.loads(self.responses.calls[0].request.content).get("reasoning", {}).get("effort")
|
||||
return json.loads(self.chat.calls[0].request.content).get("reasoning_effort")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def azure_upstream_routes(
|
||||
local_model_cost_map: None,
|
||||
respx_mock: respx.MockRouter,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
openai_api_response: dict[str, object],
|
||||
) -> AzureUpstreamRoutes:
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.delenv("LITELLM_REASONING_AUTO_SUMMARY", raising=False)
|
||||
return AzureUpstreamRoutes(
|
||||
chat=respx_mock.post(url__regex=r".*/chat/completions.*").respond(json=openai_api_response),
|
||||
responses=respx_mock.post(url__regex=r".*/responses.*").respond(
|
||||
json={
|
||||
"id": "resp_test",
|
||||
"object": "response",
|
||||
"created_at": 1734366691,
|
||||
"status": "completed",
|
||||
"model": "gpt-5.6-litellm-unlisted",
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_1",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "output_text", "text": "Done.", "annotations": []}],
|
||||
}
|
||||
],
|
||||
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("request_params", "expected_sent_effort"),
|
||||
[
|
||||
pytest.param({"reasoning_effort": "none", "reasoningSummary": "auto"}, "none", id="summary-alias"),
|
||||
pytest.param(
|
||||
{"reasoning_effort": "none", "extra_body": {"reasoningSummary": "auto"}},
|
||||
"none",
|
||||
id="summary-alias-in-extra-body",
|
||||
),
|
||||
pytest.param(
|
||||
{"reasoning_effort": {"effort": "none", "summary": "auto"}, "tools": [_FUNCTION_TOOL]},
|
||||
None,
|
||||
id="dict-with-summary-and-tool",
|
||||
),
|
||||
pytest.param(
|
||||
{"reasoning_effort": {"effort": "none"}, "reasoningSummary": "auto"}, None, id="dict-with-summary-alias"
|
||||
),
|
||||
pytest.param({"reasoning_effort": "none"}, None, id="chat"),
|
||||
pytest.param({"reasoning_effort": "none", "tools": [_FUNCTION_TOOL]}, None, id="bridged-without-summary"),
|
||||
pytest.param({"reasoning_effort": {"effort": "none", "summary": "auto"}}, None, id="dict-with-summary-on-chat"),
|
||||
pytest.param(
|
||||
{"reasoning_effort": "none", "extra_body": {"reasoning_effort": "none"}},
|
||||
"none",
|
||||
id="effort-in-extra-body",
|
||||
),
|
||||
pytest.param(
|
||||
{"reasoning_effort": "none", "tools": [_FUNCTION_TOOL], "extra_body": {"reasoning_effort": "none"}},
|
||||
"none",
|
||||
id="effort-in-extra-body-bridged",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"reasoning_effort": "none",
|
||||
"tools": [_FUNCTION_TOOL],
|
||||
"extra_body": {"reasoning_effort": {"effort": "none", "summary": "auto"}},
|
||||
},
|
||||
"none",
|
||||
id="effort-dict-in-extra-body-bridged",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"reasoning_effort": "none",
|
||||
"reasoningSummary": "auto",
|
||||
"extra_body": {"reasoning_effort": {"summary": "detailed"}},
|
||||
},
|
||||
None,
|
||||
id="summary-only-dict-in-extra-body-bridged",
|
||||
),
|
||||
pytest.param(
|
||||
{"reasoning_effort": "none", "extra_body": {"reasoning_effort": "low"}},
|
||||
"low",
|
||||
id="other-effort-in-extra-body",
|
||||
),
|
||||
pytest.param(
|
||||
{"reasoning_effort": "none", "tools": [_FUNCTION_TOOL], "extra_body": {"reasoning_effort": "low"}},
|
||||
"low",
|
||||
id="other-effort-in-extra-body-bridged",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_drop_warning_fires_unless_the_provider_gets_the_dropped_effort(
|
||||
request_params: dict[str, object],
|
||||
expected_sent_effort: str | None,
|
||||
azure_upstream_routes: AzureUpstreamRoutes,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
litellm.completion(
|
||||
model=_UNLISTED_AZURE_GPT_5_MODEL,
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
api_base=_AZURE_TEST_API_BASE,
|
||||
api_key="fake-key",
|
||||
api_version="2025-04-01-preview",
|
||||
drop_params=True,
|
||||
**request_params,
|
||||
)
|
||||
|
||||
sent_effort: Final = azure_upstream_routes.sent_reasoning_effort()
|
||||
drop_warnings: Final = [r.getMessage() for r in caplog.records if "Dropping reasoning_effort" in r.getMessage()]
|
||||
assert sent_effort == expected_sent_effort
|
||||
assert len(drop_warnings) == (0 if sent_effort == "none" else 1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"request_params",
|
||||
[
|
||||
pytest.param(
|
||||
{"provider_affinity_header": "x-session-affinity", "litellm_session_id": "a\nb"},
|
||||
id="rejected-before-dispatch",
|
||||
),
|
||||
pytest.param({}, id="rejected-by-the-provider"),
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_drop_warning_fires_once_when_the_request_fails(
|
||||
request_params: dict[str, object],
|
||||
azure_upstream_routes: AzureUpstreamRoutes,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
azure_upstream_routes.chat.respond(400, json={"error": {"message": "refused", "type": "invalid_request_error"}})
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"), pytest.raises(litellm.BadRequestError):
|
||||
litellm.completion(
|
||||
model=_UNLISTED_AZURE_GPT_5_MODEL,
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
api_base=_AZURE_TEST_API_BASE,
|
||||
api_key="fake-key",
|
||||
api_version="2025-04-01-preview",
|
||||
drop_params=True,
|
||||
reasoning_effort="none",
|
||||
**request_params,
|
||||
)
|
||||
|
||||
assert len([r for r in caplog.records if "Dropping reasoning_effort" in r.getMessage()]) == 1
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def github_copilot_access_token(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
monkeypatch.setenv("GITHUB_COPILOT_TOKEN_DIR", str(tmp_path))
|
||||
(tmp_path / "access-token").write_text("token")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "api_base", "refused_effort", "model_info"),
|
||||
[
|
||||
pytest.param("openai/gpt-5.6-litellm-unlisted", "https://api.openai.com/v1", "xhigh", None, id="openai"),
|
||||
pytest.param(
|
||||
"azure_ai/gpt-5.6-litellm-unlisted",
|
||||
"https://example.services.ai.azure.com/openai/v1",
|
||||
"xhigh",
|
||||
None,
|
||||
id="azure_ai",
|
||||
),
|
||||
pytest.param(
|
||||
"chatgpt/gpt-5.5",
|
||||
"https://chatgpt.example.com/backend-api/codex",
|
||||
"minimal",
|
||||
{"reasoning_effort_levels": ["minimal", "low", "medium", "high"]},
|
||||
id="chatgpt-declared-level",
|
||||
),
|
||||
pytest.param(
|
||||
"github_copilot/gpt-5.5",
|
||||
"https://api.githubcopilot.com",
|
||||
"minimal",
|
||||
{"reasoning_effort_levels": ["minimal", "low", "medium", "high"]},
|
||||
id="github-copilot-declared-level",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_drop_warning_stays_quiet_when_the_summary_alias_restores_the_effort(
|
||||
model: str,
|
||||
api_base: str,
|
||||
refused_effort: str,
|
||||
model_info: dict[str, object] | None,
|
||||
azure_upstream_routes: AzureUpstreamRoutes,
|
||||
github_copilot_access_token: None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
litellm.completion(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
api_base=api_base,
|
||||
api_key="fake-key",
|
||||
drop_params=True,
|
||||
reasoning_effort=refused_effort,
|
||||
reasoningSummary="auto",
|
||||
model_info=model_info,
|
||||
)
|
||||
|
||||
assert azure_upstream_routes.sent_reasoning_effort() == refused_effort
|
||||
assert not [r for r in caplog.records if "Dropping reasoning_effort" in r.getMessage()]
|
||||
|
||||
|
||||
def test_a_request_param_named_like_the_drop_holder_still_reaches_the_provider(
|
||||
azure_upstream_routes: AzureUpstreamRoutes,
|
||||
):
|
||||
litellm.completion(
|
||||
model=_UNLISTED_AZURE_GPT_5_MODEL,
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
api_base=_AZURE_TEST_API_BASE,
|
||||
api_key="fake-key",
|
||||
api_version="2025-04-01-preview",
|
||||
drop_params=True,
|
||||
reasoning_drop_logger="sent by the caller",
|
||||
)
|
||||
|
||||
sent_body: Final = json.loads(azure_upstream_routes.chat.calls[0].request.content)
|
||||
assert sent_body["reasoning_drop_logger"] == "sent by the caller"
|
||||
|
||||
|
||||
def test_mock_response_keeps_the_reasoning_effort_drop_warning(
|
||||
local_model_cost_map: None, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
litellm.completion(
|
||||
model=_UNLISTED_AZURE_GPT_5_MODEL,
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
api_base=_AZURE_TEST_API_BASE,
|
||||
api_key="fake-key",
|
||||
reasoning_effort="none",
|
||||
drop_params=True,
|
||||
mock_response="hi",
|
||||
)
|
||||
|
||||
assert any("Dropping reasoning_effort='none'" in r.getMessage() for r in caplog.records)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"drop_params, expected_reasoning",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue