fix(ollama): accept dict reasoning_effort from Responses API bridge

Responses API clients such as Codex CLI send reasoning: {"summary": "auto"}
on every request. When the Responses to chat bridge forwarded that object as
reasoning_effort, OllamaChatConfig and OllamaConfig hashed it in a set
membership check and raised TypeError: unhashable type: 'dict', which came
back as an APIConnectionError and put Codex into a reconnect loop

Unwrap the effort key when the value is a dict and skip the think mapping
when no effort is present, so {"summary": ...} alone is a no-op and
{"effort": "low", "summary": ...} behaves like "low"

Fixes #37452
This commit is contained in:
woongstardev 2026-09-22 20:41:24 +09:00
parent 90e4962c81
commit d15aa0c89b
4 changed files with 93 additions and 9 deletions

View file

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

View file

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

View file

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

View file

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