1
0

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 规范文件
This commit is contained in:
2026-04-21 18:14:10 +08:00
parent 7f0f831226
commit 395887667d
73 changed files with 3360 additions and 1374 deletions

View File

@@ -269,4 +269,91 @@ Encoder SHALL 维护状态:
#### Scenario: /rerank 接口
- **WHEN** 解码/编码 rerank 请求和响应
- **THEN** SHALL 使用 CanonicalRerankRequest/Response 做字段映射
- **THEN** SHALL 使用 CanonicalRerankRequest/Response 做字段映射
### Requirement: 模型详情路径识别
OpenAI 适配器 SHALL 正确识别包含统一模型 ID 的模型详情路径。
#### Scenario: 含斜杠的统一模型 ID 路径
- **WHEN** 路径为 `/v1/models/openai/gpt-4`
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`
#### Scenario: 含多段斜杠的统一模型 ID 路径
- **WHEN** 路径为 `/v1/models/azure/accounts/org-123/models/gpt-4`
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`
#### Scenario: 模型列表路径不受影响
- **WHEN** 路径为 `/v1/models`
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModels`
### Requirement: 提取统一模型 ID
OpenAI 适配器 SHALL 从路径中提取统一模型 ID。
#### Scenario: 标准路径提取
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/openai/gpt-4")`
- **THEN** SHALL 返回 `"openai/gpt-4"`
#### Scenario: 复杂路径提取
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/azure/accounts/org/models/gpt-4")`
- **THEN** SHALL 返回 `"azure/accounts/org/models/gpt-4"`
#### Scenario: 非模型详情路径
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models")`
- **THEN** SHALL 返回错误
### Requirement: 从请求体提取 model
OpenAI 适配器 SHALL 按 InterfaceType 从请求体中提取 model 值。
#### Scenario: Chat 请求提取 model
- **WHEN** 调用 `ExtractModelName(body, InterfaceTypeChat)`body 为 `{"model":"openai/gpt-4","messages":[...]}`
- **THEN** SHALL 返回 `"openai/gpt-4"`
#### Scenario: Embedding 请求提取 model
- **WHEN** 调用 `ExtractModelName(body, InterfaceTypeEmbeddings)`body 为 `{"model":"openai/text-embedding-3","input":"text"}`
- **THEN** SHALL 返回 `"openai/text-embedding-3"`
#### Scenario: 无 model 字段
- **WHEN** 调用 `ExtractModelName(body, ifaceType)`body 中不含 model 字段
- **THEN** SHALL 返回错误
### Requirement: 最小化改写请求体 model 字段
OpenAI 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写请求体中的 model 字段,其余字段保持原始 bytes。
#### Scenario: 请求体 model 改写(统一 ID → 上游名)
- **WHEN** 调用 `RewriteRequestModelName(body, "gpt-4", InterfaceTypeChat)`body 为 `{"model":"openai/gpt-4","messages":[...],"some_param":"value"}`
- **THEN** SHALL 返回 `{"model":"gpt-4","messages":[...],"some_param":"value"}`
- **THEN** 除 model 外的字段 SHALL 保持原始 bytes 不变
#### Scenario: 不同 InterfaceType 的请求改写
- **WHEN** 调用 `RewriteRequestModelName(body, "gpt-4", InterfaceTypeEmbeddings)`
- **THEN** SHALL 按 Embedding 接口的请求体 model 字段位置进行改写
- **WHEN** 调用 `RewriteRequestModelName(body, "gpt-4", InterfaceTypeRerank)`
- **THEN** SHALL 按 Rerank 接口的请求体 model 字段位置进行改写
### Requirement: 最小化改写响应体 model 字段
OpenAI 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写响应体中的 model 字段,其余字段保持原始 bytes。请求体和响应体的 model 字段位置可能不同,各自独立实现。
#### Scenario: 响应体 model 改写(上游名 → 统一 ID
- **WHEN** 调用 `RewriteResponseModelName(body, "openai/gpt-4", InterfaceTypeChat)`body 为上游 Chat 响应
- **THEN** SHALL 将 model 字段替换为 `"openai/gpt-4"`,其余字段原样保留
#### Scenario: 不同 InterfaceType 的响应改写
- **WHEN** 调用 `RewriteResponseModelName(body, "openai/gpt-4", InterfaceTypeEmbeddings)`
- **THEN** SHALL 按 Embedding 接口的响应体 model 字段位置进行改写