fix(models): 系统渠道模型 - 上游 ID 重命名级联价格档并补管理端可见性 (#514)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
c91c07f8f3
8 files changed
+222
-5
No files matched your search
@@ -0,0 +1,107 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
// 模型级上游键重命名后,任务请求必须使用新键;价格档中与旧值相同的上游键属于
|
||||
// “跟随模型默认”的隐式固化,保存时必须级联跟随(见 SaveAdminChannelModel)。
|
||||
func upstreamRenameFixture(t *testing.T) (*Service, *model.User, *model.ModelChannel) {
|
||||
t.Helper()
|
||||
svc, db := newChannelModelTestService(t)
|
||||
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
|
||||
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "model-gateway", BaseURL: "https://example.com/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
|
||||
if err := db.Create(&channel).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return svc, admin, &channel
|
||||
}
|
||||
|
||||
func TestSaveAdminChannelModelCascadesUpstreamKeyToMatchingTiers(t *testing.T) {
|
||||
svc, admin, channel := upstreamRenameFixture(t)
|
||||
enabled := true
|
||||
first, err := svc.SaveAdminChannelModel(admin, channel.ID, "", ChannelModelRequest{
|
||||
ModelKey: "deepseek-v4-flash", ProviderModelKey: "deepseek-v4-flash", DisplayName: "deepseek-v4-flash",
|
||||
Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion),
|
||||
CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4-flash"),
|
||||
PriceTiers: []ChannelModelPriceTierRequest{
|
||||
{Selector: map[string]string{}, ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled},
|
||||
{Selector: map[string]string{"operation": "text_generation"}, ProviderModelKey: "deepseek-v4-flash-preview", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled},
|
||||
},
|
||||
Enabled: &enabled,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("initial save failed: %v", err)
|
||||
}
|
||||
|
||||
// 管理端表单会把已存档位键原样回传(统一价格档的上游键字段不可见),重命名顶层上游键。
|
||||
renamed, err := svc.SaveAdminChannelModel(admin, channel.ID, first.ID, ChannelModelRequest{
|
||||
ModelKey: "deepseek-v4.1-flash", ProviderModelKey: "deepseek-v4.1-flash", DisplayName: "deepseek-v4.1-flash",
|
||||
Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion),
|
||||
CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4.1-flash"),
|
||||
PriceTiers: []ChannelModelPriceTierRequest{
|
||||
{Selector: map[string]string{}, ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled},
|
||||
{Selector: map[string]string{"operation": "text_generation"}, ProviderModelKey: "deepseek-v4-flash-preview", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled},
|
||||
},
|
||||
Enabled: &enabled,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("rename save failed: %v", err)
|
||||
}
|
||||
if len(renamed.PriceTiers) != 2 {
|
||||
t.Fatalf("price tiers = %#v, want 2", renamed.PriceTiers)
|
||||
}
|
||||
for _, tier := range renamed.PriceTiers {
|
||||
if tier.ProviderModelKey == "deepseek-v4-flash" {
|
||||
t.Fatalf("tier %s still carries the retired upstream key deepseek-v4-flash; it must follow the model rename", tier.ID)
|
||||
}
|
||||
}
|
||||
// 管理员显式配置的其他上游 SKU 不能被级联覆盖。
|
||||
keptDistinct := false
|
||||
for _, tier := range renamed.PriceTiers {
|
||||
if tier.ProviderModelKey == "deepseek-v4-flash-preview" {
|
||||
keptDistinct = true
|
||||
}
|
||||
}
|
||||
if !keptDistinct {
|
||||
t.Fatalf("distinct tier upstream key must survive the rename: %#v", renamed.PriceTiers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenamedUpstreamKeyReachesSystemChannelRequests(t *testing.T) {
|
||||
svc, admin, channel := upstreamRenameFixture(t)
|
||||
enabled := true
|
||||
first, err := svc.SaveAdminChannelModel(admin, channel.ID, "", ChannelModelRequest{
|
||||
ModelKey: "deepseek-v4-flash", ProviderModelKey: "deepseek-v4-flash", DisplayName: "deepseek-v4-flash",
|
||||
Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion),
|
||||
CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4-flash"),
|
||||
PriceTiers: []ChannelModelPriceTierRequest{{ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled}},
|
||||
Enabled: &enabled,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("initial save failed: %v", err)
|
||||
}
|
||||
if _, err := svc.SaveAdminChannelModel(admin, channel.ID, first.ID, ChannelModelRequest{
|
||||
ModelKey: "deepseek-v4.1-flash", ProviderModelKey: "deepseek-v4.1-flash", DisplayName: "deepseek-v4.1-flash",
|
||||
Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion),
|
||||
CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4.1-flash"),
|
||||
PriceTiers: []ChannelModelPriceTierRequest{{ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled}},
|
||||
Enabled: &enabled,
|
||||
}); err != nil {
|
||||
t.Fatalf("rename save failed: %v", err)
|
||||
}
|
||||
|
||||
// 用户在画布 Agent 选中新模型发起任务;config 形状与 cloud_agent.go 组装一致。
|
||||
resolved, err := svc.resolveSystemChannelModelSelection(map[string]any{
|
||||
"config": map[string]any{"channelId": channel.ID, "model": "deepseek-v4.1-flash", "channelModelKey": "deepseek-v4.1-flash"},
|
||||
}, "canvas_text", "")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveSystemChannelModelSelection() error = %v", err)
|
||||
}
|
||||
config := resolved["config"].(map[string]any)
|
||||
if got := config["providerModelKey"]; got != "deepseek-v4.1-flash" {
|
||||
t.Fatalf("providerModelKey = %v, want deepseek-v4.1-flash (user-selected model must reach the upstream request)", got)
|
||||
}
|
||||
}
|
||||
@@ -326,13 +326,24 @@ func (s *Service) SaveAdminChannelModel(actor *model.User, channelID string, id
|
||||
return nil, err
|
||||
}
|
||||
item := &model.ChannelModel{ID: modelID, ChannelID: channelID, Enabled: true, PriceVersion: 1}
|
||||
previousProviderModelKey := ""
|
||||
if id != "" {
|
||||
item, err = s.repo.ChannelModelByID(channelID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previousProviderModelKey = strings.TrimPrefix(strings.TrimSpace(item.ProviderModelKey), "models/")
|
||||
item.PriceVersion++
|
||||
}
|
||||
// 模型级上游键重命名时,与旧值相同的档位键属于“跟随模型默认”的隐式固化,必须级联跟随;
|
||||
// 否则任务请求会继续把旧上游键发给供应商。管理员显式配置的其他上游 SKU 不受影响。
|
||||
if previousProviderModelKey != "" && providerModelKey != previousProviderModelKey {
|
||||
for index := range tiers {
|
||||
if strings.TrimPrefix(strings.TrimSpace(tiers[index].ProviderModelKey), "models/") == previousProviderModelKey {
|
||||
tiers[index].ProviderModelKey = providerModelKey
|
||||
}
|
||||
}
|
||||
}
|
||||
item.ModelKey = modelKey
|
||||
item.ProviderModelKey = providerModelKey
|
||||
item.DisplayName = strings.TrimSpace(req.DisplayName)
|
||||
|
||||
Reference in new issue
Block a user