From 26810d94107ee5b4d8a37034484ca8bea89f8507 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Sun, 19 Apr 2026 18:10:00 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9A=82=E5=AD=98=E6=9C=AC=E6=AC=A1?= =?UTF-8?q?=E5=8F=98=E6=9B=B4=E7=9A=84=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../refactor-conversion-engine/.openspec.yaml | 2 + .../refactor-conversion-engine/design.md | 288 ++++++++++++++++++ .../refactor-conversion-engine/proposal.md | 45 +++ .../specs/anthropic-protocol-proxy/spec.md | 83 +++++ .../specs/conversion-engine/spec.md | 276 +++++++++++++++++ .../specs/error-handling/spec.md | 53 ++++ .../specs/layered-architecture/spec.md | 118 +++++++ .../specs/openai-protocol-proxy/spec.md | 99 ++++++ .../specs/protocol-adapter-anthropic/spec.md | 269 ++++++++++++++++ .../specs/protocol-adapter-openai/spec.md | 268 ++++++++++++++++ .../specs/provider-management/spec.md | 73 +++++ .../specs/request-validation/spec.md | 64 ++++ .../specs/unified-proxy-handler/spec.md | 102 +++++++ .../refactor-conversion-engine/tasks.md | 49 +++ 14 files changed, 1789 insertions(+) create mode 100644 openspec/changes/refactor-conversion-engine/.openspec.yaml create mode 100644 openspec/changes/refactor-conversion-engine/design.md create mode 100644 openspec/changes/refactor-conversion-engine/proposal.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/anthropic-protocol-proxy/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/conversion-engine/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/error-handling/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/layered-architecture/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/openai-protocol-proxy/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/protocol-adapter-anthropic/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/protocol-adapter-openai/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/provider-management/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/request-validation/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/specs/unified-proxy-handler/spec.md create mode 100644 openspec/changes/refactor-conversion-engine/tasks.md diff --git a/openspec/changes/refactor-conversion-engine/.openspec.yaml b/openspec/changes/refactor-conversion-engine/.openspec.yaml new file mode 100644 index 0000000..c8af3f5 --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-19 diff --git a/openspec/changes/refactor-conversion-engine/design.md b/openspec/changes/refactor-conversion-engine/design.md new file mode 100644 index 0000000..23449fe --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/design.md @@ -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/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 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑 diff --git a/openspec/changes/refactor-conversion-engine/proposal.md b/openspec/changes/refactor-conversion-engine/proposal.md new file mode 100644 index 0000000..950a2cb --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/proposal.md @@ -0,0 +1,45 @@ +## Why + +当前后端协议转换层以 OpenAI 类型作为内部枢纽,Anthropic 请求单向转换为 OpenAI 格式后再发往上游。这种设计导致:无法支持 OpenAI→Anthropic 的反向转换、无法对接 Anthropic 协议的上游供应商、无法实现同协议透传的零开销转发、无法横向扩展新协议。重构为基于协议中立 Canonical Model 的 Hub-and-Spoke 架构(参考 `docs/conversion_design.md`),从根本上解决这些问题。 + +## What Changes + +- **引入 Canonical Model**:定义协议无关的 `CanonicalRequest`、`CanonicalResponse`、`CanonicalStreamEvent` 等规范模型,作为所有协议间转换的统一枢纽 +- **引入 ConversionEngine**:无状态的转换引擎门面,协调 Adapter 注册、接口识别、透传判断、请求/响应转换、流式转换 +- **引入 ProtocolAdapter 接口**:统一适配器契约,每种协议实现完整的编解码(Chat 请求/响应、流式、扩展层接口、错误编码) +- **实现 OpenAI Adapter**:对照 `docs/conversion_openai.md` 实现 OpenAI 协议的完整 Adapter(含状态机流式解码器/编码器) +- **实现 Anthropic Adapter**:对照 `docs/conversion_anthropic.md` 实现 Anthropic 协议的完整 Adapter(含命名事件流式解码器/编码器) +- **统一代理 Handler**:合并 `OpenAIHandler` 和 `AnthropicHandler` 为统一的 `ProxyHandler`,支持 `/{protocol}/v1/...` URL 前缀路由 +- **同协议透传**:client == provider 时跳过 Canonical 转换,仅重建 Header 后原样转发 +- **接口分层**:核心层(Chat)走 Canonical 深度转换,扩展层(Models/Embeddings/Rerank)走轻量映射,未知接口走透传 +- **ProviderClient 简化**:移除 OpenAI Adapter 硬编码,变为协议无关的 HTTP 发送器 +- **Provider 新增 Protocol 字段**:**BREAKING** — Provider 模型新增 `protocol` 字段标识上游协议类型 +- **删除旧 protocol 包**:移除 `internal/protocol/openai/` 和 `internal/protocol/anthropic/`,全部逻辑迁入 `internal/conversion/` +- **URL 路由变更**:**BREAKING** — 代理端点从 `/v1/chat/completions` + `/v1/messages` 变更为 `/{protocol}/v1/...`,不保留旧路由 + +## Capabilities + +### New Capabilities + +- `conversion-engine`: 协议转换引擎核心能力——Canonical Model 定义、ProtocolAdapter 接口与注册表、ConversionEngine 门面(请求/响应转换、流式转换、接口识别、透传判断)、StreamDecoder/Encoder 接口、Middleware 拦截链、ConversionError 错误体系 +- `protocol-adapter-openai`: OpenAI 协议适配器——完整的 ProtocolAdapter 实现(对照 conversion_openai.md),涵盖 Chat 请求/响应编解码、流式状态机解码器(OpenAI delta chunk → CanonicalStreamEvent)和编码器(反向)、扩展层接口编解码(Models/Embeddings/Rerank)、错误编码、同协议透传 +- `protocol-adapter-anthropic`: Anthropic 协议适配器——完整的 ProtocolAdapter 实现(对照 conversion_anthropic.md),涵盖 Chat 请求/响应编解码(含角色约束处理:tool→user 合并、user/assistant 交替保证)、流式解码器(命名 SSE 事件 → CanonicalStreamEvent)和编码器(反向)、扩展层接口编解码(Models)、错误编码、同协议透传 +- `unified-proxy-handler`: 统一代理入口——合并 OpenAI/Anthropic 双 Handler 为统一 ProxyHandler,支持 `/{protocol}/v1/...` URL 前缀路由、协议识别 + +### Modified Capabilities + +- `openai-protocol-proxy`: URL 路由从硬编码 `/v1/chat/completions` 变更为 `/{protocol}/v1/...` 统一入口;请求处理从直接调用 ProviderClient 变更为经 ConversionEngine 转换;新增同协议透传能力;新增扩展层接口(Models/Embeddings/Rerank)代理 +- `anthropic-protocol-proxy`: 从单向 Anthropic→OpenAI 转换变更为双向任意协议互转;从 Handler 内直接调用 converter 变更为经 ConversionEngine;新增 Anthropic 作为上游供应商的能力;新增同协议透传能力;新增扩展层接口代理 +- `provider-management`: Provider 模型新增 `protocol` 字段(标识上游协议类型,默认 "openai");数据库迁移新增 protocol 列 +- `layered-architecture`: 新增 conversion 层(internal/conversion/)位于 handler 和 provider 之间;ProviderClient 接口简化为协议无关的 HTTP 发送器 +- `error-handling`: 新增 ConversionError 错误类型和 ErrorCode 枚举;转换失败时使用客户端协议格式编码错误响应 +- `request-validation`: 请求验证从 handler 层前移到 ProtocolAdapter 的 decodeRequest 中;验证规则按各协议规范独立定义 + +## Impact + +- **代码结构**:新增 `internal/conversion/` 包(约 20+ 文件),删除 `internal/protocol/` 包,改造 `internal/handler/` 和 `internal/provider/` +- **API 兼容性**:**BREAKING** — 代理端点 URL 变更(`/v1/chat/completions` → `/openai/v1/chat/completions`,`/v1/messages` → `/anthropic/v1/messages`),不保留旧路由 +- **数据库**:Provider 表新增 `protocol` 列,需数据库迁移 +- **依赖**:无新增外部依赖,复用现有 Go 标准库和已引入的包 +- **测试**:需为 conversion 包编写全面单元测试,覆盖每个 Adapter 的编解码、流式转换、错误处理、同协议透传 +- **文档**:需更新 README.md 中的项目结构、API 接口说明 diff --git a/openspec/changes/refactor-conversion-engine/specs/anthropic-protocol-proxy/spec.md b/openspec/changes/refactor-conversion-engine/specs/anthropic-protocol-proxy/spec.md new file mode 100644 index 0000000..601eae7 --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/anthropic-protocol-proxy/spec.md @@ -0,0 +1,83 @@ +## MODIFIED Requirements + +### Requirement: 支持 Anthropic Messages API 端点 + +网关 SHALL 提供 Anthropic Messages API 端点供外部应用调用。 + +#### Scenario: 成功的非流式请求 + +- **WHEN** 应用发送 POST 请求到 `/anthropic/v1/messages`,携带有效的 Anthropic 请求格式(非流式) +- **THEN** 网关 SHALL 通过 ConversionEngine 将 Anthropic 请求解码为 Canonical 格式 +- **THEN** 网关 SHALL 将 Canonical 请求编码为目标供应商协议格式 +- **THEN** 网关 SHALL 将供应商的响应通过 ConversionEngine 转换为 Anthropic 格式返回给应用 + +#### Scenario: 成功的流式请求 + +- **WHEN** 应用发送 POST 请求到 `/anthropic/v1/messages`,携带 `stream: true` +- **THEN** 网关 SHALL 通过 ConversionEngine 创建 StreamConverter +- **THEN** 网关 SHALL 将上游协议的 SSE 流转换为 Anthropic 命名事件格式 +- **THEN** 网关 SHALL 使用 `event: \ndata: \n\n` 格式流式返回给应用 + +#### Scenario: 同协议透传(Anthropic → Anthropic Provider) + +- **WHEN** 客户端使用 Anthropic 协议且目标供应商也是 Anthropic 协议 +- **THEN** 网关 SHALL 跳过 Canonical 转换,仅重建认证 Header 后原样转发 +- **THEN** 请求和响应 Body SHALL 保持原样 + +### Requirement: 双向协议转换 + +网关 SHALL 支持 Anthropic 协议与任意已注册协议间的双向转换。 + +#### Scenario: Anthropic 客户端 → OpenAI 供应商 + +- **WHEN** 客户端使用 Anthropic 协议且供应商使用 OpenAI 协议 +- **THEN** SHALL 将 Anthropic MessagesRequest 解码为 CanonicalRequest +- **THEN** SHALL 将 CanonicalRequest 编码为 OpenAI ChatCompletionRequest +- **THEN** SHALL 将 OpenAI ChatCompletionResponse 解码为 CanonicalResponse +- **THEN** SHALL 将 CanonicalResponse 编码为 Anthropic MessagesResponse + +#### Scenario: OpenAI 客户端 → Anthropic 供应商 + +- **WHEN** 客户端使用 OpenAI 协议且供应商使用 Anthropic 协议 +- **THEN** SHALL 将 OpenAI ChatCompletionRequest 解码为 CanonicalRequest +- **THEN** SHALL 将 CanonicalRequest 编码为 Anthropic MessagesRequest +- **THEN** SHALL 将 Anthropic MessagesResponse 解码为 CanonicalResponse +- **THEN** SHALL 将 CanonicalResponse 编码为 OpenAI ChatCompletionResponse + +### Requirement: 使用 service 层处理请求 + +Handler SHALL 通过 service 层处理业务逻辑。 + +#### Scenario: 调用 routing service + +- **WHEN** ProxyHandler 收到 Anthropic 协议请求 +- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果 +- **THEN** SHALL 从路由结果获取 Provider(含 protocol 字段) + +#### Scenario: 调用 stats service + +- **WHEN** 请求成功完成 +- **THEN** SHALL 调用 StatsService.Record() 记录统计 +- **THEN** SHALL 异步记录统计(不阻塞响应) + +### Requirement: 使用结构化错误处理 + +ProxyHandler SHALL 使用 ConversionError 和 Anthropic 的 encodeError 处理错误。 + +#### Scenario: 协议转换错误 + +- **WHEN** ConversionEngine 返回 ConversionError +- **THEN** SHALL 使用 Anthropic 的 Adapter.encodeError 编码错误响应 +- **THEN** SHALL 使用 Anthropic 错误格式(`{type: "error", error: {type, message}}`) + +#### Scenario: 路由错误处理 + +- **WHEN** RoutingService 返回错误 +- **THEN** SHALL 转换为 ConversionError +- **THEN** SHALL 使用 Anthropic 错误格式返回 + +#### Scenario: 供应商错误处理 + +- **WHEN** ProviderClient 返回错误 +- **THEN** SHALL 包装为 ConversionError +- **THEN** SHALL 使用 Anthropic 错误格式返回 diff --git a/openspec/changes/refactor-conversion-engine/specs/conversion-engine/spec.md b/openspec/changes/refactor-conversion-engine/specs/conversion-engine/spec.md new file mode 100644 index 0000000..ae46c9d --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/conversion-engine/spec.md @@ -0,0 +1,276 @@ +## ADDED Requirements + +### Requirement: 定义 CanonicalRequest 规范模型 + +系统 SHALL 定义协议无关的 `CanonicalRequest` 结构体,作为所有协议间请求转换的统一枢纽。 + +CanonicalRequest SHALL 包含以下字段: +- `model`: 字符串,模型名称 +- `system`: 可选,字符串或 SystemBlock 数组 +- `messages`: CanonicalMessage 数组 +- `tools`: 可选,CanonicalTool 数组 +- `tool_choice`: 可选,ToolChoice 联合体 +- `parameters`: RequestParameters +- `thinking`: 可选,ThinkingConfig +- `stream`: 布尔值 +- `user_id`: 可选字符串 +- `output_format`: 可选,OutputFormat +- `parallel_tool_use`: 可选布尔值 + +#### Scenario: CanonicalRequest 包含所有公共字段 + +- **WHEN** 从任意协议解码请求为 CanonicalRequest +- **THEN** CanonicalRequest SHALL 包含协议间可映射的所有公共语义字段 +- **THEN** 协议特有字段 SHALL NOT 出现在 CanonicalRequest 中 + +#### Scenario: System 消息独立存储 + +- **WHEN** 协议使用顶层 system 字段(如 Anthropic) +- **THEN** SHALL 提取为 `canonical.system` +- **WHEN** 协议使用 messages 数组中的 system 角色(如 OpenAI) +- **THEN** SHALL 从 messages 中提取为 `canonical.system` + +### Requirement: 定义 CanonicalMessage 和 ContentBlock + +系统 SHALL 定义 `CanonicalMessage` 结构体和 `ContentBlock` 联合体。 + +CanonicalMessage SHALL 包含 `role`(枚举:system/user/assistant/tool)和 `content`(ContentBlock 数组)。 + +ContentBlock SHALL 支持以下类型: +- `text`: 文本内容块 +- `tool_use`: 工具调用块(id, name, input) +- `tool_result`: 工具结果块(tool_use_id, content, is_error) +- `thinking`: 思考内容块(thinking) + +#### Scenario: ContentBlock 类型化表示 + +- **WHEN** 编码 ContentBlock +- **THEN** SHALL 使用 `type` 字段标识块类型 +- **THEN** 不同类型 SHALL 携带各自特有的字段 + +#### Scenario: Tool 输入保留原始 JSON + +- **WHEN** 解码工具调用的 input 字段 +- **THEN** SHALL 使用 `json.RawMessage` 保留原始 JSON 数据 +- **THEN** SHALL NOT 强制解析为 map 或 struct + +### Requirement: 定义 CanonicalTool 和 ToolChoice + +系统 SHALL 定义 `CanonicalTool`(name, description, input_schema)和 `ToolChoice` 联合体(auto/none/any/tool+name)。 + +#### Scenario: ToolChoice 覆盖所有语义 + +- **WHEN** 协议使用 `"required"` 表示必须调用工具(如 OpenAI) +- **THEN** SHALL 映射为 `{type: "any"}` +- **WHEN** 协议使用 `{type: "tool", name}` 指定工具 +- **THEN** SHALL 保持 `{type: "tool", name}` 映射 + +### Requirement: 定义 CanonicalResponse 规范模型 + +系统 SHALL 定义 `CanonicalResponse` 结构体,包含 `id`、`model`、`content`(ContentBlock 数组)、`stop_reason`(可选枚举)、`usage`(CanonicalUsage)。 + +CanonicalUsage SHALL 包含 `input_tokens`、`output_tokens`、可选的 `cache_read_tokens`、`cache_creation_tokens`、`reasoning_tokens`。 + +stop_reason 枚举 SHALL 包含:`end_turn`、`max_tokens`、`tool_use`、`stop_sequence`、`content_filter`、`refusal`、`pause_turn`。 + +#### Scenario: 响应内容块与请求共用类型 + +- **WHEN** 编码响应中的 content +- **THEN** SHALL 使用与请求相同的 ContentBlock 类型系统 +- **THEN** TextBlock 和 ToolUseBlock SHALL 在请求和响应中通用 + +### Requirement: 定义 CanonicalStreamEvent 联合体 + +系统 SHALL 定义类型化的 CanonicalStreamEvent 联合体,包含显式的 start/stop 生命周期。 + +事件类型 SHALL 包含: +- `message_start`: 包含 message(id, model, usage) +- `content_block_start`: 包含 index 和 content_block +- `content_block_delta`: 包含 index 和 delta(text_delta/input_json_delta/thinking_delta) +- `content_block_stop`: 包含 index +- `message_delta`: 包含 delta(stop_reason)和 usage +- `message_stop`: 无额外数据 +- `error`: 包含 error(type, message) +- `ping`: 无额外数据 + +#### Scenario: 流式事件具有完整生命周期 + +- **WHEN** 解码协议的 SSE 流为 CanonicalStreamEvent +- **THEN** SHALL 包含 message_start → content_block_start/delta/stop → message_delta → message_stop 的完整生命周期 +- **THEN** 每个事件 SHALL 包含足够的信息用于编码为任意目标协议的 SSE 格式 + +### Requirement: 定义 ProtocolAdapter 接口 + +系统 SHALL 定义 `ProtocolAdapter` 接口,每种协议实现完整适配。 + +ProtocolAdapter SHALL 包含以下方法: +- `protocolName()`: 返回协议唯一标识 +- `protocolVersion()`: 返回协议版本 +- `supportsPassthrough()`: 返回是否支持同协议透传 +- `detectInterfaceType(nativePath)`: 根据协议的 URL 路径识别接口类型 +- `buildUrl(nativePath, interfaceType)`: 映射 URL 路径 +- `buildHeaders(provider)`: 构建认证和必要 Header +- `supportsInterface(interfaceType)`: 判断是否支持某接口类型 +- `decodeRequest(raw)`: 协议格式 → CanonicalRequest +- `encodeRequest(canonical, provider)`: CanonicalRequest → 协议格式 +- `decodeResponse(raw)`: 协议格式 → CanonicalResponse +- `encodeResponse(canonical)`: CanonicalResponse → 协议格式 +- `createStreamDecoder()`: 创建 StreamDecoder 实例 +- `createStreamEncoder()`: 创建 StreamEncoder 实例 +- `encodeError(error)`: ConversionError → 协议错误格式 +- 扩展层编解码方法(Models/Embeddings/Rerank) + +#### Scenario: Adapter 注册到 Registry + +- **WHEN** 应用启动 +- **THEN** SHALL 将所有 Adapter 注册到 AdapterRegistry +- **THEN** 可通过 `registry.get(protocolName)` 获取 Adapter + +#### Scenario: Adapter 自描述接口能力 + +- **WHEN** 调用 `supportsInterface(interfaceType)` +- **THEN** SHALL 返回布尔值表示是否支持该接口类型 +- **THEN** 不支持的接口 SHALL 由引擎走透传逻辑 + +#### Scenario: Adapter 识别接口类型 + +- **WHEN** 调用 `detectInterfaceType(nativePath)` +- **THEN** SHALL 根据协议的 URL 路径模式识别接口类型 +- **THEN** SHALL 返回对应的 InterfaceType(CHAT/MODELS/MODEL_INFO/EMBEDDINGS/RERANK) +- **THEN** 无法识别的路径 SHALL 返回 PASSTHROUGH 类型 + +### Requirement: 实现 AdapterRegistry + +系统 SHALL 实现 `AdapterRegistry`,支持 Adapter 的注册和查询。 + +#### Scenario: 注册 Adapter + +- **WHEN** 调用 `registry.register(adapter)` +- **THEN** SHALL 以 adapter 的 protocolName 为 key 存储 +- **THEN** 重复注册同名协议 SHALL 返回错误 + +#### Scenario: 查询 Adapter + +- **WHEN** 调用 `registry.get(protocolName)` +- **THEN** SHALL 返回已注册的 ProtocolAdapter +- **THEN** 未注册的协议 SHALL 返回错误 + +### Requirement: 实现 ConversionEngine 门面 + +系统 SHALL 实现 `ConversionEngine` 作为无状态的转换引擎门面,线程安全、可复用。 + +ConversionEngine SHALL 提供: +- `registerAdapter(adapter)`: 注册协议适配器 +- `use(middleware)`: 注册转换中间件 +- `isPassthrough(clientProtocol, providerProtocol)`: 判断是否同协议透传 +- `convertHttpRequest(request, clientProtocol, providerProtocol, provider)`: 请求转换 +- `convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType)`: 响应转换 +- `createStreamConverter(clientProtocol, providerProtocol, provider)`: 创建流式转换器 + +#### Scenario: 跨协议 Chat 请求转换 + +- **WHEN** clientProtocol != providerProtocol 且 interfaceType == CHAT +- **THEN** SHALL 使用 clientAdapter.detectInterfaceType 识别接口类型 +- **THEN** SHALL 使用 clientAdapter.decodeRequest 解码为 CanonicalRequest +- **THEN** SHALL 经 middlewareChain 处理 +- **THEN** SHALL 使用 providerAdapter.encodeRequest 编码为目标协议格式 +- **THEN** SHALL 使用 providerAdapter.buildUrl 映射目标 URL +- **THEN** SHALL 使用 providerAdapter.buildHeaders 构建目标 Header +- **THEN** SHALL 返回 HTTPRequestSpec{URL, Method, Headers, Body} + +#### Scenario: 同协议透传 + +- **WHEN** clientProtocol == providerProtocol 且 supportsPassthrough == true +- **THEN** SHALL 使用 providerAdapter.buildHeaders 重建 Header +- **THEN** SHALL 原样传递请求 Body +- **THEN** SHALL NOT 执行 decode/encode 转换 + +#### Scenario: 未知接口透传 + +- **WHEN** clientAdapter.detectInterfaceType 返回 PASSTHROUGH 类型 +- **THEN** SHALL 使用 providerAdapter.buildUrl 和 buildHeaders 适配 URL 和 Header +- **THEN** SHALL 原样传递请求 Body + +### Requirement: 定义 StreamDecoder/StreamEncoder/StreamConverter 接口 + +系统 SHALL 定义流式转换的三层接口。 + +StreamDecoder SHALL 接口: +- `processChunk(rawChunk)`: 原始字节 → CanonicalStreamEvent 数组 +- `flush()`: 刷新缓冲区 → CanonicalStreamEvent 数组 + +StreamEncoder SHALL 接口: +- `encodeEvent(event)`: CanonicalStreamEvent → SSE 字节数组 +- `flush()`: 刷新缓冲区 → SSE 字节数组 + +StreamConverter SHALL 接口: +- `processChunk(rawChunk)`: 原始字节 → SSE 字节数组 +- `flush()`: 刷新 → SSE 字节数组 + +#### Scenario: PassthroughStreamConverter + +- **WHEN** 同协议透传时 +- **THEN** SHALL 直接传递原始 SSE 字节,不做任何解析或转换 + +#### Scenario: CanonicalStreamConverter + +- **WHEN** 跨协议转换时 +- **THEN** SHALL 使用 providerAdapter 的 StreamDecoder 解码原始 SSE +- **THEN** SHALL 经 middlewareChain 处理事件 +- **THEN** SHALL 使用 clientAdapter 的 StreamEncoder 编码为目标 SSE 格式 + +### Requirement: 定义 ConversionMiddleware 接口 + +系统 SHALL 定义 `ConversionMiddleware` 接口,用于在 decode→encode 之间拦截转换。 + +- `intercept(canonical, clientProtocol, providerProtocol, context)`: 修改或拒绝 Canonical +- `interceptStreamEvent(event, clientProtocol, providerProtocol, context)`: 修改或拒绝流式事件 + +#### Scenario: Middleware 链式执行 + +- **WHEN** 注册多个 Middleware +- **THEN** SHALL 按注册顺序链式执行 +- **THEN** 任一 Middleware 返回错误 SHALL 中断后续执行 + +#### Scenario: Middleware 中断转换 + +- **WHEN** Middleware 返回 ConversionError +- **THEN** SHALL 停止转换流程 +- **THEN** SHALL 使用 clientAdapter.encodeError 编码错误响应 + +### Requirement: 定义 ConversionError 错误体系 + +系统 SHALL 定义 `ConversionError` 和 `ErrorCode` 枚举。 + +ErrorCode SHALL 包含:INVALID_INPUT、MISSING_REQUIRED_FIELD、INCOMPATIBLE_FEATURE、FIELD_MAPPING_FAILURE、TOOL_CALL_PARSE_ERROR、JSON_PARSE_ERROR、STREAM_STATE_ERROR、UTF8_DECODE_ERROR、PROTOCOL_CONSTRAINT_VIOLATION、ENCODING_FAILURE、INTERFACE_NOT_SUPPORTED。 + +#### Scenario: 转换错误包含上下文 + +- **WHEN** 转换过程中发生错误 +- **THEN** ConversionError SHALL 包含 code、message、clientProtocol、providerProtocol、interfaceType 等上下文 +- **THEN** SHALL 支持包装原始错误(cause) + +### Requirement: 定义 InterfaceType 枚举和接口分层 + +系统 SHALL 定义 `InterfaceType` 枚举(CHAT、MODELS、MODEL_INFO、EMBEDDINGS、RERANK)和接口分层策略。 + +- 核心层(CHAT):使用 Canonical Model 深度转换 +- 扩展层(MODELS、MODEL_INFO、EMBEDDINGS、RERANK):使用轻量 Canonical Models 做字段映射 +- 透传层(未知接口):URL+Header 适配后 Body 原样转发 + +#### Scenario: 扩展层接口转换 + +- **WHEN** interfaceType 为 MODELS/MODEL_INFO/EMBEDDINGS/RERANK +- **THEN** SHALL 使用对应扩展层 Canonical Model 做轻量字段映射 +- **THEN** 双方都不支持时 SHALL 走透传逻辑 + +### Requirement: 定义 TargetProvider 结构体 + +系统 SHALL 定义 `TargetProvider` 结构体,包含 `base_url`、`api_key`、`model_name`、`adapter_config`。 + +#### Scenario: Adapter 从 TargetProvider 获取配置 + +- **WHEN** Adapter 调用 buildHeaders(provider) +- **THEN** SHALL 从 provider.api_key 提取认证信息 +- **THEN** SHALL 从 provider.adapter_config 提取协议专属配置 +- **THEN** SHALL 使用 provider.model_name 覆盖请求中的 model 字段 diff --git a/openspec/changes/refactor-conversion-engine/specs/error-handling/spec.md b/openspec/changes/refactor-conversion-engine/specs/error-handling/spec.md new file mode 100644 index 0000000..cccc44b --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/error-handling/spec.md @@ -0,0 +1,53 @@ +## MODIFIED Requirements + +### Requirement: 统一错误响应 + +系统 SHALL 统一错误响应格式,新增 ConversionError 支持。 + +#### Scenario: OpenAI 协议错误响应 + +- **WHEN** OpenAI 协议发生错误 +- **THEN** SHALL 返回标准 OpenAI 错误响应格式 +- **THEN** SHALL 包含 error.message、error.type、error.code 字段 + +#### Scenario: Anthropic 协议错误响应 + +- **WHEN** Anthropic 协议发生错误 +- **THEN** SHALL 返回标准 Anthropic 错误响应格式 +- **THEN** SHALL 包含 type、error.type、error.message 字段 + +#### Scenario: 转换错误响应 + +- **WHEN** ConversionEngine 在协议转换过程中产生 ConversionError +- **THEN** SHALL 使用客户端协议的 Adapter.encodeError 编码错误响应 +- **THEN** 错误响应 SHALL 使用客户端可理解的协议格式 + +#### Scenario: 管理 API 错误响应 + +- **WHEN** 管理 API 发生错误 +- **THEN** SHALL 返回统一的错误响应格式 +- **THEN** SHALL 包含 code、message 字段 +- **THEN** SHALL 可选包含 details 字段(验证错误详情) + +## ADDED Requirements + +### Requirement: 定义 ConversionError 错误类型 + +系统 SHALL 定义 ConversionError 结构体和 ErrorCode 枚举。 + +#### Scenario: ConversionError 结构 + +- **WHEN** 定义转换错误 +- **THEN** SHALL 包含 Code(ErrorCode 枚举)、Message 字段 +- **THEN** SHALL 可选包含 ClientProtocol、ProviderProtocol、InterfaceType、Details、Cause 字段 + +#### Scenario: ErrorCode 枚举 + +- **WHEN** 定义错误码 +- **THEN** SHALL 包含 INVALID_INPUT、MISSING_REQUIRED_FIELD、INCOMPATIBLE_FEATURE、FIELD_MAPPING_FAILURE、TOOL_CALL_PARSE_ERROR、JSON_PARSE_ERROR、STREAM_STATE_ERROR、UTF8_DECODE_ERROR、PROTOCOL_CONSTRAINT_VIOLATION、ENCODING_FAILURE、INTERFACE_NOT_SUPPORTED + +#### Scenario: 错误码到协议错误类型的映射 + +- **WHEN** 使用 encodeError 编码错误 +- **THEN** ErrorCode SHALL 映射为各协议的错误类型字符串 +- **THEN** 例如 INVALID_INPUT → OpenAI "invalid_request_error",Anthropic "invalid_request_error" diff --git a/openspec/changes/refactor-conversion-engine/specs/layered-architecture/spec.md b/openspec/changes/refactor-conversion-engine/specs/layered-architecture/spec.md new file mode 100644 index 0000000..3812fc8 --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/layered-architecture/spec.md @@ -0,0 +1,118 @@ +## MODIFIED Requirements + +### Requirement: 实现三层架构 + +系统 SHALL 实现 handler → service → repository 三层架构,并在 handler 和 provider 之间新增 conversion 层。 + +#### Scenario: Handler 层职责 + +- **WHEN** 处理 HTTP 请求 +- **THEN** handler 层 SHALL 仅负责 HTTP 请求解析、URL 路由和响应写入 +- **THEN** handler 层 SHALL 调用 ConversionEngine 处理协议转换 +- **THEN** handler 层 SHALL 调用 service 层处理业务逻辑 +- **THEN** handler 层 SHALL NOT 直接访问数据库或执行协议转换逻辑 + +#### Scenario: Conversion 层职责 + +- **WHEN** 处理协议转换 +- **THEN** conversion 层 SHALL 包含 Canonical Model 定义 +- **THEN** conversion 层 SHALL 包含各协议的 ProtocolAdapter 实现 +- **THEN** conversion 层 SHALL 包含 ConversionEngine 门面 +- **THEN** conversion 层 SHALL NOT 依赖 handler 或 service 层 + +#### Scenario: Service 层职责 + +- **WHEN** 处理业务逻辑 +- **THEN** service 层 SHALL 包含业务规则和验证 +- **THEN** service 层 SHALL 调用 repository 层访问数据 +- **THEN** service 层 SHALL NOT 包含协议转换逻辑 + +#### Scenario: Repository 层职责 + +- **WHEN** 访问数据 +- **THEN** repository 层 SHALL 仅负责数据访问 +- **THEN** repository 层 SHALL 封装数据库操作 +- **THEN** repository 层 SHALL NOT 包含业务逻辑或协议转换逻辑 + +### Requirement: 定义核心接口 + +系统 SHALL 定义清晰的接口边界。 + +#### Scenario: Service 接口定义 + +- **WHEN** 定义 service 接口 +- **THEN** SHALL 定义 ProviderService、ModelService、RoutingService、StatsService 接口 +- **THEN** SHALL 定义清晰的业务方法签名 +- **THEN** SHALL 使用 domain 类型作为参数和返回值 + +#### Scenario: Repository 接口定义 + +- **WHEN** 定义 repository 接口 +- **THEN** SHALL 定义 ProviderRepository、ModelRepository、StatsRepository 接口 +- **THEN** SHALL 定义清晰的数据访问方法签名 +- **THEN** SHALL 使用 domain 类型作为参数和返回值 + +#### Scenario: Provider Client 接口定义 + +- **WHEN** 定义 provider client 接口 +- **THEN** SHALL 定义 ProviderClient 接口 +- **THEN** SHALL 包含 Send(非流式)和 SendStream(流式)方法 +- **THEN** SHALL 接受 HTTPRequestSpec 作为参数,不绑定特定协议 +- **THEN** SHALL 支持接口 Mock + +#### Scenario: Conversion 层接口定义 + +- **WHEN** 定义 conversion 层接口 +- **THEN** SHALL 定义 ProtocolAdapter、StreamDecoder、StreamEncoder、StreamConverter、ConversionMiddleware 接口 +- **THEN** SHALL 定义 AdapterRegistry 用于 Adapter 注册和查询 +- **THEN** SHALL 定义 ConversionEngine 作为统一门面 + +### Requirement: 实现依赖注入 + +系统 SHALL 使用手动依赖注入。 + +#### Scenario: Repository 注入 + +- **WHEN** 初始化 service +- **THEN** SHALL 通过构造函数注入 repository 依赖 +- **THEN** SHALL 使用接口类型而非具体类型 + +#### Scenario: Service 注入 + +- **WHEN** 初始化 handler +- **THEN** SHALL 通过构造函数注入 service 依赖、ConversionEngine、ProviderClient +- **THEN** SHALL 使用接口类型而非具体类型 + +#### Scenario: Conversion 组装 + +- **WHEN** 应用启动 +- **THEN** SHALL 创建 AdapterRegistry 并注册所有 ProtocolAdapter +- **THEN** SHALL 创建 ConversionEngine(注入 registry 和 middleware chain) +- **THEN** SHALL 将 ConversionEngine 注入到 ProxyHandler + +#### Scenario: 主函数组装 + +- **WHEN** 应用启动 +- **THEN** main.go SHALL 按顺序构造所有依赖 +- **THEN** SHALL 先构造基础设施(logger、database) +- **THEN** SHALL 再构造 repository、service +- **THEN** SHALL 再构造 conversion 层(registry → engine) +- **THEN** SHALL 最后构造 handler + +### Requirement: 定义 Domain 模型 + +系统 SHALL 定义独立的 domain 模型。 + +#### Scenario: Domain 模型定义 + +- **WHEN** 定义领域模型 +- **THEN** SHALL 在 internal/domain/ 包中定义 +- **THEN** SHALL 包含 Provider、Model、UsageStats 等模型 +- **THEN** Provider SHALL 包含 Protocol 字段 +- **THEN** SHALL 与数据库模型分离 + +#### Scenario: Domain 模型使用 + +- **WHEN** service 和 repository 处理数据 +- **THEN** SHALL 使用 domain 模型 +- **THEN** SHALL NOT 使用数据库模型(GORM 模型) diff --git a/openspec/changes/refactor-conversion-engine/specs/openai-protocol-proxy/spec.md b/openspec/changes/refactor-conversion-engine/specs/openai-protocol-proxy/spec.md new file mode 100644 index 0000000..aaab771 --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/openai-protocol-proxy/spec.md @@ -0,0 +1,99 @@ +## MODIFIED Requirements + +### Requirement: 支持 OpenAI Chat Completions API 端点 + +网关 SHALL 提供 OpenAI Chat Completions API 端点供外部应用调用。 + +#### Scenario: 成功的非流式请求 + +- **WHEN** 应用发送 POST 请求到 `/openai/v1/chat/completions`,携带有效的 OpenAI 请求格式(非流式) +- **THEN** 网关 SHALL 通过 ConversionEngine 转换请求 +- **THEN** 网关 SHALL 将转换后的请求转发到配置的供应商 +- **THEN** 网关 SHALL 将供应商的响应通过 ConversionEngine 转换为 OpenAI 格式返回给应用 + +#### Scenario: 成功的流式请求 + +- **WHEN** 应用发送 POST 请求到 `/openai/v1/chat/completions`,携带 `stream: true` +- **THEN** 网关 SHALL 通过 ConversionEngine 创建 StreamConverter +- **THEN** 网关 SHALL 使用 SSE 格式将转换后的响应流式返回给应用 +- **THEN** 网关 SHALL 在流完成时发送 `data: [DONE]` + +#### Scenario: 同协议透传(OpenAI → OpenAI Provider) + +- **WHEN** 客户端使用 OpenAI 协议且目标供应商也是 OpenAI 协议 +- **THEN** 网关 SHALL 跳过 Canonical 转换,仅重建认证 Header 后原样转发 +- **THEN** 请求和响应 Body SHALL 保持原样 + +### Requirement: 根据模型名称路由请求 + +网关 SHALL 根据请求中的 `model` 字段将请求路由到相应的供应商。 + +#### Scenario: 有效模型路由 + +- **WHEN** 请求包含存在于配置模型中的 `model` 字段 +- **AND** 该模型已启用 +- **THEN** 网关 SHALL 将请求路由到该模型关联的供应商 +- **THEN** 网关 SHALL 从供应商的 `protocol` 字段获取 providerProtocol + +#### Scenario: 模型未找到 + +- **WHEN** 请求包含不存在于配置模型中的 `model` 字段 +- **THEN** 网关 SHALL 使用 OpenAI 格式返回错误响应 + +#### Scenario: 模型已禁用 + +- **WHEN** 请求包含已禁用模型的 `model` 字段 +- **THEN** 网关 SHALL 使用 OpenAI 格式返回错误响应 + +### Requirement: 对 OpenAI 兼容供应商透明代理 + +网关 SHALL 对 OpenAI 兼容供应商的请求和响应通过 ConversionEngine 进行转换处理。 + +#### Scenario: 跨协议请求转发 + +- **WHEN** 网关收到 OpenAI 协议请求且目标供应商使用不同协议 +- **THEN** 网关 SHALL 通过 ConversionEngine 将请求转换为目标协议格式 +- **THEN** 网关 SHALL 使用目标协议的 Adapter 构建 URL 和 Header + +#### Scenario: 扩展层接口代理 + +- **WHEN** 网关收到 `/openai/v1/models` 等 GET 请求 +- **THEN** 网关 SHALL 通过 ConversionEngine 转换扩展层接口的响应格式 + +### Requirement: 使用 service 层处理请求 + +Handler SHALL 通过 service 层处理业务逻辑。 + +#### Scenario: 调用 routing service + +- **WHEN** ProxyHandler 收到请求 +- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果 +- **THEN** SHALL 从路由结果获取 Provider(含 protocol 字段) + +#### Scenario: 调用 stats service + +- **WHEN** 请求成功完成 +- **THEN** SHALL 调用 StatsService.Record() 记录统计 +- **THEN** SHALL 异步记录统计(不阻塞响应) + +### Requirement: 使用结构化错误处理 + +ProxyHandler SHALL 使用 ConversionError 和协议对应的 encodeError 处理错误。 + +#### Scenario: 转换错误 + +- **WHEN** ConversionEngine 返回 ConversionError +- **THEN** SHALL 使用 clientProtocol 的 Adapter.encodeError 编码错误响应 +- **THEN** SHALL 使用 OpenAI 错误格式(`{error: {message, type, code}}`) + +#### Scenario: 路由错误处理 + +- **WHEN** RoutingService 返回错误 +- **THEN** SHALL 转换为 ConversionError +- **THEN** SHALL 使用 OpenAI 错误格式返回 + +#### Scenario: 供应商错误处理 + +- **WHEN** ProviderClient 返回错误 +- **THEN** SHALL 包装为 ConversionError +- **THEN** SHALL 使用 OpenAI 错误格式返回 diff --git a/openspec/changes/refactor-conversion-engine/specs/protocol-adapter-anthropic/spec.md b/openspec/changes/refactor-conversion-engine/specs/protocol-adapter-anthropic/spec.md new file mode 100644 index 0000000..254843f --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/protocol-adapter-anthropic/spec.md @@ -0,0 +1,269 @@ +## ADDED Requirements + +### Requirement: 实现 Anthropic ProtocolAdapter + +系统 SHALL 实现 Anthropic 协议的完整 ProtocolAdapter,对照 `docs/conversion_anthropic.md`。 + +- `protocolName()` SHALL 返回 `"anthropic"` +- `supportsPassthrough()` SHALL 返回 true +- `buildHeaders(provider)` SHALL 构建 `x-api-key`、`anthropic-version`、`anthropic-beta`、`Content-Type` +- `buildUrl(nativePath, interfaceType)` SHALL 按接口类型映射 URL 路径 +- `supportsInterface()` SHALL 对 CHAT、MODELS、MODEL_INFO 返回 true,对 EMBEDDINGS、RERANK 返回 false + +#### Scenario: 认证 Header 构建 + +- **WHEN** 调用 buildHeaders(provider) +- **THEN** SHALL 设置 `x-api-key: ` +- **THEN** SHALL 设置 `anthropic-version`(默认 `"2023-06-01"`,从 adapter_config 可覆盖) +- **WHEN** adapter_config 包含 anthropic_beta +- **THEN** SHALL 以逗号拼接为 `anthropic-beta` Header + +#### Scenario: URL 映射 + +- **WHEN** interfaceType == CHAT +- **THEN** SHALL 映射为 `/v1/messages` +- **WHEN** interfaceType == MODELS +- **THEN** SHALL 映射为 `/v1/models` +- **WHEN** interfaceType == EMBEDDINGS 或 RERANK +- **THEN** SHALL NOT 调用 buildUrl(supportsInterface 返回 false,引擎走透传) + +### Requirement: Anthropic 请求解码(Anthropic → Canonical) + +系统 SHALL 实现完整的 Anthropic MessagesRequest 到 CanonicalRequest 的解码。 + +#### Scenario: System 消息提取 + +- **WHEN** Anthropic 请求包含顶层 `system` 字段 +- **THEN** String 类型 SHALL 直接提取为 canonical.system +- **THEN** SystemBlock 数组 SHALL 提取为 canonical.system(Array) + +#### Scenario: User 消息中 tool_result 拆分 + +- **WHEN** Anthropic user 消息的 content 中包含 tool_result 块 +- **THEN** SHALL 将 tool_result 块拆分为独立的 CanonicalMessage{role: "tool"} +- **THEN** 非 tool_result 块 SHALL 保留为独立的 CanonicalMessage{role: "user"} +- **THEN** 仅 tool_result 块时 SHALL 只产出 tool 角色消息 + +#### Scenario: 参数映射 + +- **WHEN** 解码 Anthropic 请求参数 +- **THEN** max_tokens SHALL 直接映射 +- **THEN** temperature/top_p/top_k SHALL 直接映射 +- **THEN** stop_sequences SHALL 直接映射 + +#### Scenario: ThinkingConfig 解码 + +- **WHEN** 解码 Anthropic thinking 字段 +- **THEN** type="enabled" SHALL 映射为 Canonical thinking.type="enabled" +- **THEN** type="disabled" SHALL 映射为 Canonical thinking.type="disabled" +- **THEN** type="adaptive" SHALL 映射为 Canonical thinking.type="adaptive" +- **THEN** budget_tokens 和 output_config.effort SHALL 直接映射 + +#### Scenario: 公共字段提取 + +- **WHEN** 解码 Anthropic 公共字段 +- **THEN** metadata.user_id SHALL 提取为 user_id +- **THEN** output_config.format(json_schema 类型)SHALL 提取为 output_format +- **THEN** disable_parallel_tool_use SHALL 反转映射为 parallel_tool_use(true → false) + +#### Scenario: 协议特有字段处理 + +- **WHEN** 解码遇到 redacted_thinking +- **THEN** SHALL 丢弃,不在中间层保留 +- **WHEN** 解码遇到 cache_control +- **THEN** SHALL 忽略,不晋升为公共字段 + +### Requirement: Anthropic 请求编码(Canonical → Anthropic) + +系统 SHALL 实现完整的 CanonicalRequest 到 Anthropic MessagesRequest 的编码。 + +#### Scenario: System 消息注入 + +- **WHEN** canonical.system 不为空 +- **THEN** SHALL 编码为 Anthropic 顶层 `system` 字段 + +#### Scenario: Tool 角色合并到 User 消息 + +- **WHEN** CanonicalMessage{role: "tool"} 出现在消息序列中 +- **THEN** SHALL 将其 tool_result 块合并到相邻的 Anthropic user 消息的 content 数组中 +- **WHEN** 相邻前一条不是 user 消息 +- **THEN** SHALL 创建新的 user 消息来承载 tool_result 块 + +#### Scenario: 首消息 user 保证 + +- **WHEN** 编码后的 Anthropic messages 数组首条消息不是 user 角色 +- **THEN** SHALL 自动注入一条空 user 消息到头部 + +#### Scenario: 角色交替约束 + +- **WHEN** 编码后存在连续同角色消息 +- **THEN** SHALL 合并为单条消息(content 数组拼接) + +#### Scenario: 参数编码 + +- **WHEN** 编码 CanonicalRequest 参数 +- **THEN** parameters.max_tokens SHALL 直接映射(Anthropic 必填) +- **THEN** parameters.top_k SHALL 直接映射 +- **THEN** canonical.thinking.type="enabled" SHALL 映射为 thinking{type: "enabled", budget_tokens} +- **THEN** canonical.thinking.type="adaptive" SHALL 映射为 thinking{type: "adaptive"} + +#### Scenario: 公共字段编码 + +- **WHEN** canonical.user_id 不为空 +- **THEN** SHALL 编码为 metadata.user_id +- **WHEN** canonical.parallel_tool_use == false +- **THEN** SHALL 编码为 disable_parallel_tool_use: true +- **WHEN** canonical.output_format 存在 +- **THEN** SHALL 编码为 output_config.format + +#### Scenario: 降级处理 + +- **WHEN** canonical.output_format.type == "json_object" +- **THEN** SHALL 降级为 output_config.format{type: "json_schema", schema: {type: "object"}} +- **WHEN** canonical.output_format.type == "text" +- **THEN** SHALL 丢弃,不设置 output_config + +### Requirement: Anthropic 响应解码(Anthropic → Canonical) + +系统 SHALL 实现 Anthropic MessagesResponse 到 CanonicalResponse 的解码。 + +#### Scenario: 内容块解码 + +- **WHEN** Anthropic response 包含 text 块 +- **THEN** SHALL 解码为 TextBlock +- **WHEN** 包含 tool_use 块 +- **THEN** SHALL 解码为 ToolUseBlock +- **WHEN** 包含 thinking 块 +- **THEN** SHALL 解码为 ThinkingBlock +- **WHEN** 包含 redacted_thinking 块 +- **THEN** SHALL 丢弃 + +#### Scenario: 停止原因映射 + +- **WHEN** 解码 stop_reason +- **THEN** "end_turn" SHALL 映射为 "end_turn" +- **THEN** "max_tokens" SHALL 映射为 "max_tokens" +- **THEN** "tool_use" SHALL 映射为 "tool_use" +- **THEN** "stop_sequence" SHALL 映射为 "stop_sequence" +- **THEN** "refusal" SHALL 映射为 "refusal" +- **THEN** "pause_turn" SHALL 映射为 "pause_turn" + +#### Scenario: Usage 映射 + +- **WHEN** 解码 Anthropic usage +- **THEN** input_tokens SHALL 直接映射 +- **THEN** output_tokens SHALL 直接映射 +- **THEN** cache_read_input_tokens SHALL 映射为 cache_read_tokens +- **THEN** cache_creation_input_tokens SHALL 映射为 cache_creation_tokens + +### Requirement: Anthropic 响应编码(Canonical → Anthropic) + +系统 SHALL 实现 CanonicalResponse 到 Anthropic MessagesResponse 的编码。 + +#### Scenario: 降级处理 + +- **WHEN** canonical.stop_reason 为 "content_filter" +- **THEN** SHALL 降级映射为 "end_turn" +- **WHEN** canonical.reasoning_tokens 不为空 +- **THEN** SHALL 丢弃(Anthropic 无此字段) + +### Requirement: Anthropic 流式解码器 + +系统 SHALL 实现 AnthropicStreamDecoder,将 Anthropic 命名 SSE 事件转换为 CanonicalStreamEvent。 + +Decoder 几乎 1:1 映射,维护最小状态机: +- messageStarted: 是否已发送 MessageStartEvent +- redactedBlocks: 需要丢弃的 block index 集合 +- utf8Remainder: UTF-8 跨 chunk 安全缓冲 + +#### Scenario: 命名事件 1:1 映射 + +- **WHEN** 收到 `event: message_start` +- **THEN** SHALL 发出 MessageStartEvent +- **WHEN** 收到 `event: content_block_start` +- **THEN** SHALL 发出 ContentBlockStartEvent +- **WHEN** 收到 `event: content_block_delta` +- **THEN** SHALL 发出 ContentBlockDeltaEvent +- **WHEN** 收到 `event: content_block_stop` +- **THEN** SHALL 发出 ContentBlockStopEvent +- **WHEN** 收到 `event: message_delta` +- **THEN** SHALL 发出 MessageDeltaEvent +- **WHEN** 收到 `event: message_stop` +- **THEN** SHALL 发出 MessageStopEvent +- **WHEN** 收到 `event: ping` +- **THEN** SHALL 发出 PingEvent +- **WHEN** 收到 `event: error` +- **THEN** SHALL 发出 ErrorEvent + +#### Scenario: redacted_thinking 块丢弃 + +- **WHEN** content_block_start 事件中 content_block.type 为 "redacted_thinking" +- **THEN** SHALL 将 index 加入 redactedBlocks +- **THEN** 后续该 index 的 delta 和 stop 事件 SHALL 丢弃 + +#### Scenario: 协议特有 delta 丢弃 + +- **WHEN** delta 类型为 citations_delta 或 signature_delta +- **THEN** SHALL 丢弃,不影响 block 生命周期 + +#### Scenario: 服务端工具块丢弃 + +- **WHEN** content_block_start 事件中类型为 server_tool_use / web_search_tool_result 等 +- **THEN** SHALL 丢弃整个 block + +### Requirement: Anthropic 流式编码器 + +系统 SHALL 实现 AnthropicStreamEncoder,将 CanonicalStreamEvent 编码为 Anthropic 命名 SSE 事件。 + +#### Scenario: 直接映射,无缓冲 + +- **WHEN** 收到任意 CanonicalStreamEvent +- **THEN** SHALL 直接编码为对应的 Anthropic SSE 事件 +- **THEN** SHALL NOT 缓冲等待(与 OpenAI 编码器不同) + +#### Scenario: SSE 格式 + +- **WHEN** 编码输出 +- **THEN** SHALL 使用 `event: \ndata: \n\n` 格式 + +#### Scenario: Delta 类型编码 + +- **WHEN** delta.type == "text_delta" +- **THEN** SHALL 编码为 Anthropic text_delta +- **WHEN** delta.type == "input_json_delta" +- **THEN** SHALL 编码为 Anthropic input_json_delta +- **WHEN** delta.type == "thinking_delta" +- **THEN** SHALL 编码为 Anthropic thinking_delta + +### Requirement: Anthropic 错误编码 + +系统 SHALL 实现 Anthropic 协议的错误编码。 + +#### Scenario: 错误响应格式 + +- **WHEN** 调用 encodeError(conversionError) +- **THEN** SHALL 返回 `{type: "error", error: {type: , message: }}` + +### Requirement: Anthropic 扩展层接口编解码 + +系统 SHALL 实现 Anthropic 协议的扩展层接口编解码(仅 Models)。 + +#### Scenario: /models 列表接口 + +- **WHEN** 解码 Anthropic models 响应 +- **THEN** data[].display_name SHALL 映射为 models[].name +- **THEN** data[].created_at(RFC 3339)SHALL 转换为 models[].created(Unix 时间戳) +- **WHEN** 编码 CanonicalModelList 为 Anthropic 格式 +- **THEN** SHALL 输出包含 has_more、first_id、last_id 的结构 +- **THEN** models[].created(Unix 时间戳)SHALL 转换为 RFC 3339 字符串 + +#### Scenario: /models 详情接口 + +- **WHEN** 解码/编码 model 详情 +- **THEN** SHALL 处理 display_name ↔ name 和 RFC 3339 ↔ Unix 时间戳的转换 + +#### Scenario: EMBEDDINGS 和 RERANK 不支持 + +- **WHEN** interfaceType 为 EMBEDDINGS 或 RERANK +- **THEN** supportsInterface SHALL 返回 false +- **THEN** 引擎 SHALL 走透传或返回空响应 diff --git a/openspec/changes/refactor-conversion-engine/specs/protocol-adapter-openai/spec.md b/openspec/changes/refactor-conversion-engine/specs/protocol-adapter-openai/spec.md new file mode 100644 index 0000000..c20e8fd --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/protocol-adapter-openai/spec.md @@ -0,0 +1,268 @@ +## ADDED Requirements + +### Requirement: 实现 OpenAI ProtocolAdapter + +系统 SHALL 实现 OpenAI 协议的完整 ProtocolAdapter,对照 `docs/conversion_openai.md`。 + +- `protocolName()` SHALL 返回 `"openai"` +- `supportsPassthrough()` SHALL 返回 true +- `buildHeaders(provider)` SHALL 构建 `Authorization: Bearer ` 和 `Content-Type: application/json` +- `buildUrl(nativePath, interfaceType)` SHALL 按接口类型映射 URL 路径 +- `supportsInterface()` SHALL 对 CHAT、MODELS、MODEL_INFO、EMBEDDINGS、RERANK 返回 true + +#### Scenario: 认证 Header 构建 + +- **WHEN** 调用 buildHeaders(provider) +- **THEN** SHALL 设置 `Authorization: Bearer ` +- **THEN** SHALL 设置 `Content-Type: application/json` +- **WHEN** provider.adapter_config 包含 organization +- **THEN** SHALL 设置 `OpenAI-Organization` Header + +#### Scenario: URL 映射 + +- **WHEN** interfaceType == CHAT +- **THEN** SHALL 映射为 `/v1/chat/completions` +- **WHEN** interfaceType == MODELS +- **THEN** SHALL 映射为 `/v1/models` +- **WHEN** interfaceType == EMBEDDINGS +- **THEN** SHALL 映射为 `/v1/embeddings` + +### Requirement: OpenAI 请求解码(OpenAI → Canonical) + +系统 SHALL 实现完整的 OpenAI ChatCompletionRequest 到 CanonicalRequest 的解码。 + +#### Scenario: System/Developer 消息提取 + +- **WHEN** OpenAI messages 中包含 role="system" 或 role="developer" 的消息 +- **THEN** SHALL 提取为 canonical.system(String) +- **THEN** 多条 system/developer 消息 SHALL 合并以 `\n\n` 分隔 +- **THEN** SHALL 从 messages 数组中移除这些消息 + +#### Scenario: Assistant 消息中的 tool_calls 解码 + +- **WHEN** OpenAI assistant 消息包含 tool_calls +- **THEN** SHALL 将每个 tool_call 解码为 ContentBlock{type: "tool_use", id, name, input} +- **THEN** function.arguments(JSON 字符串)SHALL 解析为原始 JSON 对象 + +#### Scenario: Tool 消息解码 + +- **WHEN** OpenAI 消息 role="tool" +- **THEN** SHALL 解码为 CanonicalMessage{role: "tool", content: [ToolResultBlock{tool_use_id, content}]} +- **THEN** tool_call_id SHALL 映射为 tool_use_id + +#### Scenario: 参数映射 + +- **WHEN** 解码 OpenAI 请求参数 +- **THEN** max_completion_tokens(优先)或 max_tokens SHALL 映射为 parameters.max_tokens +- **THEN** stop(String 或 Array)SHALL 规范化为 parameters.stop_sequences(Array) +- **THEN** temperature/top_p/frequency_penalty/presence_penalty SHALL 直接映射 + +#### Scenario: 公共字段映射 + +- **WHEN** 解码 OpenAI 公共字段 +- **THEN** user SHALL 映射为 user_id +- **THEN** response_format SHALL 映射为 output_format +- **THEN** parallel_tool_calls SHALL 映射为 parallel_tool_use +- **THEN** reasoning_effort SHALL 映射为 thinking 配置("none" → disabled, 其他 → enabled+effort) + +#### Scenario: 废弃字段兼容 + +- **WHEN** OpenAI 请求包含 functions 或 function_call 字段 +- **THEN** SHALL 转换为对应的 tools/tool_choice 格式 +- **THEN** 仅在 tools/tool_choice 未设置时使用废弃字段 + +### Requirement: OpenAI 请求编码(Canonical → OpenAI) + +系统 SHALL 实现完整的 CanonicalRequest 到 OpenAI ChatCompletionRequest 的编码。 + +#### Scenario: 模型名称覆盖 + +- **WHEN** 编码请求 +- **THEN** SHALL 使用 provider.model_name 覆盖 canonical.model + +#### Scenario: System 消息注入 + +- **WHEN** canonical.system 不为空 +- **THEN** SHALL 编码为 messages 数组头部的 role="system" 消息 + +#### Scenario: Assistant 消息中 tool_calls 编码 + +- **WHEN** CanonicalMessage{role: "assistant"} 包含 tool_use 类型 ContentBlock +- **THEN** SHALL 提取到 message.tool_calls 数组({id, type: "function", function: {name, arguments}}) +- **THEN** arguments SHALL 序列化为 JSON 字符串 + +#### Scenario: 角色交替合并 + +- **WHEN** Canonical 消息序列中存在连续同角色消息 +- **THEN** SHALL 合并为单条 OpenAI 消息 +- **THEN** 文本内容 SHALL 合并连接 + +#### Scenario: 参数编码 + +- **WHEN** 编码 CanonicalRequest 参数 +- **THEN** parameters.max_tokens SHALL 映射为 max_completion_tokens +- **THEN** thinking.type=="disabled" SHALL 映射为 reasoning_effort="none" +- **THEN** thinking.effort SHALL 直接映射为 reasoning_effort + +### Requirement: OpenAI 响应解码(OpenAI → Canonical) + +系统 SHALL 实现 OpenAI ChatCompletionResponse 到 CanonicalResponse 的解码。 + +#### Scenario: 内容块解码 + +- **WHEN** OpenAI response.choice[0].message 包含 content +- **THEN** SHALL 解码为 TextBlock +- **WHEN** 包含 tool_calls +- **THEN** SHALL 解码为 ToolUseBlock 数组 +- **WHEN** 包含 reasoning_content(非标准,兼容提供商) +- **THEN** SHALL 解码为 ThinkingBlock + +#### Scenario: 停止原因映射 + +- **WHEN** 解码 finish_reason +- **THEN** "stop" SHALL 映射为 "end_turn" +- **THEN** "length" SHALL 映射为 "max_tokens" +- **THEN** "tool_calls" SHALL 映射为 "tool_use" +- **THEN** "content_filter" SHALL 映射为 "content_filter" + +#### Scenario: Usage 映射 + +- **WHEN** 解码 OpenAI usage +- **THEN** prompt_tokens SHALL 映射为 input_tokens +- **THEN** completion_tokens SHALL 映射为 output_tokens +- **THEN** prompt_tokens_details.cached_tokens SHALL 映射为 cache_read_tokens + +### Requirement: OpenAI 响应编码(Canonical → OpenAI) + +系统 SHALL 实现 CanonicalResponse 到 OpenAI ChatCompletionResponse 的编码。 + +#### Scenario: ThinkingBlock 编码 + +- **WHEN** CanonicalResponse 包含 ThinkingBlock +- **THEN** SHALL 编码为 message.reasoning_content(非标准字段,兼容提供商使用) + +#### Scenario: 降级处理 + +- **WHEN** canonical.stop_reason 为 "stop_sequence" 或 "refusal" +- **THEN** SHALL 映射为 finish_reason "stop" +- **WHEN** canonical.stop_reason 为 "pause_turn" +- **THEN** SHALL 映射为 finish_reason "stop"(降级) + +### Requirement: OpenAI 流式解码器 + +系统 SHALL 实现 OpenAIStreamDecoder,将 OpenAI SSE delta chunk 转换为 CanonicalStreamEvent。 + +Decoder SHALL 维护状态机: +- messageStarted: 是否已发送 MessageStartEvent +- openBlocks: 当前打开的 block index 集合 +- toolCallIdMap/toolCallNameMap/toolCallArguments: 工具调用索引映射和参数累积 +- textBlockStarted/thinkingBlockStarted: 文本/思考 block 生命周期追踪 +- utf8Remainder: UTF-8 跨 chunk 安全缓冲 + +#### Scenario: 首个 chunk 触发 MessageStartEvent + +- **WHEN** 收到第一个有效 chunk +- **THEN** SHALL 发出 MessageStartEvent,包含 id 和 model + +#### Scenario: delta.content 触发 text block 事件 + +- **WHEN** 收到 delta.content 首次出现 +- **THEN** SHALL 发出 ContentBlockStartEvent(text) + ContentBlockDeltaEvent(text_delta) +- **WHEN** 收到 delta.content 后续出现 +- **THEN** SHALL 发出 ContentBlockDeltaEvent(text_delta) + +#### Scenario: delta.tool_calls 触发 tool_use block 事件 + +- **WHEN** delta.tool_calls[i] 首次出现(含 id) +- **THEN** SHALL 发出 ContentBlockStartEvent(tool_use) +- **WHEN** delta.tool_calls[i].function.arguments 增量到达 +- **THEN** SHALL 发出 ContentBlockDeltaEvent(input_json_delta) + +#### Scenario: delta.reasoning_content 触发 thinking block 事件 + +- **WHEN** delta.reasoning_content 出现(非标准字段) +- **THEN** SHALL 发出 ContentBlockStartEvent(thinking) + ContentBlockDeltaEvent(thinking_delta) + +#### Scenario: finish_reason 触发关闭事件 + +- **WHEN** finish_reason 非空 +- **THEN** SHALL 为所有 open blocks 发出 ContentBlockStopEvent +- **THEN** SHALL 发出 MessageDeltaEvent(含 stop_reason 映射) +- **THEN** SHALL 发出 MessageStopEvent + +#### Scenario: usage chunk 处理 + +- **WHEN** 收到 choices 为空但含 usage 的 chunk +- **THEN** SHALL 发出 MessageDeltaEvent(仅含 usage) + +#### Scenario: [DONE] 信号处理 + +- **WHEN** 收到 `data: [DONE]` +- **THEN** SHALL 触发 flush() 关闭所有 open blocks + +#### Scenario: UTF-8 跨 chunk 安全 + +- **WHEN** chunk 边界截断了 UTF-8 多字节序列 +- **THEN** SHALL 使用 utf8Remainder 缓冲不完整字节 +- **THEN** 下一个 chunk 到达时 SHALL 拼接后重新解析 + +### Requirement: OpenAI 流式编码器 + +系统 SHALL 实现 OpenAIStreamEncoder,将 CanonicalStreamEvent 编码为 OpenAI SSE chunk。 + +Encoder SHALL 维护状态: +- bufferedStart: 缓冲的 ContentBlockStartEvent +- toolCallIndexMap: tool_use_id → OpenAI tool_calls 数组索引映射 + +#### Scenario: ContentBlockStart 缓冲策略 + +- **WHEN** 收到 ContentBlockStartEvent +- **THEN** SHALL NOT 立即输出,缓冲等待首次 ContentBlockDeltaEvent + +#### Scenario: ContentBlockDelta 合并输出 + +- **WHEN** 收到 ContentBlockDeltaEvent 且有缓冲的 StartEvent +- **THEN** SHALL 合并 Start 信息(如 tool id/name)与 delta 数据一起输出 +- **WHEN** 无缓冲 StartEvent +- **THEN** SHALL 仅输出 delta 数据 + +#### Scenario: MessageStopEvent 输出 [DONE] + +- **WHEN** 收到 MessageStopEvent +- **THEN** SHALL 输出 `data: [DONE]` + +#### Scenario: PingEvent 和 ErrorEvent 处理 + +- **WHEN** 收到 PingEvent 或 ErrorEvent +- **THEN** SHALL 不输出(OpenAI 无流式错误/心跳事件) + +### Requirement: OpenAI 错误编码 + +系统 SHALL 实现 OpenAI 协议的错误编码。 + +#### Scenario: 错误响应格式 + +- **WHEN** 调用 encodeError(conversionError) +- **THEN** SHALL 返回 `{error: {message, type, param: null, code}}` +- **THEN** ErrorCode SHALL 映射为 OpenAI 错误类型(如 INVALID_INPUT → "invalid_request_error") + +### Requirement: OpenAI 扩展层接口编解码 + +系统 SHALL 实现 OpenAI 协议的扩展层接口编解码。 + +#### Scenario: /models 列表接口 + +- **WHEN** 解码 OpenAI models 响应 +- **THEN** SHALL 映射为 CanonicalModelList(data[].id → models[].id, created, owned_by) +- **WHEN** 编码 CanonicalModelList 为 OpenAI 格式 +- **THEN** SHALL 输出 `{object: "list", data: [...]}` + +#### Scenario: /embeddings 接口 + +- **WHEN** 解码/编码 embedding 请求和响应 +- **THEN** SHALL 使用 CanonicalEmbeddingRequest/Response 做字段映射 + +#### Scenario: /rerank 接口 + +- **WHEN** 解码/编码 rerank 请求和响应 +- **THEN** SHALL 使用 CanonicalRerankRequest/Response 做字段映射 diff --git a/openspec/changes/refactor-conversion-engine/specs/provider-management/spec.md b/openspec/changes/refactor-conversion-engine/specs/provider-management/spec.md new file mode 100644 index 0000000..9788ba7 --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/provider-management/spec.md @@ -0,0 +1,73 @@ +## MODIFIED Requirements + +### Requirement: 创建供应商配置 + +网关 SHALL 允许通过管理 API 创建新的供应商配置。 + +#### Scenario: 使用有效数据创建供应商 + +- **WHEN** 向 `/api/providers` 发送 POST 请求,携带有效的供应商数据(id, name, api_key, base_url, protocol) +- **THEN** 网关 SHALL 在数据库中创建新的供应商记录 +- **THEN** 网关 SHALL 返回创建的供应商,状态码为 201 +- **THEN** 供应商 SHALL 默认启用 +- **THEN** protocol 字段 SHALL 默认为 "openai" + +#### Scenario: 使用重复 ID 创建供应商 + +- **WHEN** 向 `/api/providers` 发送 POST 请求,携带已存在的 ID +- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict) + +#### Scenario: 创建供应商时缺少必需字段 + +- **WHEN** 向 `/api/providers` 发送 POST 请求,缺少必需字段(id, name, api_key 或 base_url) +- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request) +- **THEN** 错误 SHALL 指示缺少哪些字段 + +### Requirement: 列出所有供应商 + +网关 SHALL 允许获取所有供应商配置。 + +#### Scenario: 成功列出供应商 + +- **WHEN** 向 `/api/providers` 发送 GET 请求 +- **THEN** 网关 SHALL 返回所有供应商的列表 +- **THEN** 每个供应商 SHALL 包含 id, name, api_key(已掩码), base_url, protocol, enabled, created_at, updated_at +- **THEN** api_key SHALL 被掩码(仅显示最后 4 个字符) + +### Requirement: 获取特定供应商 + +网关 SHALL 允许通过 ID 获取特定供应商。 + +#### Scenario: 获取存在的供应商 + +- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带有效的供应商 ID +- **THEN** 网关 SHALL 返回供应商详情 +- **THEN** SHALL 包含 protocol 字段 +- **THEN** api_key SHALL 被掩码 + +#### Scenario: 获取不存在的供应商 + +- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带不存在的 ID +- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found) + +### Requirement: 更新供应商配置 + +网关 SHALL 允许更新现有供应商配置。 + +#### Scenario: 使用有效数据更新供应商 + +- **WHEN** 向 `/api/providers/:id` 发送 PUT 请求,携带有效的供应商数据 +- **THEN** 网关 SHALL 更新数据库中的供应商记录 +- **THEN** 网关 SHALL 返回更新后的供应商 +- **THEN** 更新 SHALL 支持修改 protocol 字段 + +### Requirement: 删除供应商配置 + +网关 SHALL 允许删除供应商配置。 + +#### Scenario: 删除存在的供应商 + +- **WHEN** 向 `/api/providers/:id` 发送 DELETE 请求,携带有效的供应商 ID +- **THEN** 网关 SHALL 删除供应商记录 +- **THEN** 网关 SHALL 删除所有关联的模型(CASCADE) +- **THEN** 网关 SHALL 返回状态码 204 (No Content) diff --git a/openspec/changes/refactor-conversion-engine/specs/request-validation/spec.md b/openspec/changes/refactor-conversion-engine/specs/request-validation/spec.md new file mode 100644 index 0000000..42c7fee --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/request-validation/spec.md @@ -0,0 +1,64 @@ +## MODIFIED Requirements + +### Requirement: 验证 OpenAI 请求 + +系统 SHALL 验证 OpenAI ChatCompletionRequest,验证逻辑位于 ProtocolAdapter 的 decodeRequest 内。 + +#### Scenario: 必需字段验证 + +- **WHEN** OpenAI Adapter 的 decodeRequest 解析请求 +- **THEN** SHALL 验证 model 字段不为空 +- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息 +- **THEN** 验证失败 SHALL 返回 INVALID_INPUT 类型的 ConversionError + +#### Scenario: 参数范围验证 + +- **WHEN** OpenAI Adapter 的 decodeRequest 解析参数 +- **THEN** SHALL 验证 temperature 范围在 [0, 2] +- **THEN** SHALL 验证 max_tokens 大于 0 +- **THEN** SHALL 验证 top_p 范围在 (0, 1] + +#### Scenario: 消息内容验证 + +- **WHEN** 验证 messages 字段 +- **THEN** SHALL 验证每条消息的 role 有效(system、developer、user、assistant、tool) +- **THEN** SHALL 验证 content 不为空 + +### Requirement: 验证 Anthropic 请求 + +系统 SHALL 验证 Anthropic MessagesRequest,验证逻辑位于 ProtocolAdapter 的 decodeRequest 内。 + +#### Scenario: 必需字段验证 + +- **WHEN** Anthropic Adapter 的 decodeRequest 解析请求 +- **THEN** SHALL 验证 model 字段不为空 +- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息 +- **THEN** SHALL 验证 max_tokens 大于 0(或使用默认值) + +#### Scenario: 参数范围验证 + +- **WHEN** Anthropic Adapter 的 decodeRequest 解析参数 +- **THEN** SHALL 验证 temperature 范围在 [0, 1] +- **THEN** SHALL 验证 top_p 范围在 (0, 1] + +#### Scenario: 消息内容验证 + +- **WHEN** 验证 messages 字段 +- **THEN** SHALL 验证每条消息的 role 有效(user、assistant) +- **THEN** SHALL 验证 content 数组不为空 + +### Requirement: 返回友好的验证错误 + +系统 SHALL 返回友好的验证错误响应。 + +#### Scenario: 转换错误格式 + +- **WHEN** decodeRequest 验证失败返回 ConversionError +- **THEN** ProxyHandler SHALL 使用 clientAdapter.encodeError 编码错误响应 +- **THEN** 错误 SHALL 使用客户端协议的格式 + +#### Scenario: 多字段错误 + +- **WHEN** 多个字段验证失败 +- **THEN** ConversionError.details SHALL 包含所有验证错误 +- **THEN** 错误响应 SHALL 包含完整的验证错误信息 diff --git a/openspec/changes/refactor-conversion-engine/specs/unified-proxy-handler/spec.md b/openspec/changes/refactor-conversion-engine/specs/unified-proxy-handler/spec.md new file mode 100644 index 0000000..dafc71c --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/specs/unified-proxy-handler/spec.md @@ -0,0 +1,102 @@ +## ADDED Requirements + +### Requirement: 实现统一代理 Handler + +系统 SHALL 实现统一的 ProxyHandler,替代现有的 OpenAIHandler 和 AnthropicHandler。 + +ProxyHandler SHALL 依赖 ConversionEngine、ProviderClient、RoutingService、StatsService。 + +#### Scenario: 从 URL 提取客户端协议 + +- **WHEN** 收到 `/{protocol}/v1/{path}` 格式的请求 +- **THEN** SHALL 从 URL 第一段提取 protocol 作为 clientProtocol +- **THEN** SHALL 剥离前缀得到 nativePath + +#### Scenario: 协议前缀必须是已注册协议 + +- **WHEN** 收到的 URL 前缀不是已注册的协议名称 +- **THEN** SHALL 返回 404 错误 + +#### Scenario: 接口类型识别 + +- **WHEN** 提取 nativePath 后 +- **THEN** SHALL 通过 ConversionEngine.convertHttpRequest 内部调用 clientAdapter.detectInterfaceType(nativePath) 识别接口类型 +- **THEN** 未知路径 SHALL 使用透传模式 + +### Requirement: 非流式请求处理流程 + +ProxyHandler SHALL 按以下流程处理非流式请求。 + +#### Scenario: 完整转换流程 + +- **WHEN** 收到非流式请求 +- **THEN** SHALL 解析请求体为 JSON +- **THEN** SHALL 调用 RoutingService.Route(modelName) 获取路由结果 +- **THEN** SHALL 从路由结果的 Provider.Protocol 获取 providerProtocol +- **THEN** SHALL 构建 TargetProvider(base_url, api_key, model_name, adapter_config) +- **THEN** SHALL 调用 engine.convertHttpRequest(body, clientProtocol, providerProtocol, provider) +- **THEN** SHALL 调用 providerClient.Send(ctx, requestSpec) +- **THEN** SHALL 调用 engine.convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType) +- **THEN** SHALL 将转换后的响应返回给客户端 + +#### Scenario: 路由失败处理 + +- **WHEN** RoutingService.Route 返回错误 +- **THEN** SHALL 使用 clientProtocol 对应的 Adapter.encodeError 编码错误响应 +- **THEN** SHALL 返回适当的 HTTP 状态码 + +#### Scenario: 上游请求失败处理 + +- **WHEN** ProviderClient.Send 返回错误 +- **THEN** SHALL 使用 clientProtocol 对应的 Adapter.encodeError 编码错误响应 +- **THEN** SHALL 包装原始错误信息 + +### Requirement: 流式请求处理流程 + +ProxyHandler SHALL 按以下流程处理流式请求。 + +#### Scenario: 流式转换流程 + +- **WHEN** 请求中 stream=true 或接口类型为 CHAT 且请求体含 stream:true +- **THEN** SHALL 执行与非流式相同的前置处理(路由、构建 TargetProvider) +- **THEN** SHALL 调用 engine.convertHttpRequest 获取 HTTPRequestSpec +- **THEN** SHALL 调用 providerClient.SendStream(ctx, requestSpec) 获取流 +- **THEN** SHALL 调用 engine.createStreamConverter(clientProtocol, providerProtocol, provider) +- **THEN** SHALL 设置响应 Header `Content-Type: text/event-stream` +- **THEN** SHALL 对每个流 chunk 调用 converter.processChunk 并写入响应 +- **THEN** SHALL 在流结束时调用 converter.flush() 刷新缓冲 + +#### Scenario: 同协议透传流式 + +- **WHEN** clientProtocol == providerProtocol +- **THEN** SHALL 直接将上游 SSE 字节流写入响应 +- **THEN** SHALL NOT 做任何解析或转换 + +#### Scenario: 流式错误处理 + +- **WHEN** 流过程中发生错误 +- **THEN** SHALL 记录错误日志 +- **THEN** SHALL 关闭响应流 + +### Requirement: 统计记录 + +ProxyHandler SHALL 记录请求统计。 + +#### Scenario: 异步记录统计 + +- **WHEN** 请求处理完成(成功或失败) +- **THEN** SHALL 异步调用 StatsService.Record +- **THEN** SHALL NOT 阻塞响应返回 + +### Requirement: GET 请求透传 + +ProxyHandler SHALL 支持 GET 请求的扩展层接口代理。 + +#### Scenario: Models 接口代理 + +- **WHEN** 收到 GET /{protocol}/v1/models 请求 +- **THEN** SHALL 执行路由和协议识别 +- **THEN** SHALL 调用 engine.convertHttpRequest(GET 请求 body 为空) +- **THEN** SHALL 调用 providerClient.Send 发送请求 +- **THEN** SHALL 调用 engine.convertHttpResponse 转换响应格式 +- **THEN** SHALL 返回转换后的响应 diff --git a/openspec/changes/refactor-conversion-engine/tasks.md b/openspec/changes/refactor-conversion-engine/tasks.md new file mode 100644 index 0000000..30ad8bd --- /dev/null +++ b/openspec/changes/refactor-conversion-engine/tasks.md @@ -0,0 +1,49 @@ +## 1. 基础类型层 — Canonical Model 和核心类型定义 + +- [ ] 1.1 创建 `internal/conversion/errors.go`:定义 ConversionError 结构体(Code, Message, ClientProtocol, ProviderProtocol, InterfaceType, Details, Cause)和 ErrorCode 枚举(INVALID_INPUT, MISSING_REQUIRED_FIELD, INCOMPATIBLE_FEATURE, FIELD_MAPPING_FAILURE, TOOL_CALL_PARSE_ERROR, JSON_PARSE_ERROR, STREAM_STATE_ERROR, UTF8_DECODE_ERROR, PROTOCOL_CONSTRAINT_VIOLATION, ENCODING_FAILURE, INTERFACE_NOT_SUPPORTED),实现 error 接口 +- [ ] 1.2 创建 `internal/conversion/interface.go`:定义 InterfaceType 枚举(CHAT, MODELS, MODEL_INFO, EMBEDDINGS, RERANK) +- [ ] 1.3 创建 `internal/conversion/provider.go`:定义 TargetProvider 结构体(BaseURL, APIKey, ModelName, AdapterConfig map[string]any);编写测试 +- [ ] 1.4 创建 `internal/conversion/canonical/types.go`:定义 CanonicalRequest(model, system, messages, tools, tool_choice, parameters, thinking, stream, user_id, output_format, parallel_tool_use)、CanonicalMessage(role 枚举: system/user/assistant/tool, content []ContentBlock)、ContentBlock(使用 type 字段的 discriminated union:text/tool_use/tool_result/thinking,ToolInput 使用 json.RawMessage)、CanonicalTool(name, description, input_schema)、ToolChoice 联合体(auto/none/any/tool+name)、RequestParameters(max_tokens, temperature, top_p, top_k, frequency_penalty, presence_penalty, stop_sequences)、ThinkingConfig(type: enabled/disabled/adaptive, budget_tokens, effort)、OutputFormat(json_object/json_schema+schema/text)、CanonicalResponse(id, model, content, stop_reason 枚举, usage)、CanonicalUsage(input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, reasoning_tokens)、SystemBlock(text);编写构造和序列化测试 +- [ ] 1.5 创建 `internal/conversion/canonical/stream.go`:定义 CanonicalStreamEvent 联合体(message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop, error, ping)及各事件的具体结构(MessageStartEvent 含 message{id,model,usage}、ContentBlockStartEvent 含 index 和 content_block、ContentBlockDeltaEvent 含 index 和 delta、ContentBlockStopEvent 含 index、MessageDeltaEvent 含 delta{stop_reason} 和 usage、MessageStopEvent、ErrorEvent、PingEvent),delta 联合体(text_delta, input_json_delta, thinking_delta),content_block 联合体(text, tool_use, thinking);编写测试 +- [ ] 1.6 创建 `internal/conversion/canonical/extended.go`:定义扩展层 Canonical Models(CanonicalModelList, CanonicalModel, CanonicalModelInfo, CanonicalEmbeddingRequest, CanonicalEmbeddingResponse, CanonicalRerankRequest, CanonicalRerankResponse);编写测试 + +## 2. 接口定义层 — Adapter、Stream、Middleware 接口 + +- [ ] 2.1 创建 `internal/conversion/adapter.go`:定义 ProtocolAdapter 接口(protocolName, protocolVersion, supportsPassthrough, detectInterfaceType, buildUrl, buildHeaders, supportsInterface, decodeRequest, encodeRequest, decodeResponse, encodeResponse, createStreamDecoder, createStreamEncoder, encodeError, 扩展层编解码方法:decodeModelsResponse/encodeModelsResponse/decodeModelInfoResponse/encodeModelInfoResponse/decodeEmbeddingRequest/encodeEmbeddingRequest/decodeEmbeddingResponse/encodeEmbeddingResponse/decodeRerankRequest/encodeRerankRequest/decodeRerankResponse/encodeRerankResponse),定义 AdapterRegistry 接口(register, get, listProtocols)和 memoryRegistry 实现(sync.RWMutex 保护的 map);编写 Registry 注册/查询/重复注册测试 +- [ ] 2.2 创建 `internal/conversion/stream.go`:定义 StreamDecoder 接口(processChunk(rawChunk []byte) []CanonicalStreamEvent, flush() []CanonicalStreamEvent)、StreamEncoder 接口(encodeEvent(event CanonicalStreamEvent) [][]byte, flush() [][]byte)、StreamConverter 接口(processChunk(rawChunk []byte) [][]byte, flush() [][]byte)、PassthroughStreamConverter 实现(直接传递原始字节)、CanonicalStreamConverter 实现(组合 StreamDecoder + MiddlewareChain + StreamEncoder,processChunk 内部调用 decoder → middleware → encoder 管道);编写 PassthroughStreamConverter 测试 +- [ ] 2.3 创建 `internal/conversion/middleware.go`:定义 ConversionMiddleware 接口(intercept(canonical, clientProtocol, providerProtocol, context) (CanonicalRequest, error) 和可选的 interceptStreamEvent(event, clientProtocol, providerProtocol, context) (CanonicalStreamEvent, error))、ConversionContext 结构体(conversionId, interfaceType, timestamp, metadata)、MiddlewareChain 结构体(按注册顺序链式执行,任一返回错误则中断后续);编写链式执行和中断测试 + +## 3. 引擎层 — ConversionEngine 门面 + +- [ ] 3.1 创建 `internal/conversion/engine.go`:定义 HTTPRequestSpec(URL, Method string, Headers map[string]string, Body []byte)、HTTPResponseSpec(StatusCode int, Headers map[string]string, Body []byte)、ConversionEngine struct(registry, middlewareChain);实现 registerAdapter、use、isPassthrough、convertHttpRequest(接口识别 → 透传判断 → clientAdapter.decode → middleware → providerAdapter.encode → providerAdapter.buildUrl + buildHeaders)、convertHttpResponse(透传判断 → providerAdapter.decodeResponse → clientAdapter.encodeResponse)、createStreamConverter(透传 → PassthroughStreamConverter,否则 → CanonicalStreamConverter)、内部 convertBody 分发(CHAT 走深度转换,扩展层走轻量映射,默认透传);编写集成测试:使用 mock adapter 测试跨协议转换、同协议透传、未知接口透传 + +## 4. OpenAI Adapter 实现 + +- [ ] 4.1 创建 `internal/conversion/openai/types.go`:从旧 `internal/protocol/openai/types.go` 迁移 OpenAI 线路格式类型,补全缺失字段(developer role, custom tools, reasoning_effort, reasoning_content, max_completion_tokens, parallel_tool_calls, response_format 的 json_schema 类型, stream_options, 废弃的 functions/function_call);编写序列化测试 +- [ ] 4.2 创建 `internal/conversion/openai/decoder.go`:实现 decodeRequest(对照 conversion_openai.md §4.1:decodeSystemPrompt 提取 system+developer 消息、decodeMessage 含 tool_calls/refusal/reasoning_content 解码、tool 消息 tool_call_id→tool_use_id、decodeTools 含 function+custom 类型、decodeToolChoice 含 required→any/allowed_tools 降级、decodeParameters 含 max_completion_tokens 优先、decodeOutputFormat、decodeThinking 含 reasoning_effort→ThinkingConfig、废弃字段 functions→tools 兼容)、decodeResponse(§5.2:content/refusal/reasoning_content/tool_calls 解码、finish_reason 映射表、usage 映射含 cached_tokens/reasoning_tokens)、扩展层 decode(decodeModelsResponse、decodeEmbeddingRequest/Response、decodeRerankRequest/Response);编写完整测试覆盖每类消息和字段映射 +- [ ] 4.3 创建 `internal/conversion/openai/encoder.go`:实现 encodeRequest(对照 conversion_openai.md §4.2:provider.model_name 覆盖、system 注入到 messages[0]、encodeMessage 含 tool_calls 编码到 message 顶层、角色交替合并、encodeTools 含 function 包装、encodeToolChoice 含 any→required、encodeParameters 含 max_completion_tokens、encodeOutputFormat、encodeThinking 含 disabled→"none")、encodeResponse(§5.3:text→content、tool_use→tool_calls、thinking→reasoning_content、finish_reason 反向映射、usage 编码含 prompt_tokens_details)、扩展层 encode(encodeModelsResponse、encodeEmbeddingRequest/Response、encodeRerankRequest/Response);编写完整测试 +- [ ] 4.4 创建 `internal/conversion/openai/adapter.go`:实现 OpenAI ProtocolAdapter(protocolName→"openai"、supportsPassthrough→true、detectInterfaceType 根据正则匹配识别 /v1/chat/completions→CHAT、/v1/models→MODELS 等、buildHeaders 含 Authorization+Content-Type+OpenAI-Organization、buildUrl 按接口类型映射、supportsInterface 对 CHAT/MODELS/MODEL_INFO/EMBEDDINGS/RERANK 返回 true、encodeError 含 ErrorCode→OpenAI 错误类型映射),组合 decoder 和 encoder 方法;编写测试覆盖所有路径模式和边界情况 +- [ ] 4.5 创建 `internal/conversion/openai/stream_decoder.go`:实现 OpenAIStreamDecoder(对照 conversion_openai.md §6.2-§6.3:processChunk 解析 SSE data 行,维护状态机 messageStarted/openBlocks/toolCallIdMap/toolCallNameMap/toolCallArguments/textBlockStarted/thinkingBlockStarted/utf8Remainder/accumulatedUsage,首个 chunk→MessageStartEvent,delta.content→text block 生命周期,delta.tool_calls→tool_use block 生命周期含索引映射和参数累积,delta.reasoning_content→thinking block(非标准),delta.refusal→text block,finish_reason→关闭所有 open blocks + MessageDeltaEvent + MessageStopEvent,usage chunk→MessageDeltaEvent,[DONE]→flush 关闭);编写测试覆盖每种 delta 类型和边界情况(空 chunk、多 tool_calls、UTF-8 截断) +- [ ] 4.6 创建 `internal/conversion/openai/stream_encoder.go`:实现 OpenAIStreamEncoder(对照 conversion_openai.md §6.4:encodeEvent,ContentBlockStart 缓冲策略等待首次 ContentBlockDelta 合并输出,tool_use id/name 在首次 delta 时合并编码,text_delta 直接输出 data: {choices:[{delta:{content}}]},input_json_delta 含 tool_calls 数组编码,thinking_delta 含 reasoning_content 字段,MessageStartEvent→{choices:[{delta:{role:"assistant"}}]},MessageDeltaEvent→{choices:[{delta:{},finish_reason}]},MessageStopEvent→[DONE],PingEvent/ErrorEvent 丢弃,flush 输出缓冲区);编写测试 + +## 5. Anthropic Adapter 实现(与 Layer 4 并行) + +- [ ] 5.1 创建 `internal/conversion/anthropic/types.go`:从旧 `internal/protocol/anthropic/types.go` 迁移 Anthropic 线路格式类型,补全缺失字段(thinking.type 含 adaptive、output_config.format/effort、disable_parallel_tool_use、metadata.user_id、redacted_thinking、pause_turn/refusal stop_reason、stop_details、container、cache_control);编写序列化测试 +- [ ] 5.2 创建 `internal/conversion/anthropic/decoder.go`:实现 decodeRequest(对照 conversion_anthropic.md §4.1:decodeSystem 从顶层 system 提取、decodeMessage 含 tool_result 从 user 消息拆分为独立 tool 角色消息、参数直接映射含 top_k、decodeThinking 含 enabled/disabled/adaptive 三种类型、decodeOutputFormat 仅支持 json_schema、公共字段提取含 metadata.user_id/disable_parallel_tool_use 反转/output_config.effort、协议特有字段 redacted_thinking 丢弃/cache_control 忽略)、decodeResponse(§5.2:text/tool_use/thinking 块解码、redacted_thinking 丢弃、stop_reason 映射含 pause_turn/refusal、usage 映射含 cache_read_input_tokens/cache_creation_input_tokens)、扩展层 decode(decodeModelsResponse 含 RFC3339→Unix 时间戳转换、decodeModelInfoResponse);编写完整测试覆盖角色拆分、thinking 三种类型、时间戳转换 +- [ ] 5.3 创建 `internal/conversion/anthropic/encoder.go`:实现 encodeRequest(对照 conversion_anthropic.md §4.2:provider.model_name 覆盖、system 注入为顶层字段、encodeMessages 含 tool→user 合并(优先合并到相邻 user 消息)、首消息 user 保证(自动注入空 user)、角色交替合并、encodeThinkingConfig 含 enabled/disabled/adaptive、encodeOutputFormat 含 json_object→空 schema 降级/text 丢弃、公共字段编码含 metadata.user_id/disable_parallel_tool_use 反转/output_config、参数编码含 max_tokens 必填/top_k 直接映射)、encodeResponse(§5.3:text/tool_use/thinking 块直接编码、stop_reason 映射含 content_filter→end_turn 降级、usage 编码含 cache_read_input_tokens/cache_creation_input_tokens)、扩展层 encode(encodeModelsResponse 含 Unix→RFC3339 转换和 has_more/first_id/last_id 字段、encodeModelInfoResponse);编写完整测试覆盖角色合并、首消息注入、降级处理 +- [ ] 5.4 创建 `internal/conversion/anthropic/adapter.go`:实现 Anthropic ProtocolAdapter(protocolName→"anthropic"、supportsPassthrough→true、detectInterfaceType 根据正则匹配识别 /v1/messages→CHAT、/v1/models→MODELS 等、buildHeaders 含 x-api-key + anthropic-version + anthropic-beta + Content-Type、buildUrl 按接口类型映射、supportsInterface 对 CHAT/MODELS/MODEL_INFO 返回 true 对 EMBEDDINGS/RERANK 返回 false、encodeError 返回 {type:"error",error:{type,message}});编写测试覆盖所有路径模式和边界情况 +- [ ] 5.5 创建 `internal/conversion/anthropic/stream_decoder.go`:实现 AnthropicStreamDecoder(对照 conversion_anthropic.md §6.2-§6.3:解析命名 SSE 事件 event: message_start/data: {...},1:1 映射到 CanonicalStreamEvent,维护状态 messageStarted/redactedBlocks/utf8Remainder/accumulatedUsage,redacted_thinking 检测后加入 redactedBlocks 并丢弃后续 delta/stop,citations_delta/signature_delta 直接丢弃,server_tool_use 等服务端工具块丢弃,UTF-8 跨 chunk 安全处理);编写测试覆盖所有事件类型和 redacted_thinking 丢弃 +- [ ] 5.6 创建 `internal/conversion/anthropic/stream_encoder.go`:实现 AnthropicStreamEncoder(对照 conversion_anthropic.md §6.4:直接映射无缓冲,每个 CanonicalStreamEvent 直接编码为对应的 Anthropic 命名 SSE 事件,格式 event: ``\ndata: ``\n\n,delta 编码 text_delta/input_json_delta/thinking_delta 直接映射);编写测试 + +## 6. 基础设施改造 — Provider、Handler、Domain + +- [ ] 6.1 修改 `internal/domain/provider.go`:Provider 结构体新增 Protocol string 字段;修改 `internal/config/models.go`:GORM Provider 模型同步新增 Protocol 字段(gorm:"column:protocol;default:'openai'");修改 `internal/repository/` 中 toDomainProvider 和 toConfigProvider 转换函数同步 Protocol 字段;修改 `internal/handler/provider_handler.go`:CreateProvider 和 UpdateProvider 的请求结构体新增 Protocol 字段(可选,默认 "openai"),创建/更新 Provider 时赋值 Protocol 字段,List/Get 响应中包含 Protocol 字段;更新 `internal/service/service_test.go` 中所有创建测试 Provider 的地方补充 Protocol 字段;更新 `internal/handler/handler_test.go` 中 Provider CRUD 测试的请求体补充 Protocol 字段;创建数据库迁移文件 `backend/migrations/YYYYMMDDHHMMSS_add_provider_protocol.sql`:ALTER TABLE providers ADD COLUMN protocol TEXT DEFAULT 'openai' +- [ ] 6.2 重写 `internal/provider/client.go`:定义 HTTPRequestSpec 和 HTTPResponseSpec(或引用 conversion 包的定义),简化 ProviderClient 接口为 Send(ctx, HTTPRequestSpec) → (*HTTPResponseSpec, error) 和 SendStream(ctx, HTTPRequestSpec) → (<-chan StreamEvent, error),移除所有 openai.Adapter 硬编码依赖,Send 方法直接使用 http.NewRequest + spec.URL/Headers/Body,SendStream 保留现有 readStream goroutine 逻辑但输入改为 HTTPRequestSpec;重写 `provider/client_test.go`:删除所有基于 openai.ChatCompletionRequest 的旧测试用例,基于 HTTPRequestSpec 重写成功/失败/流式测试用例,使用 httptest.Server 验证请求构建和响应解析 +- [ ] 6.3 创建 `internal/handler/proxy_handler.go`:实现 ProxyHandler struct(依赖 ConversionEngine、ProviderClient、RoutingService、StatsService),实现 HandleProxy(w, r) 方法:从 URL 提取 clientProtocol(仅支持 `/{protocol}/v1/...` 前缀路由,不支持旧路由)、解析请求体 JSON、调用 RoutingService.Route(modelName) 获取路由结果(含 Provider.Protocol 作为 providerProtocol)、构建 TargetProvider、调用 engine.convertHttpRequest、调用 providerClient.Send/SendStream、调用 engine.convertHttpResponse、设置响应 Content-Type 和状态码、流式处理设置 text/event-stream 并用 StreamConverter 逐块转换写入、错误处理使用 clientAdapter.encodeError、异步调用 StatsService.Record;编写测试使用 httptest + mock engine/client/service +- [ ] 6.4 修改 `cmd/server/main.go`:创建 AdapterRegistry 并注册 OpenAI 和 Anthropic Adapter、创建 ConversionEngine(注入 registry)、创建 ProxyHandler(注入 engine + providerClient + routingService + statsService)、配置 Gin 路由:新增 `/{protocol}/v1/{path:*}` → ProxyHandler.HandleProxy,删除旧路由 `/v1/chat/completions` 和 `/v1/messages`,移除旧的 OpenAIHandler 和 AnthropicHandler 的路由注册,移除旧的 Adapter 创建代码 + +## 7. 清理和文档 + +- [ ] 7.1 删除旧代码:删除 `internal/protocol/openai/` 目录(types.go, adapter.go, adapter_test.go)、删除 `internal/protocol/anthropic/` 目录(types.go, converter.go, converter_test.go, stream_converter.go, stream_converter_test.go)、删除 `internal/handler/openai_handler.go` 和 `internal/handler/anthropic_handler.go`、删除 `internal/handler/handler_test.go` 中旧 OpenAI/Anthropic handler 测试用例和旧 `mockProviderClient`(基于 openai.ChatCompletionRequest 的签名)、重写 `handler_test.go` 为 ProxyHandler 测试(基于新 ProviderClient 接口和 ConversionEngine mock)、删除 `internal/protocol/` 空目录、确认所有编译通过且无残留 import +- [ ] 7.2 更新 `README.md`:更新项目结构说明(新增 internal/conversion/、删除 internal/protocol/)、更新 API 接口说明(代理接口变更:`/{protocol}/v1/...`,移除旧路由 `/v1/chat/completions` 和 `/v1/messages`)、更新配置说明(Provider 新增 protocol 字段) +- [ ] 7.3 端到端测试:在 `backend/tests/integration/` 中新增 `conversion_test.go`,使用 httptest mock 上游服务器验证完整请求流:OpenAI→OpenAI 同协议透传、Anthropic→Anthropic 同协议透传、OpenAI→Anthropic 跨协议非流式、Anthropic→OpenAI 跨协议非流式、4 种方向的流式转换(含 tool_calls 和 thinking)、Models 接口跨协议转换、错误响应格式验证(各协议格式)、旧路由 `/v1/chat/completions` 和 `/v1/messages` 返回 404;复用 `tests/helpers.go` 中的测试数据库和 Provider/Model 创建辅助函数