This commit is contained in:
양지웅 (woongstar) 2026-10-03 20:08:30 +08:00 • committed by GitHub
commit ed8cffe694
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 122 additions and 14 deletions

View file

@ -28,7 +28,7 @@ from litellm.types.llms.openai import (
)
from litellm.types.utils import ModelResponse, ModelResponseStream
from ..common_utils import OllamaError
from ..common_utils import OllamaError, think_from_reasoning_effort
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
@ -170,11 +170,17 @@ class OllamaChatConfig(BaseConfig):
if param == "response_format" and isinstance(value, dict) and value.get("type") == "json_schema":
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"}
if (
param == "reasoning_effort"
and (
think := think_from_reasoning_effort(
model,
cast(object, value), # cast-ok: [LIT006] untyped dict value, widened to object for the helper
)
)
is not None
):
optional_params["think"] = think
### FUNCTION CALLING LOGIC ###
# Ollama 0.4+ supports native tool calling - pass tools directly
# and let Ollama handle model capability detection

View file

@ -1,5 +1,6 @@
import base64
import io
from collections.abc import Mapping
from typing import Any, Final
import httpx
@ -8,6 +9,15 @@ from litellm import verbose_logger
from litellm.llms.base_llm.chat.transformation import BaseLLMException
def think_from_reasoning_effort(model: str, reasoning_effort: object) -> str | bool | None:
effort: Final = reasoning_effort.get("effort") if isinstance(reasoning_effort, Mapping) else reasoning_effort
if not isinstance(effort, str):
return None
if model.startswith("gpt-oss"):
return effort
return effort in {"low", "medium", "high"}
class OllamaError(BaseLLMException):
def __init__(self, status_code: int, message: str, headers: dict | httpx.Headers):
super().__init__(status_code=status_code, message=message, headers=headers)

View file

@ -1,7 +1,7 @@
import json
import time
from collections.abc import AsyncIterator, Iterator
from typing import TYPE_CHECKING, Any, Final
from typing import TYPE_CHECKING, Any, Final, cast
from httpx._models import Headers, Response
from pydantic import BaseModel, ConfigDict, ValidationError
@ -33,7 +33,7 @@ from litellm.types.utils import (
StreamingChoices,
)
from ..common_utils import OllamaError, OllamaModelInfo, _convert_image
from ..common_utils import OllamaError, OllamaModelInfo, _convert_image, think_from_reasoning_effort
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
@ -214,11 +214,17 @@ class OllamaConfig(BaseConfig):
optional_params["frequency_penalty"] = value
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"}
elif (
param == "reasoning_effort"
and (
think := think_from_reasoning_effort(
model,
cast(object, value), # cast-ok: [LIT006] untyped dict value, widened to object for the helper
)
)
is not None
):
optional_params["think"] = think
elif param == "response_format" and isinstance(value, dict):
if value["type"] == "json_object":
optional_params["format"] = "json"

View file

@ -1,7 +1,9 @@
import inspect
import os
import sys
from typing import cast
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final, cast
import pytest
from pydantic import BaseModel
@ -989,3 +991,44 @@ 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),
(MappingProxyType({"effort": "medium", "summary": "auto"}), True),
({"effort": "none", "summary": "detailed"}, False),
],
)
def test_reasoning_effort_string_or_dict_maps_to_think(
self, reasoning_effort: str | Mapping[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"

View file

@ -2,6 +2,9 @@ import base64
import io
import json
import sys
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
from litellm._uuid import uuid
from unittest.mock import MagicMock, patch
@ -775,3 +778,43 @@ 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),
(MappingProxyType({"effort": "medium", "summary": "auto"}), True),
({"effort": "none"}, False),
],
)
def test_reasoning_effort_string_or_dict_maps_to_think(
self, reasoning_effort: str | Mapping[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