1
0

refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包

- 新增 ConversionEngine 核心引擎,支持 OpenAI 和 Anthropic 协议转换
- 添加 stream decoder/encoder 实现
- 更新 provider client 支持新引擎
- 补充单元测试和集成测试
- 更新 specs 文档
This commit is contained in:
2026-04-20 13:01:05 +08:00
parent 1dac347d3b
commit bc1ee612d9
39 changed files with 11177 additions and 995 deletions

View File

@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-04-19

View File

@@ -1,288 +0,0 @@
## Context
### 现有架构
当前后端协议转换以 OpenAI 类型为内部枢纽,整体结构:
```
Anthropic Handler ──▶ anthropic.ConvertRequest() ──▶ openai.ChatCompletionRequest
OpenAI Handler ──────────────────────────────────▶ openai.ChatCompletionRequest
ProviderClient
(硬编码 OpenAI Adapter)
上游 OpenAI 兼容 API
```
关键文件:
- `internal/protocol/openai/types.go` — OpenAI 线路格式类型,兼作内部枢纽格式
- `internal/protocol/anthropic/converter.go` — Anthropic→OpenAI 单向转换
- `internal/protocol/anthropic/stream_converter.go` — OpenAI chunk→Anthropic SSE 单向流式转换
- `internal/handler/openai_handler.go` — OpenAI 请求处理
- `internal/handler/anthropic_handler.go` — Anthropic 请求处理,内含协议转换编排
- `internal/provider/client.go` — HTTP 客户端,硬编码 `openai.Adapter` 做序列化/反序列化
### 核心限制
1. **单向转换**:只有 Anthropic→OpenAI无反向能力
2. **OpenAI 绑定**:上游通信只能走 OpenAI 协议
3. **无透传**:即使 client==provider同协议仍走完整序列化/反序列化
4. **无扩展性**:新增协议需修改多处代码,无统一接入点
5. **仅 Chat**:只支持 `/v1/chat/completions``/v1/messages` 两个固定端点,无 Models/Embeddings/Rerank
### 设计参考
三份设计文档已完整定义目标架构和两个协议的适配细节:
- `docs/conversion_design.md` — 整体架构Hub-and-Spoke、Canonical Model、ProtocolAdapter 接口、ConversionEngine、流式管道、错误处理
- `docs/conversion_openai.md` — OpenAI 协议适配清单(字段映射、流式状态机、角色合并等)
- `docs/conversion_anthropic.md` — Anthropic 协议适配清单角色约束、thinking、流式命名事件等
## Goals / Non-Goals
**Goals:**
- 实现完整的 Hub-and-Spoke 协议转换架构,以 Canonical Model 为枢纽
- 支持任意协议对的请求/响应双向转换当前OpenAI ↔ Anthropic
- 支持同协议透传(零语义损失、零序列化开销)
- 支持流式 SSE 双向转换(含 Tool Calling、Thinking
- 支持 Chat 核心层 + Models/Embeddings/Rerank 扩展层 + 未知接口透传
- ProviderClient 支持多协议上游通信
- 统一代理入口URL 路由支持协议前缀
**Non-Goals:**
- 本阶段不实现多模态Image/Audio/VideoCanonical Model 仅预留扩展点
- 不实现 Middleware 的具体业务逻辑(仅定义接口和 Chain
- 不实现新的协议 Adapter除 OpenAI 和 Anthropic 外)
- 不实现有状态特性(架构预留 StatefulMiddleware 接口)
- 不实现前端管理界面的协议选择功能
- 不修改前端代码(前端使用管理 API代理 API 路由变更对前端透明)
## Decisions
### D1: Canonical Model 用独立 Go 结构体实现,不使用 `interface{}` 或 `map[string]any`
**选择**:为 CanonicalRequest、CanonicalResponse、CanonicalStreamEvent 等定义强类型 Go structContentBlock 使用 discriminated union 模式type 字段 + 各类型嵌入)
**理由**
- 编译期类型安全IDE 自动补全和重构友好
- 性能优于 `map[string]any`(无反射开销)
- 与 Go 生态的习惯一致
**替代方案**
- `map[string]any` — 灵活但无类型安全,重构时容易遗漏字段
- 代码生成(如 protobuf— 引入新依赖和构建步骤,过度工程化
**实现细节**
```go
type ContentBlock struct {
Type string `json:"type"`
// Text
Text string `json:"text,omitempty"`
// ToolUse
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input json.RawMessage `json:"input,omitempty"`
// ToolResult
ToolUseID string `json:"tool_use_id,omitempty"`
IsError *bool `json:"is_error,omitempty"`
// Thinking
Thinking string `json:"thinking,omitempty"`
}
```
使用 `json.RawMessage` 保留 Tool Input 的原始 JSON避免不必要的 `map` 解析。
### D2: ProtocolAdapter 接口集中定义所有方法,不用接口组合
**选择**:一个大的 `ProtocolAdapter` 接口包含所有方法Chat、流式、扩展层、错误编码不拆分为小接口
**理由**
- 对照 `docs/conversion_design.md` §5.2 的定义,接口集中便于明确所有应实现的内容
- Adapter 实现者可一目了然看到所有方法
- 不支持的功能直接返回 false`supportsInterface`)或空实现
- `detectInterfaceType` 由各协议 Adapter 实现,因为不同协议有不同的 URL 路径约定
**替代方案**
- 接口组合ChatAdapter + StreamAdapter + ExtendedAdapter——增加类型复杂度Adapter 注册和管理更繁琐
- 用空接口 + 类型断言——丢失编译期检查
- `detectInterfaceType` 放在 ConversionEngine 中——违反开闭原则,新增协议需要修改 Engine
### D3: StreamDecoder 直接解析原始 SSE 字节流
**选择**`ProviderClient.SendStream()` 返回 `<-chan []byte`(原始 SSE 字节流),`StreamDecoder.processChunk()` 负责拆分 SSE event 并解析 JSON
**理由**
- SSE 解析与协议语义紧密相关(不同协议的 SSE 格式不同OpenAI 用 `data:` 无名事件Anthropic 用命名事件 `event: xxx\ndata: xxx`
- 减少中间层,降低内存拷贝
- ProviderClient 保持最简——只做 HTTP 请求和字节流读取
**替代方案**
- ProviderClient 内做 SSE 解析——强制所有上游使用同一 SSE 格式,不符合多协议目标
- 独立 SSE Parser 层——增加不必要的抽象SSE 格式本身就是 Adapter 职责的一部分
### D4: ProviderClient 接受 `HTTPRequestSpec`,返回 `*HTTPResponseSpec`
**选择**ConversionEngine 输出 `HTTPRequestSpec{URL, Method, Headers, Body []byte}`ProviderClient 接收后发送 HTTP 请求;响应返回 `HTTPResponseSpec{StatusCode, Headers, Body []byte}`
**理由**
- ProviderClient 完全不感知协议,只做 HTTP 通信
- ConversionEngine 统一负责 URL 构建、Header 构建、Body 序列化
- 同协议透传时Engine 直接透传 body bytesClient 不做任何序列化/反序列化
**接口定义**
```go
type HTTPRequestSpec struct {
URL string
Method string
Headers map[string]string
Body []byte
}
type HTTPResponseSpec struct {
StatusCode int
Headers map[string]string
Body []byte
}
type ProviderClient interface {
Send(ctx context.Context, spec HTTPRequestSpec) (*HTTPResponseSpec, error)
SendStream(ctx context.Context, spec HTTPRequestSpec) (<-chan StreamEvent, error)
}
```
### D5: 统一代理入口使用 `/{protocol}/v1/...` URL 前缀
**选择**:新路由格式 `/{protocol}/v1/{path}`handler 从 URL 提取 protocol 前缀作为 clientProtocol
**理由**
- 符合 `docs/conversion_design.md` §2.2 的设计
- 调用方通过 URL 前缀明确指定协议,无需额外配置
- 统一入口简化 handler 数量
**兼容路由**:不保留旧路由,客户端需迁移到新路由格式。
**替代方案**
- 保持两个独立 handler——违背统一架构目标
- 请求体嗅探协议——不可靠,且设计文档明确"协议识别是调用方职责"
### D6: Provider 新增 `Protocol` 字段,存储在数据库
**选择**Provider 表新增 `protocol TEXT DEFAULT 'openai'` 列,用于标识上游供应商使用的协议
**理由**
- 上游供应商可能是 OpenAI 兼容(大多数)或 Anthropic 原生
- 路由时需要知道 providerProtocol 以选择正确的 Adapter
- 默认值 `'openai'` 确保现有数据兼容
### D7: 删除旧 `internal/protocol/` 包,在 `internal/conversion/` 中全新实现
**选择**:直接删除 `internal/protocol/openai/``internal/protocol/anthropic/`,在 `internal/conversion/` 下对照设计文档全新编写所有代码
**理由**
- 旧代码的设计模式OpenAI 类型为枢纽)与新架构根本不同,无法复用
- 保留旧代码容易导致混用两种模式,引入隐蔽 bug
- 旧代码中的类型定义不迁移,直接根据设计文档重新定义,确保与新架构一致
### 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 Adapter**Layer 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 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑

View File

@@ -1,45 +0,0 @@
## 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含状态机流式解码器/编码器),不沿用旧 `internal/protocol/openai/` 代码
- **实现 Anthropic Adapter**:对照 `docs/conversion_anthropic.md` 全新实现 Anthropic 协议的完整 Adapter含命名事件流式解码器/编码器),不沿用旧 `internal/protocol/anthropic/` 代码
- **统一代理 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 接口说明

View File

@@ -1,83 +0,0 @@
## 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: <type>\ndata: <json>\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 错误格式返回

View File

@@ -1,53 +0,0 @@
## 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 包含 CodeErrorCode 枚举、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"

View File

@@ -1,118 +0,0 @@
## 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 模型)

View File

@@ -1,99 +0,0 @@
## 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 错误格式返回

View File

@@ -1,73 +0,0 @@
## 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)

View File

@@ -1,64 +0,0 @@
## 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 包含完整的验证错误信息

View File

@@ -1,49 +0,0 @@
## 1. 基础类型层 — Canonical Model 和核心类型定义
- [x] 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 接口
- [x] 1.2 创建 `internal/conversion/interface.go`:定义 InterfaceType 枚举CHAT, MODELS, MODEL_INFO, EMBEDDINGS, RERANK
- [x] 1.3 创建 `internal/conversion/provider.go`:定义 TargetProvider 结构体BaseURL, APIKey, ModelName, AdapterConfig map[string]any编写测试
- [x] 1.4 创建 `internal/conversion/canonical/types.go`:定义 CanonicalRequestmodel, system, messages, tools, tool_choice, parameters, thinking, stream, user_id, output_format, parallel_tool_use、CanonicalMessagerole 枚举: system/user/assistant/tool, content []ContentBlock、ContentBlock使用 type 字段的 discriminated uniontext/tool_use/tool_result/thinkingToolInput 使用 json.RawMessage、CanonicalToolname, description, input_schema、ToolChoice 联合体auto/none/any/tool+name、RequestParametersmax_tokens, temperature, top_p, top_k, frequency_penalty, presence_penalty, stop_sequences、ThinkingConfigtype: enabled/disabled/adaptive, budget_tokens, effort、OutputFormatjson_object/json_schema+schema/text、CanonicalResponseid, model, content, stop_reason 枚举, usage、CanonicalUsageinput_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, reasoning_tokens、SystemBlocktext编写构造和序列化测试
- [x] 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、PingEventdelta 联合体text_delta, input_json_delta, thinking_deltacontent_block 联合体text, tool_use, thinking编写测试
- [x] 1.6 创建 `internal/conversion/canonical/extended.go`:定义扩展层 Canonical ModelsCanonicalModelList, CanonicalModel, CanonicalModelInfo, CanonicalEmbeddingRequest, CanonicalEmbeddingResponse, CanonicalRerankRequest, CanonicalRerankResponse编写测试
## 2. 接口定义层 — Adapter、Stream、Middleware 接口
- [x] 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 注册/查询/重复注册测试
- [x] 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 + StreamEncoderprocessChunk 内部调用 decoder → middleware → encoder 管道);编写 PassthroughStreamConverter 测试
- [x] 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 门面
- [x] 3.1 创建 `internal/conversion/engine.go`:定义 HTTPRequestSpecURL, Method string, Headers map[string]string, Body []byte、HTTPResponseSpecStatusCode int, Headers map[string]string, Body []byte、ConversionEngine structregistry, 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 实现
- [x] 4.1 创建 `internal/conversion/openai/types.go`:对照 `docs/conversion_openai.md` 全新定义 OpenAI 线路格式类型(不沿用旧 `internal/protocol/openai/types.go`包含完整字段developer role, custom tools, reasoning_effort, reasoning_content, max_completion_tokens, parallel_tool_calls, response_format 的 json_schema 类型, stream_options, 废弃的 functions/function_call编写序列化测试
- [x] 4.2 创建 `internal/conversion/openai/decoder.go`:实现 decodeRequest对照 conversion_openai.md §4.1decodeSystemPrompt 提取 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.2content/refusal/reasoning_content/tool_calls 解码、finish_reason 映射表、usage 映射含 cached_tokens/reasoning_tokens、扩展层 decodedecodeModelsResponse、decodeEmbeddingRequest/Response、decodeRerankRequest/Response编写完整测试覆盖每类消息和字段映射
- [x] 4.3 创建 `internal/conversion/openai/encoder.go`:实现 encodeRequest对照 conversion_openai.md §4.2provider.model_name 覆盖、system 注入到 messages[0]、encodeMessage 含 tool_calls 编码到 message 顶层、角色交替合并、encodeTools 含 function 包装、encodeToolChoice 含 any→required、encodeParameters 含 max_completion_tokens、encodeOutputFormat、encodeThinking 含 disabled→"none"、encodeResponse§5.3text→content、tool_use→tool_calls、thinking→reasoning_content、finish_reason 反向映射、usage 编码含 prompt_tokens_details、扩展层 encodeencodeModelsResponse、encodeEmbeddingRequest/Response、encodeRerankRequest/Response编写完整测试
- [x] 4.4 创建 `internal/conversion/openai/adapter.go`:实现 OpenAI ProtocolAdapterprotocolName→"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 方法;编写测试覆盖所有路径模式和边界情况
- [x] 4.5 创建 `internal/conversion/openai/stream_decoder.go`:实现 OpenAIStreamDecoder对照 conversion_openai.md §6.2-§6.3processChunk 解析 SSE data 行,维护状态机 messageStarted/openBlocks/toolCallIdMap/toolCallNameMap/toolCallArguments/textBlockStarted/thinkingBlockStarted/utf8Remainder/accumulatedUsage首个 chunk→MessageStartEventdelta.content→text block 生命周期delta.tool_calls→tool_use block 生命周期含索引映射和参数累积delta.reasoning_content→thinking block非标准delta.refusal→text blockfinish_reason→关闭所有 open blocks + MessageDeltaEvent + MessageStopEventusage chunk→MessageDeltaEvent[DONE]→flush 关闭);编写测试覆盖每种 delta 类型和边界情况(空 chunk、多 tool_calls、UTF-8 截断)
- [x] 4.6 创建 `internal/conversion/openai/stream_encoder.go`:实现 OpenAIStreamEncoder对照 conversion_openai.md §6.4encodeEventContentBlockStart 缓冲策略等待首次 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 并行)
- [x] 5.1 创建 `internal/conversion/anthropic/types.go`:对照 `docs/conversion_anthropic.md` 全新定义 Anthropic 线路格式类型(不沿用旧 `internal/protocol/anthropic/types.go`包含完整字段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编写序列化测试
- [x] 5.2 创建 `internal/conversion/anthropic/decoder.go`:实现 decodeRequest对照 conversion_anthropic.md §4.1decodeSystem 从顶层 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.2text/tool_use/thinking 块解码、redacted_thinking 丢弃、stop_reason 映射含 pause_turn/refusal、usage 映射含 cache_read_input_tokens/cache_creation_input_tokens、扩展层 decodedecodeModelsResponse 含 RFC3339→Unix 时间戳转换、decodeModelInfoResponse编写完整测试覆盖角色拆分、thinking 三种类型、时间戳转换
- [x] 5.3 创建 `internal/conversion/anthropic/encoder.go`:实现 encodeRequest对照 conversion_anthropic.md §4.2provider.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.3text/tool_use/thinking 块直接编码、stop_reason 映射含 content_filter→end_turn 降级、usage 编码含 cache_read_input_tokens/cache_creation_input_tokens、扩展层 encodeencodeModelsResponse 含 Unix→RFC3339 转换和 has_more/first_id/last_id 字段、encodeModelInfoResponse编写完整测试覆盖角色合并、首消息注入、降级处理
- [x] 5.4 创建 `internal/conversion/anthropic/adapter.go`:实现 Anthropic ProtocolAdapterprotocolName→"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}});编写测试覆盖所有路径模式和边界情况
- [x] 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/accumulatedUsageredacted_thinking 检测后加入 redactedBlocks 并丢弃后续 delta/stopcitations_delta/signature_delta 直接丢弃server_tool_use 等服务端工具块丢弃UTF-8 跨 chunk 安全处理);编写测试覆盖所有事件类型和 redacted_thinking 丢弃
- [x] 5.6 创建 `internal/conversion/anthropic/stream_encoder.go`:实现 AnthropicStreamEncoder对照 conversion_anthropic.md §6.4:直接映射无缓冲,每个 CanonicalStreamEvent 直接编码为对应的 Anthropic 命名 SSE 事件,格式 event: `<type>`\ndata: `<json>`\n\ndelta 编码 text_delta/input_json_delta/thinking_delta 直接映射);编写测试
## 6. 基础设施改造 — Provider、Handler、Domain
- [x] 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'
- [x] 6.2 重写 `internal/provider/client.go`:定义 HTTPRequestSpec 和 HTTPResponseSpec或引用 conversion 包的定义),简化 ProviderClient 接口为 Send(ctx, HTTPRequestSpec) → (*HTTPResponseSpec, error) 和 SendStream(ctx, HTTPRequestSpec) → (<-chan StreamEvent, error)移除所有旧协议硬编码依赖Send 方法直接使用 http.NewRequest + spec.URL/Headers/BodySendStream 保留现有 readStream goroutine 逻辑但输入改为 HTTPRequestSpec重写 `provider/client_test.go`:删除所有基于旧协议类型的测试用例,基于 HTTPRequestSpec 重写成功/失败/流式测试用例,使用 httptest.Server 验证请求构建和响应解析
- [x] 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
- [x] 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. 清理和文档
- [x] 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`(基于旧协议类型的签名)、重写 `handler_test.go` 为 ProxyHandler 测试(基于新 ProviderClient 接口和 ConversionEngine mock、删除 `internal/protocol/` 空目录、确认所有编译通过且无残留 import
- [x] 7.2 更新 `README.md`:更新项目结构说明(新增 internal/conversion/、删除 internal/protocol/)、更新 API 接口说明(代理接口变更:`/{protocol}/v1/...`,移除旧路由 `/v1/chat/completions``/v1/messages`、更新配置说明Provider 新增 protocol 字段)
- [x] 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 创建辅助函数

View File

@@ -4,42 +4,47 @@
### Requirement: 支持 Anthropic Messages API 端点
网关 SHALL 提供 Anthropic Messages API 端点 `POST /v1/messages` 供外部应用调用。
网关 SHALL 提供 Anthropic Messages API 端点供外部应用调用。
#### Scenario: 成功的非流式请求
- **WHEN** 应用发送 POST 请求到 `/v1/messages`,携带有效的 Anthropic 请求格式(非流式)
- **THEN** 网关 SHALL 将 Anthropic 请求转换为 OpenAI 格式
- **THEN** 网关 SHALL 将转换后的请求转发到配置的供应商
- **THEN** 网关 SHALL 将 OpenAI 响应转换 Anthropic 格式
- **THEN** 网关 SHALL 将转换后的响应返回给应用
- **WHEN** 应用发送 POST 请求到 `/anthropic/v1/messages`,携带有效的 Anthropic 请求格式(非流式)
- **THEN** 网关 SHALL 通过 ConversionEngine 将 Anthropic 请求解码为 Canonical 格式
- **THEN** 网关 SHALL 将 Canonical 请求编码为目标供应商协议格式
- **THEN** 网关 SHALL 将供应商的响应通过 ConversionEngine 转换 Anthropic 格式返回给应用
#### Scenario: 成功的流式请求
- **WHEN** 应用发送 POST 请求到 `/v1/messages`,携带 `stream: true`
- **THEN** 网关 SHALL 将 Anthropic 请求转换为 OpenAI 格式
- **THEN** 网关 SHALL 将转换后的请求转发给供应商
- **THEN** 网关 SHALL 将 OpenAI 流事件转换为 Anthropic 流事件
- **THEN** 网关 SHALL 使用 SSE 格式将转换后的事件流式返回给应用
- **WHEN** 应用发送 POST 请求到 `/anthropic/v1/messages`,携带 `stream: true`
- **THEN** 网关 SHALL 通过 ConversionEngine 创建 StreamConverter
- **THEN** 网关 SHALL 将上游协议的 SSE 流转换为 Anthropic 命名事件格式
- **THEN** 网关 SHALL 使用 `event: <type>\ndata: <json>\n\n` 格式流式返回给应用
**变更说明:** handler 通过 service 层调用,而非直接调用 config 和 provider 包。API 接口保持不变。
#### Scenario: 同协议透传Anthropic → Anthropic Provider
### Requirement: 将 Anthropic 请求转换为 OpenAI 格式
- **WHEN** 客户端使用 Anthropic 协议且目标供应商也是 Anthropic 协议
- **THEN** 网关 SHALL 跳过 Canonical 转换,仅重建认证 Header 后原样转发
- **THEN** 请求和响应 Body SHALL 保持原样
网关 SHALL 将 Anthropic Messages API 请求转换为 OpenAI Chat Completions API 格式。
### Requirement: 双向协议转换
#### Scenario: System 消息转换
网关 SHALL 支持 Anthropic 协议与任意已注册协议间的双向转换。
- **WHEN** Anthropic 请求包含 `system` 字段
- **THEN** 网关 SHALL 将其转换为 `messages` 数组中 `role: "system"` 的消息
#### Scenario: Anthropic 客户端 → OpenAI 供应商
#### Scenario: Messages 转换
- **WHEN** 客户端使用 Anthropic 协议且供应商使用 OpenAI 协议
- **THEN** SHALL 将 Anthropic MessagesRequest 解码为 CanonicalRequest
- **THEN** SHALL 将 CanonicalRequest 编码为 OpenAI ChatCompletionRequest
- **THEN** SHALL 将 OpenAI ChatCompletionResponse 解码为 CanonicalResponse
- **THEN** SHALL 将 CanonicalResponse 编码为 Anthropic MessagesResponse
- **WHEN** Anthropic 请求包含 `messages` 数组
- **THEN** 网关 SHALL 在转换后的 OpenAI 请求中保留这些消息
- **THEN** 网关 SHALL 保留每条消息的 role 和 content
#### Scenario: OpenAI 客户端 → Anthropic 供应<E4BE9B><E5BA94>
**变更说明:** 协议转换逻辑保持不变,仅调用方式改为通过 service 层。
- **WHEN** 客户端使用 OpenAI 协议且供应商使用 Anthropic 协议
- **THEN** SHALL 将 OpenAI ChatCompletionRequest 解码为 CanonicalRequest
- **THEN** SHALL 将 CanonicalRequest 编码为 Anthropic MessagesRequest
- **THEN** SHALL 将 Anthropic MessagesResponse 解码为 CanonicalResponse
- **THEN** SHALL 将 CanonicalResponse 编码为 OpenAI ChatCompletionResponse
## ADDED Requirements
@@ -49,9 +54,9 @@ Handler SHALL 通过 service 层处理业务逻辑。
#### Scenario: 调用 routing service
- **WHEN** handler 收到请求并转换为 OpenAI 格式
- **WHEN** ProxyHandler 收到 Anthropic 协议请求
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
- **THEN** SHALL 使用路由结果中的供应商信息
- **THEN** SHALL 路由结果获取 Provider含 protocol 字段)
#### Scenario: 调用 stats service
@@ -61,16 +66,22 @@ Handler SHALL 通过 service 层处理业务逻辑。
### Requirement: 使用结构化错误处理
Handler SHALL 使用结构化错误处理
ProxyHandler SHALL 使用 ConversionError 和 Anthropic 的 encodeError 处理错误
#### Scenario: 协议转换错误
- **WHEN** 协议转换失败
- **THEN** SHALL 返回结构化错误响应
- **THEN** SHALL 包含详细的错误信息
- **WHEN** ConversionEngine 返回 ConversionError
- **THEN** SHALL 使用 Anthropic 的 Adapter.encodeError 编码错误响应
- **THEN** SHALL 使用 Anthropic 错误格式(`{type: "error", error: {type, message}}`
#### Scenario: 路由错误处理
- **WHEN** RoutingService 返回错误
- **THEN** SHALL 转换为对应的 AppError
- **THEN** SHALL 返回统一的错误响应
- **THEN** SHALL 转换为 ConversionError
- **THEN** SHALL 使用 Anthropic 错误格式返回
#### Scenario: 供应商错误处理
- **WHEN** ProviderClient 返回错误
- **THEN** SHALL 包装为 ConversionError
- **THEN** SHALL 使用 Anthropic 错误格式返回

View File

@@ -1,3 +1,5 @@
# Conversion Engine
## ADDED Requirements
### Requirement: 定义 CanonicalRequest 规范模型
@@ -264,7 +266,7 @@ ErrorCode SHALL 包含INVALID_INPUT、MISSING_REQUIRED_FIELD、INCOMPATIBLE_F
- **THEN** SHALL 使用对应扩展层 Canonical Model 做轻量字段映射
- **THEN** 双方都不支持时 SHALL 走透传逻辑
### Requirement: 义 TargetProvider 结构体
### Requirement: <EFBFBD><EFBFBD>义 TargetProvider 结构体
系统 SHALL 定义 `TargetProvider` 结构体,包含 `base_url``api_key``model_name``adapter_config`
@@ -273,4 +275,4 @@ ErrorCode SHALL 包含INVALID_INPUT、MISSING_REQUIRED_FIELD、INCOMPATIBLE_F
- **WHEN** Adapter 调用 buildHeaders(provider)
- **THEN** SHALL 从 provider.api_key 提取认证信息
- **THEN** SHALL 从 provider.adapter_config 提取协议专属配置
- **THEN** SHALL 使用 provider.model_name 覆盖请求中的 model 字段
- **THEN** SHALL 使用 provider.model_name 覆盖请求中的 model 字段

View File

@@ -29,6 +29,12 @@
- **THEN** SHALL 使用 ErrModelNotFound、ErrProviderNotFound 等预定义错误
- **THEN** SHALL 设置 HTTP 状态码为 404
#### Scenario: 转换错误响应
- **WHEN** ConversionEngine 在协议转换过程中产生 ConversionError
- **THEN** SHALL 使用客户端协议的 Adapter.encodeError 编码错误响应
- **THEN** 错误响应 SHALL 使用客户端可理解的协议格式
#### Scenario: 验证错误
- **WHEN** 请求验证失败
@@ -120,3 +126,26 @@
- **WHEN** repository 层发生错误
- **THEN** SHALL 包装数据库错误
- **THEN** SHALL 转换为应用错误
## ADDED Requirements
### Requirement: 定义 ConversionError 错误类型
系统 SHALL 定义 ConversionError 结构体和 ErrorCode 枚举。
#### Scenario: ConversionError 结构
- **WHEN** 定义转换错误
- **THEN** SHALL 包含 CodeErrorCode 枚举、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"

View File

@@ -4,28 +4,37 @@
### Requirement: 实现三层架构
系统 SHALL 实现 handler → service → repository 三层架构。
系统 SHALL 实现 handler → service → repository 三层架构,并在 handler 和 provider 之间新增 conversion 层
#### Scenario: Handler 层职责
- **WHEN** 处理 HTTP 请求
- **THEN** handler 层 SHALL 仅负责 HTTP 请求解析和响应
- **THEN** handler 层 SHALL 仅负责 HTTP 请求解析、URL 路由和响应写入
- **THEN** handler 层 SHALL 调用 ConversionEngine 处理协议转换
- **THEN** handler 层 SHALL 调用 service 层处理业务逻辑
- **THEN** handler 层 SHALL NOT 直接访问数据库
- **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 协调多个 repository 的操作
- **THEN** service 层 SHALL NOT 包含协议转换逻辑
#### Scenario: Repository 层职责
- **WHEN** 访问数据
- **THEN** repository 层 SHALL 仅负责数据访问
- **THEN** repository 层 SHALL 封装数据库操作
- **THEN** repository 层 SHALL NOT 包含业务逻辑
- **THEN** repository 层 SHALL NOT 包含业务逻辑或协议转换逻辑
### Requirement: 定义核心接口
@@ -49,9 +58,17 @@
- **WHEN** 定义 provider client 接口
- **THEN** SHALL 定义 ProviderClient 接口
- **THEN** SHALL 包含 SendRequest 和 SendStreamRequest 方法
- **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 使用手动依赖注入。
@@ -65,15 +82,24 @@
#### Scenario: Service 注入
- **WHEN** 初始化 handler
- **THEN** SHALL 通过构造函数注入 service 依赖
- **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、handler
- **THEN** SHALL 再构造 repository、service
- **THEN** SHALL 再构造 conversion 层registry → engine
- **THEN** SHALL 最后构造 handler
### Requirement: 定义 Domain 模型
@@ -84,6 +110,7 @@
- **WHEN** 定义领域模型
- **THEN** SHALL 在 internal/domain/ 包中定义
- **THEN** SHALL 包含 Provider、Model、UsageStats 等模型
- **THEN** Provider SHALL 包含 Protocol 字段
- **THEN** SHALL 与数据库模型分离
#### Scenario: Domain 模型使用

View File

@@ -4,22 +4,27 @@
### Requirement: 支持 OpenAI Chat Completions API 端点
网关 SHALL 提供 OpenAI Chat Completions API 端点 `POST /v1/chat/completions` 供外部应用调用。
网关 SHALL 提供 OpenAI Chat Completions API 端点供外部应用调用。
#### Scenario: 成功的非流式请求
- **WHEN** 应用发送 POST 请求到 `/v1/chat/completions`,携带有效的 OpenAI 请求格式(非流式)
- **THEN** 网关 SHALL 将请求转发到配置的供应商
- **THEN** 网关 SHALL 将供应商的响应以 OpenAI 格式返回给应用
- **WHEN** 应用发送 POST 请求到 `/openai/v1/chat/completions`,携带有效的 OpenAI 请求格式(非流式)
- **THEN** 网关 SHALL 通过 ConversionEngine 转换请求
- **THEN** 网关 SHALL 将转换后的请求转发到配置的供应商
- **THEN** 网关 SHALL 将供应商的响应通过 ConversionEngine 转换为 OpenAI 格式返回给应用
#### Scenario: 成功的流式请求
- **WHEN** 应用发送 POST 请求到 `/v1/chat/completions`,携带 `stream: true`
- **THEN** 网关 SHALL 将请求转发到配置的供应商
- **THEN** 网关 SHALL 使用 SSE 格式将响应流式返回给应用
- **WHEN** 应用发送 POST 请求到 `/openai/v1/chat/completions`,携带 `stream: true`
- **THEN** 网关 SHALL 通过 ConversionEngine 创建 StreamConverter
- **THEN** 网关 SHALL 使用 SSE 格式将转换后的响应流式返回给应用
- **THEN** 网关 SHALL 在流完成时发送 `data: [DONE]`
**变更说明:** handler 通过 service 层调用,而非直接调用 config 和 provider 包。API 接口保持不变。
#### Scenario: 同协议透传OpenAI → OpenAI Provider
- **WHEN** 客户端使用 OpenAI 协议且目标供应商也是 OpenAI 协议
- **THEN** 网关 SHALL 跳过 Canonical 转换,仅重建认证 Header 后原样转发
- **THEN** 请求和响应 Body SHALL 保持原样
### Requirement: 根据模型名称路由请求
@@ -30,38 +35,32 @@
- **WHEN** 请求包含存在于配置模型中的 `model` 字段
- **AND** 该模型已启用
- **THEN** 网关 SHALL 将请求路由到该模型关联的供应商
- **THEN** 网关 SHALL 从供应商的 `protocol` 字段获取 providerProtocol
#### Scenario: 模型未找到
- **WHEN** 请求包含不存在于配置模型中的 `model` 字段
- **THEN** 网关 SHALL 返回带有适当错误消息的错误响应
- **THEN** 网关 SHALL 使用 OpenAI 格式返回错误响应
#### Scenario: 模型已禁用
- **WHEN** 请求包含已禁用模型的 `model` 字段
- **THEN** 网关 SHALL 返回错误响应,指示模型不可用
**变更说明:** 路由逻辑从 router 包迁移到 RoutingService通过 service 层调用。API 接口保持不变。
- **THEN** 网关 SHALL 使用 OpenAI 格式返回错误响应
### Requirement: 对 OpenAI 兼容供应商透明代理
网关 SHALL 对 OpenAI 兼容供应商的请求和响应进行透明转发,不做修改
网关 SHALL 对 OpenAI 兼容供应商的请求和响应通过 ConversionEngine 进行转换处理
#### Scenario: 请求转发
#### Scenario: 跨协议请求转发
- **WHEN** 网关收到 OpenAI 协议请求
- **AND** 目标供应商是 OpenAI 兼容的
- **THEN** 网关 SHALL 将请求体原样转发给供应商
- **THEN** 网关 SHALL 在 Authorization 头中设置供应商的 API Key
- **THEN** 网关 SHALL 使用供应商的 base URL
- **WHEN** 网关收到 OpenAI 协议请求且目标供应商使用不同协议
- **THEN** 网关 SHALL 通过 ConversionEngine 将请求转换为目标协议格式
- **THEN** 网关 SHALL 使用目标协议的 Adapter 构建 URL 和 Header
#### Scenario: 响应转发
#### Scenario: 扩展层接口代理
- **WHEN** 供应商返回响应
- **THEN** 网关 SHALL 将响应体原样返回给应用
- **THEN** 网关 SHALL 保留所有响应头和状态码
**变更说明:** provider client 通过接口注入到 handler便于测试和替换实现。API 接口保持不变。
- **WHEN** 网关收到 `/openai/v1/models` 等 GET 请求
- **THEN** 网关 SHALL 通过 ConversionEngine 转换扩展层接口的响应格式
## ADDED Requirements
@@ -81,18 +80,40 @@ Handler SHALL 通过 service 层处理业务逻辑。
- **THEN** SHALL 调用 StatsService.Record() 记录统计
- **THEN** SHALL 异步记录统计(不阻塞响应)
### 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: 使用结构化错误处理
Handler SHALL 使用结构化错误处理
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 转换为对应的 AppError
- **THEN** SHALL 返回统一的错误响应
- **THEN** SHALL 转换为 ConversionError
- **THEN** SHALL 使用 OpenAI 错误格式返回
#### Scenario: 供应商错误处理
- **WHEN** ProviderClient 返回错误
- **THEN** SHALL 包装为 AppError
- **THEN** SHALL 包含请求上下文信息
- **THEN** SHALL 包装为 ConversionError
- **THEN** SHALL 使用 OpenAI 错误格式返回

View File

@@ -1,3 +1,5 @@
# Protocol Adapter - Anthropic
## ADDED Requirements
### Requirement: 实现 Anthropic ProtocolAdapter
@@ -266,4 +268,4 @@ Decoder 几乎 1:1 映射,维护最小状态机:
- **WHEN** interfaceType 为 EMBEDDINGS 或 RERANK
- **THEN** supportsInterface SHALL 返回 false
- **THEN** 引擎 SHALL 走透传或返回空响应
- **THEN** 引擎 SHALL 走透传或返回空响应

View File

@@ -1,3 +1,5 @@
# Protocol Adapter - OpenAI
## ADDED Requirements
### Requirement: 实现 OpenAI ProtocolAdapter
@@ -85,7 +87,7 @@
- **WHEN** canonical.system 不为空
- **THEN** SHALL 编码为 messages 数组头部的 role="system" 消息
#### Scenario: Assistant 息中 tool_calls 编码
#### Scenario: Assistant <EFBFBD><EFBFBD>息中 tool_calls 编码
- **WHEN** CanonicalMessage{role: "assistant"} 包含 tool_use 类型 ContentBlock
- **THEN** SHALL 提取到 message.tool_calls 数组({id, type: "function", function: {name, arguments}}
@@ -265,4 +267,4 @@ Encoder SHALL 维护状态:
#### Scenario: /rerank 接口
- **WHEN** 解码/编码 rerank 请求和响应
- **THEN** SHALL 使用 CanonicalRerankRequest/Response 做字段映射
- **THEN** SHALL 使用 CanonicalRerankRequest/Response 做字段映射

View File

@@ -8,10 +8,11 @@
#### Scenario: 使用有效数据创建供应商
- **WHEN** 向 `/api/providers` 发送 POST 请求携带有效的供应商数据id, name, api_key, base_url
- **WHEN** 向 `/api/providers` 发送 POST 请求携带有效的供应商数据id, name, api_key, base_url, protocol
- **THEN** 网关 SHALL 在数据库中创建新的供应商记录
- **THEN** 网关 SHALL 返回创建的供应商,状态码为 201
- **THEN** 供应商 SHALL 默认启用
- **THEN** protocol 字段 SHALL 默认为 "openai"
#### Scenario: 使用重复 ID 创建供应商
@@ -34,7 +35,7 @@
- **WHEN** 向 `/api/providers` 发送 GET 请求
- **THEN** 网关 SHALL 返回所有供应商的列表
- **THEN** 每个供应商 SHALL 包含 id, name, api_key已掩码, base_url, enabled, created_at, updated_at
- **THEN** 每个供应商 SHALL 包含 id, name, api_key已掩码, base_url, protocol, enabled, created_at, updated_at
- **THEN** api_key SHALL 被掩码(仅显示最后 4 个字符)
**变更说明:** 数据访问从 config 包迁移到 ProviderRepository。API 接口保持不变。
@@ -47,6 +48,7 @@
- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带有效的供应商 ID
- **THEN** 网关 SHALL 返回供应商详情
- **THEN** SHALL 包含 protocol 字段
- **THEN** api_key SHALL 被掩码
#### Scenario: 获取不存在的供应商
@@ -65,7 +67,7 @@
- **WHEN** 向 `/api/providers/:id` 发送 PUT 请求,携带有效的供应商数据
- **THEN** 网关 SHALL 更新数据库中的供应商记录
- **THEN** 网关 SHALL 返回更新后的供应商
- **THEN** updated_at 时间戳 SHALL 被更新
- **THEN** 更新 SHALL 支持修改 protocol 字段
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。

View File

@@ -20,43 +20,42 @@
### Requirement: 验证 OpenAI 请求
系统 SHALL 验证 OpenAI ChatCompletionRequest。
系统 SHALL 验证 OpenAI ChatCompletionRequest,验证逻辑位于 ProtocolAdapter 的 decodeRequest 内
#### Scenario: 必需字段验证
- **WHEN** 收到 OpenAI 请求
- **WHEN** OpenAI Adapter 的 decodeRequest 解析请求
- **THEN** SHALL 验证 model 字段不为空
- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息
- **THEN** 验证失败 SHALL 返回 INVALID_INPUT 类型的 ConversionError
#### Scenario: 参数范围验证
- **WHEN** 收到 OpenAI 请求
- **WHEN** OpenAI Adapter 的 decodeRequest 解析参数
- **THEN** SHALL 验证 temperature 范围在 [0, 2]
- **THEN** SHALL 验证 max_tokens 大于 0
- **THEN** SHALL 验证 top_p 范围在 (0, 1]
- **THEN** SHALL 验证 frequency_penalty 范围在 [-2, 2]
- **THEN** SHALL 验证 presence_penalty 范围在 [-2, 2]
#### Scenario: 消息内容验证
- **WHEN** 验证 messages 字段
- **THEN** SHALL 验证每条消息的 role 有效system、user、assistant、tool
- **THEN** SHALL 验证每条消息的 role 有效system、developer、user、assistant、tool
- **THEN** SHALL 验证 content 不为空
### Requirement: 验证 Anthropic 请求
系统 SHALL 验证 Anthropic MessagesRequest。
系统 SHALL 验证 Anthropic MessagesRequest,验证逻辑位于 ProtocolAdapter 的 decodeRequest 内
#### Scenario: 必需字段验证
- **WHEN** 收到 Anthropic 请求
- **WHEN** Anthropic Adapter 的 decodeRequest 解析请求
- **THEN** SHALL 验证 model 字段不为空
- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息
- **THEN** SHALL 验证 max_tokens 大于 0或使用默认值
#### Scenario: 参数范围验证
- **WHEN** 收到 Anthropic 请求
- **WHEN** Anthropic Adapter 的 decodeRequest 解析参数
- **THEN** SHALL 验证 temperature 范围在 [0, 1]
- **THEN** SHALL 验证 top_p 范围在 (0, 1]
@@ -93,26 +92,17 @@
系统 SHALL 返回友好的验证错误响应。
#### Scenario: 错误消息格式
#### Scenario: 转换错误格式
- **WHEN** 验证失败
- **THEN** SHALL 返回 400 状态码
- **THEN** SHALL 返回详细的错误消息
- **THEN** SHALL 指示哪些字段验证失败
- **WHEN** decodeRequest 验证失败返回 ConversionError
- **THEN** ProxyHandler SHALL 使用 clientAdapter.encodeError 编码错误响应
- **THEN** 错误 SHALL 使用客户端协议的格式
#### Scenario: 多字段错误
- **WHEN** 多个字段验证失败
- **THEN** SHALL 返回所有验证错误
- **THEN** SHALL 使用结构化格式(字段名 → 错误消息)
#### Scenario: 国际化支持
- **WHEN** 返回验证错误(未来)
- **THEN** SHALL 支持错误消息国际化
- **THEN** SHALL 使用错误码作为国际化 key
注:当前版本使用中文错误消息。
- **THEN** ConversionError.details SHALL 包含所有验证错误
- **THEN** 错误响应 SHALL 包含完整的验证错误信息
### Requirement: 在 handler 中应用验证

View File

@@ -1,3 +1,5 @@
# Unified Proxy Handler
## ADDED Requirements
### Requirement: 实现统一代理 Handler
@@ -92,11 +94,11 @@ ProxyHandler SHALL 记录请求统计。
ProxyHandler SHALL 支持 GET 请求的扩展层接口代理。
#### Scenario: Models 接口
#### Scenario: Models 接口<EFBFBD><EFBFBD>
- **WHEN** 收到 GET /{protocol}/v1/models 请求
- **THEN** SHALL 执行路由和协议识别
- **THEN** SHALL 调用 engine.convertHttpRequestGET 请求 body 为空)
- **THEN** SHALL 调用 providerClient.Send 发送请求
- **THEN** SHALL 调用 engine.convertHttpResponse 转换响应格式
- **THEN** SHALL 返回转换后的响应
- **THEN** SHALL 返回转换后的响应