fix(ollama): forward assistant tool call ids in ollama_chat requests (#49278)

* test(ollama): cover assistant tool call ids in ollama_chat requests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ollama): forward assistant tool call ids in ollama_chat requests

Co-authored-by: Kenneth Bingham <w@qrk.us>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(ollama): format tool call id regression assertion

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ollama): keep tool call ID handling type safe

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(ollama): format tool-call id helper

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ollama): preserve typed tool call arguments

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Kenneth Bingham <w@qrk.us>
This commit is contained in:
moe-berri 2026-10-30 12:21:30 -07:00 • committed by GitHub
parent 5898a42f5a
commit 4f7150f832
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 110 additions and 10 deletions

View file

@ -235,6 +235,19 @@ class OllamaChatConfig(BaseConfig):
litellm_params["function_name"] = function_name
tools: Final = optional_params.pop("tools", None)
def transform_tool_call(typed_tool: ChatCompletionAssistantToolCall) -> OllamaToolCall:
function: Final = OllamaToolCallFunction(
name=typed_tool["function"].get("name") or "",
arguments=json.loads(typed_tool["function"]["arguments"])
if "arguments" in typed_tool["function"]
else {},
)
tool_id: Final = typed_tool.get("id")
ollama_tool_call: Final = (
OllamaToolCall(function=function) if tool_id is None else OllamaToolCall(id=tool_id, function=function)
)
return ollama_tool_call
new_messages: Final = []
for m in messages:
if isinstance(
@ -248,16 +261,7 @@ class OllamaChatConfig(BaseConfig):
for tool in tool_calls:
typed_tool = ChatCompletionAssistantToolCall(**tool)
if typed_tool["type"] == "function":
arguments = {}
if "arguments" in typed_tool["function"]:
arguments = json.loads(typed_tool["function"]["arguments"])
ollama_tool_call = OllamaToolCall(
function=OllamaToolCallFunction(
name=typed_tool["function"].get("name") or "",
arguments=arguments,
)
)
new_tools.append(ollama_tool_call)
new_tools.append(transform_tool_call(typed_tool))
reasoning_content, parsed_content = extract_reasoning_content(cast(dict, m))
content_str = convert_content_list_to_str(cast(AllMessageValues, m))
images = extract_images_from_message(cast(AllMessageValues, m))

View file

@ -1,4 +1,6 @@
from typing_extensions import (
NotRequired,
ReadOnly,
Required,
TypedDict,
)
@ -12,6 +14,7 @@ class OllamaToolCallFunction(
class OllamaToolCall(TypedDict):
id: NotRequired[ReadOnly[str]]
function: OllamaToolCallFunction

View file

@ -856,6 +856,99 @@ class TestOllamaReasoningContentStreaming:
class TestOllamaToolCallTransformation:
@pytest.mark.parametrize("stream", [False, True])
def test_transform_request_keeps_parallel_call_ids_paired_with_results(self, stream: bool):
config: Final = OllamaChatConfig()
messages: Final = cast(
list[AllMessageValues],
[
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_first",
"type": "function",
"function": {
"name": "read_fixture",
"arguments": '{"path": "first.txt"}',
},
},
{
"id": "call_second",
"type": "function",
"function": {
"name": "read_fixture",
"arguments": '{"path": "second.txt"}',
},
},
],
},
{
"role": "tool",
"tool_call_id": "call_second",
"content": "second result",
},
{
"role": "tool",
"tool_call_id": "call_first",
"content": "first result",
},
],
)
result: Final = config.transform_request(
model="ollama_chat/test-model",
messages=messages,
optional_params={"stream": stream},
litellm_params={},
headers={},
)
assert result["messages"][0]["tool_calls"] == [
{
"id": "call_first",
"function": {"name": "read_fixture", "arguments": {"path": "first.txt"}},
},
{
"id": "call_second",
"function": {"name": "read_fixture", "arguments": {"path": "second.txt"}},
},
]
assert [message.get("tool_call_id") for message in result["messages"][1:]] == [
"call_second",
"call_first",
]
def test_transform_request_does_not_invent_an_id_for_an_unidentified_call(self):
config: Final = OllamaChatConfig()
messages: Final = cast(
list[AllMessageValues],
[
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": None,
"type": "function",
"function": {"name": "read_fixture", "arguments": "{}"},
}
],
}
],
)
result: Final = config.transform_request(
model="ollama_chat/test-model",
messages=messages,
optional_params={},
litellm_params={},
headers={},
)
assert result["messages"][0]["tool_calls"] == [{"function": {"name": "read_fixture", "arguments": {}}}]
def test_transform_request_preserves_tool_calls(self):
"""
tool_calls on assistant messages must survive transform_request.