mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"private": true,
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.2",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<usize> {
|
||||
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<usize> = 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<usize> {
|
||||
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<String> = 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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user