引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间 无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化 ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
15 KiB
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做序列化/反序列化
核心限制
- 单向转换:只有 Anthropic→OpenAI,无反向能力
- OpenAI 绑定:上游通信只能走 OpenAI 协议
- 无透传:即使 client==provider(同协议),仍走完整序列化/反序列化
- 无扩展性:新增协议需修改多处代码,无统一接入点
- 仅 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)— 引入新依赖和构建步骤,过度工程化
实现细节:
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 不做任何序列化/反序列化
接口定义:
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
- 旧代码中的类型定义不迁移,直接根据设计文档重新定义,确保与新架构一致
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 线路格式类型(对照 conversion_openai.md 全新定义)
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 线路格式类型(对照 conversion_anthropic.md 全新定义)
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
步骤
- 创建
internal/conversion/包:实现 Layer 1-3(Canonical Model、接口定义、Engine),不改动现有代码 - 全新实现 OpenAI Adapter 和 Anthropic Adapter:Layer 4-5,对照设计文档在 conversion 包内全新编写,不沿用旧 protocol 包代码
- 编写全面测试:覆盖编解码、流式转换、错误处理、同协议透传
- 改造
domain.Provider:新增Protocol字段 - 创建数据库迁移:
ALTER TABLE providers ADD COLUMN protocol TEXT DEFAULT 'openai' - 改造
ProviderClient:简化为接受HTTPRequestSpec的 HTTP 发送器 - 创建
ProxyHandler:统一代理入口,集成 ConversionEngine - 更新
cmd/server/main.go:注册 Adapter、创建 Engine、配置新路由 - 删除旧
internal/protocol/包:直接删除,不迁移代码,确认新架构完全替代 - 更新 README.md:项目结构、API 接口、路由说明
兼容策略
- 旧路由
/v1/chat/completions和/v1/messages不再保留,客户端需迁移 - 现有 Provider 数据通过
DEFAULT 'openai'自动获得协议标识 - 前端管理 API 不受影响
回滚策略
- Git 分支隔离:在新分支开发,合并前充分测试
- 旧
internal/protocol/包在删除前确认新架构所有测试通过,删除后不可恢复旧代码(从 git 历史仍可找回) - 数据库迁移向下兼容(仅 ADD COLUMN)
Open Questions
是否需要为兼容路由→ 决定:不保留旧路由,客户端直接迁移到/v1/chat/completions和/v1/messages设置 deprecation 期限?/{protocol}/v1/...扩展层接口(Models/Embeddings/Rerank)在本阶段是否全部实现,还是先做 Models,其余后续迭代?→ 决定:本阶段全部实现(对照三份文档的字段映射已在 spec 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑