mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(ollama_chat): set finish_reason to tool_calls when tool calls arrive before the final chunk
Ollama streams tool_calls in chunks with done=false, then sends a final done chunk with no tool_calls and done_reason=stop. chunk_parser only overrode finish_reason when the final chunk itself carried tool_calls, so streamed tool calls ended with finish_reason=stop and spec-strict clients never executed them. Track whether any chunk in the stream carried tool_calls on the iterator and use that when the done chunk arrives Fixes #35663
This commit is contained in:
parent
9da0b30888
commit
2f55eb664b
2 changed files with 54 additions and 2 deletions
|
|
@ -423,6 +423,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):
|
||||
|
|
@ -468,6 +469,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:
|
||||
|
|
@ -508,9 +510,10 @@ 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
|
||||
# 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:
|
||||
finish_reason = "tool_calls"
|
||||
choices = [
|
||||
StreamingChoices(
|
||||
|
|
|
|||
|
|
@ -478,6 +478,55 @@ 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}'"
|
||||
|
||||
|
||||
class TestOllamaFinishReasonLength:
|
||||
"""Tests for done_reason 'length' → finish_reason 'length' mapping.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue