mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(logging): warn on the remaining Ollama, DeepSeek and Tencent reasoning drops
The ollama/ completion route now shares the chat route's think mapping, so a level it turns off logs the same warning. DeepSeek warns when it drops thinking.budget_tokens, and Tencent warns when an adaptive-only model gets a non-integer budget. Magistral no longer warns on low, medium, high or enabled thinking, the values its reasoning prompt stands in for. The Gemini 3 budget warning no longer claims the model uses its default level.
This commit is contained in:
parent
53414c83b2
commit
949c6365db
11 changed files with 184 additions and 23 deletions
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import httpx
|
|||
from typing_extensions import TypeIs # noqa: TID251 # TypeIs reaches typing only on 3.13
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
convert_content_list_to_str,
|
||||
|
|
@ -59,6 +60,17 @@ 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."
|
||||
_DOCUMENTED_MAGISTRAL_EFFORTS: Final = frozenset({"low", "medium", "high"})
|
||||
_REASONING_ON_THINKING_TYPES: Final = frozenset({"enabled", "adaptive"})
|
||||
|
||||
|
||||
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)
|
||||
if param == "thinking" and thinking is not None and thinking.get("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
|
||||
|
|
@ -218,9 +230,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_dropped_reasoning(
|
||||
param, OPAQUE_VALUE.validate_python(value), model, _REASONING_PROMPT_REASON, drop_params_gated=False
|
||||
)
|
||||
_warn_unless_reasoning_prompt_honors(param, OPAQUE_VALUE.validate_python(value), model)
|
||||
if param == "parallel_tool_calls":
|
||||
optional_params["parallel_tool_calls"] = value
|
||||
return optional_params
|
||||
|
|
|
|||
|
|
@ -14,7 +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.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
|
||||
|
|
@ -30,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
|
||||
|
|
@ -40,9 +40,6 @@ 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):
|
||||
"""
|
||||
|
|
@ -176,14 +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 _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
|
||||
)
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ _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. "
|
||||
"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()
|
||||
|
|
|
|||
|
|
@ -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) == ()
|
||||
|
|
|
|||
|
|
@ -1676,7 +1676,8 @@ def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str,
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param, value", (("reasoning_effort", "none"), ("thinking", {"type": "enabled", "budget_tokens": 1024}))
|
||||
"param, value",
|
||||
(("reasoning_effort", "none"), ("reasoning_effort", "xhigh"), ("thinking", {"type": "disabled"})),
|
||||
)
|
||||
def test_magistral_reasoning_request_replaced_by_prompt_warns_once(
|
||||
param: str, value: object, caplog: pytest.LogCaptureFixture
|
||||
|
|
@ -1695,6 +1696,30 @@ def test_magistral_reasoning_request_replaced_by_prompt_warns_once(
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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,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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue