1
0
Files
nex/openspec/changes/refactor-conversion-engine/design.md

289 lines
14 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.
## Context
### 现有架构
当前后端协议转换以 OpenAI 类型为内部枢纽,整体结构:
```
Anthropic Handler ──▶ anthropic.ConvertRequest() ──▶ openai.ChatCompletionRequest
OpenAI Handler ──────────────────────────────────▶ openai.ChatCompletionRequest
ProviderClient
(硬编码 OpenAI Adapter)
上游 OpenAI 兼容 API
```
关键文件:
- `internal/protocol/openai/types.go` — OpenAI 线路格式类型,兼作内部枢纽格式
- `internal/protocol/anthropic/converter.go` — Anthropic→OpenAI 单向转换
- `internal/protocol/anthropic/stream_converter.go` — OpenAI chunk→Anthropic SSE 单向流式转换
- `internal/handler/openai_handler.go` — OpenAI 请求处理
- `internal/handler/anthropic_handler.go` — Anthropic 请求处理,内含协议转换编排
- `internal/provider/client.go` — HTTP 客户端,硬编码 `openai.Adapter` 做序列化/反序列化
### 核心限制
1. **单向转换**:只有 Anthropic→OpenAI无反向能力
2. **OpenAI 绑定**:上游通信只能走 OpenAI 协议
3. **无透传**:即使 client==provider同协议仍走完整序列化/反序列化
4. **无扩展性**:新增协议需修改多处代码,无统一接入点
5. **仅 Chat**:只支持 `/v1/chat/completions``/v1/messages` 两个固定端点,无 Models/Embeddings/Rerank
### 设计参考
三份设计文档已完整定义目标架构和两个协议的适配细节:
- `docs/conversion_design.md` — 整体架构Hub-and-Spoke、Canonical Model、ProtocolAdapter 接口、ConversionEngine、流式管道、错误处理
- `docs/conversion_openai.md` — OpenAI 协议适配清单(字段映射、流式状态机、角色合并等)
- `docs/conversion_anthropic.md` — Anthropic 协议适配清单角色约束、thinking、流式命名事件等
## Goals / Non-Goals
**Goals:**
- 实现完整的 Hub-and-Spoke 协议转换架构,以 Canonical Model 为枢纽
- 支持任意协议对的请求/响应双向转换当前OpenAI ↔ Anthropic
- 支持同协议透传(零语义损失、零序列化开销)
- 支持流式 SSE 双向转换(含 Tool Calling、Thinking
- 支持 Chat 核心层 + Models/Embeddings/Rerank 扩展层 + 未知接口透传
- ProviderClient 支持多协议上游通信
- 统一代理入口URL 路由支持协议前缀
**Non-Goals:**
- 本阶段不实现多模态Image/Audio/VideoCanonical Model 仅预留扩展点
- 不实现 Middleware 的具体业务逻辑(仅定义接口和 Chain
- 不实现新的协议 Adapter除 OpenAI 和 Anthropic 外)
- 不实现有状态特性(架构预留 StatefulMiddleware 接口)
- 不实现前端管理界面的协议选择功能
- 不修改前端代码(前端使用管理 API代理 API 路由变更对前端透明)
## Decisions
### D1: Canonical Model 用独立 Go 结构体实现,不使用 `interface{}` 或 `map[string]any`
**选择**:为 CanonicalRequest、CanonicalResponse、CanonicalStreamEvent 等定义强类型 Go structContentBlock 使用 discriminated union 模式type 字段 + 各类型嵌入)
**理由**
- 编译期类型安全IDE 自动补全和重构友好
- 性能优于 `map[string]any`(无反射开销)
- 与 Go 生态的习惯一致
**替代方案**
- `map[string]any` — 灵活但无类型安全,重构时容易遗漏字段
- 代码生成(如 protobuf— 引入新依赖和构建步骤,过度工程化
**实现细节**
```go
type ContentBlock struct {
Type string `json:"type"`
// Text
Text string `json:"text,omitempty"`
// ToolUse
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input json.RawMessage `json:"input,omitempty"`
// ToolResult
ToolUseID string `json:"tool_use_id,omitempty"`
IsError *bool `json:"is_error,omitempty"`
// Thinking
Thinking string `json:"thinking,omitempty"`
}
```
使用 `json.RawMessage` 保留 Tool Input 的原始 JSON避免不必要的 `map` 解析。
### D2: ProtocolAdapter 接口集中定义所有方法,不用接口组合
**选择**:一个大的 `ProtocolAdapter` 接口包含所有方法Chat、流式、扩展层、错误编码不拆分为小接口
**理由**
- 对照 `docs/conversion_design.md` §5.2 的定义,接口集中便于明确所有应实现的内容
- Adapter 实现者可一目了然看到所有方法
- 不支持的功能直接返回 false`supportsInterface`)或空实现
- `detectInterfaceType` 由各协议 Adapter 实现,因为不同协议有不同的 URL 路径约定
**替代方案**
- 接口组合ChatAdapter + StreamAdapter + ExtendedAdapter——增加类型复杂度Adapter 注册和管理更繁琐
- 用空接口 + 类型断言——丢失编译期检查
- `detectInterfaceType` 放在 ConversionEngine 中——违反开闭原则,新增协议需要修改 Engine
### D3: StreamDecoder 直接解析原始 SSE 字节流
**选择**`ProviderClient.SendStream()` 返回 `<-chan []byte`(原始 SSE 字节流),`StreamDecoder.processChunk()` 负责拆分 SSE event 并解析 JSON
**理由**
- SSE 解析与协议语义紧密相关(不同协议的 SSE 格式不同OpenAI 用 `data:` 无名事件Anthropic 用命名事件 `event: xxx\ndata: xxx`
- 减少中间层,降低内存拷贝
- ProviderClient 保持最简——只做 HTTP 请求和字节流读取
**替代方案**
- ProviderClient 内做 SSE 解析——强制所有上游使用同一 SSE 格式,不符合多协议目标
- 独立 SSE Parser 层——增加不必要的抽象SSE 格式本身就是 Adapter 职责的一部分
### D4: ProviderClient 接受 `HTTPRequestSpec`,返回 `*HTTPResponseSpec`
**选择**ConversionEngine 输出 `HTTPRequestSpec{URL, Method, Headers, Body []byte}`ProviderClient 接收后发送 HTTP 请求;响应返回 `HTTPResponseSpec{StatusCode, Headers, Body []byte}`
**理由**
- ProviderClient 完全不感知协议,只做 HTTP 通信
- ConversionEngine 统一负责 URL 构建、Header 构建、Body 序列化
- 同协议透传时Engine 直接透传 body bytesClient 不做任何序列化/反序列化
**接口定义**
```go
type HTTPRequestSpec struct {
URL string
Method string
Headers map[string]string
Body []byte
}
type HTTPResponseSpec struct {
StatusCode int
Headers map[string]string
Body []byte
}
type ProviderClient interface {
Send(ctx context.Context, spec HTTPRequestSpec) (*HTTPResponseSpec, error)
SendStream(ctx context.Context, spec HTTPRequestSpec) (<-chan StreamEvent, error)
}
```
### D5: 统一代理入口使用 `/{protocol}/v1/...` URL 前缀
**选择**:新路由格式 `/{protocol}/v1/{path}`handler 从 URL 提取 protocol 前缀作为 clientProtocol
**理由**
- 符合 `docs/conversion_design.md` §2.2 的设计
- 调用方通过 URL 前缀明确指定协议,无需额外配置
- 统一入口简化 handler 数量
**兼容路由**:不保留旧路由,客户端需迁移到新路由格式。
**替代方案**
- 保持两个独立 handler——违背统一架构目标
- 请求体嗅探协议——不可靠,且设计文档明确"协议识别是调用方职责"
### D6: Provider 新增 `Protocol` 字段,存储在数据库
**选择**Provider 表新增 `protocol TEXT DEFAULT 'openai'` 列,用于标识上游供应商使用的协议
**理由**
- 上游供应商可能是 OpenAI 兼容(大多数)或 Anthropic 原生
- 路由时需要知道 providerProtocol 以选择正确的 Adapter
- 默认值 `'openai'` 确保现有数据兼容
### D7: 删除旧 `internal/protocol/` 包,在 `internal/conversion/` 中重建
**选择**:直接删除 `internal/protocol/openai/``internal/protocol/anthropic/`,在 `internal/conversion/` 下从零构建新架构
**理由**
- 旧代码的设计模式OpenAI 类型为枢纽)与新架构根本不同
- 保留旧代码容易导致混用两种模式,引入隐蔽 bug
- 旧代码中的类型定义可以迁移copy-paste但组织方式需重建
### D8: 目标包结构
```
internal/conversion/
canonical/
types.go # CanonicalRequest/Response/Message/ContentBlock/Tool/ToolChoice/ThinkingConfig/OutputFormat
stream.go # CanonicalStreamEvent 联合体 + 所有事件类型
extended.go # CanonicalModelList/ModelInfo/Embedding/Rerank
errors.go # ConversionError + ErrorCode 枚举
interface.go # InterfaceType 枚举
provider.go # TargetProvider struct
adapter.go # ProtocolAdapter 接口 + AdapterRegistry 接口和实现
stream.go # StreamDecoder/StreamEncoder/StreamConverter 接口 + Passthrough/Canonical 实现
middleware.go # ConversionMiddleware 接口 + MiddlewareChain
engine.go # ConversionEngine 门面 + HTTPRequestSpec/HTTPResponseSpec
openai/
types.go # OpenAI 线路格式类型(从旧 protocol/openai/types.go 迁移并补全)
adapter.go # ProtocolAdapter 实现detectInterfaceType/buildUrl/buildHeaders/supportsInterface/encodeError
decoder.go # decodeRequest/decodeResponse/扩展层 decode 方法
encoder.go # encodeRequest/encodeResponse/扩展层 encode 方法
stream_decoder.go # OpenAIStreamDecoderdelta chunk 状态机)
stream_encoder.go # OpenAIStreamEncoder缓冲策略
anthropic/
types.go # Anthropic 线路格式类型(从旧 protocol/anthropic/types.go 迁移并补全)
adapter.go # ProtocolAdapter 实现detectInterfaceType/buildUrl/buildHeaders/supportsInterface/encodeError
decoder.go # decodeRequest/decodeResponse/扩展层 decode 方法
encoder.go # encodeRequest/encodeResponse/扩展层 encode 方法
stream_decoder.go # AnthropicStreamDecoder命名事件 1:1 映射)
stream_encoder.go # AnthropicStreamEncoder直接映射无缓冲
```
## Risks / Trade-offs
### R1: Anthropic 角色约束处理复杂度高
**风险**Anthropic 要求 user/assistant 严格交替、首消息必须为 user、tool_result 必须嵌入 user 消息。从 Canonical 编码为 Anthropic 时需要合并/拆分/注入消息,逻辑容易出错。
**缓解**
- 编写详尽的测试用例覆盖所有边界情况(连续 tool 消息、首条 assistant 消息、空 user 消息注入等)
- 将角色约束处理封装为独立函数,与内容编码逻辑分离
### R2: OpenAI 流式状态机复杂
**风险**OpenAI 的 delta chunk 没有显式生命周期(无 start/stopStreamDecoder 需要状态机推断 block 边界,管理工具调用索引映射和参数累积。
**缓解**
- 严格对照 `docs/conversion_openai.md` §6.2-§6.3 的伪代码实现
- 为每种 delta 类型编写独立测试text、tool_calls、reasoning_content、refusal、usage chunk
- UTF-8 跨 chunk 截断使用 `utf8Remainder` 缓冲
### R3: 全量重构影响范围大
**风险**:同时删除旧代码、新建包、改造 handler/provider/domain可能导致系统长时间不可用。
**缓解**
- 旧代码在删除前确认新代码所有测试通过
- Git 分支隔离开发,完成后再合并
- 新路由 `/{protocol}/v1/...` 确保协议明确指定
### R4: Canonical Model 字段演进
**风险**Canonical Model 的字段集反映当前已适配协议的公共语义,未来新增协议时可能需要频繁修改。
**缓解**
- 字段晋升规范已在 `docs/conversion_design.md` 附录 C 中定义
- `json:"-"` 标签控制序列化输出,新增可选字段不影响已有编解码
- 协议特有字段不纳入 Canonical通过同协议透传保留
### R5: 性能——双重序列化开销
**风险**:跨协议转换时经过 decode→canonical→encode 两次序列化/反序列化,相比直接转换多一次拷贝。
**权衡**:接受此开销以换取架构清晰和可扩展性。同协议透传路径零开销补偿。实际 LLM API 延迟(数百毫秒到数秒)远大于 JSON 序列化开销(微秒级)。
## Migration Plan
### 步骤
1. **创建 `internal/conversion/` 包**:实现 Layer 1-3Canonical Model、接口定义、Engine不改动现有代码
2. **实现 OpenAI Adapter 和 Anthropic Adapter**Layer 4-5在 conversion 包内自包含
3. **编写全面测试**:覆盖编解码、流式转换、错误处理、同协议透传
4. **改造 `domain.Provider`**:新增 `Protocol` 字段
5. **创建数据库迁移**`ALTER TABLE providers ADD COLUMN protocol TEXT DEFAULT 'openai'`
6. **改造 `ProviderClient`**:简化为接受 `HTTPRequestSpec` 的 HTTP 发送器
7. **创建 `ProxyHandler`**:统一代理入口,集成 ConversionEngine
8. **更新 `cmd/server/main.go`**:注册 Adapter、创建 Engine、配置新路由
9. **删除旧 `internal/protocol/` 包**:确认新架构完全替代后删除
10. **更新 README.md**项目结构、API 接口、路由说明
### 兼容策略
- 旧路由 `/v1/chat/completions``/v1/messages` 不再保留,客户端需迁移
- 现有 Provider 数据通过 `DEFAULT 'openai'` 自动获得协议标识
- 前端管理 API 不受影响
### 回滚策略
- Git 分支隔离:在新分支开发,合并前充分测试
-`internal/protocol/` 包在确认新架构稳定后再删除
- 数据库迁移向下兼容(仅 ADD COLUMN
## Open Questions
- ~~是否需要为兼容路由 `/v1/chat/completions``/v1/messages` 设置 deprecation 期限?~~ → **决定**:不保留旧路由,客户端直接迁移到 `/{protocol}/v1/...`
- ~~扩展层接口Models/Embeddings/Rerank在本阶段是否全部实现还是先做 Models其余后续迭代~~ → **决定**:本阶段全部实现(对照三份文档的字段映射已在 spec 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑