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:
@@ -9,11 +9,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"nex/backend/internal/config"
|
||||
"nex/backend/internal/domain"
|
||||
"nex/backend/internal/handler"
|
||||
"nex/backend/internal/handler/middleware"
|
||||
@@ -27,23 +24,13 @@ func init() {
|
||||
|
||||
func setupIntegrationTest(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open(dir+"/test.db"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
err = db.AutoMigrate(&config.Provider{}, &config.Model{}, &config.UsageStats{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
sqlDB, _ := db.DB()
|
||||
if sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
})
|
||||
db := setupTestDB(t)
|
||||
|
||||
providerRepo := repository.NewProviderRepository(db)
|
||||
modelRepo := repository.NewModelRepository(db)
|
||||
statsRepo := repository.NewStatsRepository(db)
|
||||
|
||||
providerService := service.NewProviderService(providerRepo)
|
||||
providerService := service.NewProviderService(providerRepo, modelRepo)
|
||||
modelService := service.NewModelService(modelRepo, providerRepo)
|
||||
_ = service.NewRoutingService(modelRepo, providerRepo)
|
||||
statsService := service.NewStatsService(statsRepo)
|
||||
@@ -97,13 +84,16 @@ func TestOpenAI_CompleteFlow(t *testing.T) {
|
||||
|
||||
// 2. 创建 Model
|
||||
modelBody, _ := json.Marshal(map[string]string{
|
||||
"id": "gpt4", "provider_id": "openai", "model_name": "gpt-4",
|
||||
"provider_id": "openai", "model_name": "gpt-4",
|
||||
})
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/models", bytes.NewReader(modelBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 201, w.Code)
|
||||
var createdModel domain.Model
|
||||
json.Unmarshal(w.Body.Bytes(), &createdModel)
|
||||
assert.NotEmpty(t, createdModel.ID)
|
||||
|
||||
// 3. 列出 Provider
|
||||
w = httptest.NewRecorder()
|
||||
@@ -135,7 +125,7 @@ func TestOpenAI_CompleteFlow(t *testing.T) {
|
||||
|
||||
// 6. 删除 Model
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("DELETE", "/api/models/gpt4", nil)
|
||||
req = httptest.NewRequest("DELETE", "/api/models/"+createdModel.ID, nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 204, w.Code)
|
||||
|
||||
@@ -160,17 +150,19 @@ func TestAnthropic_ModelCreation(t *testing.T) {
|
||||
assert.Equal(t, 201, w.Code)
|
||||
|
||||
modelBody, _ := json.Marshal(map[string]string{
|
||||
"id": "claude3", "provider_id": "anthropic", "model_name": "claude-3-opus-20240229",
|
||||
"provider_id": "anthropic", "model_name": "claude-3-opus-20240229",
|
||||
})
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/models", bytes.NewReader(modelBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 201, w.Code)
|
||||
var createdModel domain.Model
|
||||
json.Unmarshal(w.Body.Bytes(), &createdModel)
|
||||
|
||||
// 验证创建成功
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("GET", "/api/models/claude3", nil)
|
||||
req = httptest.NewRequest("GET", "/api/models/"+createdModel.ID, nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
}
|
||||
@@ -188,7 +180,7 @@ func TestStats_RecordingAndQuery(t *testing.T) {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
modelBody, _ := json.Marshal(map[string]string{
|
||||
"id": "m1", "provider_id": "p1", "model_name": "gpt-4",
|
||||
"provider_id": "p1", "model_name": "gpt-4",
|
||||
})
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/models", bytes.NewReader(modelBody))
|
||||
|
||||
Reference in New Issue
Block a user