From 6940a4b2085c66cd4ba3460d7c0a6e11e0abf984 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 7 Jun 2026 12:41:59 +0800 Subject: [PATCH] fix(proxy): distinguish truncated chat streams from normal completion Replace the unconditional finalize at chat-to-responses stream end with a three-way guard: complete normally when finish_reason or [DONE] arrived, emit an incomplete response when substantive output exists without a finish_reason, and emit a failed (stream_truncated) event for empty truncation instead of masking it as completed. Also propagate late-arriving reasoning_content onto still-active tool-call items. --- .../proxy/providers/streaming_codex_chat.rs | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/proxy/providers/streaming_codex_chat.rs b/src-tauri/src/proxy/providers/streaming_codex_chat.rs index 318fe36a9..7039c1b27 100644 --- a/src-tauri/src/proxy/providers/streaming_codex_chat.rs +++ b/src-tauri/src/proxy/providers/streaming_codex_chat.rs @@ -141,6 +141,7 @@ impl ChatToResponsesState { if let Some(delta) = choice.get("delta") { if let Some(reasoning) = chat_delta_reasoning_text(delta) { events.extend(self.push_reasoning_delta(&reasoning)); + self.append_reasoning_to_active_tools(&reasoning); } if let Some(content) = delta.get("content").and_then(|v| v.as_str()) { @@ -522,6 +523,34 @@ impl ChatToResponsesState { events } + fn append_reasoning_to_active_tools(&mut self, delta: &str) { + if delta.trim().is_empty() { + return; + } + + for state in self.tools.values_mut().filter(|state| !state.done) { + if state.reasoning_content.is_empty() { + state.reasoning_content = delta.trim_start().to_string(); + } else { + state.reasoning_content.push_str(delta); + } + } + } + + fn has_substantive_output(&self) -> bool { + !self.text.text.trim().is_empty() + || !self.reasoning.text.trim().is_empty() + || !self.inline_think.buffer.trim().is_empty() + || !self.output_items.is_empty() + || self.tools.values().any(|state| { + state.added + || !state.call_id.trim().is_empty() + || !state.name.trim().is_empty() + || !state.arguments.trim().is_empty() + || !state.reasoning_content.trim().is_empty() + }) + } + fn finalize(&mut self) -> Vec { if self.completed { return Vec::new(); @@ -949,8 +978,20 @@ pub fn create_responses_sse_stream_from_chat_with_context