Skip to content

fix(ollama_chat): set finish_reason to tool_calls when tool calls arrive before the final chunk - #35782

Open
ShreeBohara wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
ShreeBohara:fix-ollama-chat-stream-tool-calls-finish-reason-35663
Open

fix(ollama_chat): set finish_reason to tool_calls when tool calls arrive before the final chunk#35782
ShreeBohara wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
ShreeBohara:fix-ollama-chat-stream-tool-calls-finish-reason-35663

Conversation

@ShreeBohara

Copy link
Copy Markdown

TLDR

Problem this solves:

  • ollama_chat streamed tool calls ended with finish_reason "stop"
  • spec strict OpenAI clients never executed the streamed tool call

How it solves it:

  • iterator remembers tool_calls seen in any earlier chunk
  • final done chunk then reports finish_reason "tool_calls"

Relevant issues

Fixes #35663

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Environment for both runs: local proxy started with python -m litellm.proxy.proxy_cli --model ollama_chat/llama3.2 --host 127.0.0.1 --port 4000, ollama 0.15.6 serving llama3.2 locally, real streaming LLM calls, no mocks

curl -sN http://127.0.0.1:4000/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "ollama_chat/llama3.2", "stream": true, "temperature": 0,
  "messages": [{"role": "user", "content": "What is the weather in San Francisco right now? Use the get_weather tool."}],
  "tools": [{"type": "function", "function": {"name": "get_weather", "description": "Get current weather for a location", "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}}]}'

Before, at cb8c734 (litellm_internal_staging without this fix): the stream carries the tool call delta but the final chunk reports finish_reason "stop" (SSE trimmed to the two relevant chunks)

data: {"id":"fdd457f5-68f4-421f-aca9-d09ac47c36c8","created":1785814307,"model":"ollama_chat/llama3.2","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"64712615-3171-4639-ad60-515c161a8480","function":{"arguments":"{\"location\": \"San Francisco\"}","name":"get_weather"},"type":"function","index":0}]}}]}
data: {"id":"ca3be654-bdea-463e-ab0c-6fa74abf5917","object":"chat.completion.chunk","created":1785814307,"model":"ollama_chat/llama3.2","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

After, at d216383 (this PR): same request, the tool call delta is unchanged and the final chunk now reports finish_reason "tool_calls"

data: {"id":"6e73d45b-ba59-4a62-a7c3-aaf072f2850b","created":1785814351,"model":"ollama_chat/llama3.2","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"id":"b0a71062-7b37-4fd6-9670-f080976bdb77","function":{"arguments":"{\"location\": \"San Francisco\"}","name":"get_weather"},"type":"function","index":0}]}}]}
data: {"id":"b4f96644-1e33-4b15-9318-c1fe82e01e7c","object":"chat.completion.chunk","created":1785814351,"model":"ollama_chat/llama3.2","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}

Same fix observed through /v1/messages, where the finish reason surfaces as stop_reason. Request:

curl -sN http://127.0.0.1:4000/v1/messages -H 'Content-Type: application/json' -H 'anthropic-version: 2023-06-01' -d '{
  "model": "ollama_chat/llama3.2", "max_tokens": 256, "stream": true, "temperature": 0,
  "messages": [{"role": "user", "content": "What is the weather in San Francisco right now? Use the get_weather tool."}],
  "tools": [{"name": "get_weather", "description": "Get current weather for a location", "input_schema": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}]}'

Before, at cb8c734, the tool_use block streams but the message ends as end_turn

data: {"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 158, "output_tokens": 18}}

After, at d216383, the same request ends with stop_reason "tool_use"

data: {"type": "message_delta", "delta": {"stop_reason": "tool_use"}, "usage": {"input_tokens": 158, "output_tokens": 18}}

/v1/responses was also checked with the equivalent streamed request at both commits; the full event sequences are identical before and after, because the Responses emulation derives its function_call output items from the tool call deltas rather than from finish_reason, so that endpoint's output is unchanged by this PR

Type

🐛 Bug Fix

Changes

OllamaChatCompletionResponseIterator.chunk_parser only overrode finish_reason to "tool_calls" when the tool calls appeared on the same chunk as done: true. Ollama emits tool calls in a chunk with done: false, then a separate final chunk with an empty message and done_reason: "stop", so streamed tool calls ended with finish_reason "stop" and clients that gate tool execution on the finish reason discarded them. The non-streaming path already sets finish_reason correctly for this case

The iterator now sets an emitted_tool_calls flag when any chunk carries tool calls, and the done branch checks that flag instead of only the current chunk. The flag is per-stream state on the iterator instance, following the existing started_reasoning_content pattern in the same class. Behavior for tool calls arriving on the done chunk itself is unchanged since the flag is set earlier in that same call, and done_reason: "length" handling is untouched when no tool calls were streamed

Added a regression test that feeds the iterator a tool call chunk followed by a bare done chunk and asserts the final chunk reports finish_reason "tool_calls"; it fails at cb8c734 and passes with this change. Related but distinct: #35711 tracks tool call streaming for the ollama/ generate endpoint, which this PR does not touch

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

…ive 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 BerriAI#35663
@CLAassistant

CLAassistant commented Aug 4, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR tracks whether an Ollama chat stream emitted tool calls and uses that state to correct the terminal finish reason

  • Adds per-iterator tool-call state across streamed chunks
  • Adds a regression test for a tool-call chunk followed by a bare stop chunk

Confidence Score: 4/5

The token-limit termination case after a streamed tool call should be fixed before merging

The new unconditional override converts done_reason="length" to "tool_calls" after any tool-call delta, hiding truncation and potentially presenting incomplete arguments as executable

Files Needing Attention: litellm/llms/ollama/chat/transformation.py, tests/test_litellm/llms/ollama/test_ollama_chat_transformation.py

Important Files Changed

Filename Overview
litellm/llms/ollama/chat/transformation.py Corrects split-chunk tool-call termination, but also masks token-limit termination after a tool-call delta
tests/test_litellm/llms/ollama/test_ollama_chat_transformation.py Covers the reported stop case but omits the existing length termination combined with an earlier tool call

Reviews (1): Last reviewed commit: "fix(ollama_chat): set finish_reason to t..." | Re-trigger Greptile

Comment on lines +517 to +518
# https://github.com/BerriAI/litellm/issues/35663
if self.emitted_tool_calls:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Tool calls mask length termination

When Ollama emits a tool-call delta and then terminates with done_reason="length", this branch replaces the truncation signal with finish_reason="tool_calls", causing clients to treat potentially incomplete tool-call arguments as executable.

Rule Used: What: avoid backwards-incompatible changes without... (source)

Knowledge Base Used: LLM Provider Adapters

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2163837e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +518 to 519
if self.emitted_tool_calls:
finish_reason = "tool_calls"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve length finish reasons after tool-call chunks

When Ollama ends a stream with done_reason="length" after a prior tool-call chunk, this unconditional override changes the terminal finish_reason from length to tool_calls. That masks max-token truncation, so strict clients may execute a tool call whose arguments were cut off instead of treating the response as incomplete; the existing streaming finalizer only upgrades stop to tool_calls for this reason. Please gate this override to stop-like endings while preserving non-stop reasons such as length.

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing ShreeBohara:fix-ollama-chat-stream-tool-calls-finish-reason-35663 (d216383) with litellm_internal_staging (956d517)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: ollama_chat streaming tool calls end with finish_reason stop instead of tool_calls (tool_calls arrive in a chunk separate from the done chunk)

2 participants