## 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/Video),Canonical Model 仅预留扩展点 - 不实现 Middleware 的具体业务逻辑(仅定义接口和 Chain) - 不实现新的协议 Adapter(除 OpenAI 和 Anthropic 外) - 不实现有状态特性(架构预留 StatefulMiddleware 接口) - 不实现前端管理界面的协议选择功能 - 不修改前端代码(前端使用管理 API,代理 API 路由变更对前端透明) ## Decisions ### D1: Canonical Model 用独立 Go 结构体实现,不使用 `interface{}` 或 `map[string]any` **选择**:为 CanonicalRequest、CanonicalResponse、CanonicalStreamEvent 等定义强类型 Go struct,ContentBlock 使用 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 bytes,Client 不做任何序列化/反序列化 **接口定义**: ```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 # OpenAIStreamDecoder(delta 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/stop),StreamDecoder 需要状态机推断 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-3(Canonical 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 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑