From 2dd7b2bb1ae9bf84afb56fb3718be247c5a22a52 Mon Sep 17 00:00:00 2001 From: ly-wang19 Date: Mon, 22 Jun 2026 12:56:48 +0800 Subject: [PATCH] fix(oci): also report finish_reason="tool_calls" on streaming terminal chunks Extends the finish_reason fix to the streaming handlers (per review): the terminal SSE chunk normalized OCI's "COMPLETE" to "stop" even when tool calls were emitted. handle_cohere_stream_chunk now reports "tool_calls" when tool calls are present on the chunk or were emitted earlier (prior_tool_calls_emitted, since the terminal chunk suppresses already-streamed tool calls), and handle_generic_stream_chunk reports "tool_calls" when the chunk carries them. --- litellm/llms/oci/chat/cohere.py | 7 +++ litellm/llms/oci/chat/generic.py | 3 ++ .../llms/oci/chat/test_oci_finish_reason.py | 51 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/litellm/llms/oci/chat/cohere.py b/litellm/llms/oci/chat/cohere.py index 010c0578ec7..aeea4d0f209 100644 --- a/litellm/llms/oci/chat/cohere.py +++ b/litellm/llms/oci/chat/cohere.py @@ -375,6 +375,13 @@ def handle_cohere_stream_chunk( ] finish_reason = _normalize_oci_finish_reason(typed_chunk.finishReason) + # OpenAI semantics: the terminal chunk must report finish_reason="tool_calls" + # when tool calls were emitted — on this chunk or earlier in the stream. OCI + # Cohere's terminal finishReason is "COMPLETE" (normalized to "stop"), and the + # terminal chunk suppresses already-emitted tool calls, so rely on + # prior_tool_calls_emitted as well. + if finish_reason is not None and (tool_calls or prior_tool_calls_emitted): + finish_reason = "tool_calls" return ModelResponseStream( choices=[ diff --git a/litellm/llms/oci/chat/generic.py b/litellm/llms/oci/chat/generic.py index 275a5fe3da0..58555c30c6d 100644 --- a/litellm/llms/oci/chat/generic.py +++ b/litellm/llms/oci/chat/generic.py @@ -463,6 +463,9 @@ def handle_generic_stream_chunk(dict_chunk: dict) -> ModelResponseStream: finish_reason: Optional[str] = _normalize_oci_finish_reason( typed_chunk.finishReason ) + # OpenAI semantics: a chunk carrying tool calls reports finish_reason="tool_calls". + if tool_calls: + finish_reason = "tool_calls" return ModelResponseStream( choices=[ diff --git a/tests/test_litellm/llms/oci/chat/test_oci_finish_reason.py b/tests/test_litellm/llms/oci/chat/test_oci_finish_reason.py index d97b0592de1..41b4b886ded 100644 --- a/tests/test_litellm/llms/oci/chat/test_oci_finish_reason.py +++ b/tests/test_litellm/llms/oci/chat/test_oci_finish_reason.py @@ -3,6 +3,8 @@ import datetime import httpx from litellm import ModelResponse +from litellm.llms.oci.chat.cohere import handle_cohere_stream_chunk +from litellm.llms.oci.chat.generic import handle_generic_stream_chunk from litellm.llms.oci.chat.transformation import OCIChatConfig @@ -94,3 +96,52 @@ def test_generic_tool_calls_report_finish_reason_tool_calls(): result = _transform("meta.llama-3.3-70b-instruct", _generic_body_with_tool_calls()) assert result.choices[0].message.tool_calls is not None assert result.choices[0].finish_reason == "tool_calls" + + +# --- streaming handlers (terminal chunk finish_reason) --- + + +def _cohere_terminal_chunk() -> dict: + return { + "apiFormat": "COHERE", + "text": "All done.", + "finishReason": "COMPLETE", + "chatHistory": [{"role": "CHATBOT", "message": "All done."}], + "index": 0, + } + + +def test_cohere_stream_terminal_chunk_reports_tool_calls_when_emitted(): + # On the terminal chunk, OCI Cohere reports finishReason="COMPLETE" and + # suppresses already-streamed tool calls; finish_reason must still be + # "tool_calls" when tool calls were emitted earlier in the stream. + result = handle_cohere_stream_chunk( + _cohere_terminal_chunk(), prior_tool_calls_emitted=True + ) + assert result.choices[0].finish_reason == "tool_calls" + + +def test_cohere_stream_terminal_chunk_reports_stop_without_tool_calls(): + result = handle_cohere_stream_chunk( + _cohere_terminal_chunk(), prior_tool_calls_emitted=False + ) + assert result.choices[0].finish_reason == "stop" + + +def test_generic_stream_chunk_with_tool_calls_reports_tool_calls(): + chunk = { + "finishReason": "STOP", + "message": { + "toolCalls": [ + { + "id": "call_0", + "type": "FUNCTION", + "name": "get_weather", + "arguments": "{}", + } + ] + }, + "index": 0, + } + result = handle_generic_stream_chunk(chunk) + assert result.choices[0].finish_reason == "tool_calls"