From c034c97be1d3949a1ab14c5226c2d5ab2a4d9964 Mon Sep 17 00:00:00 2001 From: coso Date: Thu, 8 Jan 2026 19:48:24 +0800 Subject: [PATCH] fix: Kiro Opus streaming response interruption (fixes #65) Improve AWS Event Stream parsing to fix streaming response interruption: 1. Change JSON start detection to search for specific patterns instead of simple '{' character. AWS Event Stream contains binary headers that may have '{' in unexpected positions. Now searches for valid JSON patterns: - {"content": - text content - {"name": - tool call start - {"toolUseId": - tool use event - {"input": - tool input continuation - {"stop": - stop event - {"usage": - usage info - {"contextUsagePercentage": - context usage 2. Handle standalone {"input":...} events for tool call input continuation. Kiro may send tool calls in multiple events: - {"name":"xxx","toolUseId":"xxx"} - start - {"input":"..."} - input data (may be multiple) - {"stop":true} - end 3. Handle standalone {"stop":true} events that close active tool calls. This mirrors the fix from AIClient-2-API project. Co-Authored-By: Claude Opus 4.5 --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- .../src/stream/parsers/aws_event_stream.rs | 91 ++++++++++++++++++- src-tauri/tauri.conf.json | 2 +- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 61e2069f4..9f8676e48 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.36.1", + "version": "0.36.2", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f6b0430bf..bb5e1f5a9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.36.1" +version = "0.36.2" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/stream/parsers/aws_event_stream.rs b/src-tauri/src/stream/parsers/aws_event_stream.rs index 8e748bb22..b1e97f8dc 100644 --- a/src-tauri/src/stream/parsers/aws_event_stream.rs +++ b/src-tauri/src/stream/parsers/aws_event_stream.rs @@ -276,11 +276,57 @@ impl AwsEventStreamParser { } /// 查找 JSON 对象的开始位置 + /// + /// AWS Event Stream 包含二进制头部,简单的 `{` 可能匹配到错误位置。 + /// 我们只搜索有效的 JSON 模式: + /// - `{"content":` - 文本内容 + /// - `{"name":` - 工具调用开始(包含 toolUseId) + /// - `{"toolUseId":` - 工具调用事件 + /// - `{"input":` - 工具参数续传 + /// - `{"stop":` - 停止事件 + /// - `{"usage":` - 使用量 + /// - `{"contextUsagePercentage":` - 上下文使用百分比 + /// - `{"followupPrompt":` - 后续提示(跳过) fn find_json_start(&self, from: usize) -> Option { - self.buffer[from..] - .iter() - .position(|&b| b == b'{') - .map(|p| from + p) + let buffer = &self.buffer[from..]; + + // 定义所有有效的 JSON 模式 + let patterns: &[&[u8]] = &[ + b"{\"content\":", + b"{\"name\":", + b"{\"toolUseId\":", + b"{\"input\":", + b"{\"stop\":", + b"{\"usage\":", + b"{\"contextUsagePercentage\":", + b"{\"followupPrompt\":", + ]; + + // 查找所有模式的位置,返回最早出现的 + let mut earliest: Option = None; + + for pattern in patterns { + if let Some(pos) = Self::find_pattern(buffer, pattern) { + match earliest { + None => earliest = Some(pos), + Some(e) if pos < e => earliest = Some(pos), + _ => {} + } + } + } + + earliest.map(|p| from + p) + } + + /// 在缓冲区中查找模式 + fn find_pattern(buffer: &[u8], pattern: &[u8]) -> Option { + if pattern.is_empty() || buffer.len() < pattern.len() { + return None; + } + + buffer + .windows(pattern.len()) + .position(|window| window == pattern) } /// 从缓冲区中提取完整的 JSON 对象 @@ -439,8 +485,45 @@ impl AwsEventStreamParser { } } } + // 处理独立的 input 事件(工具调用的 input 续传,没有 toolUseId) + // Kiro 的工具调用可能分多个事件发送: + // 1. {"name":"xxx","toolUseId":"xxx"} - 开始 + // 2. {"input":"..."} - input 数据(可能多次) + // 3. {"stop":true} - 结束 + else if let Some(input_chunk) = value.get("input").and_then(|v| v.as_str()) { + // 找到当前活跃的工具调用(应该只有一个) + if let Some((tool_id, accumulator)) = self.tool_accumulators.iter_mut().next() { + let tool_id = tool_id.clone(); + accumulator.input.push_str(input_chunk); + events.push(StreamEvent::ToolUseInputDelta { + id: tool_id, + partial_json: input_chunk.to_string(), + }); + } else { + tracing::warn!( + "[AWS_PARSER] 收到独立的 input 事件但没有活跃的工具调用: {}", + input_chunk + ); + } + } // 处理独立的 stop 事件 else if value.get("stop").and_then(|v| v.as_bool()).unwrap_or(false) { + // 首先检查是否有活跃的工具调用需要关闭 + // 这处理 Kiro 发送 {"stop":true} 来结束工具调用的情况 + if !self.tool_accumulators.is_empty() { + // 关闭所有活跃的工具调用 + let tool_ids: Vec = self.tool_accumulators.keys().cloned().collect(); + for tool_id in tool_ids { + if let Some(acc) = self.tool_accumulators.remove(&tool_id) { + self.context.remove_tool_call(&tool_id); + events.push(StreamEvent::ToolUseStop { id: tool_id }); + events.push(StreamEvent::ContentBlockStop { + index: acc.block_index, + }); + } + } + } + // 关闭文本块(如果有) if let Some(index) = self.text_block_index.take() { self.in_text_block = false; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 29501dcd3..7ed2e357e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.36.0", + "version": "0.36.2", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev",