From 2b1413f10f4c9f7f138f60ebecd86fcda4cf82b4 Mon Sep 17 00:00:00 2001 From: Saylee Date: Mon, 12 Oct 2026 00:16:24 +0530 Subject: [PATCH 1/3] fix(openai): json_mode stop finish_reason after folding tool call When json_mode folds a tool call into JSON content, the transform sets finish_reason to "stop" but then overwrites it by re-reading the raw provider finish_reason ("tool_calls"), so the choice reports finish_reason "tool_calls" while message.tool_calls is None. Report the finish_reason the transform already computed instead. Normal tool-call responses keep the existing "tool_calls" correction. Signed-off-by: Saylee --- .../llms/openai/chat/gpt_transformation.py | 2 +- .../chat/test_openai_gpt_transformation.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 936ec67ae92..42108601f58 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -669,7 +669,7 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): ) translated_choice.finish_reason = map_finish_reason( - self._get_finish_reason(translated_message, choice["finish_reason"]) + self._get_finish_reason(translated_message, finish_reason) ) transformed_choices.append(translated_choice) diff --git a/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py b/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py index cdb1b068987..83ba5e42f63 100644 --- a/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py +++ b/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py @@ -1557,3 +1557,32 @@ class TestToolSchemaCombinatorFlatteningForOpenAI: ) assert request["tools"][0] is tool + + +def test_transform_choices_json_mode_reports_stop_when_tool_call_becomes_content(): + """json_mode turns a tool call into JSON content; the choice must then report + finish_reason 'stop' instead of 'tool_calls' with no tool_calls attached.""" + choice = [ + { + "finish_reason": "tool_calls", + "index": 0, + "logprobs": None, + "message": { + "content": None, + "role": "assistant", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": "json_tool_call", "arguments": '{"a": 1}'}, + } + ], + }, + } + ] + + out = OpenAIGPTConfig()._transform_choices(choices=choice, json_mode=True) + + assert out[0].message.content == '{"a": 1}' + assert out[0].message.tool_calls is None + assert out[0].finish_reason == "stop" From 648eef1b4473ad8e43cf8fbfe097d02e810eb78b Mon Sep 17 00:00:00 2001 From: Saylee Date: Mon, 12 Oct 2026 22:40:05 +0530 Subject: [PATCH 2/3] fix(openai): only fold tool-call finish reasons to stop in json_mode The previous change folded every json_mode tool call to finish_reason "stop", which rewrote a provider "length" reason to "stop" and hid truncated output from callers. Map only "tool_calls" and "function_call" to "stop" and pass every other reason through, and parametrize the regression test over tool_calls, function_call, length and stop. Signed-off-by: Saylee --- .../llms/openai/chat/gpt_transformation.py | 2 +- .../chat/test_openai_gpt_transformation.py | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 42108601f58..0bb99c43cfa 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -640,7 +640,7 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): json_mode_content_str: str | None = str(new_tool_calls[0]["function"].get("arguments", "")) or None if json_mode_content_str is not None: translated_message = Message(content=json_mode_content_str) - finish_reason = "stop" + finish_reason = "stop" if choice["finish_reason"] in ("tool_calls", "function_call") else choice["finish_reason"] if translated_message is None: ## get the reasoning content diff --git a/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py b/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py index 83ba5e42f63..a3148b4ff8e 100644 --- a/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py +++ b/tests/unit/llms/openai/chat/test_openai_gpt_transformation.py @@ -1559,12 +1559,23 @@ class TestToolSchemaCombinatorFlatteningForOpenAI: assert request["tools"][0] is tool -def test_transform_choices_json_mode_reports_stop_when_tool_call_becomes_content(): - """json_mode turns a tool call into JSON content; the choice must then report - finish_reason 'stop' instead of 'tool_calls' with no tool_calls attached.""" +@pytest.mark.parametrize( + "provider_finish_reason, expected_finish_reason", + [ + ("tool_calls", "stop"), + ("function_call", "stop"), + ("length", "length"), + ("stop", "stop"), + ], +) +def test_transform_choices_json_mode_reports_stop_only_for_tool_call_reasons( + provider_finish_reason, expected_finish_reason +): + """json_mode folds a tool call into JSON content. Only tool-call reasons become + 'stop'; a truncated ('length') or already-final reason must survive untouched.""" choice = [ { - "finish_reason": "tool_calls", + "finish_reason": provider_finish_reason, "index": 0, "logprobs": None, "message": { @@ -1585,4 +1596,4 @@ def test_transform_choices_json_mode_reports_stop_when_tool_call_becomes_content assert out[0].message.content == '{"a": 1}' assert out[0].message.tool_calls is None - assert out[0].finish_reason == "stop" + assert out[0].finish_reason == expected_finish_reason From e7a250f656c18e730085e56e35c5e6ec394a73e0 Mon Sep 17 00:00:00 2001 From: Saylee Date: Mon, 12 Oct 2026 22:44:14 +0530 Subject: [PATCH 3/3] style(openai): wrap json_mode finish_reason ternary for ruff format Signed-off-by: Saylee --- litellm/llms/openai/chat/gpt_transformation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 0bb99c43cfa..415639d8186 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -640,7 +640,11 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): json_mode_content_str: str | None = str(new_tool_calls[0]["function"].get("arguments", "")) or None if json_mode_content_str is not None: translated_message = Message(content=json_mode_content_str) - finish_reason = "stop" if choice["finish_reason"] in ("tool_calls", "function_call") else choice["finish_reason"] + finish_reason = ( + "stop" + if choice["finish_reason"] in ("tool_calls", "function_call") + else choice["finish_reason"] + ) if translated_message is None: ## get the reasoning content