1
0
Files
nex/openspec/changes/refactor-conversion-engine/design.md
lanyuanxiaoyao 1dac347d3b refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
引入 Canonical Model 和 ProtocolAdapter 架构,支持 OpenAI/Anthropic 协议间
无缝转换,统一 ProxyHandler 替代分散的 OpenAI/Anthropic Handler,简化
ProviderClient 为协议无关的 HTTP 发送器,Provider 新增 protocol 字段。
2026-04-20 00:36:27 +08:00

15 KiB
Raw Blame History

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— 引入新依赖和构建步骤,过度工程化

实现细节

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 实现者可一目了然看到所有方法
  • 不支持的功能直接返回 falsesupportsInterface)或空实现
  • 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 不做任何序列化/反序列化

接口定义

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            # OpenAIStreamDecoderdelta 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/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 AdapterLayer 4-5对照设计文档在 conversion 包内全新编写,不沿用旧 protocol 包代码
  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/ 包在删除前确认新架构所有测试通过,删除后不可恢复旧代码(从 git 历史仍可找回)
  • 数据库迁移向下兼容(仅 ADD COLUMN

Open Questions

  • 是否需要为兼容路由 /v1/chat/completions/v1/messages 设置 deprecation 期限?决定:不保留旧路由,客户端直接迁移到 /{protocol}/v1/...
  • 扩展层接口Models/Embeddings/Rerank在本阶段是否全部实现还是先做 Models其余后续迭代决定:本阶段全部实现(对照三份文档的字段映射已在 spec 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑