fix(ollama_chat): preserve non-stop finish reasons when tool calls were streamed

Only upgrade a "stop" ending to "tool_calls". Ollama can terminate a stream with
done_reason="length" after a tool call has already been emitted, and reporting that
as "tool_calls" hides the truncation, so a client may execute a tool call whose
arguments were cut off.

Matches the existing guard in litellm's streaming finalizer, which only upgrades a
"stop" ending for the same reason.
This commit is contained in:
Shree Bohara 2026-08-06 06:36:33 -07:00
parent 2f55eb664b
commit a2f49c3fc4
No known key found for this signature in database
2 changed files with 51 additions and 2 deletions

View file

@ -510,10 +510,12 @@ class OllamaChatCompletionResponseIterator(BaseModelResponseIterator):
if chunk["done"] is True:
finish_reason = chunk.get("done_reason") or "stop"
# Override finish_reason when tool_calls were emitted in any chunk of the stream
# 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
# https://github.com/BerriAI/litellm/issues/35663
if self.emitted_tool_calls:
if self.emitted_tool_calls and finish_reason == "stop":
finish_reason = "tool_calls"
choices = [
StreamingChoices(

View file

@ -527,6 +527,53 @@ class TestOllamaToolCalling:
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.