refactor: 实现 ConversionEngine 协议转换引擎,替代旧 protocol 包
- 新增 ConversionEngine 核心引擎,支持 OpenAI 和 Anthropic 协议转换 - 添加 stream decoder/encoder 实现 - 更新 provider client 支持新引擎 - 补充单元测试和集成测试 - 更新 specs 文档
This commit is contained in:
278
openspec/specs/conversion-engine/spec.md
Normal file
278
openspec/specs/conversion-engine/spec.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Conversion Engine
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 定义 CanonicalRequest 规范模型
|
||||
|
||||
系统 SHALL 定义协议无关的 `CanonicalRequest` 结构体,作为所有协议间请求转换的统一枢纽。
|
||||
|
||||
CanonicalRequest SHALL 包含以下字段:
|
||||
- `model`: 字符串,模型名称
|
||||
- `system`: 可选,字符串或 SystemBlock 数组
|
||||
- `messages`: CanonicalMessage 数组
|
||||
- `tools`: 可选,CanonicalTool 数组
|
||||
- `tool_choice`: 可选,ToolChoice 联合体
|
||||
- `parameters`: RequestParameters
|
||||
- `thinking`: 可选,ThinkingConfig
|
||||
- `stream`: 布尔值
|
||||
- `user_id`: 可选字符串
|
||||
- `output_format`: 可选,OutputFormat
|
||||
- `parallel_tool_use`: 可选布尔值
|
||||
|
||||
#### Scenario: CanonicalRequest 包含所有公共字段
|
||||
|
||||
- **WHEN** 从任意协议解码请求为 CanonicalRequest
|
||||
- **THEN** CanonicalRequest SHALL 包含协议间可映射的所有公共语义字段
|
||||
- **THEN** 协议特有字段 SHALL NOT 出现在 CanonicalRequest 中
|
||||
|
||||
#### Scenario: System 消息独立存储
|
||||
|
||||
- **WHEN** 协议使用顶层 system 字段(如 Anthropic)
|
||||
- **THEN** SHALL 提取为 `canonical.system`
|
||||
- **WHEN** 协议使用 messages 数组中的 system 角色(如 OpenAI)
|
||||
- **THEN** SHALL 从 messages 中提取为 `canonical.system`
|
||||
|
||||
### Requirement: 定义 CanonicalMessage 和 ContentBlock
|
||||
|
||||
系统 SHALL 定义 `CanonicalMessage` 结构体和 `ContentBlock` 联合体。
|
||||
|
||||
CanonicalMessage SHALL 包含 `role`(枚举:system/user/assistant/tool)和 `content`(ContentBlock 数组)。
|
||||
|
||||
ContentBlock SHALL 支持以下类型:
|
||||
- `text`: 文本内容块
|
||||
- `tool_use`: 工具调用块(id, name, input)
|
||||
- `tool_result`: 工具结果块(tool_use_id, content, is_error)
|
||||
- `thinking`: 思考内容块(thinking)
|
||||
|
||||
#### Scenario: ContentBlock 类型化表示
|
||||
|
||||
- **WHEN** 编码 ContentBlock
|
||||
- **THEN** SHALL 使用 `type` 字段标识块类型
|
||||
- **THEN** 不同类型 SHALL 携带各自特有的字段
|
||||
|
||||
#### Scenario: Tool 输入保留原始 JSON
|
||||
|
||||
- **WHEN** 解码工具调用的 input 字段
|
||||
- **THEN** SHALL 使用 `json.RawMessage` 保留原始 JSON 数据
|
||||
- **THEN** SHALL NOT 强制解析为 map 或 struct
|
||||
|
||||
### Requirement: 定义 CanonicalTool 和 ToolChoice
|
||||
|
||||
系统 SHALL 定义 `CanonicalTool`(name, description, input_schema)和 `ToolChoice` 联合体(auto/none/any/tool+name)。
|
||||
|
||||
#### Scenario: ToolChoice 覆盖所有语义
|
||||
|
||||
- **WHEN** 协议使用 `"required"` 表示必须调用工具(如 OpenAI)
|
||||
- **THEN** SHALL 映射为 `{type: "any"}`
|
||||
- **WHEN** 协议使用 `{type: "tool", name}` 指定工具
|
||||
- **THEN** SHALL 保持 `{type: "tool", name}` 映射
|
||||
|
||||
### Requirement: 定义 CanonicalResponse 规范模型
|
||||
|
||||
系统 SHALL 定义 `CanonicalResponse` 结构体,包含 `id`、`model`、`content`(ContentBlock 数组)、`stop_reason`(可选枚举)、`usage`(CanonicalUsage)。
|
||||
|
||||
CanonicalUsage SHALL 包含 `input_tokens`、`output_tokens`、可选的 `cache_read_tokens`、`cache_creation_tokens`、`reasoning_tokens`。
|
||||
|
||||
stop_reason 枚举 SHALL 包含:`end_turn`、`max_tokens`、`tool_use`、`stop_sequence`、`content_filter`、`refusal`、`pause_turn`。
|
||||
|
||||
#### Scenario: 响应内容块与请求共用类型
|
||||
|
||||
- **WHEN** 编码响应中的 content
|
||||
- **THEN** SHALL 使用与请求相同的 ContentBlock 类型系统
|
||||
- **THEN** TextBlock 和 ToolUseBlock SHALL 在请求和响应中通用
|
||||
|
||||
### Requirement: 定义 CanonicalStreamEvent 联合体
|
||||
|
||||
系统 SHALL 定义类型化的 CanonicalStreamEvent 联合体,包含显式的 start/stop 生命周期。
|
||||
|
||||
事件类型 SHALL 包含:
|
||||
- `message_start`: 包含 message(id, model, usage)
|
||||
- `content_block_start`: 包含 index 和 content_block
|
||||
- `content_block_delta`: 包含 index 和 delta(text_delta/input_json_delta/thinking_delta)
|
||||
- `content_block_stop`: 包含 index
|
||||
- `message_delta`: 包含 delta(stop_reason)和 usage
|
||||
- `message_stop`: 无额外数据
|
||||
- `error`: 包含 error(type, message)
|
||||
- `ping`: 无额外数据
|
||||
|
||||
#### Scenario: 流式事件具有完整生命周期
|
||||
|
||||
- **WHEN** 解码协议的 SSE 流为 CanonicalStreamEvent
|
||||
- **THEN** SHALL 包含 message_start → content_block_start/delta/stop → message_delta → message_stop 的完整生命周期
|
||||
- **THEN** 每个事件 SHALL 包含足够的信息用于编码为任意目标协议的 SSE 格式
|
||||
|
||||
### Requirement: 定义 ProtocolAdapter 接口
|
||||
|
||||
系统 SHALL 定义 `ProtocolAdapter` 接口,每种协议实现完整适配。
|
||||
|
||||
ProtocolAdapter SHALL 包含以下方法:
|
||||
- `protocolName()`: 返回协议唯一标识
|
||||
- `protocolVersion()`: 返回协议版本
|
||||
- `supportsPassthrough()`: 返回是否支持同协议透传
|
||||
- `detectInterfaceType(nativePath)`: 根据协议的 URL 路径识别接口类型
|
||||
- `buildUrl(nativePath, interfaceType)`: 映射 URL 路径
|
||||
- `buildHeaders(provider)`: 构建认证和必要 Header
|
||||
- `supportsInterface(interfaceType)`: 判断是否支持某接口类型
|
||||
- `decodeRequest(raw)`: 协议格式 → CanonicalRequest
|
||||
- `encodeRequest(canonical, provider)`: CanonicalRequest → 协议格式
|
||||
- `decodeResponse(raw)`: 协议格式 → CanonicalResponse
|
||||
- `encodeResponse(canonical)`: CanonicalResponse → 协议格式
|
||||
- `createStreamDecoder()`: 创建 StreamDecoder 实例
|
||||
- `createStreamEncoder()`: 创建 StreamEncoder 实例
|
||||
- `encodeError(error)`: ConversionError → 协议错误格式
|
||||
- 扩展层编解码方法(Models/Embeddings/Rerank)
|
||||
|
||||
#### Scenario: Adapter 注册到 Registry
|
||||
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** SHALL 将所有 Adapter 注册到 AdapterRegistry
|
||||
- **THEN** 可通过 `registry.get(protocolName)` 获取 Adapter
|
||||
|
||||
#### Scenario: Adapter 自描述接口能力
|
||||
|
||||
- **WHEN** 调用 `supportsInterface(interfaceType)`
|
||||
- **THEN** SHALL 返回布尔值表示是否支持该接口类型
|
||||
- **THEN** 不支持的接口 SHALL 由引擎走透传逻辑
|
||||
|
||||
#### Scenario: Adapter 识别接口类型
|
||||
|
||||
- **WHEN** 调用 `detectInterfaceType(nativePath)`
|
||||
- **THEN** SHALL 根据协议的 URL 路径模式识别接口类型
|
||||
- **THEN** SHALL 返回对应的 InterfaceType(CHAT/MODELS/MODEL_INFO/EMBEDDINGS/RERANK)
|
||||
- **THEN** 无法识别的路径 SHALL 返回 PASSTHROUGH 类型
|
||||
|
||||
### Requirement: 实现 AdapterRegistry
|
||||
|
||||
系统 SHALL 实现 `AdapterRegistry`,支持 Adapter 的注册和查询。
|
||||
|
||||
#### Scenario: 注册 Adapter
|
||||
|
||||
- **WHEN** 调用 `registry.register(adapter)`
|
||||
- **THEN** SHALL 以 adapter 的 protocolName 为 key 存储
|
||||
- **THEN** 重复注册同名协议 SHALL 返回错误
|
||||
|
||||
#### Scenario: 查询 Adapter
|
||||
|
||||
- **WHEN** 调用 `registry.get(protocolName)`
|
||||
- **THEN** SHALL 返回已注册的 ProtocolAdapter
|
||||
- **THEN** 未注册的协议 SHALL 返回错误
|
||||
|
||||
### Requirement: 实现 ConversionEngine 门面
|
||||
|
||||
系统 SHALL 实现 `ConversionEngine` 作为无状态的转换引擎门面,线程安全、可复用。
|
||||
|
||||
ConversionEngine SHALL 提供:
|
||||
- `registerAdapter(adapter)`: 注册协议适配器
|
||||
- `use(middleware)`: 注册转换中间件
|
||||
- `isPassthrough(clientProtocol, providerProtocol)`: 判断是否同协议透传
|
||||
- `convertHttpRequest(request, clientProtocol, providerProtocol, provider)`: 请求转换
|
||||
- `convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType)`: 响应转换
|
||||
- `createStreamConverter(clientProtocol, providerProtocol, provider)`: 创建流式转换器
|
||||
|
||||
#### Scenario: 跨协议 Chat 请求转换
|
||||
|
||||
- **WHEN** clientProtocol != providerProtocol 且 interfaceType == CHAT
|
||||
- **THEN** SHALL 使用 clientAdapter.detectInterfaceType 识别接口类型
|
||||
- **THEN** SHALL 使用 clientAdapter.decodeRequest 解码为 CanonicalRequest
|
||||
- **THEN** SHALL 经 middlewareChain 处理
|
||||
- **THEN** SHALL 使用 providerAdapter.encodeRequest 编码为目标协议格式
|
||||
- **THEN** SHALL 使用 providerAdapter.buildUrl 映射目标 URL
|
||||
- **THEN** SHALL 使用 providerAdapter.buildHeaders 构建目标 Header
|
||||
- **THEN** SHALL 返回 HTTPRequestSpec{URL, Method, Headers, Body}
|
||||
|
||||
#### Scenario: 同协议透传
|
||||
|
||||
- **WHEN** clientProtocol == providerProtocol 且 supportsPassthrough == true
|
||||
- **THEN** SHALL 使用 providerAdapter.buildHeaders 重建 Header
|
||||
- **THEN** SHALL 原样传递请求 Body
|
||||
- **THEN** SHALL NOT 执行 decode/encode 转换
|
||||
|
||||
#### Scenario: 未知接口透传
|
||||
|
||||
- **WHEN** clientAdapter.detectInterfaceType 返回 PASSTHROUGH 类型
|
||||
- **THEN** SHALL 使用 providerAdapter.buildUrl 和 buildHeaders 适配 URL 和 Header
|
||||
- **THEN** SHALL 原样传递请求 Body
|
||||
|
||||
### Requirement: 定义 StreamDecoder/StreamEncoder/StreamConverter 接口
|
||||
|
||||
系统 SHALL 定义流式转换的三层接口。
|
||||
|
||||
StreamDecoder SHALL 接口:
|
||||
- `processChunk(rawChunk)`: 原始字节 → CanonicalStreamEvent 数组
|
||||
- `flush()`: 刷新缓冲区 → CanonicalStreamEvent 数组
|
||||
|
||||
StreamEncoder SHALL 接口:
|
||||
- `encodeEvent(event)`: CanonicalStreamEvent → SSE 字节数组
|
||||
- `flush()`: 刷新缓冲区 → SSE 字节数组
|
||||
|
||||
StreamConverter SHALL 接口:
|
||||
- `processChunk(rawChunk)`: 原始字节 → SSE 字节数组
|
||||
- `flush()`: 刷新 → SSE 字节数组
|
||||
|
||||
#### Scenario: PassthroughStreamConverter
|
||||
|
||||
- **WHEN** 同协议透传时
|
||||
- **THEN** SHALL 直接传递原始 SSE 字节,不做任何解析或转换
|
||||
|
||||
#### Scenario: CanonicalStreamConverter
|
||||
|
||||
- **WHEN** 跨协议转换时
|
||||
- **THEN** SHALL 使用 providerAdapter 的 StreamDecoder 解码原始 SSE
|
||||
- **THEN** SHALL 经 middlewareChain 处理事件
|
||||
- **THEN** SHALL 使用 clientAdapter 的 StreamEncoder 编码为目标 SSE 格式
|
||||
|
||||
### Requirement: 定义 ConversionMiddleware 接口
|
||||
|
||||
系统 SHALL 定义 `ConversionMiddleware` 接口,用于在 decode→encode 之间拦截转换。
|
||||
|
||||
- `intercept(canonical, clientProtocol, providerProtocol, context)`: 修改或拒绝 Canonical
|
||||
- `interceptStreamEvent(event, clientProtocol, providerProtocol, context)`: 修改或拒绝流式事件
|
||||
|
||||
#### Scenario: Middleware 链式执行
|
||||
|
||||
- **WHEN** 注册多个 Middleware
|
||||
- **THEN** SHALL 按注册顺序链式执行
|
||||
- **THEN** 任一 Middleware 返回错误 SHALL 中断后续执行
|
||||
|
||||
#### Scenario: Middleware 中断转换
|
||||
|
||||
- **WHEN** Middleware 返回 ConversionError
|
||||
- **THEN** SHALL 停止转换流程
|
||||
- **THEN** SHALL 使用 clientAdapter.encodeError 编码错误响应
|
||||
|
||||
### Requirement: 定义 ConversionError 错误体系
|
||||
|
||||
系统 SHALL 定义 `ConversionError` 和 `ErrorCode` 枚举。
|
||||
|
||||
ErrorCode SHALL 包含:INVALID_INPUT、MISSING_REQUIRED_FIELD、INCOMPATIBLE_FEATURE、FIELD_MAPPING_FAILURE、TOOL_CALL_PARSE_ERROR、JSON_PARSE_ERROR、STREAM_STATE_ERROR、UTF8_DECODE_ERROR、PROTOCOL_CONSTRAINT_VIOLATION、ENCODING_FAILURE、INTERFACE_NOT_SUPPORTED。
|
||||
|
||||
#### Scenario: 转换错误包含上下文
|
||||
|
||||
- **WHEN** 转换过程中发生错误
|
||||
- **THEN** ConversionError SHALL 包含 code、message、clientProtocol、providerProtocol、interfaceType 等上下文
|
||||
- **THEN** SHALL 支持包装原始错误(cause)
|
||||
|
||||
### Requirement: 定义 InterfaceType 枚举和接口分层
|
||||
|
||||
系统 SHALL 定义 `InterfaceType` 枚举(CHAT、MODELS、MODEL_INFO、EMBEDDINGS、RERANK)和接口分层策略。
|
||||
|
||||
- 核心层(CHAT):使用 Canonical Model 深度转换
|
||||
- 扩展层(MODELS、MODEL_INFO、EMBEDDINGS、RERANK):使用轻量 Canonical Models 做字段映射
|
||||
- 透传层(未知接口):URL+Header 适配后 Body 原样转发
|
||||
|
||||
#### Scenario: 扩展层接口转换
|
||||
|
||||
- **WHEN** interfaceType 为 MODELS/MODEL_INFO/EMBEDDINGS/RERANK
|
||||
- **THEN** SHALL 使用对应扩展层 Canonical Model 做轻量字段映射
|
||||
- **THEN** 双方都不支持时 SHALL 走透传逻辑
|
||||
|
||||
### Requirement: <20><>义 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 字段
|
||||
Reference in New Issue
Block a user