1
0
Files
nex/openspec/specs/model-management/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

144 lines
4.9 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.
# Model Management
## Purpose
管理模型的增删改查,通过 handler → service → repository 分层实现业务逻辑和数据访问,支持供应商关联验证。
### Requirement: 创建模型配置
网关 SHALL 允许为供应商创建新的模型配置。
#### Scenario: 使用有效数据创建模型
- **WHEN** 向 `/api/models` 发送 POST 请求携带有效的模型数据provider_id, model_name不提供 id 字段
- **THEN** 网关 SHALL 自动生成 UUID 作为模型 id
- **THEN** 网关 SHALL 在数据库中创建新的模型记录
- **THEN** 网关 SHALL 返回创建的模型,状态码为 201
- **THEN** 模型 SHALL 默认启用
- **THEN** 返回的模型 SHALL 包含 `unified_id` 字段,值为 `{provider_id}/{model_name}`
#### Scenario: 使用不存在的供应商创建模型
- **WHEN** 向 `/api/models` 发送 POST 请求,携带不存在的 provider_id
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
- **THEN** 错误 SHALL 指示供应商不存在
#### Scenario: 创建重复模型
- **WHEN** 向 `/api/models` 发送 POST 请求,携带已存在的 provider_id + model_name 组合
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
- **THEN** 错误 SHALL 指示同一供应商下模型名称已存在
### Requirement: 列出所有模型
网关 SHALL 允许获取所有模型配置。
#### Scenario: 成功列出模型
- **WHEN** 向 `/api/models` 发送 GET 请求
- **THEN** 网关 SHALL 返回所有模型的列表
- **THEN** 每个模型 SHALL 包含 id, provider_id, model_name, unified_id, enabled, created_at
### Requirement: 按供应商列出模型
网关 SHALL 允许获取特定供应商的模型。
#### Scenario: 列出存在供应商的模型
- **WHEN** 向 `/api/models?provider_id=<provider_id>` 发送 GET 请求
- **THEN** 网关 SHALL 返回指定供应商的模型列表
- **THEN** 每个模型 SHALL 包含 unified_id 字段
### Requirement: 更新模型配置
网关 SHALL 允许更新现有模型配置。
#### Scenario: 使用有效数据更新模型
- **WHEN** 向 `/api/models/:id` 发送 PUT 请求,携带有效的模型数据
- **THEN** 网关 SHALL 更新数据库中的模型记录
- **THEN** 网关 SHALL 返回更新后的模型
- **THEN** 返回的模型 SHALL 包含更新后的 unified_id
#### Scenario: 更新模型为重复组合
- **WHEN** 向 `/api/models/:id` 发送 PUT 请求,更新 provider_id 或 model_name 导致与已有记录重复
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
### Requirement: 删除模型配置
网关 SHALL 允许删除模型配置。
#### Scenario: 删除存在的模型
- **WHEN** 向 `/api/models/:id` 发送 DELETE 请求,携带有效的模型 ID
- **THEN** 网关 SHALL 删除模型记录
- **THEN** 网关 SHALL 返回状态码 204 (No Content)
### Requirement: 使用 service 层处理业务逻辑
Handler SHALL 通过 ModelService 处理业务逻辑。
#### Scenario: 调用 service 方法
- **WHEN** handler 收到请求
- **THEN** SHALL 调用对应的 ModelService 方法Create、Get、List、Update、Delete
- **THEN** SHALL 使用 domain.Model 类型
- **THEN** Create 时 SHALL 调用 `uuid.New()` 生成 id
#### Scenario: 供应商验证和唯一性校验
- **WHEN** 创建或更新模型
- **THEN** SHALL 在 service 层验证供应商存在
- **THEN** SHALL 在 service 层验证 provider_id + model_name 联合唯一
### Requirement: 联合唯一约束并发处理
创建或更新模型时SHALL 使用应用层校验 + 数据库约束双重保险处理联合唯一约束。
#### Scenario: 应用层快速失败
- **WHEN** 创建或更新模型前
- **THEN** SHALL 先检查 provider_id + model_name 是否已存在
- **THEN** 如已存在SHALL 返回 HTTP 409 Conflict
- **THEN** SHALL 返回错误格式:
```json
{
"error": "同一供应商下模型名称已存在",
"code": "duplicate_model"
}
```
#### Scenario: 数据库约束兜底
- **WHEN** 并发创建导致应用层校验通过但数据库写入失败
- **THEN** SHALL 捕获数据库 UNIQUE 约束错误
- **THEN** SHALL 转换为 HTTP 409 Conflict 错误返回
- **THEN** SHALL 返回错误格式:
```json
{
"error": "同一供应商下模型名称已存在",
"code": "duplicate_model"
}
```
#### Scenario: SQLite UNIQUE 约束错误检测
- **WHEN** 捕获数据库错误
- **THEN** SHALL 检查错误信息是否包含 "UNIQUE constraint failed"
- **THEN** 如匹配SHALL 识别为联合唯一约束冲突
### Requirement: 使用 repository 层访问数据
Service SHALL 通过 ModelRepository 访问数据。
#### Scenario: 联合查询
- **WHEN** service 需要按 provider 和 model_name 查询模型
- **THEN** SHALL 调用 `FindByProviderAndModelName(providerID, modelName)` 方法
#### Scenario: 查询所有启用模型
- **WHEN** proxy handler 需要聚合模型列表
- **THEN** SHALL 调用 `ListEnabled()` 方法,返回所有 enabled 的模型(关联 enabled 的供应商)