feat: auto fetch upstream models (#2979)
* feat: add upstream model update detection with scheduled sync and manual apply flows * feat: support upstream model removal sync and selectable deletes in update modal * feat: add detect-only upstream updates and show compact +/- model badges * feat: improve upstream model update UX * feat: improve upstream model update UX * fix: respect model_mapping in upstream update detection * feat: improve upstream update modal to prevent missed add/remove actions * feat: add admin upstream model update notifications with digest and truncation * fix: avoid repeated partial-submit confirmation in upstream update modal * feat: improve ui/ux * feat: suppress upstream update alerts for unchanged channel-count within 24h * fix: submit upstream update choices even when no models are selected * feat: improve upstream model update flow and split frontend updater * fix merge conflict
This commit is contained in:
167
controller/channel_upstream_update_test.go
Normal file
167
controller/channel_upstream_update_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeModelNames(t *testing.T) {
|
||||
result := normalizeModelNames([]string{
|
||||
" gpt-4o ",
|
||||
"",
|
||||
"gpt-4o",
|
||||
"gpt-4.1",
|
||||
" ",
|
||||
})
|
||||
|
||||
require.Equal(t, []string{"gpt-4o", "gpt-4.1"}, result)
|
||||
}
|
||||
|
||||
func TestMergeModelNames(t *testing.T) {
|
||||
result := mergeModelNames(
|
||||
[]string{"gpt-4o", "gpt-4.1"},
|
||||
[]string{"gpt-4.1", " gpt-4.1-mini ", "gpt-4o"},
|
||||
)
|
||||
|
||||
require.Equal(t, []string{"gpt-4o", "gpt-4.1", "gpt-4.1-mini"}, result)
|
||||
}
|
||||
|
||||
func TestSubtractModelNames(t *testing.T) {
|
||||
result := subtractModelNames(
|
||||
[]string{"gpt-4o", "gpt-4.1", "gpt-4.1-mini"},
|
||||
[]string{"gpt-4.1", "not-exists"},
|
||||
)
|
||||
|
||||
require.Equal(t, []string{"gpt-4o", "gpt-4.1-mini"}, result)
|
||||
}
|
||||
|
||||
func TestIntersectModelNames(t *testing.T) {
|
||||
result := intersectModelNames(
|
||||
[]string{"gpt-4o", "gpt-4.1", "gpt-4.1", "not-exists"},
|
||||
[]string{"gpt-4.1", "gpt-4o-mini", "gpt-4o"},
|
||||
)
|
||||
|
||||
require.Equal(t, []string{"gpt-4o", "gpt-4.1"}, result)
|
||||
}
|
||||
|
||||
func TestApplySelectedModelChanges(t *testing.T) {
|
||||
t.Run("add and remove together", func(t *testing.T) {
|
||||
result := applySelectedModelChanges(
|
||||
[]string{"gpt-4o", "gpt-4.1", "claude-3"},
|
||||
[]string{"gpt-4.1-mini"},
|
||||
[]string{"claude-3"},
|
||||
)
|
||||
|
||||
require.Equal(t, []string{"gpt-4o", "gpt-4.1", "gpt-4.1-mini"}, result)
|
||||
})
|
||||
|
||||
t.Run("add wins when conflict with remove", func(t *testing.T) {
|
||||
result := applySelectedModelChanges(
|
||||
[]string{"gpt-4o"},
|
||||
[]string{"gpt-4.1"},
|
||||
[]string{"gpt-4.1"},
|
||||
)
|
||||
|
||||
require.Equal(t, []string{"gpt-4o", "gpt-4.1"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCollectPendingApplyUpstreamModelChanges(t *testing.T) {
|
||||
settings := dto.ChannelOtherSettings{
|
||||
UpstreamModelUpdateLastDetectedModels: []string{" gpt-4o ", "gpt-4o", "gpt-4.1"},
|
||||
UpstreamModelUpdateLastRemovedModels: []string{" old-model ", "", "old-model"},
|
||||
}
|
||||
|
||||
pendingAddModels, pendingRemoveModels := collectPendingApplyUpstreamModelChanges(settings)
|
||||
|
||||
require.Equal(t, []string{"gpt-4o", "gpt-4.1"}, pendingAddModels)
|
||||
require.Equal(t, []string{"old-model"}, pendingRemoveModels)
|
||||
}
|
||||
|
||||
func TestNormalizeChannelModelMapping(t *testing.T) {
|
||||
modelMapping := `{
|
||||
" alias-model ": " upstream-model ",
|
||||
"": "invalid",
|
||||
"invalid-target": ""
|
||||
}`
|
||||
channel := &model.Channel{
|
||||
ModelMapping: &modelMapping,
|
||||
}
|
||||
|
||||
result := normalizeChannelModelMapping(channel)
|
||||
require.Equal(t, map[string]string{
|
||||
"alias-model": "upstream-model",
|
||||
}, result)
|
||||
}
|
||||
|
||||
func TestCollectPendingUpstreamModelChangesFromModels_WithModelMapping(t *testing.T) {
|
||||
pendingAddModels, pendingRemoveModels := collectPendingUpstreamModelChangesFromModels(
|
||||
[]string{"alias-model", "gpt-4o", "stale-model"},
|
||||
[]string{"gpt-4o", "gpt-4.1", "mapped-target"},
|
||||
[]string{"gpt-4.1"},
|
||||
map[string]string{
|
||||
"alias-model": "mapped-target",
|
||||
},
|
||||
)
|
||||
|
||||
require.Equal(t, []string{}, pendingAddModels)
|
||||
require.Equal(t, []string{"stale-model"}, pendingRemoveModels)
|
||||
}
|
||||
|
||||
func TestBuildUpstreamModelUpdateTaskNotificationContent_OmitOverflowDetails(t *testing.T) {
|
||||
channelSummaries := make([]upstreamModelUpdateChannelSummary, 0, 12)
|
||||
for i := 0; i < 12; i++ {
|
||||
channelSummaries = append(channelSummaries, upstreamModelUpdateChannelSummary{
|
||||
ChannelName: "channel-" + string(rune('A'+i)),
|
||||
AddCount: i + 1,
|
||||
RemoveCount: i,
|
||||
})
|
||||
}
|
||||
|
||||
content := buildUpstreamModelUpdateTaskNotificationContent(
|
||||
24,
|
||||
12,
|
||||
56,
|
||||
21,
|
||||
9,
|
||||
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
||||
channelSummaries,
|
||||
[]string{
|
||||
"gpt-4.1", "gpt-4.1-mini", "o3", "o4-mini", "gemini-2.5-pro", "claude-3.7-sonnet",
|
||||
"qwen-max", "deepseek-r1", "llama-3.3-70b", "mistral-large", "command-r-plus", "doubao-pro-32k",
|
||||
"hunyuan-large",
|
||||
},
|
||||
[]string{
|
||||
"gpt-3.5-turbo", "claude-2.1", "gemini-1.5-pro", "mixtral-8x7b", "qwen-plus", "glm-4",
|
||||
"yi-large", "moonshot-v1", "doubao-lite",
|
||||
},
|
||||
)
|
||||
|
||||
require.Contains(t, content, "其余 4 个渠道已省略")
|
||||
require.Contains(t, content, "其余 1 个已省略")
|
||||
require.Contains(t, content, "失败渠道 ID(展示 10/12)")
|
||||
require.Contains(t, content, "其余 2 个已省略")
|
||||
}
|
||||
|
||||
func TestShouldSendUpstreamModelUpdateNotification(t *testing.T) {
|
||||
channelUpstreamModelUpdateNotifyState.Lock()
|
||||
channelUpstreamModelUpdateNotifyState.lastNotifiedAt = 0
|
||||
channelUpstreamModelUpdateNotifyState.lastChangedChannels = 0
|
||||
channelUpstreamModelUpdateNotifyState.lastFailedChannels = 0
|
||||
channelUpstreamModelUpdateNotifyState.Unlock()
|
||||
|
||||
baseTime := int64(2000000)
|
||||
|
||||
require.True(t, shouldSendUpstreamModelUpdateNotification(baseTime, 6, 0))
|
||||
require.False(t, shouldSendUpstreamModelUpdateNotification(baseTime+3600, 6, 0))
|
||||
require.True(t, shouldSendUpstreamModelUpdateNotification(baseTime+3600, 7, 0))
|
||||
require.False(t, shouldSendUpstreamModelUpdateNotification(baseTime+7200, 7, 0))
|
||||
require.True(t, shouldSendUpstreamModelUpdateNotification(baseTime+8000, 0, 3))
|
||||
require.False(t, shouldSendUpstreamModelUpdateNotification(baseTime+9000, 0, 3))
|
||||
require.True(t, shouldSendUpstreamModelUpdateNotification(baseTime+10000, 0, 4))
|
||||
require.True(t, shouldSendUpstreamModelUpdateNotification(baseTime+90000, 7, 0))
|
||||
require.True(t, shouldSendUpstreamModelUpdateNotification(baseTime+90001, 0, 0))
|
||||
}
|
||||
Reference in New Issue
Block a user