实现统一模型 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 规范文件
159 lines
5.7 KiB
Markdown
159 lines
5.7 KiB
Markdown
# Provider Management
|
||
|
||
## Purpose
|
||
|
||
管理 AI 供应商的完整生命周期,包括创建、查询、更新、删除供应商配置,通过分层架构(Handler -> Service -> Repository)处理业务逻辑和数据访问。
|
||
|
||
## Requirements
|
||
|
||
### Requirement: 创建供应商配置
|
||
|
||
网关 SHALL 允许通过管理 API 创建新的供应商配置。
|
||
|
||
#### Scenario: 使用有效数据创建供应商
|
||
|
||
- **WHEN** 向 `/api/providers` 发送 POST 请求,携带有效的供应商数据(id, name, api_key, base_url, protocol)
|
||
- **THEN** 网关 SHALL 在数据库中创建新的供应商记录
|
||
- **THEN** 网关 SHALL 返回创建的供应商,状态码为 201
|
||
- **THEN** 供应商 SHALL 默认启用
|
||
- **THEN** protocol 字段 SHALL 默认为 "openai"
|
||
|
||
#### Scenario: 使用重复 ID 创建供应商
|
||
|
||
- **WHEN** 向 `/api/providers` 发送 POST 请求,携带已存在的 ID
|
||
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
|
||
|
||
#### Scenario: 创建供应商时缺少必需字段
|
||
|
||
- **WHEN** 向 `/api/providers` 发送 POST 请求,缺少必需字段(id, name, api_key 或 base_url)
|
||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||
- **THEN** 错误 SHALL 指示缺少哪些字段
|
||
|
||
#### Scenario: 创建供应商时 ID 包含非法字符
|
||
|
||
- **WHEN** 向 `/api/providers` 发送 POST 请求,id 包含非 `[a-zA-Z0-9_]` 字符
|
||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||
- **THEN** 错误 SHALL 指示 id 仅允许字母、数字、下划线
|
||
|
||
#### Scenario: 创建供应商时 ID 过长
|
||
|
||
- **WHEN** 向 `/api/providers` 发送 POST 请求,id 长度超过 64
|
||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||
|
||
### Requirement: 供应商 ID 不允许修改
|
||
|
||
供应商 ID 是主键,用于构建统一模型 ID,不允许修改。
|
||
|
||
#### Scenario: 尝试修改供应商 ID
|
||
|
||
- **WHEN** 向 `/api/providers/:id` 发送 PUT 请求,请求体中包含 `id` 字段
|
||
- **THEN** ProviderService.Update SHALL 在 service 层校验并返回错误
|
||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||
- **THEN** 错误 SHALL 指示供应商 ID 不允许修改
|
||
- **THEN** 错误格式 SHALL 为:
|
||
```json
|
||
{
|
||
"error": "供应商 ID 不允许修改",
|
||
"code": "IMMUTABLE_FIELD"
|
||
}
|
||
```
|
||
|
||
**校验位置:** Service 层(`ProviderService.Update`)
|
||
- Service 层校验保证所有调用方(handler、CLI、未来 API)统一遵守
|
||
- Handler 层负责捕获 `ErrImmutableField` 并转换为 HTTP 400 响应
|
||
|
||
**原因:**
|
||
- 供应商 ID 是主键,用于构建统一模型 ID(provider_id/model_name)
|
||
- 修改 ID 会导致所有统一模型 ID 失效
|
||
- 客户端缓存的模型 ID 全部失效
|
||
- 如需修改,应创建新供应商并迁移模型
|
||
|
||
### Requirement: 列出所有供应商
|
||
|
||
网关 SHALL 允许获取所有供应商配置。
|
||
|
||
#### Scenario: 成功列出供应商
|
||
|
||
- **WHEN** 向 `/api/providers` 发送 GET 请求
|
||
- **THEN** 网关 SHALL 返回所有供应商的列表
|
||
- **THEN** 每个供应商 SHALL 包含 id, name, api_key(已掩码), base_url, protocol, enabled, created_at, updated_at
|
||
- **THEN** api_key SHALL 被掩码(仅显示最后 4 个字符)
|
||
|
||
**变更说明:** 数据访问从 config 包迁移到 ProviderRepository。API 接口保持不变。
|
||
|
||
### Requirement: 获取特定供应商
|
||
|
||
网关 SHALL 允许通过 ID 获取特定供应商。
|
||
|
||
#### Scenario: 获取存在的供应商
|
||
|
||
- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带有效的供应商 ID
|
||
- **THEN** 网关 SHALL 返回供应商详情
|
||
- **THEN** SHALL 包含 protocol 字段
|
||
- **THEN** api_key SHALL 被掩码
|
||
|
||
#### Scenario: 获取不存在的供应商
|
||
|
||
- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带不存在的 ID
|
||
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
|
||
|
||
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。
|
||
|
||
### Requirement: 更新供应商配置
|
||
|
||
网关 SHALL 允许更新现有供应商配置。
|
||
|
||
#### Scenario: 使用有效数据更新供应商
|
||
|
||
- **WHEN** 向 `/api/providers/:id` 发送 PUT 请求,携带有效的供应商数据
|
||
- **THEN** 网关 SHALL 更新数据库中的供应商记录
|
||
- **THEN** 网关 SHALL 返回更新后的供应商
|
||
- **THEN** 更新 SHALL 支持修改 protocol 字段
|
||
|
||
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。
|
||
|
||
### Requirement: 删除供应商配置
|
||
|
||
网关 SHALL 允许删除供应商配置。
|
||
|
||
#### Scenario: 删除存在的供应商
|
||
|
||
- **WHEN** 向 `/api/providers/:id` 发送 DELETE 请求,携带有效的供应商 ID
|
||
- **THEN** 网关 SHALL 删除供应商记录
|
||
- **THEN** 网关 SHALL 删除所有关联的模型(CASCADE)
|
||
- **THEN** 网关 SHALL 返回状态码 204 (No Content)
|
||
|
||
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。
|
||
|
||
### Requirement: 使用 service 层处理业务逻辑
|
||
|
||
Handler SHALL 通过 ProviderService 处理业务逻辑。
|
||
|
||
#### Scenario: 调用 service 方法
|
||
|
||
- **WHEN** handler 收到请求
|
||
- **THEN** SHALL 调用对应的 ProviderService 方法(Create、Get、List、Update、Delete)
|
||
- **THEN** SHALL 使用 domain.Provider 类型
|
||
|
||
#### Scenario: 错误处理
|
||
|
||
- **WHEN** service 返回错误
|
||
- **THEN** SHALL 转换为 HTTP 错误响应
|
||
- **THEN** SHALL 使用结构化错误处理
|
||
|
||
### Requirement: 使用 repository 层访问数据
|
||
|
||
Service SHALL 通过 ProviderRepository 访问数据。
|
||
|
||
#### Scenario: 调用 repository 方法
|
||
|
||
- **WHEN** service 处理业务逻辑
|
||
- **THEN** SHALL 调用对应的 ProviderRepository 方法
|
||
- **THEN** SHALL 使用 domain.Provider 类型
|
||
|
||
#### Scenario: 数据验证
|
||
|
||
- **WHEN** 创建或更新供应商
|
||
- **THEN** SHALL 在 service 层验证业务规则
|
||
- **THEN** SHALL 在 repository 层执行数据库操作
|