fix(logging): warn on the Gemini 3, Magistral, Ollama, Fireworks and DeepSeek ARN reasoning drops

Gemini 3 and later now log a dropped thinking budget, Magistral logs the
level it swaps for a reasoning prompt, Ollama logs a level it maps to
think off, Fireworks logs chat_template_kwargs effort keys dropped for a
model the map does not list as reasoning, and DeepSeek on Bedrock logs a
drop when the model is addressed by an ARN that advertises the param.
This commit is contained in:
mateo-berri 2026-10-29 10:30:00 +00:00
parent 1172cb6ab7
commit 5b85f78300
13 changed files with 282 additions and 16 deletions

View file

@ -3,6 +3,8 @@ 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 (
@ -19,6 +21,7 @@ _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."
_REASONING_PARAMS: Final = ("reasoning", "reasoning_effort", "thinking")
OPAQUE_VALUE: Final = TypeAdapter(object)
def warn_dropped_reasoning(

View file

@ -48,6 +48,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,
@ -183,6 +184,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:
@ -635,6 +638,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.
@ -1138,9 +1145,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 (
@ -1174,10 +1180,7 @@ class AmazonConverseConfig(BaseConfig):
drop_params=drop_params,
)
elif param == "reasoning_effort" and isinstance(value, str) and drop_reasoning_effort_param:
verbose_logger.debug(
"Dropping unsupported `reasoning_effort` param for Bedrock model=%s; it always reasons and rejects it.",
model,
)
self._warn_native_reasoning_drop(param, value, model, _DEEPSEEK_R1_EFFORT_REASON)
elif (
param == "reasoning_effort"
and isinstance(value, str)

View file

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

View file

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

View file

@ -20,6 +20,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,
@ -57,6 +58,7 @@ def _accepted_reasoning_effort(model: str, requested: str, custom_llm_provider:
_NO_TOOL_CALL_TURN: Final[tuple[int, frozenset[str]]] = (-1, frozenset())
_REASONING_PROMPT_REASON: Final = "litellm sends Magistral a reasoning system prompt instead of the requested level."
def _is_json_array(value: object) -> TypeIs[Sequence[object]]: # guard-ok: a decoded JSON array is a list
@ -216,6 +218,9 @@ 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_dropped_reasoning(
param, OPAQUE_VALUE.validate_python(value), model, _REASONING_PROMPT_REASON, drop_params_gated=False
)
if param == "parallel_tool_calls":
optional_params["parallel_tool_calls"] = value
return optional_params

View file

@ -14,6 +14,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
extract_images_from_message,
extract_reasoning_content,
)
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
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
@ -39,6 +40,9 @@ if TYPE_CHECKING:
else:
LiteLLMLoggingObj = Any
_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."
class OllamaChatConfig(BaseConfig):
"""
@ -175,7 +179,11 @@ class OllamaChatConfig(BaseConfig):
if model.startswith("gpt-oss"):
optional_params["think"] = value
else:
optional_params["think"] = value in {"low", "medium", "high"}
optional_params["think"] = value in _THINK_ON_EFFORTS
if isinstance(value, str) and value not in _THINK_ON_EFFORTS and value != "none":
warn_dropped_reasoning(
"reasoning_effort", value, model, _THINK_OFF_REASON, drop_params_gated=False
)
### FUNCTION CALLING LOGIC ###
# Ollama 0.4+ supports native tool calling - pass tools directly
# and let Ollama handle model capability detection

View file

@ -204,6 +204,10 @@ _SAMPLING_DEPRECATION_UNMAPPED_MODELS: Final = "models outside litellm's model m
_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 it uses its default level. "
"Send reasoning_effort to pick a level."
)
_sampling_deprecation_lock: Final = threading.Lock()
@ -1469,6 +1473,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"] = (

View file

@ -1,6 +1,7 @@
import base64
import copy
import json
import logging
import os
import re
from collections.abc import Iterator, Mapping
@ -12,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,
@ -10789,3 +10791,49 @@ def test_a_deployment_with_a_batch_control_plane_endpoint_keeps_it_out_of_chat_r
assert response.choices[0].message.content == "ok"
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) == ()

View file

@ -1,4 +1,5 @@
import json
import logging
from typing import Final
from unittest.mock import MagicMock, patch
@ -8,6 +9,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
@ -2358,3 +2360,36 @@ 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") == ()

View file

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

View file

@ -1,8 +1,10 @@
import logging
from typing import Final, List, cast
from unittest.mock import MagicMock, patch
import pytest
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,
@ -1665,3 +1667,42 @@ 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"), ("thinking", {"type": "enabled", "budget_tokens": 1024}))
)
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]
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") == ()

View file

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

View file

@ -1730,6 +1730,40 @@ def test_gemini_integer_thinking_budget_is_sent_without_warning(caplog: pytest.L
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 = {}