From 3c9d419726465e00633b6c9727f6a27bd5d7a599 Mon Sep 17 00:00:00 2001 From: mmc <85631065@qq.com> Date: Sat, 25 Apr 2026 15:20:13 +0800 Subject: [PATCH] fix: stop replaying OpenAI stream text Avoid replaying buffered text at the end of OpenAI streams so text-only responses are emitted once while forced tool fallback behavior stays intact. Co-Authored-By: Claude Opus 4.7 --- app/main.py | 4 +--- tests/test_tool_call_bridge.py | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index f54ee8d..6b8d77a 100644 --- a/app/main.py +++ b/app/main.py @@ -650,9 +650,7 @@ async def v1_chat_completions(req: ChatCompletionsRequest, request: Request): buffered_text_parts.clear() yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" - if buffered_text_parts: - for buffered_text in buffered_text_parts: - yield _text_payload(buffered_text) + if buffered_text_parts and forced_tool_name and saw_tool_call: buffered_text_parts.clear() done_payload = { diff --git a/tests/test_tool_call_bridge.py b/tests/test_tool_call_bridge.py index 066587b..b1f37dc 100644 --- a/tests/test_tool_call_bridge.py +++ b/tests/test_tool_call_bridge.py @@ -414,12 +414,38 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase): body = await _collect_stream(response) self.assertIn('"tool_calls"', body) - self.assertIn('"content": "hello"', body) + self.assertEqual(body.count('"content": "hello"'), 1) self.assertIn('"finish_reason": "tool_calls"', body) self.assertIn('"usage"', body) self.assertIn("data: [DONE]", body) + async def test_openai_stream_emits_text_delta_only_once_without_tools(self) -> None: + fake_client = _FakeClient( + stream_events=[ + {"type": "text", "text": "你好"}, + ], + complete_result={}, + ) + req = ChatCompletionsRequest( + model="org_auto", + messages=[{"role": "user", "content": "hi"}], + stream=True, + ) + + with ( + patch.object(main, "pool", _FakePool(_FakeInstance(fake_client))), + patch.object(main, "chat_guard", _FakeGuard()), + patch.object(main, "_ensure_instance_logged_in", AsyncMock(return_value={"id": "u"})), + patch.object(main.stats_collector, "record_chat", AsyncMock(return_value=None)), + ): + response = await main.v1_chat_completions(req, _make_request("/v1/chat/completions")) + body = await _collect_stream(response) + + self.assertEqual(body.count('"content": "你好"'), 1) + self.assertIn('"finish_reason": "stop"', body) + self.assertIn("data: [DONE]", body) + async def test_openai_stream_filters_tool_events_by_allowlist(self) -> None: fake_client = _FakeClient( stream_events=[