1
0
Files
nex/openspec/specs/conversion-engine/spec.md

462 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Conversion Engine
## Purpose
定义协议无关的 Canonical Model 和 ConversionEngine 转换引擎,作为所有协议间请求/响应转换的统一枢纽。
### 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: <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 字段
### Requirement: 跨协议响应转换支持 model 覆写
ConversionEngine SHALL 在跨协议响应转换时支持 model 字段覆写。
#### Scenario: ConvertHttpResponse 接收 modelOverride 参数
- **WHEN** 调用 `ConvertHttpResponse` 时传入 `modelOverride` 参数(跨协议场景,非空字符串)
- **THEN** SHALL 在解码上游响应到 canonical 后,将 `Model` 字段设为 `modelOverride`
- **THEN** SHALL 使用覆写后的 canonical 编码为客户端协议格式
#### Scenario: modelOverride 为空
- **WHEN** 调用 `ConvertHttpResponse``modelOverride` 为空字符串
- **THEN** SHALL NOT 覆写 canonical 的 Model 字段,保持上游原始值
#### Scenario: Chat 响应 model 覆写
- **WHEN** 跨协议转换 Chat 类型响应且 `modelOverride` 非空
- **THEN** `CanonicalResponse.Model` SHALL 被设为 `modelOverride`
#### Scenario: Embedding 响应 model 覆写
- **WHEN** 跨协议转换 Embedding 类型响应且 `modelOverride` 非空
- **THEN** `CanonicalEmbeddingResponse.Model` SHALL 被设为 `modelOverride`
#### Scenario: Rerank 响应 model 覆写
- **WHEN** 跨协议转换 Rerank 类型响应且 `modelOverride` 非空
- **THEN** `CanonicalRerankResponse.Model` SHALL 被设为 `modelOverride`
### Requirement: 跨协议流式转换支持 model 覆写
ConversionEngine SHALL 在跨协议流式转换时支持 model 字段覆写。
#### Scenario: CreateStreamConverter 接收 modelOverride 参数
- **WHEN** 调用 `CreateStreamConverter` 时传入 `modelOverride` 参数(跨协议场景)
- **THEN** SHALL 在流式 canonical 事件中将 `Model` 字段设为 `modelOverride`
### Requirement: TargetProvider 字段语义
TargetProvider 的 ModelName 字段 SHALL 存储上游供应商的模型名称(即 `model_name` 字段值),语义保持不变。
#### Scenario: encoder 使用 TargetProvider.ModelName
- **WHEN** 协议适配器编码请求时
- **THEN** SHALL 使用 `TargetProvider.ModelName` 作为发给上游的 `model` 字段值(值为路由结果中的 model_name
### Requirement: 同协议请求 URL 使用 Adapter 映射
ConversionEngine SHALL 在同协议透传和 Smart Passthrough 场景下使用 providerAdapter.BuildUrl 构建上游 URL 路径。
#### Scenario: 同协议 Chat URL 映射
- **WHEN** clientProtocol == providerProtocol 且 interfaceType 为 CHAT
- **THEN** ConversionEngine SHALL 调用 providerAdapter.BuildUrl(nativePath, interfaceType)
- **THEN** 上游 URL SHALL 为 provider.BaseURL 与 BuildUrl 返回路径的组合
- **THEN** ConversionEngine SHALL NOT 直接将 provider.BaseURL 与 nativePath 拼接为上游 URL
#### Scenario: 未知接口 URL 映射
- **WHEN** interfaceType 为 PASSTHROUGH
- **THEN** providerAdapter.BuildUrl SHALL 返回适合目标协议的路径或原 nativePath
- **THEN** ConversionEngine SHALL 使用该路径构建上游 URL
### Requirement: 上游 URL 构建使用目标协议 Adapter
ConversionEngine SHALL 始终使用目标供应商协议的 adapter 构建上游 URL 路径,避免客户端协议 nativePath 中的版本段泄露到目标协议上游 URL。
#### Scenario: OpenAI 客户端到 OpenAI 供应商
- **WHEN** clientProtocol 为 `openai`providerProtocol 为 `openai`nativePath 为 `/v1/chat/completions`
- **THEN** ConversionEngine SHALL 调用 OpenAI adapter 的 `BuildUrl(nativePath, InterfaceTypeChat)`
- **THEN** 上游 path SHALL 为 `/chat/completions`
- **THEN** 当 provider.base_url 为 `https://api.openai.com/v1` 时,最终上游 URL SHALL 为 `https://api.openai.com/v1/chat/completions`
#### Scenario: OpenAI 客户端到 Anthropic 供应商
- **WHEN** clientProtocol 为 `openai`providerProtocol 为 `anthropic`nativePath 为 `/v1/chat/completions`
- **THEN** ConversionEngine SHALL 使用 OpenAI adapter 识别接口类型为 `InterfaceTypeChat`
- **THEN** ConversionEngine SHALL 调用 Anthropic adapter 的 `BuildUrl(nativePath, InterfaceTypeChat)`
- **THEN** 上游 path SHALL 为 `/v1/messages`
- **THEN** OpenAI nativePath 中的 `/v1/chat/completions` SHALL NOT 被直接拼接到 Anthropic 上游 URL
#### Scenario: Anthropic 客户端到 OpenAI 供应商
- **WHEN** clientProtocol 为 `anthropic`providerProtocol 为 `openai`nativePath 为 `/v1/messages`
- **THEN** ConversionEngine SHALL 使用 Anthropic adapter 识别接口类型为 `InterfaceTypeChat`
- **THEN** ConversionEngine SHALL 调用 OpenAI adapter 的 `BuildUrl(nativePath, InterfaceTypeChat)`
- **THEN** 上游 path SHALL 为 `/chat/completions`
- **THEN** 当 provider.base_url 为 `https://api.openai.com/v1` 时,最终上游 URL SHALL 为 `https://api.openai.com/v1/chat/completions`
### Requirement: SSE Frame 级 Smart Passthrough
系统 SHALL 支持同协议流式 Smart Passthrough 对 SSE frame 中的 model 字段进行最小化改写。
#### Scenario: 改写 SSE data JSON
- **WHEN** 同协议流式响应需要将上游模型名改写为统一模型 ID
- **THEN** 系统 SHALL 按 SSE frame 解析上游字节流
- **THEN** 对包含 JSON payload 的 `data` 行 SHALL 调用 adapter.RewriteResponseModelName 改写 model 字段
- **THEN** SHALL 重建合法 SSE frame 输出
#### Scenario: 保留 DONE 事件
- **WHEN** SSE frame 的 data payload 为 `[DONE]`
- **THEN** 系统 SHALL 原样输出 `[DONE]`
- **THEN** SHALL NOT 尝试按 JSON 解析
#### Scenario: 改写失败宽容降级
- **WHEN** SSE frame 解析或 model 改写失败
- **THEN** 系统 SHALL 记录 warn 日志
- **THEN** SHALL 输出原始 SSE frame
- **THEN** SHALL 继续处理后续 frame
### Requirement: Adapter 模型提取边界
ProtocolAdapter SHALL 只对明确适配的接口提供 model 提取和 model 改写能力。
#### Scenario: 已适配接口提取 model
- **WHEN** ifaceType 为 adapter 明确支持提取 model 的接口
- **THEN** ExtractModelName SHALL 按该协议和接口的请求格式提取 model 字段
#### Scenario: 未适配接口不提取 model
- **WHEN** ifaceType 为 PASSTHROUGH 或 adapter 未明确支持提取 model 的接口
- **THEN** ExtractModelName SHALL 返回错误或空结果
- **THEN** 调用方 SHALL 按无 model 请求处理
### Requirement: 跨协议多模态暂不支持
ConversionEngine SHALL 在跨协议完整转换中对当前暂不支持的多模态内容返回明确错误。
#### Scenario: 跨协议请求包含多模态内容块
- **WHEN** clientProtocol != providerProtocol 且 CanonicalRequest 中包含 image、audio、video 或 file 内容块
- **THEN** ConversionEngine SHALL 中断转换
- **THEN** SHALL 返回网关层 `UNSUPPORTED_MULTIMODAL` 错误
- **THEN** SHALL NOT 静默丢弃多模态内容
#### Scenario: 同协议多模态请求
- **WHEN** clientProtocol == providerProtocol 且请求通过 Smart Passthrough 或 raw passthrough 处理
- **THEN** 系统 SHALL 保留原始请求体中未改写字段
- **THEN** SHALL NOT 因多模态字段存在而执行跨协议多模态校验
### Requirement: 上游非 2xx 响应不进入转换
ConversionEngine SHALL 只转换调用方传入的成功响应ProxyHandler SHALL 在调用转换前过滤上游非 2xx 响应。
#### Scenario: 非 2xx 响应绕过响应转换
- **WHEN** 上游响应状态码不是 2xx
- **THEN** ProxyHandler SHALL NOT 调用 ConversionEngine.ConvertHttpResponse
- **THEN** ProxyHandler SHALL 直接透传该响应
#### Scenario: 流式非 2xx 响应绕过流式转换
- **WHEN** 流式请求收到上游非 2xx 响应
- **THEN** ProxyHandler SHALL NOT 调用 ConversionEngine.CreateStreamConverter
- **THEN** ProxyHandler SHALL 直接透传该响应
### Requirement: 协议路径来源
ProtocolAdapter SHALL 以对应协议的本地 API reference 文档作为 URL 识别和 URL 映射的事实来源。
#### Scenario: OpenAI 路径来源
- **WHEN** 实现或测试 OpenAI adapter 的 DetectInterfaceType 或 BuildUrl
- **THEN** SHALL 参考 `docs/api_reference/openai` 中的接口路径
- **THEN** SHALL 忽略 `docs/api_reference/openai/responses` 目录
- **THEN** SHALL NOT 因其他协议包含 `/v1` 而给 OpenAI nativePath 添加 `/v1`
#### Scenario: Anthropic 路径来源
- **WHEN** 实现或测试 Anthropic adapter 的 DetectInterfaceType 或 BuildUrl
- **THEN** SHALL 参考 `docs/api_reference/anthropic` 中的接口路径
- **THEN** SHALL 保留文档中接口路径自带的 `/v1` 前缀
- **THEN** SHALL NOT 因 OpenAI nativePath 不含 `/v1` 而移除 Anthropic nativePath 中的 `/v1`