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:
paveln83 2026-11-10 19:38:52 -08:00 • committed by GitHub
parent cb024047ac
commit 054ed65cdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 457 additions and 70 deletions

View file

@ -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.

View file

@ -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(

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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,

View file

@ -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)

View file

@ -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",
[