1
0

docs: 暂存本次变更的设计文档

This commit is contained in:
2026-04-19 18:10:00 +08:00
parent b14685d9a5
commit 26810d9410
14 changed files with 1789 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
## 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 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑