1
0

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

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

View File

@@ -0,0 +1,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: <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

@@ -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`: 包含 messageid, model, usage
- `content_block_start`: 包含 index 和 content_block
- `content_block_delta`: 包含 index 和 deltatext_delta/input_json_delta/thinking_delta
- `content_block_stop`: 包含 index
- `message_delta`: 包含 deltastop_reason和 usage
- `message_stop`: 无额外数据
- `error`: 包含 errortype, 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 返回对应的 InterfaceTypeCHAT/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 字段

View File

@@ -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 包含 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

@@ -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 模型)

View File

@@ -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 错误格式返回

View File

@@ -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: <provider.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 调用 buildUrlsupportsInterface 返回 false引擎走透传
### Requirement: Anthropic 请求解码Anthropic → Canonical
系统 SHALL 实现完整的 Anthropic MessagesRequest 到 CanonicalRequest 的解码。
#### Scenario: System 消息提取
- **WHEN** Anthropic 请求包含顶层 `system` 字段
- **THEN** String 类型 SHALL 直接提取为 canonical.system
- **THEN** SystemBlock 数组 SHALL 提取为 canonical.systemArray
#### 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.formatjson_schema 类型SHALL 提取为 output_format
- **THEN** disable_parallel_tool_use SHALL 反转映射为 parallel_tool_usetrue → 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: <type>\ndata: <json>\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: <error_code>, message: <message>}}`
### Requirement: Anthropic 扩展层接口编解码
系统 SHALL 实现 Anthropic 协议的扩展层接口编解码(仅 Models
#### Scenario: /models 列表接口
- **WHEN** 解码 Anthropic models 响应
- **THEN** data[].display_name SHALL 映射为 models[].name
- **THEN** data[].created_atRFC 3339SHALL 转换为 models[].createdUnix 时间戳)
- **WHEN** 编码 CanonicalModelList 为 Anthropic 格式
- **THEN** SHALL 输出包含 has_more、first_id、last_id 的结构
- **THEN** models[].createdUnix 时间戳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 走透传或返回空响应

View File

@@ -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 <api_key>``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 <provider.api_key>`
- **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.systemString
- **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.argumentsJSON 字符串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** stopString 或 ArraySHALL 规范化为 parameters.stop_sequencesArray
- **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 映射为 CanonicalModelListdata[].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 做字段映射

View File

@@ -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)

View File

@@ -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 包含完整的验证错误信息

View File

@@ -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 构建 TargetProviderbase_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.convertHttpRequestGET 请求 body 为空)
- **THEN** SHALL 调用 providerClient.Send 发送请求
- **THEN** SHALL 调用 engine.convertHttpResponse 转换响应格式
- **THEN** SHALL 返回转换后的响应