proxy-playbook/docs/SSE_NORMALIZATION.md

51 lines
2.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SSE 归一化(StreamNormalizer)设计
## 为什么需要
- WorkBuddy 上游 ≈ OpenAI SSE,透传即可
- Trae SOLO 上游是私有事件协议,字段/信封和 OpenAI 不一样
- 客户端只认 data: {"choices":[{"delta":{"content":...}}]} 和 data: [DONE]
## 当前实现模式
先做 raw passthrough(实时转发上游字节),再做归一:
上游 InputStream -> respondOutputStream -> 客户端
好处:先解决“能不能连上/能不能出字”,再解决“客户端能不能解析”。
## 归一化目标映射
把 Trae 私有事件翻译成 OpenAI Chat Completions chunk:
| Trae 事件 | OpenAI chunk |
|---|---|
| 正文增量事件 | data: {"choices":[{"delta":{"content":"..."}}]} |
| 推理/思考块 | delta.reasoning_content(部分客户端支持) |
| 结束事件 | delta {} finish_reason stop + data: [DONE] |
| 用量事件 | 合并到 finish chunk 的 usage |
| 业务错误 | data: {"error":{...}} + data: [DONE] |
| 心跳注释 | : relay-keepalive(客户端自动忽略) |
## 设计原则
1. 见到样本再实现:每个上游私有格式千差万别,不要盲抄
2. 未知事件透传:宁可让客户端看到未知 data,也不要吞掉内容
3. 工具调用增量合并:Trae 可能发累计快照或增量,两种都要能收敛成 tool_calls[].function.arguments
4. 心跳:上游长时间停顿时发 SSE 注释行防超时
5. 断流兜底:上游异常断开时补一个 finish_reason,避免客户端永远 loading
## 最小实现骨架
fun normalizeLine(raw: String): String? {
if (!raw.startsWith("data:")) return null // 忽略注释/空行
val payload = raw.removePrefix("data:").trim()
if (payload == "[DONE]") return raw
val json = Json.parseToJsonElement(payload) as? JsonObject ?: return null
// 根据 event/type 字段抽取 content/reasoning/tool_calls
val content = extractTraeText(json) ?: return null
return """data: {"choices":[{"delta":{"content":"$content"}}]}"""
}
> 具体 Trae 事件字段需要以真实抓包/探针输出为准(仓库里自带“测试 Trae 流”探针)。