This commit is contained in:
Shree Bohara 2026-08-27 15:02:27 -05:00 committed by GitHub
commit eb2dd0033d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 103 additions and 2 deletions

View file

@ -421,6 +421,7 @@ class OllamaChatConfig(BaseConfig):
class OllamaChatCompletionResponseIterator(BaseModelResponseIterator):
started_reasoning_content: bool = False
finished_reasoning_content: bool = False
emitted_tool_calls: bool = False
def _is_function_call_complete(self, function_args: str | dict) -> bool:
if isinstance(function_args, dict):
@ -466,6 +467,7 @@ class OllamaChatCompletionResponseIterator(BaseModelResponseIterator):
# process tool calls - if complete function arg - add id to tool call
tool_calls: Final = chunk["message"].get("tool_calls")
if tool_calls is not None:
self.emitted_tool_calls = True
for tool_call in tool_calls:
function_args = tool_call.get("function").get("arguments")
if function_args is not None and len(function_args) > 0:
@ -506,9 +508,12 @@ class OllamaChatCompletionResponseIterator(BaseModelResponseIterator):
if chunk["done"] is True:
finish_reason = chunk.get("done_reason") or "stop"
# Override finish_reason when tool_calls are present
# Override finish_reason when tool_calls were emitted in any chunk of the
# stream. Only upgrade a "stop" ending - other terminal reasons such as
# "length" mean the tool call may be truncated, so don't report it as complete.
# Fixes: https://github.com/BerriAI/litellm/issues/18922
if tool_calls is not None:
# https://github.com/BerriAI/litellm/issues/35663
if self.emitted_tool_calls and finish_reason == "stop":
finish_reason = "tool_calls"
choices = [
StreamingChoices(

View file

@ -478,6 +478,102 @@ class TestOllamaToolCalling:
assert result.choices[0].finish_reason == "stop"
assert result.choices[0].message.tool_calls is None
def test_finish_reason_tool_calls_streaming_tool_calls_in_earlier_chunk(self):
"""Streaming: tool_calls streamed before the final done chunk must produce finish_reason='tool_calls'.
Ollama emits tool_calls in a chunk with done=False, then a separate final
chunk with done=True, an empty message, and done_reason='stop'. The final
chunk must still report finish_reason='tool_calls' per the OpenAI spec.
Regression test for https://github.com/BerriAI/litellm/issues/35663
"""
iterator = OllamaChatCompletionResponseIterator(
streaming_response=iter([]),
sync_stream=True,
)
tool_call_chunk = {
"model": "qwen3:14b",
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"function": {
"name": "get_weather",
"arguments": {"location": "San Francisco"},
}
}
],
},
"done": False,
}
done_chunk = {
"model": "qwen3:14b",
"message": {"role": "assistant", "content": ""},
"done": True,
"done_reason": "stop",
"prompt_eval_count": 100,
"eval_count": 50,
}
tool_call_result = iterator.chunk_parser(tool_call_chunk)
assert tool_call_result.choices[0].delta.tool_calls is not None
assert tool_call_result.choices[0].finish_reason is None
result = iterator.chunk_parser(done_chunk)
assert (
result.choices[0].finish_reason == "tool_calls"
), f"Expected 'tool_calls' when tool calls were streamed in an earlier chunk, got '{result.choices[0].finish_reason}'"
def test_finish_reason_length_survives_earlier_tool_call_chunk(self):
"""Streaming: a truncated stream keeps finish_reason='length' after a tool call.
Ollama emits tool_calls in an earlier chunk and can then terminate with
done_reason='length' once num_predict is reached. The tool call arguments may be
cut off, so the terminal reason must survive rather than be upgraded to
'tool_calls'. Mirrors the guard in litellm's own streaming finalizer, which only
upgrades a 'stop' ending.
"""
iterator = OllamaChatCompletionResponseIterator(
streaming_response=iter([]),
sync_stream=True,
)
tool_call_chunk = {
"model": "qwen3:14b",
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"function": {
"name": "get_weather",
"arguments": {"location": "San Francisco"},
}
}
],
},
"done": False,
}
done_chunk = {
"model": "qwen3:14b",
"message": {"role": "assistant", "content": ""},
"done": True,
"done_reason": "length",
"prompt_eval_count": 100,
"eval_count": 51,
}
iterator.chunk_parser(tool_call_chunk)
result = iterator.chunk_parser(done_chunk)
assert (
result.choices[0].finish_reason == "length"
), f"Expected 'length' to survive truncation after a tool call chunk, got '{result.choices[0].finish_reason}'"
class TestOllamaFinishReasonLength:
"""Tests for done_reason 'length' → finish_reason 'length' mapping.