package controller import ( "bytes" "errors" "fmt" "net/http" "net/http/httptest" "os" "strconv" "strings" "sync" "sync/atomic" "testing" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/pkg/billingexpr" "github.com/QuantumNous/new-api/pkg/jsplugin" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/billing_setting" "github.com/QuantumNous/new-api/setting/config" "github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func modelManagementDB(t *testing.T, kind, dsn string) *gorm.DB { t.Helper() database, isolatedDSN := newAuditTestDatabase(t, kind, dsn) previousDB, previousLogDB := model.DB, model.LOG_DB previousMain, previousLog := common.MainDatabaseType(), common.LogDatabaseType() previousMaster, previousSQLite := common.IsMasterNode, common.SQLitePath previousRedis, previousMemory := common.RedisEnabled, common.MemoryCacheEnabled previousOptions := common.OptionMap previousConfig := config.GlobalConfig.ExportAllConfigs() restoreRatios := []struct { value string restore func(string) error }{ {ratio_setting.ModelPrice2JSONString(), ratio_setting.UpdateModelPriceByJSONString}, {ratio_setting.ModelRatio2JSONString(), ratio_setting.UpdateModelRatioByJSONString}, {ratio_setting.CompletionRatio2JSONString(), ratio_setting.UpdateCompletionRatioByJSONString}, {ratio_setting.CacheRatio2JSONString(), ratio_setting.UpdateCacheRatioByJSONString}, {ratio_setting.CreateCacheRatio2JSONString(), ratio_setting.UpdateCreateCacheRatioByJSONString}, {ratio_setting.ImageRatio2JSONString(), ratio_setting.UpdateImageRatioByJSONString}, {ratio_setting.AudioRatio2JSONString(), ratio_setting.UpdateAudioRatioByJSONString}, {ratio_setting.AudioCompletionRatio2JSONString(), ratio_setting.UpdateAudioCompletionRatioByJSONString}, } common.IsMasterNode = false common.RedisEnabled, common.MemoryCacheEnabled = false, false common.OptionMap = map[string]string{} if kind == "sqlite" { common.SQLitePath = isolatedDSN isolatedDSN = "local" } t.Setenv("SQL_DSN", isolatedDSN) t.Setenv("LOG_SQL_DSN", "") require.NoError(t, model.InitDB()) database = model.DB model.LOG_DB = database require.NoError(t, database.AutoMigrate(&model.Model{}, &model.Vendor{}, &model.Channel{}, &model.Ability{}, &model.Option{}, &model.User{}, &model.AuditLog{})) for _, value := range restoreRatios { require.NoError(t, value.restore("{}")) } config.UpdateConfigFromMap(config.GlobalConfig.Get("billing_setting"), map[string]string{"billing_mode": "{}", "billing_expr": "{}", "plugin_billing_expr": "{}"}) var version string query := "SELECT version()" if kind == "sqlite" { query = "SELECT sqlite_version()" } require.NoError(t, database.Raw(query).Scan(&version).Error) t.Logf("database version: %s", version) t.Cleanup(func() { for _, value := range restoreRatios { require.NoError(t, value.restore(value.value)) } config.UpdateConfigFromMap(config.GlobalConfig.Get("billing_setting"), map[string]string{"billing_mode": previousConfig["billing_setting.billing_mode"], "billing_expr": previousConfig["billing_setting.billing_expr"], "plugin_billing_expr": previousConfig[billing_setting.PluginBillingExprOption]}) common.OptionMap = previousOptions common.IsMasterNode, common.SQLitePath = previousMaster, previousSQLite common.RedisEnabled, common.MemoryCacheEnabled = previousRedis, previousMemory common.SetDatabaseTypes(previousMain, previousLog) connection, err := database.DB() if err == nil { require.NoError(t, connection.Close()) } model.DB, model.LOG_DB = previousDB, previousLogDB }) return database } func modelManagementRequest(t *testing.T, handler gin.HandlerFunc, method, path string, body any, output any) *httptest.ResponseRecorder { t.Helper() encoded, err := common.Marshal(body) require.NoError(t, err) recorder := httptest.NewRecorder() context, _ := gin.CreateTestContext(recorder) context.Request = httptest.NewRequest(method, path, bytes.NewReader(encoded)) context.Set("role", common.RoleRootUser) handler(context) if output != nil { require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), output), recorder.Body.String()) } return recorder } func TestModelPricingConversionDatabaseMatrix(t *testing.T) { previousQuota := common.QuotaPerUnit common.QuotaPerUnit = 500000 t.Cleanup(func() { common.QuotaPerUnit = previousQuota }) for _, dialect := range []struct{ kind, env string }{{"sqlite", ""}, {"mysql", "TEST_MYSQL_DSN"}, {"postgres", "TEST_POSTGRES_DSN"}} { t.Run(dialect.kind, func(t *testing.T) { if dialect.env != "" && os.Getenv(dialect.env) == "" { t.Skip("set " + dialect.env + " to run this database") } db := modelManagementDB(t, dialect.kind, os.Getenv(dialect.env)) contract, err := os.ReadFile("../pkg/billingexpr/testdata/frontend_simulation.json") require.NoError(t, err) var fixtures []struct { Name, Expression string Conversion *struct { ModelName string `json:"model_name"` Pricing model.PricingValues `json:"pricing"` } } require.NoError(t, common.Unmarshal(contract, &fixtures)) for _, fixture := range fixtures { if fixture.Conversion == nil { continue } t.Run("frontend_contract/"+fixture.Name, func(t *testing.T) { var response struct { Success bool Data model.ModelPricingConversion } modelManagementRequest(t, PreviewModelPricingConversion, http.MethodPost, "/api/option/model_pricing/convert", fixture.Conversion, &response) require.True(t, response.Success) assert.Equal(t, fixture.Expression, response.Data.Expression) }) } // The preview must resolve the draft, even when the running process // still has a different saved output multiplier for that model. require.NoError(t, ratio_setting.UpdateCompletionRatioByJSONString(`{"conversion-defaults":99}`)) for _, tc := range []struct { name string draft model.PricingValues expression, reason string }{ {"conversion-defaults", model.PricingValues{"ModelRatio": float64(2)}, `tier("base", p * 4 + c * 4)`, ""}, {"gpt-4-32k", model.PricingValues{"ModelRatio": 30.0, "CacheRatio": 1.0}, `tier("base", p * 60 + c * 120)`, ""}, {"conversion-free-cache", model.PricingValues{"ModelRatio": 0.0, "CacheRatio": 0.0}, `tier("base", p * 0 + c * 0 + cr * 0)`, ""}, {"claude-3-7-sonnet-20250219", model.PricingValues{"ModelRatio": float64(1.5), "CacheRatio": float64(0.1), "CreateCacheRatio": float64(1.25)}, `tier("base", p * 3 + c * 15 + cr * 0.3 + cc * 3.75 + cc1h * 6)`, ""}, {"conversion-image-default", model.PricingValues{"ModelRatio": float64(2), "ImageRatio": float64(1)}, `tier("base", p * 4 + c * 4)`, ""}, {"conversion-image-free", model.PricingValues{"ModelRatio": float64(2), "ImageRatio": float64(0)}, `tier("base", p * 4 + c * 4 + cr * 4 + img * 0)`, ""}, {"deepseek-chat", model.PricingValues{"ModelRatio": float64(0.135), "CacheRatio": float64(0.25)}, `tier("base", p * 0.27 + c * 0.27 + cr * 0.0675)`, ""}, {"gpt-4o-custom", model.PricingValues{"ModelRatio": float64(2)}, `tier("base", p * 4 + c * 16)`, ""}, {"gemini-2.5-pro-custom", model.PricingValues{"ModelRatio": float64(2)}, `tier("base", p * 4 + c * 32)`, ""}, {"vendor/claude-sonnet-4", model.PricingValues{"ModelRatio": float64(2), "CompletionRatio": float64(0)}, `tier("base", p * 4 + c * 0 + cr * 4 + cc * 5 + cc1h * 8)`, ""}, {"conversion-custom", model.PricingValues{"ModelRatio": float64(2), "CompletionRatio": float64(3), "CacheRatio": float64(0), "CreateCacheRatio": float64(1.5), "ImageRatio": float64(2)}, `tier("base", p * 4 + c * 12 + cr * 0 + cc * 6 + img * 8)`, ""}, {"deepseek-reasoner", model.PricingValues{"ModelRatio": 0.275, "CacheRatio": 0.25}, `tier("base", p * 0.55 + c * 0.55 + cr * 0.1375)`, ""}, {"gpt-5.6-sol", model.PricingValues{"ModelRatio": float64(2), "CompletionRatio": float64(2)}, `tier("base", p * 4 + c * 8)`, ""}, {"gpt-5.5", model.PricingValues{"ModelRatio": float64(2), "CompletionRatio": float64(2)}, `tier("base", p * 4 + c * 8)`, ""}, {"gpt-6-astra", model.PricingValues{"ModelRatio": float64(2), "CompletionRatio": float64(2)}, `tier("base", p * 4 + c * 8)`, ""}, {"conversion-free", model.PricingValues{"ModelPrice": float64(0)}, `tier("request", fixed(0))`, ""}, {"conversion-fixed", model.PricingValues{"ModelPrice": float64(0.25), "ModelRatio": float64(7)}, `tier("request", fixed(0.25))`, ""}, {"gpt-4o-2024-05-13", model.PricingValues{"ModelRatio": float64(2), "CompletionRatio": float64(99)}, `tier("base", p * 4 + c * 12)`, ""}, {"gpt-image-2", model.PricingValues{"ModelPrice": float64(1)}, `tier("image", fixed(1)) * image_count`, ""}, {"qwen-image-3.0-pro", model.PricingValues{"ModelPrice": float64(1)}, `tier("image", fixed(1)) * image_count`, ""}, {"wan2.7-image-pro", model.PricingValues{"ModelPrice": float64(1)}, "", "Task pricing must be converted manually using the task usage schema."}, {"video-summarizer", model.PricingValues{"ModelPrice": float64(1)}, `tier("request", fixed(1))`, ""}, {"sora-transcript-helper", model.PricingValues{"ModelPrice": float64(1)}, `tier("request", fixed(1))`, ""}, {"gpt-realtime", model.PricingValues{"ModelRatio": float64(1)}, "", "Realtime pricing must be converted manually."}, {"conversion-audio", model.PricingValues{"ModelRatio": float64(1), "AudioRatio": float64(2)}, `tier("base", p * 2 + c * 2 + ai * 4 + ao * 4)`, ""}, } { t.Run(tc.name, func(t *testing.T) { before, err := model.GetModelPricingSnapshot([]string{tc.name}) require.NoError(t, err) var response struct { Success bool Data model.ModelPricingConversion } modelManagementRequest(t, PreviewModelPricingConversion, http.MethodPost, "/api/option/model_pricing/convert", map[string]any{"model_name": tc.name, "pricing": tc.draft}, &response) require.True(t, response.Success) assert.Equal(t, tc.expression, response.Data.Expression) assert.Equal(t, tc.reason, response.Data.UnsupportedReason) if tc.name == "claude-3-7-sonnet-20250219" { // Both providers include images in input; Claude reports cache // separately and does not report an image token breakdown. for _, usage := range []dto.Usage{ {PromptTokens: 1000, CompletionTokens: 100, UsageSemantic: "openai", PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 200, CachedCreationTokens: 50, ImageTokens: 300}}, {PromptTokens: 750, CompletionTokens: 100, UsageSemantic: "anthropic", ClaudeCacheCreation5mTokens: 50, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 200, CachedCreationTokens: 50}}, } { tokens := service.BuildTieredTokenParams(&usage, usage.UsageSemantic == "anthropic", billingexpr.UsedVars(response.Data.Expression)) cost, _, err := billingexpr.RunExpr(response.Data.Expression, tokens) require.NoError(t, err) assert.Equal(t, float64(750), tokens.P) assert.Equal(t, 3997.5, cost, "image input must remain billed exactly once") } usage := dto.Usage{PromptTokens: 750, CompletionTokens: 100, UsageSemantic: "anthropic", ClaudeCacheCreation5mTokens: 30, ClaudeCacheCreation1hTokens: 20, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 200, CachedCreationTokens: 50}} tokens := service.BuildTieredTokenParams(&usage, true, billingexpr.UsedVars(response.Data.Expression)) cost, _, err := billingexpr.RunExpr(response.Data.Expression, tokens) require.NoError(t, err) assert.Equal(t, 4042.5, cost, "mixed TTLs preserve both legacy cache-write prices") } if tc.name == "deepseek-chat" { for _, usage := range []dto.Usage{ {PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 200}}, {PromptTokens: 800, CompletionTokens: 100, UsageSemantic: "anthropic", PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 200}}, } { tokens := service.BuildTieredTokenParams(&usage, usage.UsageSemantic == "anthropic", billingexpr.UsedVars(response.Data.Expression)) cost, _, err := billingexpr.RunExpr(response.Data.Expression, tokens) require.NoError(t, err) assert.Equal(t, 256.5, cost, "cache reads remain billed across both response formats") } } after, err := model.GetModelPricingSnapshot([]string{tc.name}) require.NoError(t, err) assert.Equal(t, before.Entries, after.Entries, "preview must not write pricing") if tc.expression == "" { return } pricing := tc.draft pricing["billing_setting.billing_mode"] = "tiered_expr" pricing["billing_setting.billing_expr"] = tc.expression change := model.ModelPricingChange{ModelName: tc.name, ExpectedVersion: before.Entries[0].Version, Pricing: pricing} require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) after, err = model.GetModelPricingSnapshot([]string{tc.name}) require.NoError(t, err) assert.Equal(t, pricing, after.Entries[0].Configured) assert.ErrorIs(t, model.UpdateModelPricing([]model.ModelPricingChange{change}), model.ErrModelPricingConflict) change.ExpectedVersion = after.Entries[0].Version change.Pricing["billing_setting.billing_mode"] = "ratio" require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) after, err = model.GetModelPricingSnapshot([]string{tc.name}) require.NoError(t, err) assert.Equal(t, "ratio", after.Entries[0].Effective["billing_setting.billing_mode"]) if _, fixed := pricing["ModelPrice"]; !fixed { var preview struct { Success bool Data model.ModelPricingDescription } modelManagementRequest(t, PreviewModelPricing, http.MethodPost, "/api/option/model_pricing/preview", map[string]any{"model_name": tc.name, "pricing": pricing}, &preview) require.True(t, preview.Success) assert.Equal(t, after.Entries[0].Effective, preview.Data.Effective) require.NotEmpty(t, response.Data.Effective) for field, value := range response.Data.Effective { assert.Equal(t, value, preview.Data.Effective[field], "conversion preview field %s", field) } cache, _ := ratio_setting.GetCacheRatio(tc.name) createCache, _ := ratio_setting.GetCreateCacheRatio(tc.name) image, _ := ratio_setting.GetImageRatio(tc.name) assert.Equal(t, ratio_setting.GetCompletionRatio(tc.name), preview.Data.Effective["CompletionRatio"]) assert.Equal(t, cache, preview.Data.Effective["CacheRatio"]) assert.Equal(t, createCache, preview.Data.Effective["CreateCacheRatio"]) assert.Equal(t, image, preview.Data.Effective["ImageRatio"]) } }) } require.NoError(t, (&model.Channel{Name: "Mapped image", Models: "conversion-alias", Type: 1, ModelMapping: common.GetPointer(`{"conversion-alias":"conversion-hop","conversion-hop":"gpt-image-2"}`)}).Insert()) preview, err := model.PreviewModelPricingConversion("conversion-alias", model.PricingValues{"ModelPrice": float64(1)}) require.NoError(t, err) assert.Equal(t, `tier("image", fixed(1)) * image_count`, preview.Expression) assert.Empty(t, preview.UnsupportedReason) t.Run("cache_writes_follow_configured_prices_and_claude_names", func(t *testing.T) { for _, tc := range []struct { name string configured *float64 mode model.CacheWriteMode }{ {"unrecognized-model", nil, model.CacheWriteNone}, {"gpt-5.6-terra", nil, model.CacheWriteNone}, {"gpt-5.6-terra", common.GetPointer(1.25), model.CacheWriteStandard}, {"vendor/claude-sonnet-4-6-high", nil, model.CacheWriteClaudeTTL}, {"custom-CLAUDE-alias", common.GetPointer(0.0), model.CacheWriteClaudeTTL}, {"deepseek-reasoner", common.GetPointer(0.0), model.CacheWriteStandard}, {"custom-cache", common.GetPointer(1.5), model.CacheWriteStandard}, } { t.Run(tc.name+"/"+string(tc.mode), func(t *testing.T) { draft := model.PricingValues{"ModelRatio": 2.0, "CompletionRatio": 2.0, "CacheRatio": 0.25, "billing_setting.billing_mode": "ratio"} if tc.configured != nil { draft["CreateCacheRatio"] = *tc.configured } var response struct { Success bool Data model.ModelPricingConversion } modelManagementRequest(t, PreviewModelPricingConversion, http.MethodPost, "/api/option/model_pricing/convert", map[string]any{"model_name": tc.name, "pricing": draft}, &response) require.True(t, response.Success) require.NotEmpty(t, response.Data.Expression) assert.Equal(t, tc.mode, response.Data.CacheWriteMode) var prices struct { Success bool Data model.ModelPricingDescription } modelManagementRequest(t, PreviewModelPricing, http.MethodPost, "/api/option/model_pricing/preview", map[string]any{"model_name": tc.name, "pricing": draft}, &prices) require.True(t, prices.Success) assert.Equal(t, response.Data.Effective, prices.Data.Effective) assert.Equal(t, response.Data.CacheWriteMode, prices.Data.CacheWriteMode) vars := billingexpr.UsedVars(response.Data.Expression) assert.Equal(t, tc.mode != model.CacheWriteNone, vars["cc"]) assert.Equal(t, tc.mode == model.CacheWriteClaudeTTL, vars["cc1h"]) if tc.configured != nil && *tc.configured == 0 { cost, _, err := billingexpr.RunExpr(response.Data.Expression, billingexpr.TokenParams{CC: 100, CC1h: 100}) require.NoError(t, err) assert.Zero(t, cost, "explicit zero cache prices must stay free") } }) } // The billing name remains authoritative for cache terms. require.NoError(t, (&model.Channel{Name: "Cache alias", Type: 1, Models: "opaque-cache-alias", ModelMapping: common.GetPointer(`{"opaque-cache-alias":"claude-sonnet-4-6"}`)}).Insert()) alias, err := model.PreviewModelPricingConversion("opaque-cache-alias", model.PricingValues{"ModelRatio": 2.0}) require.NoError(t, err) assert.Equal(t, `tier("base", p * 4 + c * 4)`, alias.Expression) }) t.Run("same_price_cache_reads_preserve_overlapping_categories", func(t *testing.T) { for _, tc := range []struct { field, term string ratio float64 details dto.InputTokenDetails cost, unpricedCacheCost float64 }{ {"CreateCacheRatio", "cc * 75", 1.25, dto.InputTokenDetails{CachedTokens: 800, CacheWriteTokens: 500}, 97500, 79500}, {"ImageRatio", "img * 120", 2, dto.InputTokenDetails{CachedTokens: 600, ImageTokens: 500}, 108000, 102000}, } { t.Run(tc.field, func(t *testing.T) { converted, err := model.PreviewModelPricingConversion("overlapping-cache", model.PricingValues{"ModelRatio": 30.0, "CompletionRatio": 2.0, "CacheRatio": 1.0, tc.field: tc.ratio}) require.NoError(t, err) assert.Equal(t, `tier("base", p * 60 + c * 120 + cr * 60 + `+tc.term+`)`, converted.Expression) usage := dto.Usage{PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: tc.details} params := service.BuildTieredTokenParams(&usage, false, billingexpr.UsedVars(converted.Expression)) cost, _, err := billingexpr.RunExpr(converted.Expression, params) require.NoError(t, err) assert.Equal(t, tc.cost, cost, "preserve the existing overlapping-category charge") unpriced := `tier("base", p * 60 + c * 120 + ` + tc.term + `)` params = service.BuildTieredTokenParams(&usage, false, billingexpr.UsedVars(unpriced)) cost, _, err = billingexpr.RunExpr(unpriced, params) require.NoError(t, err) assert.Equal(t, tc.unpricedCacheCost, cost, "existing expressions without cr retain their OpenAI normalization") }) } }) t.Run("media_prices_and_request_adjustments", func(t *testing.T) { for _, quantity := range []string{"129", "-1", "0.5", "18446744073709551615"} { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/images/generations", strings.NewReader(`{"model":"z-image","parameters":{"n":`+quantity+`}}`)) ctx.Request.Header.Set("Content-Type", "application/json") Relay(ctx, types.RelayFormatOpenAIImage) assert.Equal(t, http.StatusBadRequest, recorder.Code, recorder.Body.String()) } conflicting, err := model.PreviewModelPricingConversion("gemini-2.5-flash-conflict", model.PricingValues{"ModelRatio": 0.15, "AudioRatio": 10.0}) require.NoError(t, err) assert.Contains(t, conflicting.UnsupportedReason, "Gemini and OpenAI audio prices differ") aliName := "z-image-media-conversion" require.NoError(t, (&model.Channel{Name: aliName, Models: aliName, Type: 17, Status: common.ChannelStatusEnabled}).Insert()) aliConversion, err := model.PreviewModelPricingConversion(aliName, model.PricingValues{"ModelPrice": 0.04}) require.NoError(t, err) require.Empty(t, aliConversion.UnsupportedReason) quantity := 3 aliCost, _, err := billingexpr.RunExprWithRequest(aliConversion.Expression, billingexpr.TokenParams{}, billingexpr.RequestInput{ImageCount: &quantity, Body: []byte(`{"parameters":{"prompt_extend":true}}`)}) require.NoError(t, err) assert.Equal(t, 240000.0, aliCost) require.NoError(t, (&model.Channel{Name: "Different image rules", Models: aliName, Type: 1, Status: common.ChannelStatusEnabled}).Insert()) conflicting, err = model.PreviewModelPricingConversion(aliName, model.PricingValues{"ModelPrice": 0.04}) require.NoError(t, err) assert.Contains(t, conflicting.UnsupportedReason, "different image request multipliers") for _, tc := range []struct { name string pricing model.PricingValues usage dto.Usage want float64 }{ {"gemini-2.5-flash", model.PricingValues{"ModelRatio": 0.15, "CompletionRatio": 2.0, "CacheRatio": 0.1}, dto.Usage{PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 100, AudioTokens: 200}}, 473}, {"gemini-2.5-flash-zero", model.PricingValues{"ModelRatio": 0.0}, dto.Usage{PromptTokens: 1000, PromptTokensDetails: dto.InputTokenDetails{AudioTokens: 200}}, 200}, {"media-audio", model.PricingValues{"ModelRatio": 1.0, "CompletionRatio": 3.0, "AudioRatio": 2.0, "AudioCompletionRatio": 4.0}, dto.Usage{PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: dto.InputTokenDetails{TextTokens: 800, AudioTokens: 200}, CompletionTokenDetails: dto.OutputTokenDetails{TextTokens: 50, AudioTokens: 50}}, 3500}, {"media-free-audio", model.PricingValues{"ModelRatio": 1.0, "AudioRatio": 0.0}, dto.Usage{PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: dto.InputTokenDetails{AudioTokens: 200}, CompletionTokenDetails: dto.OutputTokenDetails{AudioTokens: 100}}, 1600}, {"media-audio-cache", model.PricingValues{"ModelRatio": 1.0, "CompletionRatio": 3.0, "AudioRatio": 2.0, "CacheRatio": 0.1}, dto.Usage{PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 400, AudioTokens: 200}, CompletionTokenDetails: dto.OutputTokenDetails{AudioTokens: 50}}, 2900}, {"media-text-cache", model.PricingValues{"ModelRatio": 1.0, "CompletionRatio": 3.0, "AudioRatio": 2.0, "CacheRatio": 0.1}, dto.Usage{PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 400}}, 1880}, {"gemini-2.5-flash-overlap", model.PricingValues{"ModelRatio": 1.0}, dto.Usage{PromptTokens: 1000, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 800, AudioTokens: 500}}, 2100}, } { t.Run(tc.name, func(t *testing.T) { converted, err := model.PreviewModelPricingConversion(tc.name, tc.pricing) require.NoError(t, err) require.Empty(t, converted.UnsupportedReason) require.NotNil(t, converted.BillingDetails.AudioInputPrice) params := service.BuildTieredTokenParams(&tc.usage, false, billingexpr.UsedVars(converted.Expression)) cost, _, err := billingexpr.RunExpr(converted.Expression, params) require.NoError(t, err) assert.InDelta(t, tc.want, cost, 1e-9) }) } converted, err := model.PreviewModelPricingConversion("dall-e-3", model.PricingValues{"ModelPrice": 0.04}) require.NoError(t, err) require.Empty(t, converted.UnsupportedReason) assert.NotContains(t, converted.Expression, "256x256") assert.NotContains(t, converted.Expression, "512x512") smallImage, err := model.PreviewModelPricingConversion("dall-e-2", model.PricingValues{"ModelPrice": 0.02}) require.NoError(t, err) require.Empty(t, smallImage.UnsupportedReason) assert.NotContains(t, smallImage.Expression, "1792") smallCost, _, err := billingexpr.RunExprWithRequest(smallImage.Expression, billingexpr.TokenParams{}, billingexpr.RequestInput{Body: []byte(`{"size":"256x256"}`), ImageCount: &quantity}) require.NoError(t, err) assert.Equal(t, 24000.0, smallCost) for _, tc := range []struct { size, quality string count int cost float64 }{ {"1024x1024", "standard", 3, 120000}, {"1024x1024", "hd", 2, 160000}, {"1024x1792", "hd", 3, 360000}, } { body, err := common.Marshal(map[string]any{"size": tc.size, "quality": tc.quality}) require.NoError(t, err) cost, _, err := billingexpr.RunExprWithRequest(converted.Expression, billingexpr.TokenParams{}, billingexpr.RequestInput{Body: body, ImageCount: &tc.count}) require.NoError(t, err) assert.InDelta(t, tc.cost, cost, 1e-9) } for _, channelType := range []int{20, 58} { name := fmt.Sprintf("gemini-3.5-flash-media-%d", channelType) require.NoError(t, (&model.Channel{Name: name, Models: name, Type: channelType}).Insert()) require.NoError(t, db.Create(&model.Model{ModelName: name, Endpoints: `{"custom-text":"/v1/custom"}`}).Error) converted, err := model.PreviewModelPricingConversion(name, model.PricingValues{"ModelRatio": 0.75, "CompletionRatio": 6.0, "CacheRatio": 0.1}) require.NoError(t, err) assert.Empty(t, converted.UnsupportedReason) assert.Equal(t, `tier("base", p * 1.5 + c * 9 + cr * 0.15)`, converted.Expression) assert.Nil(t, converted.BillingDetails.AudioInputPrice) } }) require.NoError(t, db.Create(&model.Model{ModelName: "conversion-metadata", Endpoints: `{"image-generation":"/v1/images/generations"}`}).Error) preview, err = model.PreviewModelPricingConversion("conversion-metadata", model.PricingValues{"ModelPrice": float64(1)}) require.NoError(t, err) assert.Equal(t, `tier("image", fixed(1)) * image_count`, preview.Expression) assert.Empty(t, preview.UnsupportedReason) for _, tc := range []struct { name, rule string nameRule int endpoint constant.EndpointType }{ {"catalog-image-prefix-opaque", "catalog-image-prefix-", model.NameRulePrefix, constant.EndpointTypeImageGeneration}, {"opaque-catalog-image-suffix", "-catalog-image-suffix", model.NameRuleSuffix, constant.EndpointTypeImageGeneration}, {"before-catalog-image-middle-after", "catalog-image-middle", model.NameRuleContains, constant.EndpointTypeImageGeneration}, {"opaque-catalog-video", "opaque-catalog-video", model.NameRuleExact, constant.EndpointTypeOpenAIVideo}, } { t.Run("endpoint_metadata/"+tc.name, func(t *testing.T) { endpoints, err := common.Marshal(map[string]string{string(tc.endpoint): "/v1/fixture"}) require.NoError(t, err) require.NoError(t, db.Create(&model.Model{ModelName: tc.rule, NameRule: tc.nameRule, Endpoints: string(endpoints)}).Error) converted, err := model.PreviewModelPricingConversion(tc.name, model.PricingValues{"ModelPrice": 1.0}) require.NoError(t, err) if tc.endpoint == constant.EndpointTypeOpenAIVideo { assert.Empty(t, converted.Expression) assert.Equal(t, "Video pricing must be converted manually.", converted.UnsupportedReason) } else { assert.Equal(t, `tier("image", fixed(1)) * image_count`, converted.Expression) } }) } sora := &model.Channel{Name: "Opaque video route", Models: "opaque-video-route", Type: constant.ChannelTypeSora, Group: "a,b"} require.NoError(t, sora.Insert()) video, err := model.PreviewModelPricingConversion("opaque-video-route", model.PricingValues{"ModelPrice": 1.0}) require.NoError(t, err) assert.Equal(t, "Video pricing must be converted manually.", video.UnsupportedReason) }) } } func TestModelManagementDatabaseMatrix(t *testing.T) { _, err := jsplugin.DefaultRegistry.Register(` export const meta = {apiVersion: 1, key: "model-management-task", name: "Management task fixture", version: "1.0.0", author: {name: "Test"}, models: ["matrix-task"], fetchMode: "per_task", usageSchema: {seconds: {type: "number", unit: "second"}}}; export function buildSubmitRequest() { return {}; } export function parseSubmitResponse() { return {}; } export function buildQueryRequest() { return {}; } export function parseTaskResult() { return {}; } `, jsplugin.Options{}) require.NoError(t, err) t.Cleanup(func() { jsplugin.DefaultRegistry.Unregister("model-management-task") }) for _, dialect := range []struct{ kind, env string }{{"sqlite", ""}, {"mysql", "TEST_MYSQL_DSN"}, {"postgres", "TEST_POSTGRES_DSN"}} { t.Run(dialect.kind, func(t *testing.T) { if dialect.env != "" && os.Getenv(dialect.env) == "" { t.Skip("set " + dialect.env + " to run this database") } db := modelManagementDB(t, dialect.kind, os.Getenv(dialect.env)) t.Run("square_states_follow_catalog_policy", func(t *testing.T) { records := []model.Model{ {ModelName: "square-visible", Status: 1}, {ModelName: "square-hidden", Status: 0}, {ModelName: "square-catalog", Status: 1}, {ModelName: "square-hidden-catalog", Status: 0}, {ModelName: "square-partial-", NameRule: model.NameRulePrefix, Status: 1}, {ModelName: "square-partial-hidden", Status: 0}, {ModelName: "square-hidden-", NameRule: model.NameRulePrefix, Status: 0}, {ModelName: "square-hidden-override", Status: 1}, {ModelName: "square-off-", NameRule: model.NameRulePrefix, Status: 0}, {ModelName: "square-empty", NameRule: model.NameRulePrefix, Status: 1}, {ModelName: "square-empty-hidden", NameRule: model.NameRulePrefix, Status: 0}, {ModelName: "-ending", NameRule: model.NameRuleSuffix, Status: 1}, {ModelName: "square-prefix-", NameRule: model.NameRulePrefix, Status: 0}, {ModelName: "square-contains", NameRule: model.NameRuleContains, Status: 0}, } ids := make([]int, 0, len(records)) for i := range records { require.NoError(t, records[i].Insert()) ids = append(ids, records[i].Id) } active := model.Channel{Name: "Square active", Type: 1, Key: "fixture", Group: "default", Status: common.ChannelStatusEnabled, Models: "square-visible,square-hidden,square-bare,square-partial-on,square-partial-hidden,square-hidden-child,square-hidden-override,square-prefix-ending,square-contains-ending"} inactive := model.Channel{Name: "Square inactive", Type: 1, Key: "fixture", Group: "default", Status: common.ChannelStatusManuallyDisabled, Models: "square-disabled,square-partial-off,square-off-child"} for _, channel := range []*model.Channel{&active, &inactive} { require.NoError(t, channel.Insert()) } t.Cleanup(func() { require.NoError(t, db.Where("channel_id IN ?", []int{active.Id, inactive.Id}).Delete(&model.Ability{}).Error) require.NoError(t, db.Where("id IN ?", []int{active.Id, inactive.Id}).Delete(&model.Channel{}).Error) require.NoError(t, db.Unscoped().Where("id IN ?", ids).Delete(&model.Model{}).Error) model.RefreshPricing() }) var response struct { Success bool Data struct{ Items []model.Model } } modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?include_channel_models=true&keyword=square-&page_size=100", nil, &response) require.True(t, response.Success) byName := make(map[string]model.Model) for _, row := range response.Data.Items { byName[row.ModelName] = row } catalog := make(map[string]bool) model.RefreshPricing() for _, row := range model.GetPricing() { catalog[row.ModelName] = true } expected := map[string]model.ModelSquareState{ "square-visible": model.ModelSquareVisible, "square-hidden": model.ModelSquareHidden, "square-catalog": model.ModelSquareUnavailable, "square-hidden-catalog": model.ModelSquareHidden, "square-bare": model.ModelSquareVisible, "square-disabled": model.ModelSquareUnavailable, "square-partial-": model.ModelSquarePartial, "square-partial-hidden": model.ModelSquareHidden, "square-partial-on": model.ModelSquareVisible, "square-partial-off": model.ModelSquareUnavailable, "square-hidden-": model.ModelSquarePartial, "square-hidden-child": model.ModelSquareHidden, "square-hidden-override": model.ModelSquareVisible, "square-off-": model.ModelSquareHidden, "square-off-child": model.ModelSquareHidden, "square-empty": model.ModelSquareUnavailable, "square-empty-hidden": model.ModelSquareHidden, "square-prefix-ending": model.ModelSquareHidden, "square-contains-ending": model.ModelSquareVisible, "square-contains": model.ModelSquareVisible, "square-prefix-": model.ModelSquareHidden, } for name, state := range expected { row, exists := byName[name] require.True(t, exists, name) assert.Equal(t, state, row.SquareState, name) if row.NameRule == model.NameRuleExact { assert.Equal(t, state == model.ModelSquareVisible, catalog[name], name) } } assert.Zero(t, byName["square-bare"].Id) assert.Zero(t, byName["square-hidden-child"].Id) t.Run("filter_square_state_before_pagination", func(t *testing.T) { type filteredResponse struct { Success bool Data struct { Items []model.Model Total int } } for _, state := range []model.ModelSquareState{model.ModelSquareVisible, model.ModelSquareUnavailable, model.ModelSquareHidden, model.ModelSquarePartial} { t.Run(string(state), func(t *testing.T) { expectedNames := []string{} for name, expectedState := range expected { if expectedState == state { expectedNames = append(expectedNames, name) } } actualNames := []string{} for page := 1; page <= (len(expectedNames)+1)/2; page++ { var result filteredResponse path := fmt.Sprintf("/api/models/search?include_channel_models=true&keyword=square-&square_state=%s&page_size=2&p=%d", state, page) modelManagementRequest(t, SearchModelsMeta, "GET", path, nil, &result) require.True(t, result.Success) assert.Equal(t, len(expectedNames), result.Data.Total) for _, row := range result.Data.Items { assert.Equal(t, state, row.SquareState) actualNames = append(actualNames, row.ModelName) } } assert.ElementsMatch(t, expectedNames, actualNames) var beyondLastPage filteredResponse modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?include_channel_models=true&keyword=square-&square_state="+string(state)+"&page_size=2&p=99", nil, &beyondLastPage) require.True(t, beyondLastPage.Success) assert.Equal(t, len(expectedNames), beyondLastPage.Data.Total) assert.Empty(t, beyondLastPage.Data.Items) }) } for _, test := range []struct { name string handler gin.HandlerFunc query string names []string }{ {"list", GetAllModelsMeta, "?include_channel_models=true&square_state=visible", []string{"square-visible", "square-bare", "square-partial-on", "square-hidden-override", "square-contains-ending", "square-contains"}}, {"metadata_only", SearchModelsMeta, "?keyword=square-&square_state=visible", []string{"square-visible", "square-hidden-override", "square-contains"}}, {"keyword", SearchModelsMeta, "?include_channel_models=true&keyword=square-partial&square_state=hidden", []string{"square-partial-hidden"}}, {"policy", SearchModelsMeta, "?include_channel_models=true&keyword=square-&square_state=partial&status=disabled", []string{"square-hidden-"}}, {"vendor_and_sync", SearchModelsMeta, "?include_channel_models=true&keyword=square-&square_state=partial&vendor=0&sync_official=no", []string{"square-hidden-", "square-partial-"}}, {"no_matches", SearchModelsMeta, "?include_channel_models=true&keyword=square-bare&square_state=hidden", []string{}}, } { t.Run(test.name, func(t *testing.T) { var result filteredResponse modelManagementRequest(t, test.handler, "GET", "/api/models/"+test.query+"&page_size=100", nil, &result) require.True(t, result.Success) names := []string{} for _, row := range result.Data.Items { names = append(names, row.ModelName) } assert.Equal(t, len(test.names), result.Data.Total) assert.ElementsMatch(t, test.names, names) }) } for _, handler := range []gin.HandlerFunc{GetAllModelsMeta, SearchModelsMeta} { var result filteredResponse recorder := modelManagementRequest(t, handler, "GET", "/api/models/?square_state=unknown", nil, &result) assert.Equal(t, http.StatusBadRequest, recorder.Code) assert.False(t, result.Success) } for _, query := range []string{"p=-1", "page_size=-1"} { recorder := modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?square_state=visible&"+query, nil, nil) assert.Equal(t, http.StatusBadRequest, recorder.Code) } var oversizedPage filteredResponse modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?square_state=visible&keyword=square-&p="+strconv.Itoa(int(^uint(0)>>1)), nil, &oversizedPage) require.True(t, oversizedPage.Success) assert.Equal(t, 3, oversizedPage.Data.Total) assert.Empty(t, oversizedPage.Data.Items) }) var detail struct { Success bool Data model.Model } modelManagementRequest(t, func(c *gin.Context) { c.Params = gin.Params{{Key: "id", Value: strconv.Itoa(records[4].Id)}} GetModelMeta(c) }, "GET", "/api/models/"+strconv.Itoa(records[4].Id), nil, &detail) require.True(t, detail.Success) assert.Equal(t, model.ModelSquarePartial, detail.Data.SquareState) }) t.Run("channel_model_listing", func(t *testing.T) { exact := model.Model{ModelName: "listing-exact", Status: 1, SyncOfficial: 1} catalog := model.Model{ModelName: "listing-catalog", Status: 1} rule := model.Model{ModelName: "listing-rule-", NameRule: model.NameRulePrefix, Status: 1} for _, item := range []*model.Model{&exact, &catalog, &rule} { require.NoError(t, item.Insert()) } active := model.Channel{Name: "Listing active", Type: 1, Key: "fixture", Models: "listing-exact, listing-new,listing-rule-child,listing-new, ,", Group: "default", Status: common.ChannelStatusEnabled} inactive := model.Channel{Name: "Listing inactive", Type: 1, Key: "fixture", Models: "listing-new,listing-disabled", Group: "default", Status: common.ChannelStatusManuallyDisabled} for _, channel := range []*model.Channel{&active, &inactive} { require.NoError(t, channel.Insert()) } t.Cleanup(func() { require.NoError(t, db.Where("channel_id IN ?", []int{active.Id, inactive.Id}).Delete(&model.Ability{}).Error) require.NoError(t, db.Where("id IN ?", []int{active.Id, inactive.Id}).Delete(&model.Channel{}).Error) require.NoError(t, db.Unscoped().Where("model_name LIKE ?", "listing-%").Delete(&model.Model{}).Error) model.RefreshPricing() }) type listingResponse struct { Success bool Data struct { Items []model.Model Total int } } var response listingResponse modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?include_channel_models=true&keyword=listing-", nil, &response) require.True(t, response.Success) require.Equal(t, 6, response.Data.Total) names := make([]string, 0) byName := make(map[string]model.Model) for _, item := range response.Data.Items { names = append(names, item.ModelName) byName[item.ModelName] = item } assert.Equal(t, []string{"listing-rule-", "listing-catalog", "listing-exact", "listing-disabled", "listing-new", "listing-rule-child"}, names) assert.True(t, byName[exact.ModelName].HasMetadata) assert.False(t, byName["listing-new"].HasMetadata) assert.Zero(t, byName["listing-new"].Id) assert.Equal(t, 2, byName["listing-new"].ConfiguredChannelCount) assert.Equal(t, 1, byName["listing-disabled"].ConfiguredChannelCount) assert.Empty(t, byName["listing-disabled"].BoundChannels) assert.Zero(t, byName[catalog.ModelName].ConfiguredChannelCount) for _, tc := range []struct { query string total int names []string }{ {"&p=2&page_size=2", 6, []string{"listing-exact", "listing-disabled"}}, {"&p=9&page_size=2", 6, []string{}}, {"&status=enabled", 3, []string{"listing-rule-", "listing-catalog", "listing-exact"}}, {"&sync_official=no", 2, []string{"listing-rule-", "listing-catalog"}}, {"&vendor=0", 6, []string{"listing-rule-", "listing-catalog", "listing-exact", "listing-disabled", "listing-new", "listing-rule-child"}}, {"&vendor=999", 0, []string{}}, } { var page listingResponse modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?include_channel_models=true&keyword=listing-"+tc.query, nil, &page) require.True(t, page.Success) assert.Equal(t, tc.total, page.Data.Total) actual := make([]string, 0) for _, item := range page.Data.Items { actual = append(actual, item.ModelName) } assert.Equal(t, tc.names, actual) } var legacy listingResponse modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?keyword=listing-", nil, &legacy) assert.Equal(t, 3, legacy.Data.Total) var count int64 require.NoError(t, db.Model(&model.Model{}).Where("model_name LIKE ?", "listing-%").Count(&count).Error) assert.EqualValues(t, 3, count) prices, err := model.GetModelPricingSnapshot([]string{"listing-new"}) require.NoError(t, err) require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{{ModelName: "listing-new", ExpectedVersion: prices.Entries[0].Version, Pricing: model.PricingValues{"ModelPrice": float64(0)}}})) t.Cleanup(func() { snapshot, err := model.GetModelPricingSnapshot([]string{"listing-new"}) require.NoError(t, err) require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{{ModelName: "listing-new", ExpectedVersion: snapshot.Entries[0].Version, Reset: true}})) }) require.NoError(t, db.Model(&model.Model{}).Where("model_name = ?", "listing-new").Count(&count).Error) assert.Zero(t, count) for _, price := range model.GetPricing() { assert.NotEqual(t, catalog.ModelName, price.ModelName) } created := model.Model{ModelName: "listing-new", Status: 1} require.NoError(t, created.Insert()) var after listingResponse modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?include_channel_models=true&keyword=listing-new", nil, &after) require.Len(t, after.Data.Items, 1) assert.Equal(t, created.Id, after.Data.Items[0].Id) require.NoError(t, created.Delete()) after = listingResponse{} modelManagementRequest(t, SearchModelsMeta, "GET", "/api/models/search?include_channel_models=true&keyword=listing-new", nil, &after) require.Len(t, after.Data.Items, 1) assert.False(t, after.Data.Items[0].HasMetadata) require.NoError(t, db.Callback().Query().Before("gorm:query").Register("fail_listing_channels", func(tx *gorm.DB) { if tx.Statement.Table == "channels" { tx.AddError(errors.New("channel lookup failed")) } })) var failed listingResponse modelManagementRequest(t, GetAllModelsMeta, "GET", "/api/models/?include_channel_models=true", nil, &failed) require.NoError(t, db.Callback().Query().Remove("fail_listing_channels")) assert.False(t, failed.Success, "a channel lookup failure must not look like an empty configuration") }) t.Run("pricing_saves_zero_switches_modes_and_rejects_stale_batches", func(t *testing.T) { before, err := model.GetModelPricingSnapshot([]string{"matrix-priced", "matrix-other"}) require.NoError(t, err) changes := []model.ModelPricingChange{ {ModelName: "matrix-priced", ExpectedVersion: before.EmptyVersion, Pricing: model.PricingValues{"ModelPrice": float64(0), "billing_setting.billing_mode": "ratio"}}, {ModelName: "matrix-other", ExpectedVersion: before.EmptyVersion, Pricing: model.PricingValues{"ModelRatio": float64(1), "CreateCacheRatio": 1.25}}, } require.NoError(t, model.UpdateModelPricing(changes)) loaded, err := model.GetModelPricingSnapshot([]string{"matrix-priced"}) require.NoError(t, err) assert.Equal(t, float64(0), loaded.Entries[0].Effective["ModelPrice"]) stale := changes[0] changes[0].ExpectedVersion = loaded.Entries[0].Version changes[0].Pricing = model.PricingValues{"billing_setting.billing_mode": "tiered_expr", "billing_setting.billing_expr": `tier("base", p * 2 + c * 8 + cr * 0 + cc * 2.5)`, "ModelRatio": float64(1)} require.NoError(t, model.UpdateModelPricing(changes[:1])) loaded, err = model.GetModelPricingSnapshot([]string{"matrix-priced", "matrix-other"}) require.NoError(t, err) assert.Equal(t, 1.25, loaded.Entries[0].Configured["CreateCacheRatio"]) assert.Equal(t, "tiered_expr", loaded.Entries[1].Effective["billing_setting.billing_mode"]) _, oldFixed := loaded.Entries[1].Configured["ModelPrice"] assert.False(t, oldFixed) other := model.ModelPricingChange{ModelName: "matrix-other", ExpectedVersion: loaded.Entries[0].Version, Pricing: model.PricingValues{"ModelRatio": float64(9)}} err = model.UpdateModelPricing([]model.ModelPricingChange{other, stale}) assert.ErrorIs(t, err, model.ErrModelPricingConflict) after, err := model.GetModelPricingSnapshot([]string{"matrix-priced", "matrix-other"}) require.NoError(t, err) assert.Equal(t, loaded.Entries, after.Entries) invalid := other invalid.Pricing = model.PricingValues{"ModelPrice": float64(-1)} assert.Error(t, model.UpdateModelPricing([]model.ModelPricingChange{invalid})) // A physical failure after earlier option writes must roll back all // rows and leave the previously published runtime price intact. writes := 0 require.NoError(t, db.Callback().Update().Before("gorm:update").Register("fail_pricing_matrix", func(tx *gorm.DB) { if tx.Statement.Table == "options" { writes++ if writes == 3 { tx.AddError(errors.New("injected write failure")) } } })) err = model.UpdateModelPricing([]model.ModelPricingChange{other}) require.Error(t, err) require.NoError(t, db.Callback().Update().Remove("fail_pricing_matrix")) after, err = model.GetModelPricingSnapshot([]string{"matrix-priced", "matrix-other"}) require.NoError(t, err) assert.Equal(t, loaded.Entries, after.Entries) ratio, _, _ := ratio_setting.GetModelRatio("matrix-other") assert.Equal(t, float64(1), ratio) // Two saves based on the same version cannot both succeed. var wg sync.WaitGroup results := make(chan error, 2) for _, value := range []float64{3, 4} { wg.Add(1) go func(value float64) { defer wg.Done() change := other change.Pricing = model.PricingValues{"ModelRatio": value} results <- model.UpdateModelPricing([]model.ModelPricingChange{change}) }(value) } wg.Wait() close(results) successes, conflicts := 0, 0 for err := range results { if err == nil { successes++ } else if errors.Is(err, model.ErrModelPricingConflict) { conflicts++ } else { require.NoError(t, err) } } assert.Equal(t, 1, successes) assert.Equal(t, 1, conflicts) }) t.Run("task_usage_and_builtin_reset", func(t *testing.T) { snapshot, err := model.GetModelPricingSnapshot([]string{"matrix-task"}) require.NoError(t, err) assert.Contains(t, snapshot.Entries[0].UsageSchema, "seconds") expression := `tier("base", u("seconds") * 0.25)` change := model.ModelPricingChange{ModelName: "matrix-task", ExpectedVersion: snapshot.Entries[0].Version, Pricing: model.PricingValues{"billing_setting.billing_mode": "tiered_expr", "billing_setting.billing_expr": expression}} require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) snapshot, err = model.GetModelPricingSnapshot([]string{"matrix-task"}) require.NoError(t, err) assert.Equal(t, expression, snapshot.Entries[0].Effective["billing_setting.billing_expr"]) change.ExpectedVersion = snapshot.Entries[0].Version change.Pricing["billing_setting.billing_expr"] = `tier("base", u("undeclared") * 1)` assert.Error(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) { const name = "gpt-6-astra" builtin, exists := billing_setting.GetBuiltinBillingExpr(name) require.True(t, exists) before, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) require.Empty(t, before.Entries[0].Configured) change = model.ModelPricingChange{ModelName: name, ExpectedVersion: before.Entries[0].Version, Pricing: model.PricingValues{"ModelPrice": float64(0)}} require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) custom, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Equal(t, float64(0), custom.Entries[0].Effective["ModelPrice"]) change.ExpectedVersion, change.Pricing, change.Reset = custom.Entries[0].Version, nil, true require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) reset, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Empty(t, reset.Entries[0].Configured) assert.Equal(t, builtin, reset.Entries[0].Effective["billing_setting.billing_expr"]) } }) t.Run("concurrent_import_creates_one_record", func(t *testing.T) { update := model.MetadataSyncUpdate{MetadataSyncSelection: model.MetadataSyncSelection{ModelName: "matrix-concurrent-import", RecordVersion: model.MetadataRecordVersion(nil, nil, nil), Create: true}, Values: model.MetadataValues{Description: "Imported", Status: 1}} var wg sync.WaitGroup results := make(chan error, 2) for range 2 { wg.Go(func() { _, err := model.ApplyMetadataSync([]model.MetadataSyncUpdate{update}, nil) results <- err }) } wg.Wait() close(results) successes, conflicts := 0, 0 for err := range results { if err == nil { successes++ } else if errors.Is(err, model.ErrMetadataSyncConflict) { conflicts++ } else { require.NoError(t, err) } } assert.Equal(t, 1, successes) assert.Equal(t, 1, conflicts) var count int64 require.NoError(t, db.Model(&model.Model{}).Where("model_name = ?", update.ModelName).Count(&count).Error) assert.EqualValues(t, 1, count) }) t.Run("metadata_keeps_pricing_and_channel_identity", func(t *testing.T) { active := model.Channel{Name: "Active route", Type: 1, Status: common.ChannelStatusEnabled} disabled := model.Channel{Name: "Disabled route", Type: 1, Status: common.ChannelStatusManuallyDisabled} require.NoError(t, db.Create(&active).Error) require.NoError(t, db.Create(&disabled).Error) require.NoError(t, db.Create(&[]model.Ability{ {Model: "matrix-hidden-unpriced", Group: "available", ChannelId: active.Id, Enabled: true}, {Model: "matrix-hidden-unpriced", Group: "disabled", ChannelId: disabled.Id, Enabled: true}, {Model: "matrix-hidden-unpriced", Group: "inactive", ChannelId: active.Id, Enabled: false}, }).Error) exact := &model.Model{ModelName: "matrix-hidden-unpriced", Status: 0, SyncOfficial: 0} require.NoError(t, exact.Insert()) rule := &model.Model{ModelName: "matrix-hidden-", NameRule: model.NameRulePrefix} enrichModels([]*model.Model{exact, rule}) assert.Equal(t, []string{"available"}, exact.EnableGroups) assert.Equal(t, []model.BoundChannel{{Name: "Active route", Type: 1}}, exact.BoundChannels) assert.Equal(t, []string{"matrix-hidden-unpriced"}, rule.MatchedModels) assert.Empty(t, exact.Endpoints, "inferred endpoints must not become stored configuration") priceBefore, err := model.GetModelPricingSnapshot([]string{"matrix-hidden-unpriced"}) require.NoError(t, err) require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{{ModelName: exact.ModelName, ExpectedVersion: priceBefore.Entries[0].Version, Pricing: model.PricingValues{"ModelPrice": float64(0)}}})) exact.ModelName = "matrix-renamed" exact.Endpoints = `{"openai":{"path":"/v1/chat/completions","method":"POST"}}` response := modelManagementRequest(t, UpdateModelMeta, http.MethodPut, "/api/models/", exact, nil) assert.Contains(t, response.Body.String(), `"success":true`) var reloaded model.Model require.NoError(t, db.First(&reloaded, exact.Id).Error) enrichModels([]*model.Model{&reloaded}) assert.Equal(t, exact.Endpoints, reloaded.Endpoints) assert.Empty(t, reloaded.BoundChannels) prices, err := model.GetModelPricingSnapshot([]string{"matrix-hidden-unpriced", "matrix-renamed"}) require.NoError(t, err) assert.Equal(t, float64(0), prices.Entries[0].Configured["ModelPrice"]) assert.Empty(t, prices.Entries[1].Configured) require.NoError(t, reloaded.Delete()) require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{{ModelName: "matrix-hidden-unpriced", ExpectedVersion: prices.Entries[0].Version, Reset: true}})) prices, err = model.GetModelPricingSnapshot([]string{"matrix-hidden-unpriced"}) require.NoError(t, err) assert.Empty(t, prices.Entries[0].Configured) var ability model.Ability require.NoError(t, db.Where("model = ? AND channel_id = ? AND enabled = ?", "matrix-hidden-unpriced", active.Id, true).First(&ability).Error) }) t.Run("metadata_preview_selection_versions_and_transaction", func(t *testing.T) { local := &model.Model{ModelName: "matrix-existing", Description: "Local description", Tags: "keep", Status: 1, SyncOfficial: 1} require.NoError(t, local.Insert()) blocked := &model.Model{ModelName: "matrix-blocked", Status: 1, SyncOfficial: 0} require.NoError(t, blocked.Insert()) require.NoError(t, db.Create(&model.Ability{Model: "matrix-new", Group: "default", ChannelId: 1, Enabled: true}).Error) var revision atomic.Int32 var failVendors atomic.Bool upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Contains(t, r.URL.Path, "/api/i18n/zh/newapi/") var payload any if strings.HasSuffix(r.URL.Path, "vendors.json") { if failVendors.Load() { w.WriteHeader(http.StatusServiceUnavailable) return } payload = []upstreamVendor{{Name: "Matrix vendor", Status: 1}} } else { payload = []upstreamModel{ {ModelName: "matrix-existing", Description: fmt.Sprintf("Upstream %d", revision.Load()), VendorName: "Matrix vendor", Tags: "changed", Status: 0, Endpoints: []byte(`{"openai":{"path":"/v1/chat/completions","method":"POST"}}`)}, {ModelName: "matrix-new", Description: "New model", VendorName: "Matrix vendor", Status: 0, Endpoints: []byte(`{"openai":"/v1/chat/completions"}`)}, {ModelName: "matrix-blocked", Description: "Do not overwrite", Status: 1}, {ModelName: "matrix-catalog", Description: "Catalog only", Status: 1}, } } encoded, err := common.Marshal(payload) require.NoError(t, err) _, _ = w.Write(encoded) })) defer upstream.Close() t.Setenv("SYNC_UPSTREAM_BASE", upstream.URL) t.Setenv("SYNC_HTTP_RETRY", "1") var preview struct { Success bool Data struct { Source metadataSyncSource Candidates []metadataSyncCandidate } } response := modelManagementRequest(t, SyncUpstreamPreview, "GET", "/api/models/sync_upstream/preview?locale=zh", nil, &preview) require.Equal(t, http.StatusOK, response.Code) require.True(t, preview.Success, response.Body.String()) byName := make(map[string]metadataSyncCandidate) for _, candidate := range preview.Data.Candidates { byName[candidate.ModelName] = candidate } assert.Equal(t, "site", byName["matrix-new"].Scope) assert.Equal(t, "catalog", byName["matrix-catalog"].Scope) assert.Equal(t, "blocked", byName["matrix-blocked"].Kind) var count int64 require.NoError(t, db.Model(&model.Vendor{}).Count(&count).Error) assert.Zero(t, count) body := map[string]any{"locale": "zh", "source_version": preview.Data.Source.Version, "selections": []model.MetadataSyncSelection{ {ModelName: "matrix-existing", RecordVersion: byName["matrix-existing"].RecordVersion, Fields: []string{"description", "endpoints"}}, {ModelName: "matrix-new", RecordVersion: byName["matrix-new"].RecordVersion, Create: true}, }} beforePricing, err := model.GetModelPricingSnapshot([]string{"matrix-priced"}) require.NoError(t, err) // A failed model insert must also undo the earlier metadata update // and the newly inserted supplier. require.NoError(t, db.Callback().Create().Before("gorm:create").Register("fail_metadata_matrix", func(tx *gorm.DB) { if tx.Statement.Table == "models" { tx.AddError(errors.New("injected metadata failure")) } })) response = modelManagementRequest(t, SyncUpstreamModels, "POST", "/api/models/sync_upstream", body, nil) assert.NotEqual(t, http.StatusOK, response.Code) require.NoError(t, db.Callback().Create().Remove("fail_metadata_matrix")) var persisted model.Model require.NoError(t, db.First(&persisted, local.Id).Error) assert.Equal(t, "Local description", persisted.Description) require.NoError(t, db.Model(&model.Vendor{}).Count(&count).Error) assert.Zero(t, count) var result struct { Success bool Data model.MetadataSyncResult } response = modelManagementRequest(t, SyncUpstreamModels, "POST", "/api/models/sync_upstream", body, &result) require.Equal(t, http.StatusOK, response.Code, response.Body.String()) require.True(t, result.Success) assert.Equal(t, []string{"matrix-new"}, result.Data.CreatedModels) assert.Equal(t, []string{"Matrix vendor"}, result.Data.CreatedVendors) require.NoError(t, db.First(&persisted, local.Id).Error) assert.Equal(t, "Upstream 0", persisted.Description) assert.Equal(t, "keep", persisted.Tags) assert.Equal(t, 1, persisted.Status) assert.Zero(t, persisted.VendorID) assert.JSONEq(t, `{"openai":{"path":"/v1/chat/completions","method":"POST"}}`, persisted.Endpoints) var created model.Model require.NoError(t, db.Where("model_name = ?", "matrix-new").First(&created).Error) assert.Equal(t, 1, created.SyncOfficial) assert.Zero(t, created.Status) afterPricing, err := model.GetModelPricingSnapshot([]string{"matrix-priced"}) require.NoError(t, err) assert.Equal(t, beforePricing.Entries, afterPricing.Entries) response = modelManagementRequest(t, SyncUpstreamModels, "POST", "/api/models/sync_upstream", body, nil) assert.Equal(t, http.StatusConflict, response.Code) response = modelManagementRequest(t, SyncUpstreamModels, "POST", "/api/models/sync_upstream", map[string]any{}, nil) assert.Equal(t, http.StatusBadRequest, response.Code) body["selections"] = []model.MetadataSyncSelection{{ModelName: "matrix-blocked", RecordVersion: byName["matrix-blocked"].RecordVersion, Fields: []string{"description"}}} response = modelManagementRequest(t, SyncUpstreamModels, "POST", "/api/models/sync_upstream", body, nil) assert.Equal(t, http.StatusBadRequest, response.Code) revision.Add(1) response = modelManagementRequest(t, SyncUpstreamModels, "POST", "/api/models/sync_upstream", body, nil) assert.Equal(t, http.StatusConflict, response.Code) failVendors.Store(true) modelManagementRequest(t, SyncUpstreamPreview, "GET", "/api/models/sync_upstream/preview?locale=zh", nil, &preview) assert.False(t, preview.Success) }) }) } } func TestMetadataSyncLocaleAndEndpointValidation(t *testing.T) { for _, locale := range []struct{ input, expected string }{{"zh", "zh"}, {"zh-CN", "zh"}, {"en", "en"}, {"ja", "ja"}} { t.Run(locale.input, func(t *testing.T) { normalized, ok := normalizeLocale(locale.input) assert.True(t, ok) assert.Equal(t, locale.expected, normalized) }) } _, valid := normalizeLocale("invalid") assert.False(t, valid) for _, endpoint := range []string{`1`, `null`, `{"openai":false}`, `{"openai":{"path":"invalid"}}`} { assert.Error(t, model.ValidateModelEndpoints(endpoint)) } } func TestVendorManagementDatabaseMatrix(t *testing.T) { for _, dialect := range []struct{ kind, env string }{{"sqlite", ""}, {"mysql", "TEST_MYSQL_DSN"}, {"postgres", "TEST_POSTGRES_DSN"}} { t.Run(dialect.kind, func(t *testing.T) { if dialect.env != "" && os.Getenv(dialect.env) == "" { t.Skip("set " + dialect.env) } db := modelManagementDB(t, dialect.kind, os.Getenv(dialect.env)) t.Run("pricing_reads_keep_default_brands_without_writing_vendors", func(t *testing.T) { channel := model.Channel{Name: "Vendor fixture", Type: 1, Status: common.ChannelStatusEnabled} require.NoError(t, db.Create(&channel).Error) require.NoError(t, db.Create(&model.Ability{Model: "gemini-vendor-fixture", Group: "default", ChannelId: channel.Id, Enabled: true}).Error) model.RefreshPricing() model.GetPricing() vendors := model.GetVendors() require.Len(t, vendors, 1) assert.Equal(t, "Google", vendors[0].Name) assert.Equal(t, "Gemini.Color", vendors[0].Icon) assert.Negative(t, vendors[0].ID) var count int64 require.NoError(t, db.Model(&model.Vendor{}).Count(&count).Error) assert.Zero(t, count) saved := model.Vendor{Name: "Google", Icon: "Gemini.Color"} require.NoError(t, saved.Insert()) assert.Equal(t, saved.Id, model.GetVendors()[0].ID) require.NoError(t, saved.Delete()) assert.Equal(t, vendors[0].ID, model.GetVendors()[0].ID) require.NoError(t, db.Model(&model.Vendor{}).Count(&count).Error) assert.Zero(t, count, "refresh must not recreate a deleted vendor") }) t.Run("metadata_ownership_preview_merge_delete_and_rollback", func(t *testing.T) { source := model.Vendor{Name: " Vendor Source ", Icon: "Gemini.Color"} target := model.Vendor{Name: "Vendor Target", Description: "Keep target", Icon: "OpenAI"} require.NoError(t, source.Insert()) require.NoError(t, target.Insert()) assert.Equal(t, "Vendor Source", source.Name) assert.Error(t, (&model.Vendor{Name: "vendor source"}).Insert()) assert.Error(t, (&model.Vendor{Name: " "}).Insert()) require.NoError(t, db.Model(&model.Vendor{}).Where("id = ?", source.Id).Update("status", 0).Error) loaded, err := model.GetVendorByID(source.Id) require.NoError(t, err) staleVersion := loaded.Version edit := model.Vendor{Id: source.Id, Name: source.Name, Description: "Updated source", Icon: source.Icon, Version: loaded.Version} require.NoError(t, edit.Update()) updated, err := model.GetVendorByID(source.Id) require.NoError(t, err) assert.Equal(t, loaded.CreatedTime, updated.CreatedTime) assert.Zero(t, updated.Status) edit.Version = staleVersion assert.ErrorIs(t, edit.Update(), model.ErrVendorConflict) one := model.Model{ModelName: "vendor-model-one", VendorID: source.Id, Icon: "Custom", Description: "Preserve description", Status: 0, SyncOfficial: 0} rule := model.Model{ModelName: "vendor-rule-", VendorID: source.Id, NameRule: model.NameRulePrefix, Status: 1, SyncOfficial: 1} require.NoError(t, one.Insert()) require.NoError(t, rule.Insert()) priceBefore, err := model.GetModelPricingSnapshot([]string{one.ModelName}) require.NoError(t, err) require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{{ModelName: one.ModelName, ExpectedVersion: priceBefore.Entries[0].Version, Pricing: model.PricingValues{"ModelPrice": float64(0.25)}}})) priceBefore, err = model.GetModelPricingSnapshot([]string{one.ModelName}) require.NoError(t, err) channel := model.Channel{Name: "Unchanged vendor channel", Type: 1, Status: common.ChannelStatusEnabled} require.NoError(t, db.Create(&channel).Error) ability := model.Ability{Model: one.ModelName, Group: "default", ChannelId: channel.Id, Enabled: true} require.NoError(t, db.Create(&ability).Error) linked, total, err := model.SearchVendors("", 0, 20, "linked") require.NoError(t, err) require.Len(t, linked, 1) assert.EqualValues(t, 1, total) assert.EqualValues(t, 2, linked[0].ModelCount) unlinked, _, err := model.SearchVendors("Vendor Target", 0, 20, "unlinked") require.NoError(t, err) require.Len(t, unlinked, 1) var references *model.VendorReferenceError err = model.DeleteVendors([]int{source.Id, target.Id}) require.ErrorAs(t, err, &references) assert.EqualValues(t, 2, references.Counts[source.Id]) _, err = model.GetVendorByID(target.Id) require.NoError(t, err, "bulk delete must not partially delete unreferenced vendors") var response struct { Success bool Code string ReferenceCounts map[int]int64 `json:"reference_counts"` } recorder := modelManagementRequest(t, PreviewVendorOperation, http.MethodPost, "/api/vendors/operations/preview", model.VendorOperation{Action: "delete", VendorIDs: []int{source.Id}}, &response) assert.Equal(t, http.StatusConflict, recorder.Code) assert.Equal(t, "VENDOR_REFERENCED", response.Code) assert.EqualValues(t, 2, response.ReferenceCounts[source.Id]) disappearing := model.Vendor{Name: "Preview target"} require.NoError(t, disappearing.Insert()) staleAssignment := model.VendorOperation{Action: "assign", ModelIDs: []int{one.Id}, TargetVendorID: disappearing.Id} stalePreview, err := model.PreviewVendorOperation(staleAssignment) require.NoError(t, err) staleAssignment.ExpectedVersion = stalePreview.Version require.NoError(t, disappearing.Delete()) recorder = modelManagementRequest(t, ApplyVendorOperation, http.MethodPost, "/api/vendors/operations", staleAssignment, &response) assert.Equal(t, http.StatusConflict, recorder.Code) assert.Equal(t, "VENDOR_CONFLICT", response.Code) assign := model.VendorOperation{Action: "assign", ModelIDs: []int{one.Id}, TargetVendorID: target.Id} preview, err := model.PreviewVendorOperation(assign) require.NoError(t, err) require.Len(t, preview.Models, 1) assign.ExpectedVersion = preview.Version one.Description = "Updated in the same timestamp" require.NoError(t, db.Model(&model.Model{}).Where("id = ?", one.Id).Update("description", one.Description).Error) _, err = model.ApplyVendorOperation(assign) assert.ErrorIs(t, err, model.ErrVendorConflict) preview, err = model.PreviewVendorOperation(assign) require.NoError(t, err) assign.ExpectedVersion = preview.Version target.Description = "New target description" require.NoError(t, target.Update()) _, err = model.ApplyVendorOperation(assign) assert.ErrorIs(t, err, model.ErrVendorConflict) preview, err = model.PreviewVendorOperation(assign) require.NoError(t, err) assign.ExpectedVersion = preview.Version result, err := model.ApplyVendorOperation(assign) require.NoError(t, err) assert.Equal(t, []int{one.Id}, result.UpdatedModels) var after model.Model after = model.Model{} require.NoError(t, db.First(&after, one.Id).Error) assert.Equal(t, target.Id, after.VendorID) assert.Equal(t, one.Description, after.Description) assert.Equal(t, one.Icon, after.Icon) assert.Equal(t, one.Status, after.Status) after = model.Model{} require.NoError(t, db.First(&after, rule.Id).Error) assert.Equal(t, source.Id, after.VendorID) assign.TargetVendorID = 0 preview, err = model.PreviewVendorOperation(assign) require.NoError(t, err) assign.ExpectedVersion = preview.Version _, err = model.ApplyVendorOperation(assign) require.NoError(t, err) after = model.Model{} require.NoError(t, db.First(&after, one.Id).Error) assert.Zero(t, after.VendorID) after.VendorID = -1001 assert.Error(t, after.Update(), "display-only vendors cannot become stored references") merge := model.VendorOperation{Action: "merge", VendorIDs: []int{source.Id}, TargetVendorID: target.Id} preview, err = model.PreviewVendorOperation(merge) require.NoError(t, err) merge.ExpectedVersion = preview.Version require.NoError(t, db.Callback().Delete().Before("gorm:delete").Register("vendor_delete_failure", func(tx *gorm.DB) { if tx.Statement.Table == "vendors" { tx.AddError(errors.New("injected vendor delete failure")) } })) _, err = model.ApplyVendorOperation(merge) require.Error(t, err) require.NoError(t, db.Callback().Delete().Remove("vendor_delete_failure")) after = model.Model{} require.NoError(t, db.First(&after, rule.Id).Error) assert.Equal(t, source.Id, after.VendorID, "ownership updates roll back when deletion fails") _, err = model.GetVendorByID(source.Id) require.NoError(t, err) _, err = model.ApplyVendorOperation(merge) require.NoError(t, err) _, err = model.GetVendorByID(source.Id) assert.ErrorIs(t, err, gorm.ErrRecordNotFound) retained, err := model.GetVendorByID(target.Id) require.NoError(t, err) assert.Equal(t, target.Description, retained.Description) assert.Equal(t, "OpenAI", retained.Icon) assert.EqualValues(t, 1, retained.ModelCount) after = one after.VendorID = source.Id assert.Error(t, after.Update(), "deleted vendors cannot acquire new references") priceAfter, err := model.GetModelPricingSnapshot([]string{one.ModelName}) require.NoError(t, err) assert.Equal(t, priceBefore.Entries[0], priceAfter.Entries[0], "assignment and merge preserve model pricing") var retainedChannel model.Channel require.NoError(t, db.First(&retainedChannel, channel.Id).Error) assert.Equal(t, channel.Name, retainedChannel.Name) assert.Equal(t, channel.Status, retainedChannel.Status) var retainedAbility model.Ability require.NoError(t, db.Where("model = ? AND channel_id = ?", one.ModelName, channel.Id).First(&retainedAbility).Error) assert.Equal(t, ability.Group, retainedAbility.Group) assert.Equal(t, ability.Enabled, retainedAbility.Enabled) }) t.Run("concurrent_create_and_delete_never_orphan_model", func(t *testing.T) { vendor := model.Vendor{Name: "Concurrent owner"} require.NoError(t, vendor.Insert()) var createErr, deleteErr error var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() createErr = (&model.Model{ModelName: "concurrent-owned-model", VendorID: vendor.Id}).Insert() }() go func() { defer wg.Done(); deleteErr = vendor.Delete() }() wg.Wait() if createErr == nil { require.Error(t, deleteErr) } else { require.NoError(t, deleteErr) } var models []model.Model require.NoError(t, db.Where("model_name = ?", "concurrent-owned-model").Find(&models).Error) if len(models) != 0 { _, err := model.GetVendorByID(models[0].VendorID) require.NoError(t, err) } }) }) } } func TestModelDeletionDatabaseMatrix(t *testing.T) { for _, dialect := range []struct{ kind, env string }{{"sqlite", ""}, {"mysql", "TEST_MYSQL_DSN"}, {"postgres", "TEST_POSTGRES_DSN"}} { t.Run(dialect.kind, func(t *testing.T) { if dialect.env != "" && os.Getenv(dialect.env) == "" { t.Skip("set " + dialect.env + " to run this database") } db := modelManagementDB(t, dialect.kind, os.Getenv(dialect.env)) var response struct { Success bool Data model.ModelDeleteResult } metadataOnly := model.Model{ModelName: "metadata-only", Status: 1} require.NoError(t, metadataOnly.Insert()) channel := model.Channel{Name: "Retained channel", Type: 1, Key: "fixture-key", Models: metadataOnly.ModelName, Group: "default", Status: common.ChannelStatusEnabled} require.NoError(t, db.Create(&channel).Error) require.NoError(t, channel.UpdateAbilities(db)) recorder := modelManagementRequest(t, func(c *gin.Context) { c.Params = gin.Params{{Key: "id", Value: strconv.Itoa(metadataOnly.Id)}} DeleteModelMeta(c) }, http.MethodDelete, "/api/models/"+strconv.Itoa(metadataOnly.Id), nil, &response) require.True(t, response.Success, recorder.Body.String()) assert.Equal(t, model.ModelDeleteResult{DeletedCount: 1}, response.Data) var retained model.Channel require.NoError(t, db.First(&retained, channel.Id).Error) assert.Equal(t, channel.Models, retained.Models) var count int64 require.NoError(t, db.Model(&model.Ability{}).Where("channel_id = ?", channel.Id).Count(&count).Error) assert.EqualValues(t, 1, count) for _, rule := range []int{model.NameRuleExact, model.NameRulePrefix, model.NameRuleContains, model.NameRuleSuffix} { t.Run(fmt.Sprintf("rule_%d_exact_names_only_atomic_and_cached", rule), func(t *testing.T) { name := fmt.Sprintf("delete-rule-%d", rule) first := model.Model{ModelName: name, NameRule: rule, Status: 1} second := model.Model{ModelName: name + "-second", Status: 1} require.NoError(t, first.Insert()) require.NoError(t, second.Insert()) mapping := `{"` + name + `":"upstream-name"}` priority, weight := int64(7), uint(9) channels := []model.Channel{ {Name: "Enabled", Type: 1, Key: "fixture-key", Models: name + "," + name + "-keep," + second.ModelName, Group: "default,vip", Status: common.ChannelStatusEnabled, ModelMapping: &mapping, Priority: &priority, Weight: &weight}, {Name: "Disabled", Type: 1, Models: name + ",prefix-" + name, Group: "disabled-group", Status: common.ChannelStatusManuallyDisabled}, {Name: "Last model", Type: 1, Models: name, Group: "last-model-group", Status: common.ChannelStatusEnabled}, {Name: "Case-sensitive name", Type: 1, Models: strings.ToUpper(name), Group: "case-group", Status: common.ChannelStatusEnabled}, } for i := range channels { require.NoError(t, db.Create(&channels[i]).Error) require.NoError(t, channels[i].UpdateAbilities(db)) } common.MemoryCacheEnabled = true model.InitChannelCache() cached, err := model.GetRandomSatisfiedChannel("default", name, 0, nil) require.NoError(t, err) require.NotNil(t, cached) baseline, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{{ModelName: name, ExpectedVersion: baseline.EmptyVersion, Pricing: model.PricingValues{"ModelPrice": float64(2)}}})) pricingBefore, err := model.GetModelPricingSnapshot([]string{name, second.ModelName}) require.NoError(t, err) if rule != model.NameRuleExact { _, err := model.DeleteModelMetadata([]int{first.Id, second.Id}, true, true) require.EqualError(t, err, "only exact-match models can be removed from channels") after, err := model.GetModelPricingSnapshot([]string{name, second.ModelName}) require.NoError(t, err) assert.Equal(t, pricingBefore, after) require.NoError(t, db.Model(&model.Model{}).Where("id IN ?", []int{first.Id, second.Id}).Count(&count).Error) assert.EqualValues(t, 2, count) for _, original := range channels { var after model.Channel require.NoError(t, db.First(&after, original.Id).Error) assert.Equal(t, original, after) } return } body := map[string]any{"model_ids": []int{first.Id, second.Id, first.Id}, "remove_from_channels": true} // A failure at the final metadata delete must undo every earlier // channel/ability write and keep the published cache intact. require.NoError(t, db.Callback().Delete().Before("gorm:delete").Register("fail_model_deletion", func(tx *gorm.DB) { if tx.Statement.Table == "models" { tx.AddError(errors.New("injected model deletion failure")) } })) result, err := model.DeleteModelMetadata([]int{first.Id, second.Id}, true, false) require.Error(t, err) assert.Zero(t, result) require.NoError(t, db.Callback().Delete().Remove("fail_model_deletion")) for _, original := range channels { var after model.Channel require.NoError(t, db.First(&after, original.Id).Error) assert.Equal(t, original, after) } cached, err = model.GetRandomSatisfiedChannel("default", name, 0, nil) require.NoError(t, err) require.NotNil(t, cached) recorder := modelManagementRequest(t, BatchDeleteModelMeta, http.MethodPost, "/api/models/delete", body, &response) require.True(t, response.Success, recorder.Body.String()) assert.Equal(t, model.ModelDeleteResult{DeletedCount: 2, UpdatedChannels: 3}, response.Data) for i, original := range channels { var after model.Channel require.NoError(t, db.First(&after, original.Id).Error) original.Models = []string{name + "-keep", "prefix-" + name, "", strings.ToUpper(name)}[i] assert.Equal(t, original, after, "only the model list changes") } var abilities []model.Ability require.NoError(t, db.Where("channel_id IN ?", []int{channels[0].Id, channels[1].Id, channels[2].Id, channels[3].Id}).Find(&abilities).Error) assert.Len(t, abilities, 4) for _, ability := range abilities { assert.NotEqual(t, name, ability.Model) assert.NotEqual(t, second.ModelName, ability.Model) assert.NotEmpty(t, ability.Model) if ability.ChannelId == channels[0].Id { assert.Equal(t, &priority, ability.Priority) assert.Equal(t, weight, ability.Weight) assert.True(t, ability.Enabled) } if ability.ChannelId == channels[1].Id { assert.False(t, ability.Enabled) } } for _, group := range []string{"default", "vip", "last-model-group"} { cached, _ = model.GetRandomSatisfiedChannel(group, name, 0, nil) assert.Nil(t, cached) } cached, err = model.GetRandomSatisfiedChannel("default", name+"-keep", 0, nil) require.NoError(t, err) require.NotNil(t, cached) pricingAfter, err := model.GetModelPricingSnapshot([]string{name, second.ModelName}) require.NoError(t, err) assert.Equal(t, pricingBefore, pricingAfter) require.NoError(t, db.Model(&model.Model{}).Where("id IN ?", []int{first.Id, second.Id}).Count(&count).Error) assert.Zero(t, count) _, err = model.DeleteModelMetadata([]int{first.Id}, true, false) assert.Error(t, err, "stale selections cannot delete newly created records") }) } for _, removeChannels := range []bool{false, true} { t.Run(fmt.Sprintf("pricing_removal_channels_%t", removeChannels), func(t *testing.T) { name := fmt.Sprintf("remove-pricing-%t", removeChannels) metadata := model.Model{ModelName: name, NameRule: model.NameRuleExact, Status: 1} require.NoError(t, metadata.Insert()) keep := name + "-keep" channel := model.Channel{Name: "Independent pricing removal", Type: 1, Models: name + "," + keep, Group: "pricing-removal", Status: common.ChannelStatusEnabled} require.NoError(t, db.Create(&channel).Error) require.NoError(t, channel.UpdateAbilities(db)) model.InitChannelCache() baseline, err := model.GetModelPricingSnapshot([]string{name, keep}) require.NoError(t, err) pricing := model.PricingValues{"ModelRatio": float64(1), "ModelPrice": float64(0), "CompletionRatio": float64(2), "CacheRatio": float64(0.1), "CreateCacheRatio": float64(1.25), "ImageRatio": float64(3), "AudioRatio": float64(4), "AudioCompletionRatio": float64(5), "billing_setting.billing_mode": "tiered_expr", "billing_setting.billing_expr": `tier("base", p * 2 + c * 4)`} require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{ {ModelName: name, ExpectedVersion: baseline.EmptyVersion, Pricing: pricing}, {ModelName: keep, ExpectedVersion: baseline.EmptyVersion, Pricing: model.PricingValues{"ModelPrice": float64(9)}}, })) before, err := model.GetModelPricingSnapshot([]string{name, keep}) require.NoError(t, err) body := map[string]any{"model_ids": []int{metadata.Id}, "remove_from_channels": removeChannels, "remove_pricing": true} for _, single := range []bool{false, true} { recorder := modelManagementRequest(t, func(c *gin.Context) { c.Set("role", common.RoleAdminUser) if single { c.Params = gin.Params{{Key: "id", Value: strconv.Itoa(metadata.Id)}} DeleteModelMeta(c) } else { BatchDeleteModelMeta(c) } }, http.MethodPost, "/api/models/delete?remove_pricing=true", body, nil) assert.Equal(t, http.StatusForbidden, recorder.Code, "pricing permissions cannot be bypassed through deletion") } updates := 0 require.NoError(t, db.Callback().Update().Before("gorm:update").Register("fail_deleted_pricing", func(tx *gorm.DB) { if tx.Statement.Table == "options" { updates++ if updates == 3 { tx.AddError(errors.New("injected pricing deletion failure")) } } })) _, err = model.DeleteModelMetadata([]int{metadata.Id}, removeChannels, true) require.Error(t, err) require.NoError(t, db.Callback().Update().Remove("fail_deleted_pricing")) after, err := model.GetModelPricingSnapshot([]string{name, keep}) require.NoError(t, err) assert.Equal(t, before, after, "partial option writes roll back") assert.Equal(t, billing_setting.BillingModeTieredExpr, billing_setting.GetBillingMode(name), "failed deletion must not publish new runtime pricing") var retained model.Model require.NoError(t, db.First(&retained, metadata.Id).Error) var channelAfter model.Channel require.NoError(t, db.First(&channelAfter, channel.Id).Error) assert.Equal(t, channel.Models, channelAfter.Models) _, err = model.DeleteModelMetadata([]int{metadata.Id, 999999}, removeChannels, true) assert.Error(t, err, "a missing model aborts the whole batch") recorder := modelManagementRequest(t, BatchDeleteModelMeta, http.MethodPost, "/api/models/delete", body, &response) require.True(t, response.Success, recorder.Body.String()) after, err = model.GetModelPricingSnapshot([]string{name, keep}) require.NoError(t, err) assert.Empty(t, after.Entries[0].Configured) assert.Equal(t, before.Entries[1], after.Entries[1], "name rules do not expand pricing deletion") assert.Equal(t, billing_setting.BillingModeRatio, billing_setting.GetBillingMode(name)) _, hasExpr := billing_setting.GetBillingExpr(name) assert.False(t, hasExpr) require.NoError(t, db.First(&channelAfter, channel.Id).Error) expectedModels := channel.Models if removeChannels { expectedModels = keep } assert.Equal(t, expectedModels, channelAfter.Models) require.NoError(t, db.Model(&model.Model{}).Where("id = ?", metadata.Id).Count(&count).Error) assert.Zero(t, count) }) } for _, ids := range [][]int{nil, {0}, {-1}, make([]int, 1001)} { _, err := model.DeleteModelMetadata(ids, true, false) assert.Error(t, err) } }) } } func TestSharedModelPluginPricingDatabaseMatrix(t *testing.T) { const name = "shared-model::priced" const base = `tier("base", u("seconds") * 0.4)` const variant = `tier("beta", u("credits") * 2)` for _, dialect := range []struct{ kind, env string }{{"sqlite", ""}, {"mysql", "TEST_MYSQL_DSN"}, {"postgres", "TEST_POSTGRES_DSN"}} { t.Run(dialect.kind, func(t *testing.T) { if dialect.env != "" && os.Getenv(dialect.env) == "" { t.Skip("set " + dialect.env + " to run this database") } db := modelManagementDB(t, dialect.kind, os.Getenv(dialect.env)) for _, spec := range []struct{ key, field, unit string }{{"matrix-alpha", "seconds", "second"}, {"matrix-beta", "credits", "credit"}} { source := fmt.Sprintf(` export const meta = {apiVersion:1,key:%q,name:%q,version:"1.0.0",author:{name:"Test"},models:[%q],fetchMode:"per_task",usageSchema:{%s:{type:"number",unit:%q}}}; export function buildSubmitRequest(){return {};} export function parseSubmitResponse(){return {};} export function buildQueryRequest(){return {};} export function parseTaskResult(){return {};} `, spec.key, spec.key, name, spec.field, spec.unit) _, err := jsplugin.DefaultRegistry.Register(source, jsplugin.Options{}) require.NoError(t, err) t.Cleanup(func() { jsplugin.DefaultRegistry.Unregister(spec.key) }) } // Representative existing options have no plugin-expression row. baseJSON, err := common.Marshal(map[string]string{name: base}) require.NoError(t, err) modeJSON, err := common.Marshal(map[string]string{name: "tiered_expr"}) require.NoError(t, err) require.NoError(t, db.Create(&[]model.Option{ {Key: "ModelPrice", Value: `{"unrelated-model":0.75}`}, {Key: "billing_setting.billing_expr", Value: string(baseJSON)}, {Key: "billing_setting.billing_mode", Value: string(modeJSON)}, }).Error) require.NoError(t, config.GlobalConfig.LoadFromDB(map[string]string{ "billing_setting.billing_expr": string(baseJSON), "billing_setting.billing_mode": string(modeJSON), })) before, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) require.Len(t, before.Entries, 1) require.Len(t, before.Entries[0].PluginVariants, 2) assert.True(t, before.Entries[0].PluginVariants[0].Compatible) assert.False(t, before.Entries[0].PluginVariants[1].Compatible) assert.Equal(t, base, before.Entries[0].PluginVariants[1].Effective) change := model.ModelPricingChange{ModelName: name, ExpectedVersion: before.Entries[0].Version, Pricing: model.PricingValues{ "billing_setting.billing_mode": "tiered_expr", "billing_setting.billing_expr": base, billing_setting.PluginBillingExprOption: map[string]any{"matrix-beta": variant}, }} require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) loaded, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Equal(t, change.Pricing, loaded.Entries[0].Configured) require.Len(t, loaded.Entries[0].PluginVariants, 2) assert.True(t, loaded.Entries[0].PluginVariants[0].Compatible) assert.Equal(t, base, loaded.Entries[0].PluginVariants[0].Effective) assert.True(t, loaded.Entries[0].PluginVariants[1].Compatible) assert.Equal(t, variant, loaded.Entries[0].PluginVariants[1].Configured) var flat map[string]string require.NoError(t, common.UnmarshalJsonStr(loaded.Options[billing_setting.PluginBillingExprOption], &flat)) assert.Equal(t, map[string]string{"matrix-beta::" + name: variant}, flat) all, err := model.GetModelPricingSnapshot(nil) require.NoError(t, err) for _, entry := range all.Entries { assert.NotEqual(t, "matrix-beta::"+name, entry.ModelName) } unrelated, err := model.GetModelPricingSnapshot([]string{"unrelated-model"}) require.NoError(t, err) assert.Equal(t, float64(0.75), unrelated.Entries[0].Configured["ModelPrice"]) assert.NotContains(t, billing_setting.GetPricingSyncData(map[string]any{}), "plugin_billing_expr") assert.NotContains(t, billing_setting.GetPricingSyncData(map[string]any{})["billing_expr"], "matrix-beta::"+name) // Restart/re-read is idempotent; a second save based on its version succeeds. require.NoError(t, db.AutoMigrate(&model.Option{})) change.ExpectedVersion = loaded.Entries[0].Version require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) repeated, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Equal(t, loaded.Entries[0].Version, repeated.Entries[0].Version) // Changing only the provider price changes the model version. change.Pricing[billing_setting.PluginBillingExprOption] = map[string]any{"matrix-beta": `tier("free", u("credits") * 0)`} require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) updated, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.NotEqual(t, loaded.Entries[0].Version, updated.Entries[0].Version) assert.ErrorIs(t, model.UpdateModelPricing([]model.ModelPricingChange{change}), model.ErrModelPricingConflict) // The legacy option API validates the full draft and cannot drop a required override. response := modelManagementRequest(t, UpdateOption, http.MethodPut, "/api/option/", OptionUpdateRequest{Key: billing_setting.PluginBillingExprOption, Value: `{}`}, nil) assert.Contains(t, response.Body.String(), `"success":false`) assert.Contains(t, response.Body.String(), "matrix-beta") afterFailure, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Equal(t, updated.Entries[0].Version, afterFailure.Entries[0].Version) // Model-level legacy saves also skip providers with a stored override. response = modelManagementRequest(t, UpdateOption, http.MethodPut, "/api/option/", OptionUpdateRequest{Key: "billing_setting.billing_expr", Value: string(baseJSON)}, nil) assert.Contains(t, response.Body.String(), `"success":true`) flat["matrix-beta::"+name] = variant raw, err := common.Marshal(flat) require.NoError(t, err) response = modelManagementRequest(t, UpdateOption, http.MethodPut, "/api/option/", OptionUpdateRequest{Key: billing_setting.PluginBillingExprOption, Value: string(raw)}, nil) assert.Contains(t, response.Body.String(), `"success":true`) final, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Equal(t, variant, final.Entries[0].PluginVariants[1].Effective) // A provider may disappear without making every legacy price save fail. require.NoError(t, jsplugin.DefaultRegistry.Unregister("matrix-beta")) stale, err := model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) require.Len(t, stale.Entries[0].PluginVariants, 2) assert.True(t, stale.Entries[0].PluginVariants[1].Stale) assert.Equal(t, variant, stale.Entries[0].PluginVariants[1].Configured) assert.Empty(t, stale.Entries[0].PluginVariants[1].Effective) _, err = model.PreviewModelPricing(name, stale.Entries[0].Configured) require.NoError(t, err) ratioJSON, err := common.Marshal(map[string]float64{name: 2}) require.NoError(t, err) response = modelManagementRequest(t, UpdateOption, http.MethodPut, "/api/option/", OptionUpdateRequest{Key: "ModelRatio", Value: string(ratioJSON)}, nil) assert.Contains(t, response.Body.String(), `"success":true`) response = modelManagementRequest(t, UpdateOption, http.MethodPut, "/api/option/", OptionUpdateRequest{Key: "billing_setting.billing_expr", Value: string(baseJSON)}, nil) assert.Contains(t, response.Body.String(), `"success":true`) final, err = model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Equal(t, variant, final.Entries[0].PluginVariants[1].Configured) // The versioned save also preserves an unchanged stale expression. change = model.ModelPricingChange{ModelName: name, ExpectedVersion: final.Entries[0].Version, Pricing: final.Entries[0].Configured} require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{change})) change.Pricing[billing_setting.PluginBillingExprOption] = map[string]any{"matrix-beta": "1"} // Even if this process's cache is ahead of storage, a changed stale // override is validated against the locked database value. tamperedJSON, err := common.Marshal(map[string]string{"matrix-beta::" + name: "1"}) require.NoError(t, err) require.NoError(t, config.GlobalConfig.LoadFromDB(map[string]string{billing_setting.PluginBillingExprOption: string(tamperedJSON)})) require.ErrorContains(t, model.UpdateModelPricing([]model.ModelPricingChange{change}), "does not declare this model") require.NoError(t, config.GlobalConfig.LoadFromDB(map[string]string{billing_setting.PluginBillingExprOption: final.Options[billing_setting.PluginBillingExprOption]})) // Updating a plugin to stop declaring this model also leaves the // stored override visible and inert, even with no active providers. _, err = jsplugin.DefaultRegistry.Register(` export const meta = {apiVersion:1,key:"matrix-beta",name:"Beta updated",version:"2.0.0",author:{name:"Test"},models:["replacement-model"],fetchMode:"per_task",usageSchema:{credits:{type:"number",unit:"credit"}}}; export function buildSubmitRequest(){return {};} export function parseSubmitResponse(){return {};} export function buildQueryRequest(){return {};} export function parseTaskResult(){return {};} `, jsplugin.Options{}) require.NoError(t, err) require.NoError(t, jsplugin.DefaultRegistry.Unregister("matrix-alpha")) stale, err = model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) require.Len(t, stale.Entries[0].PluginVariants, 1) assert.True(t, stale.Entries[0].PluginVariants[0].Stale) assert.Equal(t, "Beta updated", stale.Entries[0].PluginVariants[0].PluginName) require.NoError(t, model.UpdateModelPricingOptions(map[string]string{"ModelRatio": string(ratioJSON)})) // Removing just the stale override succeeds and leaves other prices. response = modelManagementRequest(t, UpdateOption, http.MethodPut, "/api/option/", OptionUpdateRequest{Key: billing_setting.PluginBillingExprOption, Value: `{}`}, nil) assert.Contains(t, response.Body.String(), `"success":true`) final, err = model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Empty(t, final.Entries[0].PluginVariants) assert.Equal(t, base, final.Entries[0].Configured["billing_setting.billing_expr"]) // A complete reset removes all provider keys, including models containing ::. require.NoError(t, model.UpdateModelPricing([]model.ModelPricingChange{{ModelName: name, ExpectedVersion: final.Entries[0].Version, Reset: true}})) final, err = model.GetModelPricingSnapshot([]string{name}) require.NoError(t, err) assert.Empty(t, final.Entries[0].Configured) assert.Equal(t, "{}", final.Options[billing_setting.PluginBillingExprOption]) }) } }