fix(oci): only emit streaming finish_reason="tool_calls" on the terminal chunk

Addresses @greptileai's P1. handle_generic_stream_chunk set
finish_reason="tool_calls" whenever tool_calls were present, including on
intermediate chunks — OCI streams tool calls progressively, so a non-terminal
chunk arrives with finishReason=null and toolCalls populated. Per OpenAI
streaming semantics finish_reason must be null on non-final deltas; a non-null
value there signals premature end-of-stream to callers that stop on
finish_reason is not None.

Guard the override with `finish_reason is not None` (matching the cohere
streaming handler). Adds a regression test for an intermediate chunk
(finishReason=null + toolCalls) asserting finish_reason stays None.
This commit is contained in:
ly-wang19 2026-06-23 18:41:44 +08:00
parent 2dd7b2bb1a
commit 298a2fd0b9
No known key found for this signature in database
2 changed files with 29 additions and 2 deletions

View file

@ -463,8 +463,12 @@ 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:
# OpenAI semantics: the terminal chunk reports finish_reason="tool_calls" when
# tool calls are present. Guard with `finish_reason is not None` so intermediate
# chunks (OCI streams tool calls progressively, with finishReason=null and
# toolCalls populated) keep finish_reason=None — a non-null value on a
# non-terminal chunk would signal premature stream termination to callers.
if finish_reason is not None and tool_calls:
finish_reason = "tool_calls"
return ModelResponseStream(

View file

@ -145,3 +145,26 @@ def test_generic_stream_chunk_with_tool_calls_reports_tool_calls():
}
result = handle_generic_stream_chunk(chunk)
assert result.choices[0].finish_reason == "tool_calls"
def test_generic_stream_intermediate_chunk_keeps_finish_reason_none():
# OCI streams tool calls progressively: an intermediate chunk carries
# toolCalls but finishReason=null. finish_reason must stay None on a
# non-terminal chunk — emitting "tool_calls" here would signal premature
# stream termination to callers that stop on finish_reason is not None.
chunk = {
"finishReason": None,
"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 is None