diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index cb3080e6534..10bf3736e64 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -171,10 +171,12 @@ class OllamaChatConfig(BaseConfig): if value.get("json_schema") and value["json_schema"].get("schema"): optional_params["format"] = value["json_schema"]["schema"] if param == "reasoning_effort" and value is not None: - if model.startswith("gpt-oss"): - optional_params["think"] = value - else: - optional_params["think"] = value in {"low", "medium", "high"} + effort: Final = value.get("effort") if isinstance(value, dict) else value + if effort is not None: + if model.startswith("gpt-oss"): + optional_params["think"] = effort + else: + optional_params["think"] = effort in {"low", "medium", "high"} ### FUNCTION CALLING LOGIC ### # Ollama 0.4+ supports native tool calling - pass tools directly # and let Ollama handle model capability detection diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py index b1f69220de7..e8dfb209b19 100644 --- a/litellm/llms/ollama/completion/transformation.py +++ b/litellm/llms/ollama/completion/transformation.py @@ -215,10 +215,12 @@ 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"} + effort: Final = value.get("effort") if isinstance(value, dict) else value + if effort is not None: + if model.startswith("gpt-oss"): + optional_params["think"] = effort + else: + optional_params["think"] = effort in {"low", "medium", "high"} elif param == "response_format" and isinstance(value, dict): if value["type"] == "json_object": optional_params["format"] = "json" diff --git a/tests/unit/llms/ollama/test_ollama_chat_transformation.py b/tests/unit/llms/ollama/test_ollama_chat_transformation.py index c7fba21d222..58595502357 100644 --- a/tests/unit/llms/ollama/test_ollama_chat_transformation.py +++ b/tests/unit/llms/ollama/test_ollama_chat_transformation.py @@ -1,7 +1,7 @@ import inspect import os import sys -from typing import cast +from typing import Final, cast import pytest from pydantic import BaseModel @@ -989,3 +989,43 @@ class TestOllamaStreamingUsage: ) assert result.usage is None + + +class TestOllamaChatReasoningEffort: + @pytest.mark.parametrize( + "reasoning_effort, expected_think", + [ + ("low", True), + ("high", True), + ("none", False), + ({"effort": "medium"}, True), + ({"effort": "medium", "summary": "auto"}, True), + ({"effort": "none", "summary": "detailed"}, False), + ], + ) + def test_reasoning_effort_string_or_dict_maps_to_think( + self, reasoning_effort: str | dict[str, str], expected_think: bool + ) -> None: + optional_params: Final = get_optional_params( + model="ollama_chat/qwen3:8b", + custom_llm_provider="ollama_chat", + reasoning_effort=reasoning_effort, + ) + assert optional_params["think"] is expected_think + + def test_reasoning_dict_without_effort_sets_nothing(self) -> None: + optional_params: Final = get_optional_params( + model="ollama_chat/qwen3:8b", + custom_llm_provider="ollama_chat", + reasoning_effort={"summary": "auto"}, + ) + assert "think" not in optional_params + + def test_reasoning_dict_gpt_oss_forwards_effort_string(self) -> None: + optional_params: Final = OllamaChatConfig().map_openai_params( + non_default_params={"reasoning_effort": {"effort": "high", "summary": "auto"}}, + optional_params={}, + model="gpt-oss:20b", + drop_params=False, + ) + assert optional_params["think"] == "high" diff --git a/tests/unit/llms/ollama/test_ollama_completion_transformation.py b/tests/unit/llms/ollama/test_ollama_completion_transformation.py index d6215a742f0..e51e5979deb 100644 --- a/tests/unit/llms/ollama/test_ollama_completion_transformation.py +++ b/tests/unit/llms/ollama/test_ollama_completion_transformation.py @@ -2,6 +2,7 @@ import base64 import io import json import sys +from typing import Final from litellm._uuid import uuid from unittest.mock import MagicMock, patch @@ -775,3 +776,42 @@ def test_transform_request_leaves_unreadable_images_untouched(payload: str) -> N data = _transform_image_request(payload, "png") assert data["images"] == [payload] + + +class TestOllamaConfigReasoningEffort: + @pytest.mark.parametrize( + "reasoning_effort, expected_think", + [ + ("medium", True), + ({"effort": "medium", "summary": "auto"}, True), + ({"effort": "none"}, False), + ], + ) + def test_reasoning_effort_string_or_dict_maps_to_think( + self, reasoning_effort: str | dict[str, str], expected_think: bool + ) -> None: + optional_params: Final = OllamaConfig().map_openai_params( + non_default_params={"reasoning_effort": reasoning_effort}, + optional_params={}, + model="qwen3:8b", + drop_params=False, + ) + assert optional_params["think"] is expected_think + + def test_reasoning_dict_gpt_oss_forwards_effort_string(self) -> None: + optional_params: Final = OllamaConfig().map_openai_params( + non_default_params={"reasoning_effort": {"effort": "high", "summary": "auto"}}, + optional_params={}, + model="gpt-oss:20b", + drop_params=False, + ) + assert optional_params["think"] == "high" + + def test_reasoning_dict_without_effort_sets_nothing(self) -> None: + optional_params: Final = OllamaConfig().map_openai_params( + non_default_params={"reasoning_effort": {"summary": "auto"}}, + optional_params={}, + model="qwen3:8b", + drop_params=False, + ) + assert "think" not in optional_params