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

@@ -0,0 +1,79 @@
package tests
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"nex/backend/internal/config"
)
func TestMigration_ModelsUUIDPrimaryKey(t *testing.T) {
db := SetupTestDB(t)
defer CleanupTestDB(t, db)
// 创建供应商
_ = CreateTestProvider(t, db, "openai")
// 创建模型使用 UUID 作为 id
model, err := CreateTestModel(t, db, "550e8400-e29b-41d4-a716-446655440000", "openai", "gpt-4")
require.NoError(t, err)
// 通过 UUID 查询
var result config.Model
require.NoError(t, db.First(&result, "id = ?", model.ID).Error)
assert.Equal(t, "gpt-4", result.ModelName)
}
func TestMigration_UniqueProviderModelConstraint(t *testing.T) {
db := SetupTestDB(t)
defer CleanupTestDB(t, db)
// 创建供应商
_ = CreateTestProvider(t, db, "openai")
// 创建第一个模型
_, err := CreateTestModel(t, db, "uuid-1", "openai", "gpt-4")
require.NoError(t, err)
// 尝试创建相同 (provider_id, model_name) 的模型应失败
_, err = CreateTestModel(t, db, "uuid-2", "openai", "gpt-4")
assert.Error(t, err, "UNIQUE(provider_id, model_name) 约束应阻止重复")
// 不同 provider_id 下相同 model_name 应成功
_ = CreateTestProvider(t, db, "anthropic")
_, err = CreateTestModel(t, db, "uuid-3", "anthropic", "gpt-4")
require.NoError(t, err, "不同 provider_id 下相同 model_name 应允许")
}
func TestMigration_CascadeDelete(t *testing.T) {
db := SetupTestDB(t)
defer CleanupTestDB(t, db)
// 创建供应商和模型
provider := CreateTestProvider(t, db, "openai")
_, err := CreateTestModel(t, db, "uuid-1", "openai", "gpt-4")
require.NoError(t, err)
// 删除供应商应级联删除模型
require.NoError(t, db.Delete(&provider).Error)
var count int64
db.Model(&config.Model{}).Where("provider_id = ?", "openai").Count(&count)
assert.Equal(t, int64(0), count, "删除供应商后其模型应被级联删除")
}
func TestMigration_ModelDefaultEnabled(t *testing.T) {
db := SetupTestDB(t)
defer CleanupTestDB(t, db)
_ = CreateTestProvider(t, db, "openai")
// 创建模型不指定 enabled应默认为 true
_, err := CreateTestModel(t, db, "uuid-1", "openai", "gpt-4")
require.NoError(t, err)
var result config.Model
require.NoError(t, db.First(&result, "id = ?", "uuid-1").Error)
assert.True(t, result.Enabled, "enabled 字段默认应为 true")
}