1
0
Files
nex/openspec/specs/conversion-engine/spec.md
lanyuanxiaoyao 395887667d feat: 实现统一模型 ID 机制
实现统一模型 ID 格式 (provider_id/model_name),支持跨协议模型标识和 Smart Passthrough。

核心变更:
- 新增 pkg/modelid 包:解析、格式化、校验统一模型 ID
- 数据库迁移:models 表使用 UUID 主键 + UNIQUE(provider_id, model_name) 约束
- Repository 层:FindByProviderAndModelName、ListEnabled 方法
- Service 层:联合唯一校验、provider ID 字符集校验
- Conversion 层:ExtractModelName、RewriteRequestModelName/RewriteResponseModelName 方法
- Handler 层:统一模型 ID 路由、Smart Passthrough、Models API 本地聚合
- 新增 error-responses、unified-model-id 规范

测试覆盖:
- 单元测试:modelid、conversion、handler、service、repository
- 集成测试:统一模型 ID 路由、Smart Passthrough 保真性、跨协议转换
- 迁移测试:UUID 主键、UNIQUE 约束、级联删除

OpenSpec:
- 归档 unified-model-id 变更到 archive/2026-04-21-unified-model-id
- 同步 11 个 delta specs 到 main specs
- 新增 error-responses、unified-model-id 规范文件
2026-04-21 18:14:10 +08:00

328 lines
14 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