-
Notifications
You must be signed in to change notification settings - Fork 11.2k
feat(model): auto create vendor and bind model metadata on channel insert/update #6742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package model | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/constant" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestEnsureVendorAndAutoBindModelsForNewChannel(t *testing.T) { | ||
| require.NoError(t, DB.AutoMigrate(&Channel{}, &Ability{}, &Vendor{}, &Model{})) | ||
|
|
||
| uniqueChannelName := fmt.Sprintf("AutoVendorChan_%d", common.GetTimestamp()) | ||
| testModel1 := fmt.Sprintf("auto-test-model-1-%d", common.GetTimestamp()) | ||
| testModel2 := fmt.Sprintf("auto-test-model-2-%d", common.GetTimestamp()) | ||
|
|
||
| channel := &Channel{ | ||
| Type: constant.ChannelTypeAdvancedCustom, | ||
| Name: uniqueChannelName, | ||
| Key: "sk-test-key", | ||
| Models: fmt.Sprintf("%s,%s", testModel1, testModel2), | ||
| Status: common.ChannelStatusEnabled, | ||
| } | ||
|
|
||
| err := channel.Insert() | ||
| require.NoError(t, err) | ||
|
|
||
| // 1. Verify Vendor was auto-created | ||
| var vendor Vendor | ||
| err = DB.Where("name = ? AND deleted_at IS NULL", uniqueChannelName).First(&vendor).Error | ||
| require.NoError(t, err) | ||
| assert.Equal(t, uniqueChannelName, vendor.Name) | ||
| assert.Equal(t, 1, vendor.Status) | ||
|
|
||
| // 2. Verify model metadata was auto-created and bound to vendor.Id | ||
| var meta1 Model | ||
| err = DB.Where("model_name = ? AND deleted_at IS NULL", testModel1).First(&meta1).Error | ||
| require.NoError(t, err) | ||
| assert.Equal(t, vendor.Id, meta1.VendorID) | ||
|
|
||
| var meta2 Model | ||
| err = DB.Where("model_name = ? AND deleted_at IS NULL", testModel2).First(&meta2).Error | ||
| require.NoError(t, err) | ||
| assert.Equal(t, vendor.Id, meta2.VendorID) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| package model | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/constant" | ||
|
|
||
| "gorm.io/gorm" | ||
| ) | ||
|
|
@@ -86,3 +91,123 @@ func SearchVendors(keyword string, offset int, limit int) ([]*Vendor, int64, err | |
| } | ||
| return vendors, total, nil | ||
| } | ||
|
|
||
| func resolveVendorNameAndIcon(channel *Channel) (string, string) { | ||
| if channel == nil { | ||
| return "Custom", "Globe" | ||
| } | ||
|
|
||
| name := strings.TrimSpace(channel.Name) | ||
| switch channel.Type { | ||
| case constant.ChannelTypeAdvancedCustom, constant.ChannelTypeNewAPI, constant.ChannelTypeSub2API, constant.ChannelTypeCustom: | ||
| if name != "" { | ||
| return name, "Globe" | ||
| } | ||
| return constant.GetChannelTypeName(channel.Type), "Globe" | ||
| default: | ||
| typeName := constant.GetChannelTypeName(channel.Type) | ||
| if typeName != "" && typeName != "Unknown" { | ||
| return typeName, "Globe" | ||
| } | ||
| if name != "" { | ||
| return name, "Globe" | ||
| } | ||
| return "Custom", "Globe" | ||
| } | ||
| } | ||
|
|
||
| // EnsureVendorForChannel 检查并自动创建渠道对应的供应商记录,返回 Vendor ID | ||
| func EnsureVendorForChannel(channel *Channel, tx *gorm.DB) (int, error) { | ||
| if channel == nil { | ||
| return 0, nil | ||
| } | ||
|
|
||
| name, icon := resolveVendorNameAndIcon(channel) | ||
| if name == "" { | ||
| return 0, nil | ||
| } | ||
|
|
||
| useDB := DB | ||
| if tx != nil { | ||
| useDB = tx | ||
| } | ||
|
|
||
| var vendor Vendor | ||
| err := useDB.Where("name = ? AND deleted_at IS NULL", name).First(&vendor).Error | ||
| if err == nil { | ||
| return vendor.Id, nil | ||
| } | ||
|
|
||
| if errors.Is(err, gorm.ErrRecordNotFound) { | ||
| newVendor := Vendor{ | ||
| Name: name, | ||
| Description: fmt.Sprintf("%s 渠道自动创建供应商", name), | ||
| Icon: icon, | ||
| Status: 1, | ||
| CreatedTime: common.GetTimestamp(), | ||
| UpdatedTime: common.GetTimestamp(), | ||
| } | ||
| if err := useDB.Create(&newVendor).Error; err != nil { | ||
| return 0, err | ||
| } | ||
| return newVendor.Id, nil | ||
|
Comment on lines
+136
to
+153
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Prevent duplicate active vendor rows.
Use a portable active-name uniqueness strategy. Then handle a conflicting create by loading the existing vendor. As per coding guidelines, database code must support SQLite, MySQL >= 5.7.8, and PostgreSQL >= 9.6. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
|
|
||
| return 0, err | ||
| } | ||
|
|
||
| // AutoBindChannelModelsToVendor 为渠道拥有的模型自动补充元数据并绑定 Vendor ID | ||
| func AutoBindChannelModelsToVendor(channel *Channel, vendorID int, tx *gorm.DB) error { | ||
| if channel == nil || channel.Models == "" || vendorID <= 0 { | ||
| return nil | ||
| } | ||
|
|
||
| useDB := DB | ||
| if tx != nil { | ||
| useDB = tx | ||
| } | ||
|
|
||
| models := strings.Split(channel.Models, ",") | ||
| now := common.GetTimestamp() | ||
|
|
||
| for _, modelName := range models { | ||
| modelName = strings.TrimSpace(modelName) | ||
| if modelName == "" { | ||
| continue | ||
| } | ||
|
|
||
| var existing Model | ||
| err := useDB.Where("model_name = ? AND deleted_at IS NULL", modelName).First(&existing).Error | ||
| if errors.Is(err, gorm.ErrRecordNotFound) { | ||
| newModel := Model{ | ||
| ModelName: modelName, | ||
| VendorID: vendorID, | ||
| Status: 1, | ||
| SyncOfficial: 0, | ||
| Endpoints: `["openai"]`, | ||
| CreatedTime: now, | ||
| UpdatedTime: now, | ||
| } | ||
| if err := useDB.Create(&newModel).Error; err != nil { | ||
| common.SysError(fmt.Sprintf("auto bind model failed: %s, err: %v", modelName, err)) | ||
| } | ||
| } else if err == nil && existing.VendorID == 0 { | ||
| useDB.Model(&existing).Update("vendor_id", vendorID) | ||
| } | ||
|
Comment on lines
+180
to
+196
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Do not report successful synchronization after a metadata write fails.
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // EnsureChannelVendorAndModels 自动确保渠道对应的供应商和模型元数据已关联 | ||
| func EnsureChannelVendorAndModels(channel *Channel, tx *gorm.DB) error { | ||
| if channel == nil { | ||
| return nil | ||
| } | ||
| vendorID, err := EnsureVendorForChannel(channel, tx) | ||
| if err != nil { | ||
| common.SysError(fmt.Sprintf("EnsureVendorForChannel failed for channel %d (%s): %v", channel.Id, channel.Name, err)) | ||
| return err | ||
| } | ||
| return AutoBindChannelModelsToVendor(channel, vendorID, tx) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,8 @@ var ModelList = []string{ | |
| // stable version | ||
| "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", | ||
| "gemini-2.0-flash-001", "gemini-2.0-flash-lite-001", "gemini-2.0-flash-lite", | ||
| "gemini-2.5-flash-lite", | ||
| "gemini-2.5-flash-lite", "gemini-3.1-flash-lite", "gemini-3.5-flash-lite", | ||
| "gemini-3.6", "gemini-3.6-flash", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify whether an explicit Gemini upstream-name translation exists.
ast-grep outline relay/channel/gemini --items all
rg -n --type=go -C 5 'gemini-3\.6|UpstreamModelName|OriginModelName|ModelList' .Repository: QuantumNous/new-api Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Gemini files:\n'
git ls-files 'relay/channel/gemini/*'
printf '\nGemini constants/adaptor outline/selected content:\n'
sed -n '1,80p' relay/channel/gemini/constant.go
printf '\n'
sed -n '1,260p' relay/channel/gemini/adaptor.go
printf '\nRelevant ratio settings:\n'
sed -n '1,45p' setting/ratio_setting/cache_ratio.go
sed -n '180,210p' setting/ratio_setting/model_ratio.go
sed -n '330,348p' setting/ratio_setting/model_ratio.go
sed -n '580,602p' setting/ratio_setting/model_ratio.go
sed -n '24,38p' setting/ratio_setting/model_ratio_test.go
printf '\nFocused upstream-name translation/search for Gemini 3 identifiers:\n'
rg -n --type=go -C 3 'gemini-3\.6|gemini-3\.6-flash|UpstreamModelName|OriginModelName|ConvertOpenAIRequest|RequestURL.*gemini|Info\.UpstreamModelName|body\.Model' relay setting/dto relay/channel/gemini | sed -n '1,240p'Repository: QuantumNous/new-api Length of output: 32367 🌐 Web query:
💡 Result: As of August 10, 2026, Gemini 3.6 Flash is a generally available (GA) model in the Gemini API and the Gemini Enterprise Agent Platform [1][2][3]. Gemini API Pricing for Gemini 3.6 Flash (Paid Tier) The following pricing applies per 1 million tokens for the gemini-3.6-flash model [4]: Input Price: $1.50 per 1 million tokens [4]. Output Price: $7.50 per 1 million tokens (includes thinking tokens) [4]. Context Caching Storage Price: $1.00 per 1 million tokens per hour [4]. Grounding with Google Search/Maps: The first 5,000 requests per month are free (shared across the Gemini 3 model family); subsequent requests are $14.00 per 1,000 search queries [4]. Key Model Details Model ID: gemini-3.6-flash [1][2] Release Date: July 21, 2026 [2][3] Context Window: 1,048,576 tokens [1][2] Max Output Tokens: 65,536 tokens [1][2] Capabilities: Includes support for function calling, structured outputs, search grounding, context caching, and code execution [1][2]. Note: There is no Free Tier for gemini-3.6-flash in the Gemini API [4]. While a free tier exists for some models within the Gemini API, pricing and availability depend on the specific model selected [5]. You can verify the most current rates and specific model availability directly through the official Gemini API pricing documentation [4]. Citations:
Remove the unsupported upstream identifier
📍 Affects 4 files
🤖 Prompt for AI Agents |
||
| "gemini-3-pro-image", "gemini-3.1-flash-image", | ||
| // latest version | ||
| "gemini-flash-latest", "gemini-flash-lite-latest", "gemini-pro-latest", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package ratio_setting_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/QuantumNous/new-api/setting/ratio_setting" | ||
| ) | ||
|
|
||
| func TestGemini3ModelRatios(t *testing.T) { | ||
| ratio_setting.InitRatioSettings() | ||
|
|
||
| tests := []struct { | ||
| model string | ||
| expectedModelRatio float64 | ||
| expectedCompRatio float64 | ||
|
Comment on lines
+15
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Test the cache-ratio billing invariant. These cases do not call Proposed test update tests := []struct {
model string
expectedModelRatio float64
expectedCompRatio float64
+ expectedCacheRatio float64
}{
{
model: "gemini-3.1-flash-lite",
expectedModelRatio: 0.125,
expectedCompRatio: 6.0,
+ expectedCacheRatio: 0.1,
}, compRatio := ratio_setting.GetCompletionRatio(tt.model)
assert.InDelta(t, tt.expectedCompRatio, compRatio, 0.0001, "completion ratio mismatch for %s", tt.model)
+
+ cacheRatio, ok := ratio_setting.GetCacheRatio(tt.model)
+ require.True(t, ok, "cache ratio for %s should exist", tt.model)
+ assert.InDelta(t, tt.expectedCacheRatio, cacheRatio, 0.0001, "cache ratio mismatch for %s", tt.model)As per coding guidelines, “Backend tests must protect … billing/accounting invariants.” Also applies to: 42-49 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| }{ | ||
| { | ||
| model: "gemini-3.1-flash-lite", | ||
| expectedModelRatio: 0.125, | ||
| expectedCompRatio: 6.0, | ||
| }, | ||
| { | ||
| model: "gemini-3.5-flash-lite", | ||
| expectedModelRatio: 0.15, | ||
| expectedCompRatio: 2.5 / 0.3, | ||
| }, | ||
| { | ||
| model: "gemini-3.6", | ||
| expectedModelRatio: 0.75, | ||
| expectedCompRatio: 5.0, | ||
| }, | ||
| { | ||
| model: "gemini-3.6-flash", | ||
| expectedModelRatio: 0.75, | ||
| expectedCompRatio: 5.0, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.model, func(t *testing.T) { | ||
| ratio, ok, _ := ratio_setting.GetModelRatio(tt.model) | ||
| require.True(t, ok, "model ratio for %s should exist", tt.model) | ||
| assert.InDelta(t, tt.expectedModelRatio, ratio, 0.0001, "model ratio mismatch for %s", tt.model) | ||
|
|
||
| compRatio := ratio_setting.GetCompletionRatio(tt.model) | ||
| assert.InDelta(t, tt.expectedCompRatio, compRatio, 0.0001, "completion ratio mismatch for %s", tt.model) | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Create an isolated database fixture.
This test uses the global
DBwithout initializing or restoring it. It can depend on suite order and leave migrated tables and rows in a shared test database.Initialize an isolated test database, assign and restore
DB, and clean up throught.Cleanup. As per coding guidelines, backend tests must initialize database state explicitly in test fixtures.🤖 Prompt for AI Agents
Source: Coding guidelines