mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge de067c957c into fe87b187c6
This commit is contained in:
commit
eb2dd0033d
2 changed files with 103 additions and 2 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue