diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
index b8b880eab0..99fedb5b1c 100644
--- a/backend/internal/config/config.go
+++ b/backend/internal/config/config.go
@@ -969,6 +969,9 @@ type GatewayOpenAIWSSchedulerScoreWeights struct {
Reset float64 `mapstructure:"reset"`
// QuotaHeadroom 倾向 7d 剩余额度更健康的账号;默认 0(关闭,不改变原有行为)。
QuotaHeadroom float64 `mapstructure:"quota_headroom"`
+ // PreviousResponse/SessionSticky 仅在开启 OpenAI 高级调度的粘性加权时生效。
+ PreviousResponse float64 `mapstructure:"previous_response"`
+ SessionSticky float64 `mapstructure:"session_sticky"`
}
// GatewayOpenAISchedulerConfig OpenAI 高级调度器配置。
@@ -1891,6 +1894,8 @@ func setDefaults() {
viper.SetDefault("gateway.openai_ws.scheduler_score_weights.ttft", 0.5)
viper.SetDefault("gateway.openai_ws.scheduler_score_weights.reset", 0.0)
viper.SetDefault("gateway.openai_ws.scheduler_score_weights.quota_headroom", 0.0)
+ viper.SetDefault("gateway.openai_ws.scheduler_score_weights.previous_response", 5.0)
+ viper.SetDefault("gateway.openai_ws.scheduler_score_weights.session_sticky", 3.0)
// OpenAI HTTP upstream protocol strategy
viper.SetDefault("gateway.openai_http2.enabled", true)
viper.SetDefault("gateway.openai_http2.allow_proxy_fallback_to_http1", true)
@@ -2674,7 +2679,9 @@ func (c *Config) Validate() error {
c.Gateway.OpenAIWS.SchedulerScoreWeights.Queue < 0 ||
c.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate < 0 ||
c.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT < 0 ||
- c.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom < 0 {
+ c.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom < 0 ||
+ c.Gateway.OpenAIWS.SchedulerScoreWeights.PreviousResponse < 0 ||
+ c.Gateway.OpenAIWS.SchedulerScoreWeights.SessionSticky < 0 {
return fmt.Errorf("gateway.openai_ws.scheduler_score_weights.* must be non-negative")
}
weightSum := c.Gateway.OpenAIWS.SchedulerScoreWeights.Priority +
diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go
index 8d4a5dfea6..efe95801c3 100644
--- a/backend/internal/handler/admin/account_handler.go
+++ b/backend/internal/handler/admin/account_handler.go
@@ -171,15 +171,39 @@ type CheckMixedChannelRequest struct {
// AccountWithConcurrency extends Account with real-time concurrency info
type AccountWithConcurrency struct {
*dto.Account
- CurrentConcurrency int `json:"current_concurrency"`
+ CurrentConcurrency int `json:"current_concurrency"`
+ SchedulerScore *AccountSchedulerScore `json:"scheduler_score,omitempty"`
+ SchedulerScores []AccountSchedulerGroupScore `json:"scheduler_scores,omitempty"`
// 以下字段仅对 Anthropic OAuth/SetupToken 账号有效,且仅在启用相应功能时返回
CurrentWindowCost *float64 `json:"current_window_cost,omitempty"` // 当前窗口费用
ActiveSessions *int `json:"active_sessions,omitempty"` // 当前活跃会话数
CurrentRPM *int `json:"current_rpm,omitempty"` // 当前分钟 RPM 计数
}
+type AccountSchedulerScore struct {
+ BaseScore float64 `json:"base_score"`
+ StickyScore float64 `json:"sticky_score"`
+ StickyScoreInfinity bool `json:"sticky_score_infinity"`
+ StickyWeightedEnabled bool `json:"sticky_weighted_enabled"`
+}
+
+type AccountSchedulerGroupScore struct {
+ GroupID *int64 `json:"group_id"`
+ GroupName string `json:"group_name,omitempty"`
+ GroupPriority *int `json:"group_priority,omitempty"`
+ AccountSchedulerScore
+}
+
const accountListGroupUngroupedQueryValue = "ungrouped"
+type openAIAccountSchedulerScorePoolLister interface {
+ ListOpenAISchedulableAccountsForSchedulerScore(ctx context.Context, groupID *int64) ([]service.Account, error)
+}
+
+type accountSchedulerScoreFilterPoolLister interface {
+ ListAccountsForSchedulerScoreFilter(ctx context.Context, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, error)
+}
+
func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, account *service.Account) AccountWithConcurrency {
item := AccountWithConcurrency{
Account: dto.AccountFromService(account),
@@ -226,6 +250,185 @@ func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, ac
return item
}
+func (h *AccountHandler) scoreOpenAIAccountSchedulerPool(ctx context.Context, accounts []service.Account) map[int64]AccountSchedulerScore {
+ if len(accounts) == 0 {
+ return nil
+ }
+
+ openAIAccounts := make([]*service.Account, 0, len(accounts))
+ loadReq := make([]service.AccountWithConcurrency, 0, len(accounts))
+ for i := range accounts {
+ account := &accounts[i]
+ if account.Platform != service.PlatformOpenAI {
+ continue
+ }
+ openAIAccounts = append(openAIAccounts, account)
+ loadReq = append(loadReq, service.AccountWithConcurrency{
+ ID: account.ID,
+ MaxConcurrency: account.EffectiveLoadFactor(),
+ })
+ }
+ if len(openAIAccounts) == 0 {
+ return nil
+ }
+
+ loadMap := map[int64]*service.AccountLoadInfo{}
+ if h.concurrencyService != nil {
+ if batchLoad, err := h.concurrencyService.GetAccountsLoadBatch(ctx, loadReq); err == nil && batchLoad != nil {
+ loadMap = batchLoad
+ }
+ }
+
+ var scores map[int64]service.OpenAIAccountSchedulerScoreSnapshot
+ if h.rateLimitService != nil {
+ scores = h.rateLimitService.BuildOpenAIAccountSchedulerScoreSnapshot(ctx, openAIAccounts, loadMap)
+ } else {
+ scores = service.BuildOpenAIAccountSchedulerScoreSnapshot(openAIAccounts, loadMap)
+ }
+ result := make(map[int64]AccountSchedulerScore, len(scores))
+ for accountID, score := range scores {
+ result[accountID] = AccountSchedulerScore{
+ BaseScore: score.BaseScore,
+ StickyScore: score.StickyScore,
+ StickyScoreInfinity: score.StickyScoreInfinity,
+ StickyWeightedEnabled: score.StickyWeightedEnabled,
+ }
+ }
+ return result
+}
+
+func (h *AccountHandler) buildOpenAIAccountSchedulerScores(
+ ctx context.Context,
+ accounts []service.Account,
+ filterPool []service.Account,
+) (map[int64]*AccountSchedulerScore, map[int64][]AccountSchedulerGroupScore) {
+ if len(accounts) == 0 {
+ return nil, nil
+ }
+ if len(filterPool) == 0 {
+ filterPool = accounts
+ }
+
+ baseScores := make(map[int64]*AccountSchedulerScore)
+ for accountID, score := range h.scoreOpenAIAccountSchedulerPool(ctx, filterPool) {
+ copiedScore := score
+ baseScores[accountID] = &copiedScore
+ }
+
+ pageOpenAIAccountIDs := make(map[int64]struct{})
+ groupIDs := make(map[int64]struct{})
+ for i := range accounts {
+ account := &accounts[i]
+ if account.Platform != service.PlatformOpenAI {
+ continue
+ }
+ pageOpenAIAccountIDs[account.ID] = struct{}{}
+ if len(account.AccountGroups) == 0 && len(account.GroupIDs) == 0 {
+ continue
+ }
+ for _, accountGroup := range account.AccountGroups {
+ if accountGroup.GroupID > 0 {
+ groupIDs[accountGroup.GroupID] = struct{}{}
+ }
+ }
+ for _, groupID := range account.GroupIDs {
+ if groupID > 0 {
+ groupIDs[groupID] = struct{}{}
+ }
+ }
+ }
+ if len(pageOpenAIAccountIDs) == 0 {
+ return baseScores, nil
+ }
+
+ groupScoresByAccount := make(map[int64][]AccountSchedulerGroupScore)
+ scoreGroupPool := func(groupID *int64, groupNameByID map[int64]string, groupPriorityByAccount map[int64]int, pool []service.Account) {
+ if len(pool) == 0 {
+ return
+ }
+ scores := h.scoreOpenAIAccountSchedulerPool(ctx, pool)
+ for accountID, schedulerScore := range scores {
+ if _, ok := pageOpenAIAccountIDs[accountID]; !ok {
+ continue
+ }
+ groupScore := AccountSchedulerGroupScore{
+ GroupID: groupID,
+ AccountSchedulerScore: schedulerScore,
+ }
+ if groupID != nil {
+ groupScore.GroupName = groupNameByID[*groupID]
+ if priority, ok := groupPriorityByAccount[accountID]; ok {
+ groupScore.GroupPriority = &priority
+ }
+ }
+ groupScoresByAccount[accountID] = append(groupScoresByAccount[accountID], groupScore)
+ }
+ }
+
+ if lister, ok := h.adminService.(openAIAccountSchedulerScorePoolLister); ok {
+ groupIDList := make([]int64, 0, len(groupIDs))
+ for groupID := range groupIDs {
+ groupIDList = append(groupIDList, groupID)
+ }
+ sort.Slice(groupIDList, func(i, j int) bool { return groupIDList[i] < groupIDList[j] })
+
+ for _, groupID := range groupIDList {
+ gid := groupID
+ pool, err := lister.ListOpenAISchedulableAccountsForSchedulerScore(ctx, &gid)
+ if err != nil {
+ slog.Warn("openai_scheduler_group_score_pool_failed", "group_id", gid, "error", err)
+ continue
+ }
+ groupNameByID := make(map[int64]string)
+ groupPriorityByAccount := make(map[int64]int)
+ for i := range pool {
+ account := &pool[i]
+ for _, accountGroup := range account.AccountGroups {
+ if accountGroup.GroupID != gid {
+ continue
+ }
+ groupPriorityByAccount[account.ID] = accountGroup.Priority
+ if accountGroup.Group != nil {
+ groupNameByID[gid] = accountGroup.Group.Name
+ }
+ }
+ }
+ scoreGroupPool(&gid, groupNameByID, groupPriorityByAccount, pool)
+ }
+
+ }
+
+ for accountID := range groupScoresByAccount {
+ sort.SliceStable(groupScoresByAccount[accountID], func(i, j int) bool {
+ left := groupScoresByAccount[accountID][i]
+ right := groupScoresByAccount[accountID][j]
+ return *left.GroupID < *right.GroupID
+ })
+ }
+ return baseScores, groupScoresByAccount
+}
+
+func (h *AccountHandler) listAccountSchedulerScoreFilterPool(
+ ctx context.Context,
+ platform, accountType, status, search string,
+ groupID int64,
+ privacyMode string,
+) []service.Account {
+ if h.adminService == nil || (platform != "" && platform != service.PlatformOpenAI) {
+ return nil
+ }
+ lister, ok := h.adminService.(accountSchedulerScoreFilterPoolLister)
+ if !ok {
+ return nil
+ }
+ accounts, err := lister.ListAccountsForSchedulerScoreFilter(ctx, platform, accountType, status, search, groupID, privacyMode)
+ if err != nil {
+ slog.Warn("openai_scheduler_filter_score_pool_failed", "error", err)
+ return nil
+ }
+ return accounts
+}
+
// List handles listing all accounts with pagination
// GET /api/v1/admin/accounts
func (h *AccountHandler) List(c *gin.Context) {
@@ -278,6 +481,8 @@ func (h *AccountHandler) List(c *gin.Context) {
var windowCosts map[int64]float64
var activeSessions map[int64]int
var rpmCounts map[int64]int
+ schedulerFilterPool := h.listAccountSchedulerScoreFilterPool(c.Request.Context(), platform, accountType, status, search, groupID, privacyMode)
+ schedulerScores, schedulerGroupScores := h.buildOpenAIAccountSchedulerScores(c.Request.Context(), accounts, schedulerFilterPool)
// 始终获取并发数(Redis ZCARD,极低开销)
if h.concurrencyService != nil {
@@ -358,6 +563,8 @@ func (h *AccountHandler) List(c *gin.Context) {
item := AccountWithConcurrency{
Account: dto.AccountFromService(acc),
CurrentConcurrency: concurrencyCounts[acc.ID],
+ SchedulerScore: schedulerScores[acc.ID],
+ SchedulerScores: schedulerGroupScores[acc.ID],
}
// 添加窗口费用(仅当启用时)
diff --git a/backend/internal/handler/admin/account_handler_list_test.go b/backend/internal/handler/admin/account_handler_list_test.go
index 4d628365df..29e36ad865 100644
--- a/backend/internal/handler/admin/account_handler_list_test.go
+++ b/backend/internal/handler/admin/account_handler_list_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"time"
+ "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
@@ -50,3 +51,222 @@ func TestAccountHandlerListIncludesCreatedAt(t *testing.T) {
_, offset := parsed.Zone()
require.Equal(t, 0, offset)
}
+
+func TestAccountHandlerListReturnsSchedulerScoresPerGroup(t *testing.T) {
+ router, adminSvc := setupAccountListRouter()
+ now := time.Now().UTC()
+ groupID := int64(41)
+ adminSvc.accounts = []service.Account{
+ {
+ ID: 101,
+ Name: "account-high-priority",
+ Platform: service.PlatformOpenAI,
+ Type: service.AccountTypeAPIKey,
+ Status: service.StatusActive,
+ Schedulable: true,
+ Concurrency: 10,
+ Priority: 1,
+ AccountGroups: []service.AccountGroup{
+ {AccountID: 101, GroupID: groupID, Priority: 100, Group: &service.Group{ID: groupID, Name: "openai"}},
+ },
+ GroupIDs: []int64{groupID},
+ CreatedAt: now,
+ UpdatedAt: now,
+ },
+ {
+ ID: 102,
+ Name: "account-low-priority",
+ Platform: service.PlatformOpenAI,
+ Type: service.AccountTypeAPIKey,
+ Status: service.StatusActive,
+ Schedulable: true,
+ Concurrency: 10,
+ Priority: 100000,
+ AccountGroups: []service.AccountGroup{
+ {AccountID: 102, GroupID: groupID, Priority: 1, Group: &service.Group{ID: groupID, Name: "openai"}},
+ },
+ GroupIDs: []int64{groupID},
+ CreatedAt: now,
+ UpdatedAt: now,
+ },
+ }
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=20&platform=openai", nil)
+ router.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+ var payload struct {
+ Data struct {
+ Items []struct {
+ ID int64 `json:"id"`
+ SchedulerScore struct {
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_score"`
+ SchedulerScores []struct {
+ GroupID *int64 `json:"group_id"`
+ GroupName string `json:"group_name"`
+ GroupPriority *int `json:"group_priority"`
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_scores"`
+ } `json:"items"`
+ } `json:"data"`
+ }
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
+ require.Len(t, payload.Data.Items, 2)
+
+ var high, low *struct {
+ ID int64 `json:"id"`
+ SchedulerScore struct {
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_score"`
+ SchedulerScores []struct {
+ GroupID *int64 `json:"group_id"`
+ GroupName string `json:"group_name"`
+ GroupPriority *int `json:"group_priority"`
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_scores"`
+ }
+ for i := range payload.Data.Items {
+ item := &payload.Data.Items[i]
+ switch item.ID {
+ case 101:
+ high = item
+ case 102:
+ low = item
+ }
+ }
+ require.NotNil(t, high)
+ require.NotNil(t, low)
+ require.Len(t, high.SchedulerScores, 1)
+ require.Len(t, low.SchedulerScores, 1)
+ require.Equal(t, groupID, *high.SchedulerScores[0].GroupID)
+ require.Equal(t, "openai", high.SchedulerScores[0].GroupName)
+ require.Equal(t, 100, *high.SchedulerScores[0].GroupPriority)
+ require.Equal(t, 1, *low.SchedulerScores[0].GroupPriority)
+ require.Greater(t, high.SchedulerScores[0].BaseScore, low.SchedulerScores[0].BaseScore)
+}
+
+func TestAccountHandlerListKeepsSchedulerScoreScopedToFilter(t *testing.T) {
+ router, adminSvc := setupAccountListRouter()
+ now := time.Now().UTC()
+ groupID := int64(42)
+ visibleAccount := service.Account{
+ ID: 201,
+ Name: "visible-low-priority",
+ Platform: service.PlatformOpenAI,
+ Type: service.AccountTypeAPIKey,
+ Status: service.StatusActive,
+ Schedulable: true,
+ Concurrency: 10,
+ Priority: 100000,
+ AccountGroups: []service.AccountGroup{
+ {AccountID: 201, GroupID: groupID, Priority: 1, Group: &service.Group{ID: groupID, Name: "openai"}},
+ },
+ GroupIDs: []int64{groupID},
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ hiddenGroupPeer := service.Account{
+ ID: 202,
+ Name: "hidden-high-priority",
+ Platform: service.PlatformOpenAI,
+ Type: service.AccountTypeAPIKey,
+ Status: service.StatusActive,
+ Schedulable: true,
+ Concurrency: 10,
+ Priority: 1,
+ AccountGroups: []service.AccountGroup{
+ {AccountID: 202, GroupID: groupID, Priority: 2, Group: &service.Group{ID: groupID, Name: "openai"}},
+ },
+ GroupIDs: []int64{groupID},
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ adminSvc.accounts = []service.Account{visibleAccount}
+ adminSvc.accountSchedulerScoreFilterAccounts = []service.Account{visibleAccount, hiddenGroupPeer}
+ adminSvc.openAISchedulerScorePoolAccounts = []service.Account{visibleAccount, hiddenGroupPeer}
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=1&platform=openai", nil)
+ router.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+ var payload struct {
+ Data struct {
+ Items []struct {
+ ID int64 `json:"id"`
+ SchedulerScore struct {
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_score"`
+ SchedulerScores []struct {
+ GroupID *int64 `json:"group_id"`
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_scores"`
+ } `json:"items"`
+ } `json:"data"`
+ }
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
+ require.Len(t, payload.Data.Items, 1)
+ item := payload.Data.Items[0]
+ require.Equal(t, int64(201), item.ID)
+ require.Len(t, item.SchedulerScores, 1)
+ require.Equal(t, groupID, *item.SchedulerScores[0].GroupID)
+ require.Equal(t, item.SchedulerScores[0].BaseScore, item.SchedulerScore.BaseScore)
+}
+
+func TestAccountHandlerListSchedulerScoreIgnoresPagination(t *testing.T) {
+ router, adminSvc := setupAccountListRouter()
+ now := time.Now().UTC()
+ visibleAccount := service.Account{
+ ID: 301,
+ Name: "visible-low-priority",
+ Platform: service.PlatformOpenAI,
+ Type: service.AccountTypeAPIKey,
+ Status: service.StatusActive,
+ Schedulable: true,
+ Concurrency: 10,
+ Priority: 100000,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ hiddenFilterPeer := service.Account{
+ ID: 302,
+ Name: "hidden-high-priority",
+ Platform: service.PlatformOpenAI,
+ Type: service.AccountTypeAPIKey,
+ Status: service.StatusActive,
+ Schedulable: true,
+ Concurrency: 10,
+ Priority: 1,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+ adminSvc.accounts = []service.Account{visibleAccount}
+ adminSvc.accountSchedulerScoreFilterAccounts = []service.Account{visibleAccount, hiddenFilterPeer}
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/accounts?page=1&page_size=1&platform=openai", nil)
+ router.ServeHTTP(rec, req)
+
+ require.Equal(t, http.StatusOK, rec.Code)
+ var payload struct {
+ Data struct {
+ Items []struct {
+ ID int64 `json:"id"`
+ SchedulerScore struct {
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_score"`
+ SchedulerScores []struct {
+ GroupID *int64 `json:"group_id"`
+ BaseScore float64 `json:"base_score"`
+ } `json:"scheduler_scores"`
+ } `json:"items"`
+ } `json:"data"`
+ }
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
+ require.Len(t, payload.Data.Items, 1)
+ require.Equal(t, int64(301), payload.Data.Items[0].ID)
+ require.Less(t, payload.Data.Items[0].SchedulerScore.BaseScore, 3.75)
+ require.Empty(t, payload.Data.Items[0].SchedulerScores)
+}
diff --git a/backend/internal/handler/admin/admin_service_stub_test.go b/backend/internal/handler/admin/admin_service_stub_test.go
index f7bab13253..187925e33a 100644
--- a/backend/internal/handler/admin/admin_service_stub_test.go
+++ b/backend/internal/handler/admin/admin_service_stub_test.go
@@ -10,27 +10,29 @@ import (
)
type stubAdminService struct {
- users []service.User
- apiKeys []service.APIKey
- groups []service.Group
- accounts []service.Account
- proxies []service.Proxy
- proxyCounts []service.ProxyWithAccountCount
- redeems []service.RedeemCode
- boundAuthIdentity *service.AdminBindAuthIdentityInput
- boundAuthIdentityFor int64
- createdAccounts []*service.CreateAccountInput
- createdProxies []*service.CreateProxyInput
- updatedProxyIDs []int64
- updatedProxies []*service.UpdateProxyInput
- testedProxyIDs []int64
- getUserErr error
- createAccountErr error
- createSparkShadowErr error
- updateAccountErr error
- bulkUpdateAccountErr error
- checkMixedErr error
- lastMixedCheck struct {
+ users []service.User
+ apiKeys []service.APIKey
+ groups []service.Group
+ accounts []service.Account
+ accountSchedulerScoreFilterAccounts []service.Account
+ openAISchedulerScorePoolAccounts []service.Account
+ proxies []service.Proxy
+ proxyCounts []service.ProxyWithAccountCount
+ redeems []service.RedeemCode
+ boundAuthIdentity *service.AdminBindAuthIdentityInput
+ boundAuthIdentityFor int64
+ createdAccounts []*service.CreateAccountInput
+ createdProxies []*service.CreateProxyInput
+ updatedProxyIDs []int64
+ updatedProxies []*service.UpdateProxyInput
+ testedProxyIDs []int64
+ getUserErr error
+ createAccountErr error
+ createSparkShadowErr error
+ updateAccountErr error
+ bulkUpdateAccountErr error
+ checkMixedErr error
+ lastMixedCheck struct {
accountID int64
platform string
groupIDs []int64
@@ -329,7 +331,56 @@ func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int,
s.lastListAccounts.sortBy = sortBy
s.lastListAccounts.sortOrder = sortOrder
s.lastListAccounts.calls++
- return s.accounts, int64(len(s.accounts)), nil
+ accounts := s.accounts
+ total := len(accounts)
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 {
+ pageSize = total
+ }
+ start := (page - 1) * pageSize
+ if start >= total {
+ return []service.Account{}, int64(total), nil
+ }
+ end := start + pageSize
+ if end > total {
+ end = total
+ }
+ return accounts[start:end], int64(total), nil
+}
+
+func (s *stubAdminService) ListAccountsForSchedulerScoreFilter(_ context.Context, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, error) {
+ if s.accountSchedulerScoreFilterAccounts != nil {
+ return s.accountSchedulerScoreFilterAccounts, nil
+ }
+ return s.accounts, nil
+}
+
+func (s *stubAdminService) ListOpenAISchedulableAccountsForSchedulerScore(_ context.Context, groupID *int64) ([]service.Account, error) {
+ accounts := s.openAISchedulerScorePoolAccounts
+ if accounts == nil {
+ accounts = s.accounts
+ }
+ out := make([]service.Account, 0, len(accounts))
+ for _, account := range accounts {
+ if account.Platform != service.PlatformOpenAI || !account.IsSchedulable() {
+ continue
+ }
+ if groupID == nil {
+ if len(account.AccountGroups) == 0 && len(account.GroupIDs) == 0 {
+ out = append(out, account)
+ }
+ continue
+ }
+ for _, accountGroup := range account.AccountGroups {
+ if accountGroup.GroupID == *groupID {
+ out = append(out, account)
+ break
+ }
+ }
+ }
+ return out, nil
}
func (s *stubAdminService) GetAccount(ctx context.Context, id int64) (*service.Account, error) {
diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go
index 0f50ab723b..624fddb1e6 100644
--- a/backend/internal/handler/admin/setting_handler.go
+++ b/backend/internal/handler/admin/setting_handler.go
@@ -119,188 +119,210 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
}
payload := dto.SystemSettings{
- RegistrationEnabled: settings.RegistrationEnabled,
- EmailVerifyEnabled: settings.EmailVerifyEnabled,
- RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
- PromoCodeEnabled: settings.PromoCodeEnabled,
- PasswordResetEnabled: settings.PasswordResetEnabled,
- FrontendURL: settings.FrontendURL,
- InvitationCodeEnabled: settings.InvitationCodeEnabled,
- TotpEnabled: settings.TotpEnabled,
- TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
- LoginAgreementEnabled: settings.LoginAgreementEnabled,
- LoginAgreementMode: settings.LoginAgreementMode,
- LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt,
- LoginAgreementDocuments: loginAgreementDocumentsToDTO(settings.LoginAgreementDocuments),
- SMTPHost: settings.SMTPHost,
- SMTPPort: settings.SMTPPort,
- SMTPUsername: settings.SMTPUsername,
- SMTPPasswordConfigured: settings.SMTPPasswordConfigured,
- SMTPFrom: settings.SMTPFrom,
- SMTPFromName: settings.SMTPFromName,
- SMTPUseTLS: settings.SMTPUseTLS,
- TurnstileEnabled: settings.TurnstileEnabled,
- TurnstileSiteKey: settings.TurnstileSiteKey,
- TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
- APIKeyACLTrustForwardedIP: settings.APIKeyACLTrustForwardedIP,
- LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
- LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
- LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
- LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL,
- DingTalkConnectEnabled: settings.DingTalkConnectEnabled,
- DingTalkConnectClientID: settings.DingTalkConnectClientID,
- DingTalkConnectClientSecretConfigured: settings.DingTalkConnectClientSecretConfigured,
- DingTalkConnectRedirectURL: settings.DingTalkConnectRedirectURL,
- DingTalkConnectCorpRestrictionPolicy: settings.DingTalkConnectCorpRestrictionPolicy,
- DingTalkConnectInternalCorpID: settings.DingTalkConnectInternalCorpID,
- DingTalkConnectBypassRegistration: settings.DingTalkConnectBypassRegistration,
- DingTalkConnectSyncCorpEmail: settings.DingTalkConnectSyncCorpEmail,
- DingTalkConnectSyncDisplayName: settings.DingTalkConnectSyncDisplayName,
- DingTalkConnectSyncDept: settings.DingTalkConnectSyncDept,
- DingTalkConnectSyncCorpEmailAttrKey: settings.DingTalkConnectSyncCorpEmailAttrKey,
- DingTalkConnectSyncDisplayNameAttrKey: settings.DingTalkConnectSyncDisplayNameAttrKey,
- DingTalkConnectSyncDeptAttrKey: settings.DingTalkConnectSyncDeptAttrKey,
- DingTalkConnectSyncCorpEmailAttrName: settings.DingTalkConnectSyncCorpEmailAttrName,
- DingTalkConnectSyncDisplayNameAttrName: settings.DingTalkConnectSyncDisplayNameAttrName,
- DingTalkConnectSyncDeptAttrName: settings.DingTalkConnectSyncDeptAttrName,
- WeChatConnectEnabled: settings.WeChatConnectEnabled,
- WeChatConnectAppID: settings.WeChatConnectAppID,
- WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured,
- WeChatConnectOpenAppID: settings.WeChatConnectOpenAppID,
- WeChatConnectOpenAppSecretConfigured: settings.WeChatConnectOpenAppSecretConfigured,
- WeChatConnectMPAppID: settings.WeChatConnectMPAppID,
- WeChatConnectMPAppSecretConfigured: settings.WeChatConnectMPAppSecretConfigured,
- WeChatConnectMobileAppID: settings.WeChatConnectMobileAppID,
- WeChatConnectMobileAppSecretConfigured: settings.WeChatConnectMobileAppSecretConfigured,
- WeChatConnectOpenEnabled: settings.WeChatConnectOpenEnabled,
- WeChatConnectMPEnabled: settings.WeChatConnectMPEnabled,
- WeChatConnectMobileEnabled: settings.WeChatConnectMobileEnabled,
- WeChatConnectMode: settings.WeChatConnectMode,
- WeChatConnectScopes: settings.WeChatConnectScopes,
- WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL,
- WeChatConnectFrontendRedirectURL: settings.WeChatConnectFrontendRedirectURL,
- OIDCConnectEnabled: settings.OIDCConnectEnabled,
- OIDCConnectProviderName: settings.OIDCConnectProviderName,
- OIDCConnectClientID: settings.OIDCConnectClientID,
- OIDCConnectClientSecretConfigured: settings.OIDCConnectClientSecretConfigured,
- OIDCConnectIssuerURL: settings.OIDCConnectIssuerURL,
- OIDCConnectDiscoveryURL: settings.OIDCConnectDiscoveryURL,
- OIDCConnectAuthorizeURL: settings.OIDCConnectAuthorizeURL,
- OIDCConnectTokenURL: settings.OIDCConnectTokenURL,
- OIDCConnectUserInfoURL: settings.OIDCConnectUserInfoURL,
- OIDCConnectJWKSURL: settings.OIDCConnectJWKSURL,
- OIDCConnectScopes: settings.OIDCConnectScopes,
- OIDCConnectRedirectURL: settings.OIDCConnectRedirectURL,
- OIDCConnectFrontendRedirectURL: settings.OIDCConnectFrontendRedirectURL,
- OIDCConnectTokenAuthMethod: settings.OIDCConnectTokenAuthMethod,
- OIDCConnectUsePKCE: settings.OIDCConnectUsePKCE,
- OIDCConnectValidateIDToken: settings.OIDCConnectValidateIDToken,
- OIDCConnectAllowedSigningAlgs: settings.OIDCConnectAllowedSigningAlgs,
- OIDCConnectClockSkewSeconds: settings.OIDCConnectClockSkewSeconds,
- OIDCConnectRequireEmailVerified: settings.OIDCConnectRequireEmailVerified,
- OIDCConnectUserInfoEmailPath: settings.OIDCConnectUserInfoEmailPath,
- OIDCConnectUserInfoIDPath: settings.OIDCConnectUserInfoIDPath,
- OIDCConnectUserInfoUsernamePath: settings.OIDCConnectUserInfoUsernamePath,
- GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
- GitHubOAuthClientID: settings.GitHubOAuthClientID,
- GitHubOAuthClientSecretConfigured: settings.GitHubOAuthClientSecretConfigured,
- GitHubOAuthRedirectURL: settings.GitHubOAuthRedirectURL,
- GitHubOAuthFrontendRedirectURL: settings.GitHubOAuthFrontendRedirectURL,
- GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
- GoogleOAuthClientID: settings.GoogleOAuthClientID,
- GoogleOAuthClientSecretConfigured: settings.GoogleOAuthClientSecretConfigured,
- GoogleOAuthRedirectURL: settings.GoogleOAuthRedirectURL,
- GoogleOAuthFrontendRedirectURL: settings.GoogleOAuthFrontendRedirectURL,
- SiteName: settings.SiteName,
- SiteLogo: settings.SiteLogo,
- SiteSubtitle: settings.SiteSubtitle,
- APIBaseURL: settings.APIBaseURL,
- ContactInfo: settings.ContactInfo,
- DocURL: settings.DocURL,
- HomeContent: settings.HomeContent,
- HideCcsImportButton: settings.HideCcsImportButton,
- PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
- PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
- TableDefaultPageSize: settings.TableDefaultPageSize,
- TablePageSizeOptions: settings.TablePageSizeOptions,
- CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems),
- CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
- DefaultConcurrency: settings.DefaultConcurrency,
- DefaultBalance: settings.DefaultBalance,
- RiskControlEnabled: settings.RiskControlEnabled,
- CyberSessionBlockEnabled: settings.CyberSessionBlockEnabled,
- CyberSessionBlockTTLSeconds: settings.CyberSessionBlockTTLSeconds,
- AffiliateRebateRate: settings.AffiliateRebateRate,
- AffiliateRebateFreezeHours: settings.AffiliateRebateFreezeHours,
- AffiliateRebateDurationDays: settings.AffiliateRebateDurationDays,
- AffiliateRebatePerInviteeCap: settings.AffiliateRebatePerInviteeCap,
- DefaultUserRPMLimit: settings.DefaultUserRPMLimit,
- DefaultSubscriptions: defaultSubscriptions,
- EnableModelFallback: settings.EnableModelFallback,
- FallbackModelAnthropic: settings.FallbackModelAnthropic,
- FallbackModelOpenAI: settings.FallbackModelOpenAI,
- FallbackModelGemini: settings.FallbackModelGemini,
- FallbackModelAntigravity: settings.FallbackModelAntigravity,
- EnableIdentityPatch: settings.EnableIdentityPatch,
- IdentityPatchPrompt: settings.IdentityPatchPrompt,
- OpsMonitoringEnabled: opsEnabled && settings.OpsMonitoringEnabled,
- OpsRealtimeMonitoringEnabled: settings.OpsRealtimeMonitoringEnabled,
- OpsQueryModeDefault: settings.OpsQueryModeDefault,
- OpsMetricsIntervalSeconds: settings.OpsMetricsIntervalSeconds,
- MinClaudeCodeVersion: settings.MinClaudeCodeVersion,
- MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion,
- AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling,
- BackendModeEnabled: settings.BackendModeEnabled,
- EnableFingerprintUnification: settings.EnableFingerprintUnification,
- EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
- EnableCCHSigning: settings.EnableCCHSigning,
- EnableClaudeOAuthSystemPromptInjection: settings.EnableClaudeOAuthSystemPromptInjection,
- ClaudeOAuthSystemPrompt: settings.ClaudeOAuthSystemPrompt,
- ClaudeOAuthSystemPromptBlocks: settings.ClaudeOAuthSystemPromptBlocks,
- EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
- RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
- EnableClientDatelineNormalization: settings.EnableClientDatelineNormalization,
- AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
- OpenAICodexUserAgent: settings.OpenAICodexUserAgent,
- MinCodexVersion: settings.MinCodexVersion,
- MaxCodexVersion: settings.MaxCodexVersion,
- CodexCLIOnlyBlacklist: settings.CodexCLIOnlyBlacklist,
- CodexCLIOnlyWhitelist: settings.CodexCLIOnlyWhitelist,
- CodexCLIOnlyAllowAppServerClients: settings.CodexCLIOnlyAllowAppServerClients,
- CodexCLIOnlyEngineFingerprintSignals: settings.CodexCLIOnlyEngineFingerprintSignals,
- WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
- PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
- PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
- PaymentVisibleMethodAlipayEnabled: settings.PaymentVisibleMethodAlipayEnabled,
- PaymentVisibleMethodWxpayEnabled: settings.PaymentVisibleMethodWxpayEnabled,
- OpenAIAdvancedSchedulerEnabled: settings.OpenAIAdvancedSchedulerEnabled,
- BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
- BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
- BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
- SubscriptionExpiryNotifyEnabled: settings.SubscriptionExpiryNotifyEnabled,
- AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
- AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(settings.AccountQuotaNotifyEmails),
- PaymentEnabled: paymentCfg.Enabled,
- PaymentMinAmount: paymentCfg.MinAmount,
- PaymentMaxAmount: paymentCfg.MaxAmount,
- PaymentDailyLimit: paymentCfg.DailyLimit,
- PaymentOrderTimeoutMin: paymentCfg.OrderTimeoutMin,
- PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders,
- PaymentEnabledTypes: paymentCfg.EnabledTypes,
- PaymentBalanceDisabled: paymentCfg.BalanceDisabled,
- PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier,
- PaymentRechargeFeeRate: paymentCfg.RechargeFeeRate,
- PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy,
- PaymentProductNamePrefix: paymentCfg.ProductNamePrefix,
- PaymentProductNameSuffix: paymentCfg.ProductNameSuffix,
- PaymentHelpImageURL: paymentCfg.HelpImageURL,
- PaymentHelpText: paymentCfg.HelpText,
- PaymentCancelRateLimitEnabled: paymentCfg.CancelRateLimitEnabled,
- PaymentCancelRateLimitMax: paymentCfg.CancelRateLimitMax,
- PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow,
- PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit,
- PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode,
- PaymentAlipayForceQRCode: paymentCfg.AlipayForceQRCode,
+ RegistrationEnabled: settings.RegistrationEnabled,
+ EmailVerifyEnabled: settings.EmailVerifyEnabled,
+ RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
+ PromoCodeEnabled: settings.PromoCodeEnabled,
+ PasswordResetEnabled: settings.PasswordResetEnabled,
+ FrontendURL: settings.FrontendURL,
+ InvitationCodeEnabled: settings.InvitationCodeEnabled,
+ TotpEnabled: settings.TotpEnabled,
+ TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
+ LoginAgreementEnabled: settings.LoginAgreementEnabled,
+ LoginAgreementMode: settings.LoginAgreementMode,
+ LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt,
+ LoginAgreementDocuments: loginAgreementDocumentsToDTO(settings.LoginAgreementDocuments),
+ SMTPHost: settings.SMTPHost,
+ SMTPPort: settings.SMTPPort,
+ SMTPUsername: settings.SMTPUsername,
+ SMTPPasswordConfigured: settings.SMTPPasswordConfigured,
+ SMTPFrom: settings.SMTPFrom,
+ SMTPFromName: settings.SMTPFromName,
+ SMTPUseTLS: settings.SMTPUseTLS,
+ TurnstileEnabled: settings.TurnstileEnabled,
+ TurnstileSiteKey: settings.TurnstileSiteKey,
+ TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
+ APIKeyACLTrustForwardedIP: settings.APIKeyACLTrustForwardedIP,
+ LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
+ LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
+ LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
+ LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL,
+ DingTalkConnectEnabled: settings.DingTalkConnectEnabled,
+ DingTalkConnectClientID: settings.DingTalkConnectClientID,
+ DingTalkConnectClientSecretConfigured: settings.DingTalkConnectClientSecretConfigured,
+ DingTalkConnectRedirectURL: settings.DingTalkConnectRedirectURL,
+ DingTalkConnectCorpRestrictionPolicy: settings.DingTalkConnectCorpRestrictionPolicy,
+ DingTalkConnectInternalCorpID: settings.DingTalkConnectInternalCorpID,
+ DingTalkConnectBypassRegistration: settings.DingTalkConnectBypassRegistration,
+ DingTalkConnectSyncCorpEmail: settings.DingTalkConnectSyncCorpEmail,
+ DingTalkConnectSyncDisplayName: settings.DingTalkConnectSyncDisplayName,
+ DingTalkConnectSyncDept: settings.DingTalkConnectSyncDept,
+ DingTalkConnectSyncCorpEmailAttrKey: settings.DingTalkConnectSyncCorpEmailAttrKey,
+ DingTalkConnectSyncDisplayNameAttrKey: settings.DingTalkConnectSyncDisplayNameAttrKey,
+ DingTalkConnectSyncDeptAttrKey: settings.DingTalkConnectSyncDeptAttrKey,
+ DingTalkConnectSyncCorpEmailAttrName: settings.DingTalkConnectSyncCorpEmailAttrName,
+ DingTalkConnectSyncDisplayNameAttrName: settings.DingTalkConnectSyncDisplayNameAttrName,
+ DingTalkConnectSyncDeptAttrName: settings.DingTalkConnectSyncDeptAttrName,
+ WeChatConnectEnabled: settings.WeChatConnectEnabled,
+ WeChatConnectAppID: settings.WeChatConnectAppID,
+ WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured,
+ WeChatConnectOpenAppID: settings.WeChatConnectOpenAppID,
+ WeChatConnectOpenAppSecretConfigured: settings.WeChatConnectOpenAppSecretConfigured,
+ WeChatConnectMPAppID: settings.WeChatConnectMPAppID,
+ WeChatConnectMPAppSecretConfigured: settings.WeChatConnectMPAppSecretConfigured,
+ WeChatConnectMobileAppID: settings.WeChatConnectMobileAppID,
+ WeChatConnectMobileAppSecretConfigured: settings.WeChatConnectMobileAppSecretConfigured,
+ WeChatConnectOpenEnabled: settings.WeChatConnectOpenEnabled,
+ WeChatConnectMPEnabled: settings.WeChatConnectMPEnabled,
+ WeChatConnectMobileEnabled: settings.WeChatConnectMobileEnabled,
+ WeChatConnectMode: settings.WeChatConnectMode,
+ WeChatConnectScopes: settings.WeChatConnectScopes,
+ WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL,
+ WeChatConnectFrontendRedirectURL: settings.WeChatConnectFrontendRedirectURL,
+ OIDCConnectEnabled: settings.OIDCConnectEnabled,
+ OIDCConnectProviderName: settings.OIDCConnectProviderName,
+ OIDCConnectClientID: settings.OIDCConnectClientID,
+ OIDCConnectClientSecretConfigured: settings.OIDCConnectClientSecretConfigured,
+ OIDCConnectIssuerURL: settings.OIDCConnectIssuerURL,
+ OIDCConnectDiscoveryURL: settings.OIDCConnectDiscoveryURL,
+ OIDCConnectAuthorizeURL: settings.OIDCConnectAuthorizeURL,
+ OIDCConnectTokenURL: settings.OIDCConnectTokenURL,
+ OIDCConnectUserInfoURL: settings.OIDCConnectUserInfoURL,
+ OIDCConnectJWKSURL: settings.OIDCConnectJWKSURL,
+ OIDCConnectScopes: settings.OIDCConnectScopes,
+ OIDCConnectRedirectURL: settings.OIDCConnectRedirectURL,
+ OIDCConnectFrontendRedirectURL: settings.OIDCConnectFrontendRedirectURL,
+ OIDCConnectTokenAuthMethod: settings.OIDCConnectTokenAuthMethod,
+ OIDCConnectUsePKCE: settings.OIDCConnectUsePKCE,
+ OIDCConnectValidateIDToken: settings.OIDCConnectValidateIDToken,
+ OIDCConnectAllowedSigningAlgs: settings.OIDCConnectAllowedSigningAlgs,
+ OIDCConnectClockSkewSeconds: settings.OIDCConnectClockSkewSeconds,
+ OIDCConnectRequireEmailVerified: settings.OIDCConnectRequireEmailVerified,
+ OIDCConnectUserInfoEmailPath: settings.OIDCConnectUserInfoEmailPath,
+ OIDCConnectUserInfoIDPath: settings.OIDCConnectUserInfoIDPath,
+ OIDCConnectUserInfoUsernamePath: settings.OIDCConnectUserInfoUsernamePath,
+ GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
+ GitHubOAuthClientID: settings.GitHubOAuthClientID,
+ GitHubOAuthClientSecretConfigured: settings.GitHubOAuthClientSecretConfigured,
+ GitHubOAuthRedirectURL: settings.GitHubOAuthRedirectURL,
+ GitHubOAuthFrontendRedirectURL: settings.GitHubOAuthFrontendRedirectURL,
+ GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
+ GoogleOAuthClientID: settings.GoogleOAuthClientID,
+ GoogleOAuthClientSecretConfigured: settings.GoogleOAuthClientSecretConfigured,
+ GoogleOAuthRedirectURL: settings.GoogleOAuthRedirectURL,
+ GoogleOAuthFrontendRedirectURL: settings.GoogleOAuthFrontendRedirectURL,
+ SiteName: settings.SiteName,
+ SiteLogo: settings.SiteLogo,
+ SiteSubtitle: settings.SiteSubtitle,
+ APIBaseURL: settings.APIBaseURL,
+ ContactInfo: settings.ContactInfo,
+ DocURL: settings.DocURL,
+ HomeContent: settings.HomeContent,
+ HideCcsImportButton: settings.HideCcsImportButton,
+ PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
+ PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
+ TableDefaultPageSize: settings.TableDefaultPageSize,
+ TablePageSizeOptions: settings.TablePageSizeOptions,
+ CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems),
+ CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
+ DefaultConcurrency: settings.DefaultConcurrency,
+ DefaultBalance: settings.DefaultBalance,
+ RiskControlEnabled: settings.RiskControlEnabled,
+ CyberSessionBlockEnabled: settings.CyberSessionBlockEnabled,
+ CyberSessionBlockTTLSeconds: settings.CyberSessionBlockTTLSeconds,
+ AffiliateRebateRate: settings.AffiliateRebateRate,
+ AffiliateRebateFreezeHours: settings.AffiliateRebateFreezeHours,
+ AffiliateRebateDurationDays: settings.AffiliateRebateDurationDays,
+ AffiliateRebatePerInviteeCap: settings.AffiliateRebatePerInviteeCap,
+ DefaultUserRPMLimit: settings.DefaultUserRPMLimit,
+ DefaultSubscriptions: defaultSubscriptions,
+ EnableModelFallback: settings.EnableModelFallback,
+ FallbackModelAnthropic: settings.FallbackModelAnthropic,
+ FallbackModelOpenAI: settings.FallbackModelOpenAI,
+ FallbackModelGemini: settings.FallbackModelGemini,
+ FallbackModelAntigravity: settings.FallbackModelAntigravity,
+ EnableIdentityPatch: settings.EnableIdentityPatch,
+ IdentityPatchPrompt: settings.IdentityPatchPrompt,
+ OpsMonitoringEnabled: opsEnabled && settings.OpsMonitoringEnabled,
+ OpsRealtimeMonitoringEnabled: settings.OpsRealtimeMonitoringEnabled,
+ OpsQueryModeDefault: settings.OpsQueryModeDefault,
+ OpsMetricsIntervalSeconds: settings.OpsMetricsIntervalSeconds,
+ MinClaudeCodeVersion: settings.MinClaudeCodeVersion,
+ MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion,
+ AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling,
+ BackendModeEnabled: settings.BackendModeEnabled,
+ EnableFingerprintUnification: settings.EnableFingerprintUnification,
+ EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
+ EnableCCHSigning: settings.EnableCCHSigning,
+ EnableClaudeOAuthSystemPromptInjection: settings.EnableClaudeOAuthSystemPromptInjection,
+ ClaudeOAuthSystemPrompt: settings.ClaudeOAuthSystemPrompt,
+ ClaudeOAuthSystemPromptBlocks: settings.ClaudeOAuthSystemPromptBlocks,
+ EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
+ RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
+ EnableClientDatelineNormalization: settings.EnableClientDatelineNormalization,
+ AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
+ OpenAICodexUserAgent: settings.OpenAICodexUserAgent,
+ MinCodexVersion: settings.MinCodexVersion,
+ MaxCodexVersion: settings.MaxCodexVersion,
+ CodexCLIOnlyBlacklist: settings.CodexCLIOnlyBlacklist,
+ CodexCLIOnlyWhitelist: settings.CodexCLIOnlyWhitelist,
+ CodexCLIOnlyAllowAppServerClients: settings.CodexCLIOnlyAllowAppServerClients,
+ CodexCLIOnlyEngineFingerprintSignals: settings.CodexCLIOnlyEngineFingerprintSignals,
+ WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
+ PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
+ PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
+ PaymentVisibleMethodAlipayEnabled: settings.PaymentVisibleMethodAlipayEnabled,
+ PaymentVisibleMethodWxpayEnabled: settings.PaymentVisibleMethodWxpayEnabled,
+ OpenAIAdvancedSchedulerEnabled: settings.OpenAIAdvancedSchedulerEnabled,
+ OpenAIAdvancedSchedulerStickyWeightedEnabled: settings.OpenAIAdvancedSchedulerStickyWeightedEnabled,
+ OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
+ OpenAIAdvancedSchedulerLBTopK: settings.OpenAIAdvancedSchedulerLBTopK,
+ OpenAIAdvancedSchedulerWeightPriority: settings.OpenAIAdvancedSchedulerWeightPriority,
+ OpenAIAdvancedSchedulerWeightLoad: settings.OpenAIAdvancedSchedulerWeightLoad,
+ OpenAIAdvancedSchedulerWeightQueue: settings.OpenAIAdvancedSchedulerWeightQueue,
+ OpenAIAdvancedSchedulerWeightErrorRate: settings.OpenAIAdvancedSchedulerWeightErrorRate,
+ OpenAIAdvancedSchedulerWeightTTFT: settings.OpenAIAdvancedSchedulerWeightTTFT,
+ OpenAIAdvancedSchedulerWeightReset: settings.OpenAIAdvancedSchedulerWeightReset,
+ OpenAIAdvancedSchedulerWeightQuotaHeadroom: settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
+ OpenAIAdvancedSchedulerWeightPreviousResponse: settings.OpenAIAdvancedSchedulerWeightPreviousResponse,
+ OpenAIAdvancedSchedulerWeightSessionSticky: settings.OpenAIAdvancedSchedulerWeightSessionSticky,
+ OpenAIAdvancedSchedulerEffectiveLBTopK: settings.OpenAIAdvancedSchedulerEffectiveLBTopK,
+ OpenAIAdvancedSchedulerEffectiveWeightPriority: settings.OpenAIAdvancedSchedulerEffectiveWeightPriority,
+ OpenAIAdvancedSchedulerEffectiveWeightLoad: settings.OpenAIAdvancedSchedulerEffectiveWeightLoad,
+ OpenAIAdvancedSchedulerEffectiveWeightQueue: settings.OpenAIAdvancedSchedulerEffectiveWeightQueue,
+ OpenAIAdvancedSchedulerEffectiveWeightErrorRate: settings.OpenAIAdvancedSchedulerEffectiveWeightErrorRate,
+ OpenAIAdvancedSchedulerEffectiveWeightTTFT: settings.OpenAIAdvancedSchedulerEffectiveWeightTTFT,
+ OpenAIAdvancedSchedulerEffectiveWeightReset: settings.OpenAIAdvancedSchedulerEffectiveWeightReset,
+ OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom: settings.OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom,
+ OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse: settings.OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse,
+ OpenAIAdvancedSchedulerEffectiveWeightSessionSticky: settings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky,
+ BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
+ BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
+ BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
+ SubscriptionExpiryNotifyEnabled: settings.SubscriptionExpiryNotifyEnabled,
+ AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
+ AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(settings.AccountQuotaNotifyEmails),
+ PaymentEnabled: paymentCfg.Enabled,
+ PaymentMinAmount: paymentCfg.MinAmount,
+ PaymentMaxAmount: paymentCfg.MaxAmount,
+ PaymentDailyLimit: paymentCfg.DailyLimit,
+ PaymentOrderTimeoutMin: paymentCfg.OrderTimeoutMin,
+ PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders,
+ PaymentEnabledTypes: paymentCfg.EnabledTypes,
+ PaymentBalanceDisabled: paymentCfg.BalanceDisabled,
+ PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier,
+ PaymentRechargeFeeRate: paymentCfg.RechargeFeeRate,
+ PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy,
+ PaymentProductNamePrefix: paymentCfg.ProductNamePrefix,
+ PaymentProductNameSuffix: paymentCfg.ProductNameSuffix,
+ PaymentHelpImageURL: paymentCfg.HelpImageURL,
+ PaymentHelpText: paymentCfg.HelpText,
+ PaymentCancelRateLimitEnabled: paymentCfg.CancelRateLimitEnabled,
+ PaymentCancelRateLimitMax: paymentCfg.CancelRateLimitMax,
+ PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow,
+ PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit,
+ PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode,
+ PaymentAlipayForceQRCode: paymentCfg.AlipayForceQRCode,
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
@@ -618,7 +640,19 @@ type UpdateSettingsRequest struct {
PaymentVisibleMethodWxpayEnabled *bool `json:"payment_visible_method_wxpay_enabled"`
// OpenAI account scheduling
- OpenAIAdvancedSchedulerEnabled *bool `json:"openai_advanced_scheduler_enabled"`
+ OpenAIAdvancedSchedulerEnabled *bool `json:"openai_advanced_scheduler_enabled"`
+ OpenAIAdvancedSchedulerStickyWeightedEnabled *bool `json:"openai_advanced_scheduler_sticky_weighted_enabled"`
+ OpenAIAdvancedSchedulerSubscriptionPriorityEnabled *bool `json:"openai_advanced_scheduler_subscription_priority_enabled"`
+ OpenAIAdvancedSchedulerLBTopK *string `json:"openai_advanced_scheduler_lb_top_k"`
+ OpenAIAdvancedSchedulerWeightPriority *string `json:"openai_advanced_scheduler_weight_priority"`
+ OpenAIAdvancedSchedulerWeightLoad *string `json:"openai_advanced_scheduler_weight_load"`
+ OpenAIAdvancedSchedulerWeightQueue *string `json:"openai_advanced_scheduler_weight_queue"`
+ OpenAIAdvancedSchedulerWeightErrorRate *string `json:"openai_advanced_scheduler_weight_error_rate"`
+ OpenAIAdvancedSchedulerWeightTTFT *string `json:"openai_advanced_scheduler_weight_ttft"`
+ OpenAIAdvancedSchedulerWeightReset *string `json:"openai_advanced_scheduler_weight_reset"`
+ OpenAIAdvancedSchedulerWeightQuotaHeadroom *string `json:"openai_advanced_scheduler_weight_quota_headroom"`
+ OpenAIAdvancedSchedulerWeightPreviousResponse *string `json:"openai_advanced_scheduler_weight_previous_response"`
+ OpenAIAdvancedSchedulerWeightSessionSticky *string `json:"openai_advanced_scheduler_weight_session_sticky"`
// 余额不足提醒
BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
@@ -1792,6 +1826,28 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.OpenAIAdvancedSchedulerEnabled
}(),
+ OpenAIAdvancedSchedulerStickyWeightedEnabled: func() bool {
+ if req.OpenAIAdvancedSchedulerStickyWeightedEnabled != nil {
+ return *req.OpenAIAdvancedSchedulerStickyWeightedEnabled
+ }
+ return previousSettings.OpenAIAdvancedSchedulerStickyWeightedEnabled
+ }(),
+ OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: func() bool {
+ if req.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled != nil {
+ return *req.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled
+ }
+ return previousSettings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled
+ }(),
+ OpenAIAdvancedSchedulerLBTopK: stringSetting(req.OpenAIAdvancedSchedulerLBTopK, previousSettings.OpenAIAdvancedSchedulerLBTopK),
+ OpenAIAdvancedSchedulerWeightPriority: stringSetting(req.OpenAIAdvancedSchedulerWeightPriority, previousSettings.OpenAIAdvancedSchedulerWeightPriority),
+ OpenAIAdvancedSchedulerWeightLoad: stringSetting(req.OpenAIAdvancedSchedulerWeightLoad, previousSettings.OpenAIAdvancedSchedulerWeightLoad),
+ OpenAIAdvancedSchedulerWeightQueue: stringSetting(req.OpenAIAdvancedSchedulerWeightQueue, previousSettings.OpenAIAdvancedSchedulerWeightQueue),
+ OpenAIAdvancedSchedulerWeightErrorRate: stringSetting(req.OpenAIAdvancedSchedulerWeightErrorRate, previousSettings.OpenAIAdvancedSchedulerWeightErrorRate),
+ OpenAIAdvancedSchedulerWeightTTFT: stringSetting(req.OpenAIAdvancedSchedulerWeightTTFT, previousSettings.OpenAIAdvancedSchedulerWeightTTFT),
+ OpenAIAdvancedSchedulerWeightReset: stringSetting(req.OpenAIAdvancedSchedulerWeightReset, previousSettings.OpenAIAdvancedSchedulerWeightReset),
+ OpenAIAdvancedSchedulerWeightQuotaHeadroom: stringSetting(req.OpenAIAdvancedSchedulerWeightQuotaHeadroom, previousSettings.OpenAIAdvancedSchedulerWeightQuotaHeadroom),
+ OpenAIAdvancedSchedulerWeightPreviousResponse: stringSetting(req.OpenAIAdvancedSchedulerWeightPreviousResponse, previousSettings.OpenAIAdvancedSchedulerWeightPreviousResponse),
+ OpenAIAdvancedSchedulerWeightSessionSticky: stringSetting(req.OpenAIAdvancedSchedulerWeightSessionSticky, previousSettings.OpenAIAdvancedSchedulerWeightSessionSticky),
BalanceLowNotifyEnabled: func() bool {
if req.BalanceLowNotifyEnabled != nil {
return *req.BalanceLowNotifyEnabled
@@ -2014,184 +2070,206 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
payload := dto.SystemSettings{
- RegistrationEnabled: updatedSettings.RegistrationEnabled,
- EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled,
- RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist,
- PromoCodeEnabled: updatedSettings.PromoCodeEnabled,
- PasswordResetEnabled: updatedSettings.PasswordResetEnabled,
- FrontendURL: updatedSettings.FrontendURL,
- InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled,
- TotpEnabled: updatedSettings.TotpEnabled,
- TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
- LoginAgreementEnabled: updatedSettings.LoginAgreementEnabled,
- LoginAgreementMode: updatedSettings.LoginAgreementMode,
- LoginAgreementUpdatedAt: updatedSettings.LoginAgreementUpdatedAt,
- LoginAgreementDocuments: loginAgreementDocumentsToDTO(updatedSettings.LoginAgreementDocuments),
- SMTPHost: updatedSettings.SMTPHost,
- SMTPPort: updatedSettings.SMTPPort,
- SMTPUsername: updatedSettings.SMTPUsername,
- SMTPPasswordConfigured: updatedSettings.SMTPPasswordConfigured,
- SMTPFrom: updatedSettings.SMTPFrom,
- SMTPFromName: updatedSettings.SMTPFromName,
- SMTPUseTLS: updatedSettings.SMTPUseTLS,
- TurnstileEnabled: updatedSettings.TurnstileEnabled,
- TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
- TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
- APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
- LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
- LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
- LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
- LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
- DingTalkConnectEnabled: updatedSettings.DingTalkConnectEnabled,
- DingTalkConnectClientID: updatedSettings.DingTalkConnectClientID,
- DingTalkConnectClientSecretConfigured: updatedSettings.DingTalkConnectClientSecretConfigured,
- DingTalkConnectRedirectURL: updatedSettings.DingTalkConnectRedirectURL,
- DingTalkConnectCorpRestrictionPolicy: updatedSettings.DingTalkConnectCorpRestrictionPolicy,
- DingTalkConnectInternalCorpID: updatedSettings.DingTalkConnectInternalCorpID,
- DingTalkConnectBypassRegistration: updatedSettings.DingTalkConnectBypassRegistration,
- DingTalkConnectSyncCorpEmail: updatedSettings.DingTalkConnectSyncCorpEmail,
- DingTalkConnectSyncDisplayName: updatedSettings.DingTalkConnectSyncDisplayName,
- DingTalkConnectSyncDept: updatedSettings.DingTalkConnectSyncDept,
- DingTalkConnectSyncCorpEmailAttrKey: updatedSettings.DingTalkConnectSyncCorpEmailAttrKey,
- DingTalkConnectSyncDisplayNameAttrKey: updatedSettings.DingTalkConnectSyncDisplayNameAttrKey,
- DingTalkConnectSyncDeptAttrKey: updatedSettings.DingTalkConnectSyncDeptAttrKey,
- DingTalkConnectSyncCorpEmailAttrName: updatedSettings.DingTalkConnectSyncCorpEmailAttrName,
- DingTalkConnectSyncDisplayNameAttrName: updatedSettings.DingTalkConnectSyncDisplayNameAttrName,
- DingTalkConnectSyncDeptAttrName: updatedSettings.DingTalkConnectSyncDeptAttrName,
- WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled,
- WeChatConnectAppID: updatedSettings.WeChatConnectAppID,
- WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured,
- WeChatConnectOpenAppID: updatedSettings.WeChatConnectOpenAppID,
- WeChatConnectOpenAppSecretConfigured: updatedSettings.WeChatConnectOpenAppSecretConfigured,
- WeChatConnectMPAppID: updatedSettings.WeChatConnectMPAppID,
- WeChatConnectMPAppSecretConfigured: updatedSettings.WeChatConnectMPAppSecretConfigured,
- WeChatConnectMobileAppID: updatedSettings.WeChatConnectMobileAppID,
- WeChatConnectMobileAppSecretConfigured: updatedSettings.WeChatConnectMobileAppSecretConfigured,
- WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled,
- WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled,
- WeChatConnectMobileEnabled: updatedSettings.WeChatConnectMobileEnabled,
- WeChatConnectMode: updatedSettings.WeChatConnectMode,
- WeChatConnectScopes: updatedSettings.WeChatConnectScopes,
- WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL,
- WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL,
- OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled,
- OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName,
- OIDCConnectClientID: updatedSettings.OIDCConnectClientID,
- OIDCConnectClientSecretConfigured: updatedSettings.OIDCConnectClientSecretConfigured,
- OIDCConnectIssuerURL: updatedSettings.OIDCConnectIssuerURL,
- OIDCConnectDiscoveryURL: updatedSettings.OIDCConnectDiscoveryURL,
- OIDCConnectAuthorizeURL: updatedSettings.OIDCConnectAuthorizeURL,
- OIDCConnectTokenURL: updatedSettings.OIDCConnectTokenURL,
- OIDCConnectUserInfoURL: updatedSettings.OIDCConnectUserInfoURL,
- OIDCConnectJWKSURL: updatedSettings.OIDCConnectJWKSURL,
- OIDCConnectScopes: updatedSettings.OIDCConnectScopes,
- OIDCConnectRedirectURL: updatedSettings.OIDCConnectRedirectURL,
- OIDCConnectFrontendRedirectURL: updatedSettings.OIDCConnectFrontendRedirectURL,
- OIDCConnectTokenAuthMethod: updatedSettings.OIDCConnectTokenAuthMethod,
- OIDCConnectUsePKCE: updatedSettings.OIDCConnectUsePKCE,
- OIDCConnectValidateIDToken: updatedSettings.OIDCConnectValidateIDToken,
- OIDCConnectAllowedSigningAlgs: updatedSettings.OIDCConnectAllowedSigningAlgs,
- OIDCConnectClockSkewSeconds: updatedSettings.OIDCConnectClockSkewSeconds,
- OIDCConnectRequireEmailVerified: updatedSettings.OIDCConnectRequireEmailVerified,
- OIDCConnectUserInfoEmailPath: updatedSettings.OIDCConnectUserInfoEmailPath,
- OIDCConnectUserInfoIDPath: updatedSettings.OIDCConnectUserInfoIDPath,
- OIDCConnectUserInfoUsernamePath: updatedSettings.OIDCConnectUserInfoUsernamePath,
- GitHubOAuthEnabled: updatedSettings.GitHubOAuthEnabled,
- GitHubOAuthClientID: updatedSettings.GitHubOAuthClientID,
- GitHubOAuthClientSecretConfigured: updatedSettings.GitHubOAuthClientSecretConfigured,
- GitHubOAuthRedirectURL: updatedSettings.GitHubOAuthRedirectURL,
- GitHubOAuthFrontendRedirectURL: updatedSettings.GitHubOAuthFrontendRedirectURL,
- GoogleOAuthEnabled: updatedSettings.GoogleOAuthEnabled,
- GoogleOAuthClientID: updatedSettings.GoogleOAuthClientID,
- GoogleOAuthClientSecretConfigured: updatedSettings.GoogleOAuthClientSecretConfigured,
- GoogleOAuthRedirectURL: updatedSettings.GoogleOAuthRedirectURL,
- GoogleOAuthFrontendRedirectURL: updatedSettings.GoogleOAuthFrontendRedirectURL,
- SiteName: updatedSettings.SiteName,
- SiteLogo: updatedSettings.SiteLogo,
- SiteSubtitle: updatedSettings.SiteSubtitle,
- APIBaseURL: updatedSettings.APIBaseURL,
- ContactInfo: updatedSettings.ContactInfo,
- DocURL: updatedSettings.DocURL,
- HomeContent: updatedSettings.HomeContent,
- HideCcsImportButton: updatedSettings.HideCcsImportButton,
- PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
- PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
- TableDefaultPageSize: updatedSettings.TableDefaultPageSize,
- TablePageSizeOptions: updatedSettings.TablePageSizeOptions,
- CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
- CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
- DefaultConcurrency: updatedSettings.DefaultConcurrency,
- DefaultBalance: updatedSettings.DefaultBalance,
- AffiliateRebateRate: updatedSettings.AffiliateRebateRate,
- AffiliateRebateFreezeHours: updatedSettings.AffiliateRebateFreezeHours,
- AffiliateRebateDurationDays: updatedSettings.AffiliateRebateDurationDays,
- AffiliateRebatePerInviteeCap: updatedSettings.AffiliateRebatePerInviteeCap,
- DefaultUserRPMLimit: updatedSettings.DefaultUserRPMLimit,
- DefaultSubscriptions: updatedDefaultSubscriptions,
- EnableModelFallback: updatedSettings.EnableModelFallback,
- FallbackModelAnthropic: updatedSettings.FallbackModelAnthropic,
- FallbackModelOpenAI: updatedSettings.FallbackModelOpenAI,
- FallbackModelGemini: updatedSettings.FallbackModelGemini,
- FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity,
- EnableIdentityPatch: updatedSettings.EnableIdentityPatch,
- IdentityPatchPrompt: updatedSettings.IdentityPatchPrompt,
- OpsMonitoringEnabled: updatedSettings.OpsMonitoringEnabled,
- OpsRealtimeMonitoringEnabled: updatedSettings.OpsRealtimeMonitoringEnabled,
- OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault,
- OpsMetricsIntervalSeconds: updatedSettings.OpsMetricsIntervalSeconds,
- MinClaudeCodeVersion: updatedSettings.MinClaudeCodeVersion,
- MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion,
- AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling,
- BackendModeEnabled: updatedSettings.BackendModeEnabled,
- EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification,
- EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
- EnableCCHSigning: updatedSettings.EnableCCHSigning,
- EnableClaudeOAuthSystemPromptInjection: updatedSettings.EnableClaudeOAuthSystemPromptInjection,
- ClaudeOAuthSystemPrompt: updatedSettings.ClaudeOAuthSystemPrompt,
- ClaudeOAuthSystemPromptBlocks: updatedSettings.ClaudeOAuthSystemPromptBlocks,
- EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
- RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
- EnableClientDatelineNormalization: updatedSettings.EnableClientDatelineNormalization,
- AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
- OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
- MinCodexVersion: updatedSettings.MinCodexVersion,
- MaxCodexVersion: updatedSettings.MaxCodexVersion,
- CodexCLIOnlyBlacklist: updatedSettings.CodexCLIOnlyBlacklist,
- CodexCLIOnlyWhitelist: updatedSettings.CodexCLIOnlyWhitelist,
- CodexCLIOnlyAllowAppServerClients: updatedSettings.CodexCLIOnlyAllowAppServerClients,
- CodexCLIOnlyEngineFingerprintSignals: updatedSettings.CodexCLIOnlyEngineFingerprintSignals,
- PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
- PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
- PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
- PaymentVisibleMethodWxpayEnabled: updatedSettings.PaymentVisibleMethodWxpayEnabled,
- OpenAIAdvancedSchedulerEnabled: updatedSettings.OpenAIAdvancedSchedulerEnabled,
- BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
- BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
- BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL,
- SubscriptionExpiryNotifyEnabled: updatedSettings.SubscriptionExpiryNotifyEnabled,
- AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled,
- AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
- PaymentEnabled: updatedPaymentCfg.Enabled,
- PaymentMinAmount: updatedPaymentCfg.MinAmount,
- PaymentMaxAmount: updatedPaymentCfg.MaxAmount,
- PaymentDailyLimit: updatedPaymentCfg.DailyLimit,
- PaymentOrderTimeoutMin: updatedPaymentCfg.OrderTimeoutMin,
- PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders,
- PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes,
- PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled,
- PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier,
- PaymentRechargeFeeRate: updatedPaymentCfg.RechargeFeeRate,
- PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy,
- PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix,
- PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix,
- PaymentHelpImageURL: updatedPaymentCfg.HelpImageURL,
- PaymentHelpText: updatedPaymentCfg.HelpText,
- PaymentCancelRateLimitEnabled: updatedPaymentCfg.CancelRateLimitEnabled,
- PaymentCancelRateLimitMax: updatedPaymentCfg.CancelRateLimitMax,
- PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow,
- PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit,
- PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode,
- PaymentAlipayForceQRCode: updatedPaymentCfg.AlipayForceQRCode,
+ RegistrationEnabled: updatedSettings.RegistrationEnabled,
+ EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled,
+ RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist,
+ PromoCodeEnabled: updatedSettings.PromoCodeEnabled,
+ PasswordResetEnabled: updatedSettings.PasswordResetEnabled,
+ FrontendURL: updatedSettings.FrontendURL,
+ InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled,
+ TotpEnabled: updatedSettings.TotpEnabled,
+ TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
+ LoginAgreementEnabled: updatedSettings.LoginAgreementEnabled,
+ LoginAgreementMode: updatedSettings.LoginAgreementMode,
+ LoginAgreementUpdatedAt: updatedSettings.LoginAgreementUpdatedAt,
+ LoginAgreementDocuments: loginAgreementDocumentsToDTO(updatedSettings.LoginAgreementDocuments),
+ SMTPHost: updatedSettings.SMTPHost,
+ SMTPPort: updatedSettings.SMTPPort,
+ SMTPUsername: updatedSettings.SMTPUsername,
+ SMTPPasswordConfigured: updatedSettings.SMTPPasswordConfigured,
+ SMTPFrom: updatedSettings.SMTPFrom,
+ SMTPFromName: updatedSettings.SMTPFromName,
+ SMTPUseTLS: updatedSettings.SMTPUseTLS,
+ TurnstileEnabled: updatedSettings.TurnstileEnabled,
+ TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
+ TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
+ APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
+ LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
+ LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
+ LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
+ LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
+ DingTalkConnectEnabled: updatedSettings.DingTalkConnectEnabled,
+ DingTalkConnectClientID: updatedSettings.DingTalkConnectClientID,
+ DingTalkConnectClientSecretConfigured: updatedSettings.DingTalkConnectClientSecretConfigured,
+ DingTalkConnectRedirectURL: updatedSettings.DingTalkConnectRedirectURL,
+ DingTalkConnectCorpRestrictionPolicy: updatedSettings.DingTalkConnectCorpRestrictionPolicy,
+ DingTalkConnectInternalCorpID: updatedSettings.DingTalkConnectInternalCorpID,
+ DingTalkConnectBypassRegistration: updatedSettings.DingTalkConnectBypassRegistration,
+ DingTalkConnectSyncCorpEmail: updatedSettings.DingTalkConnectSyncCorpEmail,
+ DingTalkConnectSyncDisplayName: updatedSettings.DingTalkConnectSyncDisplayName,
+ DingTalkConnectSyncDept: updatedSettings.DingTalkConnectSyncDept,
+ DingTalkConnectSyncCorpEmailAttrKey: updatedSettings.DingTalkConnectSyncCorpEmailAttrKey,
+ DingTalkConnectSyncDisplayNameAttrKey: updatedSettings.DingTalkConnectSyncDisplayNameAttrKey,
+ DingTalkConnectSyncDeptAttrKey: updatedSettings.DingTalkConnectSyncDeptAttrKey,
+ DingTalkConnectSyncCorpEmailAttrName: updatedSettings.DingTalkConnectSyncCorpEmailAttrName,
+ DingTalkConnectSyncDisplayNameAttrName: updatedSettings.DingTalkConnectSyncDisplayNameAttrName,
+ DingTalkConnectSyncDeptAttrName: updatedSettings.DingTalkConnectSyncDeptAttrName,
+ WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled,
+ WeChatConnectAppID: updatedSettings.WeChatConnectAppID,
+ WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured,
+ WeChatConnectOpenAppID: updatedSettings.WeChatConnectOpenAppID,
+ WeChatConnectOpenAppSecretConfigured: updatedSettings.WeChatConnectOpenAppSecretConfigured,
+ WeChatConnectMPAppID: updatedSettings.WeChatConnectMPAppID,
+ WeChatConnectMPAppSecretConfigured: updatedSettings.WeChatConnectMPAppSecretConfigured,
+ WeChatConnectMobileAppID: updatedSettings.WeChatConnectMobileAppID,
+ WeChatConnectMobileAppSecretConfigured: updatedSettings.WeChatConnectMobileAppSecretConfigured,
+ WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled,
+ WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled,
+ WeChatConnectMobileEnabled: updatedSettings.WeChatConnectMobileEnabled,
+ WeChatConnectMode: updatedSettings.WeChatConnectMode,
+ WeChatConnectScopes: updatedSettings.WeChatConnectScopes,
+ WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL,
+ WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL,
+ OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled,
+ OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName,
+ OIDCConnectClientID: updatedSettings.OIDCConnectClientID,
+ OIDCConnectClientSecretConfigured: updatedSettings.OIDCConnectClientSecretConfigured,
+ OIDCConnectIssuerURL: updatedSettings.OIDCConnectIssuerURL,
+ OIDCConnectDiscoveryURL: updatedSettings.OIDCConnectDiscoveryURL,
+ OIDCConnectAuthorizeURL: updatedSettings.OIDCConnectAuthorizeURL,
+ OIDCConnectTokenURL: updatedSettings.OIDCConnectTokenURL,
+ OIDCConnectUserInfoURL: updatedSettings.OIDCConnectUserInfoURL,
+ OIDCConnectJWKSURL: updatedSettings.OIDCConnectJWKSURL,
+ OIDCConnectScopes: updatedSettings.OIDCConnectScopes,
+ OIDCConnectRedirectURL: updatedSettings.OIDCConnectRedirectURL,
+ OIDCConnectFrontendRedirectURL: updatedSettings.OIDCConnectFrontendRedirectURL,
+ OIDCConnectTokenAuthMethod: updatedSettings.OIDCConnectTokenAuthMethod,
+ OIDCConnectUsePKCE: updatedSettings.OIDCConnectUsePKCE,
+ OIDCConnectValidateIDToken: updatedSettings.OIDCConnectValidateIDToken,
+ OIDCConnectAllowedSigningAlgs: updatedSettings.OIDCConnectAllowedSigningAlgs,
+ OIDCConnectClockSkewSeconds: updatedSettings.OIDCConnectClockSkewSeconds,
+ OIDCConnectRequireEmailVerified: updatedSettings.OIDCConnectRequireEmailVerified,
+ OIDCConnectUserInfoEmailPath: updatedSettings.OIDCConnectUserInfoEmailPath,
+ OIDCConnectUserInfoIDPath: updatedSettings.OIDCConnectUserInfoIDPath,
+ OIDCConnectUserInfoUsernamePath: updatedSettings.OIDCConnectUserInfoUsernamePath,
+ GitHubOAuthEnabled: updatedSettings.GitHubOAuthEnabled,
+ GitHubOAuthClientID: updatedSettings.GitHubOAuthClientID,
+ GitHubOAuthClientSecretConfigured: updatedSettings.GitHubOAuthClientSecretConfigured,
+ GitHubOAuthRedirectURL: updatedSettings.GitHubOAuthRedirectURL,
+ GitHubOAuthFrontendRedirectURL: updatedSettings.GitHubOAuthFrontendRedirectURL,
+ GoogleOAuthEnabled: updatedSettings.GoogleOAuthEnabled,
+ GoogleOAuthClientID: updatedSettings.GoogleOAuthClientID,
+ GoogleOAuthClientSecretConfigured: updatedSettings.GoogleOAuthClientSecretConfigured,
+ GoogleOAuthRedirectURL: updatedSettings.GoogleOAuthRedirectURL,
+ GoogleOAuthFrontendRedirectURL: updatedSettings.GoogleOAuthFrontendRedirectURL,
+ SiteName: updatedSettings.SiteName,
+ SiteLogo: updatedSettings.SiteLogo,
+ SiteSubtitle: updatedSettings.SiteSubtitle,
+ APIBaseURL: updatedSettings.APIBaseURL,
+ ContactInfo: updatedSettings.ContactInfo,
+ DocURL: updatedSettings.DocURL,
+ HomeContent: updatedSettings.HomeContent,
+ HideCcsImportButton: updatedSettings.HideCcsImportButton,
+ PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
+ PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
+ TableDefaultPageSize: updatedSettings.TableDefaultPageSize,
+ TablePageSizeOptions: updatedSettings.TablePageSizeOptions,
+ CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
+ CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
+ DefaultConcurrency: updatedSettings.DefaultConcurrency,
+ DefaultBalance: updatedSettings.DefaultBalance,
+ AffiliateRebateRate: updatedSettings.AffiliateRebateRate,
+ AffiliateRebateFreezeHours: updatedSettings.AffiliateRebateFreezeHours,
+ AffiliateRebateDurationDays: updatedSettings.AffiliateRebateDurationDays,
+ AffiliateRebatePerInviteeCap: updatedSettings.AffiliateRebatePerInviteeCap,
+ DefaultUserRPMLimit: updatedSettings.DefaultUserRPMLimit,
+ DefaultSubscriptions: updatedDefaultSubscriptions,
+ EnableModelFallback: updatedSettings.EnableModelFallback,
+ FallbackModelAnthropic: updatedSettings.FallbackModelAnthropic,
+ FallbackModelOpenAI: updatedSettings.FallbackModelOpenAI,
+ FallbackModelGemini: updatedSettings.FallbackModelGemini,
+ FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity,
+ EnableIdentityPatch: updatedSettings.EnableIdentityPatch,
+ IdentityPatchPrompt: updatedSettings.IdentityPatchPrompt,
+ OpsMonitoringEnabled: updatedSettings.OpsMonitoringEnabled,
+ OpsRealtimeMonitoringEnabled: updatedSettings.OpsRealtimeMonitoringEnabled,
+ OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault,
+ OpsMetricsIntervalSeconds: updatedSettings.OpsMetricsIntervalSeconds,
+ MinClaudeCodeVersion: updatedSettings.MinClaudeCodeVersion,
+ MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion,
+ AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling,
+ BackendModeEnabled: updatedSettings.BackendModeEnabled,
+ EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification,
+ EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
+ EnableCCHSigning: updatedSettings.EnableCCHSigning,
+ EnableClaudeOAuthSystemPromptInjection: updatedSettings.EnableClaudeOAuthSystemPromptInjection,
+ ClaudeOAuthSystemPrompt: updatedSettings.ClaudeOAuthSystemPrompt,
+ ClaudeOAuthSystemPromptBlocks: updatedSettings.ClaudeOAuthSystemPromptBlocks,
+ EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
+ RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
+ EnableClientDatelineNormalization: updatedSettings.EnableClientDatelineNormalization,
+ AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
+ OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
+ MinCodexVersion: updatedSettings.MinCodexVersion,
+ MaxCodexVersion: updatedSettings.MaxCodexVersion,
+ CodexCLIOnlyBlacklist: updatedSettings.CodexCLIOnlyBlacklist,
+ CodexCLIOnlyWhitelist: updatedSettings.CodexCLIOnlyWhitelist,
+ CodexCLIOnlyAllowAppServerClients: updatedSettings.CodexCLIOnlyAllowAppServerClients,
+ CodexCLIOnlyEngineFingerprintSignals: updatedSettings.CodexCLIOnlyEngineFingerprintSignals,
+ PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
+ PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
+ PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
+ PaymentVisibleMethodWxpayEnabled: updatedSettings.PaymentVisibleMethodWxpayEnabled,
+ OpenAIAdvancedSchedulerEnabled: updatedSettings.OpenAIAdvancedSchedulerEnabled,
+ OpenAIAdvancedSchedulerStickyWeightedEnabled: updatedSettings.OpenAIAdvancedSchedulerStickyWeightedEnabled,
+ OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: updatedSettings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
+ OpenAIAdvancedSchedulerLBTopK: updatedSettings.OpenAIAdvancedSchedulerLBTopK,
+ OpenAIAdvancedSchedulerWeightPriority: updatedSettings.OpenAIAdvancedSchedulerWeightPriority,
+ OpenAIAdvancedSchedulerWeightLoad: updatedSettings.OpenAIAdvancedSchedulerWeightLoad,
+ OpenAIAdvancedSchedulerWeightQueue: updatedSettings.OpenAIAdvancedSchedulerWeightQueue,
+ OpenAIAdvancedSchedulerWeightErrorRate: updatedSettings.OpenAIAdvancedSchedulerWeightErrorRate,
+ OpenAIAdvancedSchedulerWeightTTFT: updatedSettings.OpenAIAdvancedSchedulerWeightTTFT,
+ OpenAIAdvancedSchedulerWeightReset: updatedSettings.OpenAIAdvancedSchedulerWeightReset,
+ OpenAIAdvancedSchedulerWeightQuotaHeadroom: updatedSettings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
+ OpenAIAdvancedSchedulerWeightPreviousResponse: updatedSettings.OpenAIAdvancedSchedulerWeightPreviousResponse,
+ OpenAIAdvancedSchedulerWeightSessionSticky: updatedSettings.OpenAIAdvancedSchedulerWeightSessionSticky,
+ OpenAIAdvancedSchedulerEffectiveLBTopK: updatedSettings.OpenAIAdvancedSchedulerEffectiveLBTopK,
+ OpenAIAdvancedSchedulerEffectiveWeightPriority: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightPriority,
+ OpenAIAdvancedSchedulerEffectiveWeightLoad: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightLoad,
+ OpenAIAdvancedSchedulerEffectiveWeightQueue: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightQueue,
+ OpenAIAdvancedSchedulerEffectiveWeightErrorRate: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightErrorRate,
+ OpenAIAdvancedSchedulerEffectiveWeightTTFT: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightTTFT,
+ OpenAIAdvancedSchedulerEffectiveWeightReset: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightReset,
+ OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom,
+ OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse,
+ OpenAIAdvancedSchedulerEffectiveWeightSessionSticky: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky,
+ BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
+ BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
+ BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL,
+ SubscriptionExpiryNotifyEnabled: updatedSettings.SubscriptionExpiryNotifyEnabled,
+ AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled,
+ AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
+ PaymentEnabled: updatedPaymentCfg.Enabled,
+ PaymentMinAmount: updatedPaymentCfg.MinAmount,
+ PaymentMaxAmount: updatedPaymentCfg.MaxAmount,
+ PaymentDailyLimit: updatedPaymentCfg.DailyLimit,
+ PaymentOrderTimeoutMin: updatedPaymentCfg.OrderTimeoutMin,
+ PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders,
+ PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes,
+ PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled,
+ PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier,
+ PaymentRechargeFeeRate: updatedPaymentCfg.RechargeFeeRate,
+ PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy,
+ PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix,
+ PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix,
+ PaymentHelpImageURL: updatedPaymentCfg.HelpImageURL,
+ PaymentHelpText: updatedPaymentCfg.HelpText,
+ PaymentCancelRateLimitEnabled: updatedPaymentCfg.CancelRateLimitEnabled,
+ PaymentCancelRateLimitMax: updatedPaymentCfg.CancelRateLimitMax,
+ PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow,
+ PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit,
+ PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode,
+ PaymentAlipayForceQRCode: updatedPaymentCfg.AlipayForceQRCode,
ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
@@ -2677,6 +2755,42 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.OpenAIAdvancedSchedulerEnabled != after.OpenAIAdvancedSchedulerEnabled {
changed = append(changed, "openai_advanced_scheduler_enabled")
}
+ if before.OpenAIAdvancedSchedulerStickyWeightedEnabled != after.OpenAIAdvancedSchedulerStickyWeightedEnabled {
+ changed = append(changed, "openai_advanced_scheduler_sticky_weighted_enabled")
+ }
+ if before.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled != after.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled {
+ changed = append(changed, "openai_advanced_scheduler_subscription_priority_enabled")
+ }
+ if before.OpenAIAdvancedSchedulerLBTopK != after.OpenAIAdvancedSchedulerLBTopK {
+ changed = append(changed, "openai_advanced_scheduler_lb_top_k")
+ }
+ if before.OpenAIAdvancedSchedulerWeightPriority != after.OpenAIAdvancedSchedulerWeightPriority {
+ changed = append(changed, "openai_advanced_scheduler_weight_priority")
+ }
+ if before.OpenAIAdvancedSchedulerWeightLoad != after.OpenAIAdvancedSchedulerWeightLoad {
+ changed = append(changed, "openai_advanced_scheduler_weight_load")
+ }
+ if before.OpenAIAdvancedSchedulerWeightQueue != after.OpenAIAdvancedSchedulerWeightQueue {
+ changed = append(changed, "openai_advanced_scheduler_weight_queue")
+ }
+ if before.OpenAIAdvancedSchedulerWeightErrorRate != after.OpenAIAdvancedSchedulerWeightErrorRate {
+ changed = append(changed, "openai_advanced_scheduler_weight_error_rate")
+ }
+ if before.OpenAIAdvancedSchedulerWeightTTFT != after.OpenAIAdvancedSchedulerWeightTTFT {
+ changed = append(changed, "openai_advanced_scheduler_weight_ttft")
+ }
+ if before.OpenAIAdvancedSchedulerWeightReset != after.OpenAIAdvancedSchedulerWeightReset {
+ changed = append(changed, "openai_advanced_scheduler_weight_reset")
+ }
+ if before.OpenAIAdvancedSchedulerWeightQuotaHeadroom != after.OpenAIAdvancedSchedulerWeightQuotaHeadroom {
+ changed = append(changed, "openai_advanced_scheduler_weight_quota_headroom")
+ }
+ if before.OpenAIAdvancedSchedulerWeightPreviousResponse != after.OpenAIAdvancedSchedulerWeightPreviousResponse {
+ changed = append(changed, "openai_advanced_scheduler_weight_previous_response")
+ }
+ if before.OpenAIAdvancedSchedulerWeightSessionSticky != after.OpenAIAdvancedSchedulerWeightSessionSticky {
+ changed = append(changed, "openai_advanced_scheduler_weight_session_sticky")
+ }
// 余额、订阅到期与账号限额通知
if before.BalanceLowNotifyEnabled != after.BalanceLowNotifyEnabled {
changed = append(changed, "balance_low_notify_enabled")
@@ -3829,3 +3943,10 @@ func equalPlatformQuotaSettings(before, after map[string]*service.DefaultPlatfor
}
return true
}
+
+func stringSetting(value *string, fallback string) string {
+ if value == nil {
+ return fallback
+ }
+ return *value
+}
diff --git a/backend/internal/handler/admin/setting_handler_auth_source_defaults_test.go b/backend/internal/handler/admin/setting_handler_auth_source_defaults_test.go
index f953f76760..1626007f19 100644
--- a/backend/internal/handler/admin/setting_handler_auth_source_defaults_test.go
+++ b/backend/internal/handler/admin/setting_handler_auth_source_defaults_test.go
@@ -217,12 +217,13 @@ func TestSettingHandler_UpdateSettings_PersistsPaymentVisibleMethodsAndAdvancedS
handler := NewSettingHandler(svc, nil, nil, nil, nil, nil, nil)
body := map[string]any{
- "promo_code_enabled": true,
- "payment_visible_method_alipay_source": "easypay",
- "payment_visible_method_wxpay_source": "wxpay",
- "payment_visible_method_alipay_enabled": true,
- "payment_visible_method_wxpay_enabled": false,
- "openai_advanced_scheduler_enabled": true,
+ "promo_code_enabled": true,
+ "payment_visible_method_alipay_source": "easypay",
+ "payment_visible_method_wxpay_source": "wxpay",
+ "payment_visible_method_alipay_enabled": true,
+ "payment_visible_method_wxpay_enabled": false,
+ "openai_advanced_scheduler_enabled": true,
+ "openai_advanced_scheduler_subscription_priority_enabled": true,
}
rawBody, err := json.Marshal(body)
require.NoError(t, err)
@@ -240,6 +241,7 @@ func TestSettingHandler_UpdateSettings_PersistsPaymentVisibleMethodsAndAdvancedS
require.Equal(t, "true", repo.values[service.SettingPaymentVisibleMethodAlipayEnabled])
require.Equal(t, "false", repo.values[service.SettingPaymentVisibleMethodWxpayEnabled])
require.Equal(t, "true", repo.values["openai_advanced_scheduler_enabled"])
+ require.Equal(t, "true", repo.values[service.SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled])
var resp response.Response
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
@@ -250,6 +252,7 @@ func TestSettingHandler_UpdateSettings_PersistsPaymentVisibleMethodsAndAdvancedS
require.Equal(t, true, data["payment_visible_method_alipay_enabled"])
require.Equal(t, false, data["payment_visible_method_wxpay_enabled"])
require.Equal(t, true, data["openai_advanced_scheduler_enabled"])
+ require.Equal(t, true, data["openai_advanced_scheduler_subscription_priority_enabled"])
}
func TestSettingHandler_UpdateSettings_PreservesLegacyBlankPaymentVisibleMethodSource(t *testing.T) {
diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go
index 1d6f73bb99..9a20191a0c 100644
--- a/backend/internal/handler/dto/settings.go
+++ b/backend/internal/handler/dto/settings.go
@@ -208,7 +208,29 @@ type SystemSettings struct {
PaymentVisibleMethodWxpayEnabled bool `json:"payment_visible_method_wxpay_enabled"`
// OpenAI account scheduling
- OpenAIAdvancedSchedulerEnabled bool `json:"openai_advanced_scheduler_enabled"`
+ OpenAIAdvancedSchedulerEnabled bool `json:"openai_advanced_scheduler_enabled"`
+ OpenAIAdvancedSchedulerStickyWeightedEnabled bool `json:"openai_advanced_scheduler_sticky_weighted_enabled"`
+ OpenAIAdvancedSchedulerSubscriptionPriorityEnabled bool `json:"openai_advanced_scheduler_subscription_priority_enabled"`
+ OpenAIAdvancedSchedulerLBTopK string `json:"openai_advanced_scheduler_lb_top_k"`
+ OpenAIAdvancedSchedulerWeightPriority string `json:"openai_advanced_scheduler_weight_priority"`
+ OpenAIAdvancedSchedulerWeightLoad string `json:"openai_advanced_scheduler_weight_load"`
+ OpenAIAdvancedSchedulerWeightQueue string `json:"openai_advanced_scheduler_weight_queue"`
+ OpenAIAdvancedSchedulerWeightErrorRate string `json:"openai_advanced_scheduler_weight_error_rate"`
+ OpenAIAdvancedSchedulerWeightTTFT string `json:"openai_advanced_scheduler_weight_ttft"`
+ OpenAIAdvancedSchedulerWeightReset string `json:"openai_advanced_scheduler_weight_reset"`
+ OpenAIAdvancedSchedulerWeightQuotaHeadroom string `json:"openai_advanced_scheduler_weight_quota_headroom"`
+ OpenAIAdvancedSchedulerWeightPreviousResponse string `json:"openai_advanced_scheduler_weight_previous_response"`
+ OpenAIAdvancedSchedulerWeightSessionSticky string `json:"openai_advanced_scheduler_weight_session_sticky"`
+ OpenAIAdvancedSchedulerEffectiveLBTopK string `json:"openai_advanced_scheduler_effective_lb_top_k"`
+ OpenAIAdvancedSchedulerEffectiveWeightPriority string `json:"openai_advanced_scheduler_effective_weight_priority"`
+ OpenAIAdvancedSchedulerEffectiveWeightLoad string `json:"openai_advanced_scheduler_effective_weight_load"`
+ OpenAIAdvancedSchedulerEffectiveWeightQueue string `json:"openai_advanced_scheduler_effective_weight_queue"`
+ OpenAIAdvancedSchedulerEffectiveWeightErrorRate string `json:"openai_advanced_scheduler_effective_weight_error_rate"`
+ OpenAIAdvancedSchedulerEffectiveWeightTTFT string `json:"openai_advanced_scheduler_effective_weight_ttft"`
+ OpenAIAdvancedSchedulerEffectiveWeightReset string `json:"openai_advanced_scheduler_effective_weight_reset"`
+ OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom string `json:"openai_advanced_scheduler_effective_weight_quota_headroom"`
+ OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse string `json:"openai_advanced_scheduler_effective_weight_previous_response"`
+ OpenAIAdvancedSchedulerEffectiveWeightSessionSticky string `json:"openai_advanced_scheduler_effective_weight_session_sticky"`
// Payment configuration
PaymentEnabled bool `json:"payment_enabled"`
diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go
index 8eec667fa6..ccafde7b02 100644
--- a/backend/internal/handler/openai_gateway_handler.go
+++ b/backend/internal/handler/openai_gateway_handler.go
@@ -1266,6 +1266,8 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "previous_response_id must be a response.id (resp_*), not a message id")
return
}
+ firstMessageToolContext := service.ValidateFunctionCallOutputContextBytes(firstMessage)
+ previousResponseCanMove := !firstMessageToolContext.HasFunctionCallOutput || firstMessageToolContext.HasToolCallContext
reqLog = reqLog.With(
zap.Bool("ws_ingress", true),
zap.String("model", reqModel),
@@ -1382,6 +1384,12 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
service.OpenAIEndpointCapabilityChatCompletions,
false,
requestPlatform,
+ func() string {
+ if previousResponseCanMove {
+ return "previous_response_can_move"
+ }
+ return ""
+ }(),
)
if err != nil {
reqLog.Warn("openai.websocket_account_select_failed",
@@ -1581,8 +1589,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
// 说明该会话链不属于本次调度到的账号,原样转发会触发上游会话链鉴权失败(“鉴权失败,请检查 API Key”)。
// 故剥离首包里的 previous_response_id,改用首包内 input 重建上下文;带 function_call_output 的
// 工具续链无法重建,保持原样。仅作用于首轮首包,后续 turn 的续链由 WS 转发层既有逻辑处理。
- if previousResponseID != "" && !scheduleDecision.StickyPreviousHit &&
- !service.ValidateFunctionCallOutputContextBytes(wsFirstMessage).HasFunctionCallOutput {
+ if previousResponseID != "" && !scheduleDecision.StickyPreviousHit && previousResponseCanMove {
wsFirstMessage = service.RemovePreviousResponseIDFromBody(wsFirstMessage)
reqLog.Debug("openai.websocket_previous_response_id_stripped_cross_group",
zap.Int64("account_id", account.ID),
diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go
index 1a21b5893c..a8015ddba0 100644
--- a/backend/internal/repository/account_repo.go
+++ b/backend/internal/repository/account_repo.go
@@ -482,7 +482,7 @@ func (r *accountRepository) List(ctx context.Context, params pagination.Paginati
return r.ListWithFilters(ctx, params, "", "", "", "", 0, "")
}
-func (r *accountRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, *pagination.PaginationResult, error) {
+func (r *accountRepository) accountListFilteredQuery(platform, accountType, status, search string, groupID int64, privacyMode string) *dbent.AccountQuery {
q := r.client.Account.Query()
if platform != "" {
@@ -575,6 +575,11 @@ func (r *accountRepository) ListWithFilters(ctx context.Context, params paginati
}))
}
+ return q
+}
+
+func (r *accountRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, *pagination.PaginationResult, error) {
+ q := r.accountListFilteredQuery(platform, accountType, status, search, groupID, privacyMode)
// Clone before Count so interceptor-appended predicates (SoftDeleteMixin's
// deleted_at IS NULL) don't accumulate on the shared builder and pollute the
// subsequent list query. Same pattern used in group_repo/promo_code_repo/user_repo
@@ -603,6 +608,14 @@ func (r *accountRepository) ListWithFilters(ctx context.Context, params paginati
return outAccounts, paginationResultFromTotal(int64(total), params), nil
}
+func (r *accountRepository) ListAllWithFilters(ctx context.Context, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, error) {
+ accounts, err := r.accountListFilteredQuery(platform, accountType, status, search, groupID, privacyMode).All(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return r.accountsToService(ctx, accounts)
+}
+
func (r *accountRepository) ListOpsAccountsForStats(ctx context.Context, platformFilter string, groupIDFilter *int64) ([]service.Account, error) {
if r == nil || r.client == nil {
return []service.Account{}, nil
diff --git a/backend/internal/repository/scheduler_cache.go b/backend/internal/repository/scheduler_cache.go
index c508f09c71..c8e1fe14e0 100644
--- a/backend/internal/repository/scheduler_cache.go
+++ b/backend/internal/repository/scheduler_cache.go
@@ -518,7 +518,7 @@ func filterSchedulerCredentials(credentials map[string]any) map[string]any {
if len(credentials) == 0 {
return nil
}
- keys := []string{"model_mapping", "compact_model_mapping", "api_key", "project_id", "oauth_type"}
+ keys := []string{"model_mapping", "compact_model_mapping", "api_key", "project_id", "oauth_type", "plan_type"}
filtered := make(map[string]any)
for _, key := range keys {
if value, ok := credentials[key]; ok && value != nil {
diff --git a/backend/internal/repository/scheduler_cache_test.go b/backend/internal/repository/scheduler_cache_test.go
new file mode 100644
index 0000000000..f438b2f456
--- /dev/null
+++ b/backend/internal/repository/scheduler_cache_test.go
@@ -0,0 +1,37 @@
+package repository
+
+import (
+ "testing"
+
+ "github.com/Wei-Shaw/sub2api/internal/service"
+ "github.com/stretchr/testify/require"
+)
+
+func TestFilterSchedulerCredentialsKeepsSubscriptionPlanType(t *testing.T) {
+ filtered := filterSchedulerCredentials(map[string]any{
+ "plan_type": "plus",
+ "access_token": "secret-access-token",
+ "refresh_token": "secret-refresh-token",
+ })
+
+ require.Equal(t, "plus", filtered["plan_type"])
+ require.NotContains(t, filtered, "access_token")
+ require.NotContains(t, filtered, "refresh_token")
+}
+
+func TestSchedulerMetadataAccountKeepsOpenAISubscriptionIdentity(t *testing.T) {
+ account := service.Account{
+ ID: 24,
+ Platform: service.PlatformOpenAI,
+ Type: service.AccountTypeOAuth,
+ Credentials: map[string]any{
+ "plan_type": "plus",
+ "access_token": "secret-access-token",
+ },
+ }
+
+ metadata := buildSchedulerMetadataAccount(account)
+
+ require.True(t, metadata.IsOpenAIChatGPTSubscription())
+ require.Empty(t, metadata.GetCredential("access_token"))
+}
diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go
index 26f976c87b..dfa480dd18 100644
--- a/backend/internal/server/api_contract_test.go
+++ b/backend/internal/server/api_contract_test.go
@@ -661,15 +661,17 @@ func TestAPIContracts(t *testing.T) {
service.SettingKeyTableDefaultPageSize: "20",
service.SettingKeyTablePageSizeOptions: "[10,20,50,100]",
- service.SettingKeyOpsMonitoringEnabled: "false",
- service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
- service.SettingKeyOpsQueryModeDefault: "auto",
- service.SettingKeyOpsMetricsIntervalSeconds: "60",
- service.SettingPaymentVisibleMethodAlipaySource: service.VisibleMethodSourceEasyPayAlipay,
- service.SettingPaymentVisibleMethodWxpaySource: service.VisibleMethodSourceOfficialWechat,
- service.SettingPaymentVisibleMethodAlipayEnabled: "true",
- service.SettingPaymentVisibleMethodWxpayEnabled: "false",
- "openai_advanced_scheduler_enabled": "true",
+ service.SettingKeyOpsMonitoringEnabled: "false",
+ service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
+ service.SettingKeyOpsQueryModeDefault: "auto",
+ service.SettingKeyOpsMetricsIntervalSeconds: "60",
+ service.SettingPaymentVisibleMethodAlipaySource: service.VisibleMethodSourceEasyPayAlipay,
+ service.SettingPaymentVisibleMethodWxpaySource: service.VisibleMethodSourceOfficialWechat,
+ service.SettingPaymentVisibleMethodAlipayEnabled: "true",
+ service.SettingPaymentVisibleMethodWxpayEnabled: "false",
+ "openai_advanced_scheduler_enabled": "true",
+ service.SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled: "false",
+ service.SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled: "false",
})
},
method: http.MethodGet,
@@ -861,6 +863,28 @@ func TestAPIContracts(t *testing.T) {
"payment_visible_method_alipay_enabled": true,
"payment_visible_method_wxpay_enabled": false,
"openai_advanced_scheduler_enabled": true,
+ "openai_advanced_scheduler_sticky_weighted_enabled": false,
+ "openai_advanced_scheduler_subscription_priority_enabled": false,
+ "openai_advanced_scheduler_lb_top_k": "",
+ "openai_advanced_scheduler_weight_priority": "",
+ "openai_advanced_scheduler_weight_load": "",
+ "openai_advanced_scheduler_weight_queue": "",
+ "openai_advanced_scheduler_weight_error_rate": "",
+ "openai_advanced_scheduler_weight_ttft": "",
+ "openai_advanced_scheduler_weight_reset": "",
+ "openai_advanced_scheduler_weight_quota_headroom": "",
+ "openai_advanced_scheduler_weight_previous_response": "",
+ "openai_advanced_scheduler_weight_session_sticky": "",
+ "openai_advanced_scheduler_effective_lb_top_k": "7",
+ "openai_advanced_scheduler_effective_weight_priority": "1",
+ "openai_advanced_scheduler_effective_weight_load": "1",
+ "openai_advanced_scheduler_effective_weight_queue": "0.7",
+ "openai_advanced_scheduler_effective_weight_error_rate": "0.8",
+ "openai_advanced_scheduler_effective_weight_ttft": "0.5",
+ "openai_advanced_scheduler_effective_weight_reset": "0",
+ "openai_advanced_scheduler_effective_weight_quota_headroom": "0",
+ "openai_advanced_scheduler_effective_weight_previous_response": "5",
+ "openai_advanced_scheduler_effective_weight_session_sticky": "3",
"openai_codex_user_agent": "",
"openai_fast_policy_settings": {
"rules": []
@@ -1110,6 +1134,28 @@ func TestAPIContracts(t *testing.T) {
"payment_visible_method_alipay_enabled": false,
"payment_visible_method_wxpay_enabled": false,
"openai_advanced_scheduler_enabled": false,
+ "openai_advanced_scheduler_sticky_weighted_enabled": false,
+ "openai_advanced_scheduler_subscription_priority_enabled": false,
+ "openai_advanced_scheduler_lb_top_k": "",
+ "openai_advanced_scheduler_weight_priority": "",
+ "openai_advanced_scheduler_weight_load": "",
+ "openai_advanced_scheduler_weight_queue": "",
+ "openai_advanced_scheduler_weight_error_rate": "",
+ "openai_advanced_scheduler_weight_ttft": "",
+ "openai_advanced_scheduler_weight_reset": "",
+ "openai_advanced_scheduler_weight_quota_headroom": "",
+ "openai_advanced_scheduler_weight_previous_response": "",
+ "openai_advanced_scheduler_weight_session_sticky": "",
+ "openai_advanced_scheduler_effective_lb_top_k": "7",
+ "openai_advanced_scheduler_effective_weight_priority": "1",
+ "openai_advanced_scheduler_effective_weight_load": "1",
+ "openai_advanced_scheduler_effective_weight_queue": "0.7",
+ "openai_advanced_scheduler_effective_weight_error_rate": "0.8",
+ "openai_advanced_scheduler_effective_weight_ttft": "0.5",
+ "openai_advanced_scheduler_effective_weight_reset": "0",
+ "openai_advanced_scheduler_effective_weight_quota_headroom": "0",
+ "openai_advanced_scheduler_effective_weight_previous_response": "5",
+ "openai_advanced_scheduler_effective_weight_session_sticky": "3",
"openai_codex_user_agent": "",
"openai_fast_policy_settings": {
"rules": []
diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go
index 26af0a7281..8db5805e9d 100644
--- a/backend/internal/service/account.go
+++ b/backend/internal/service/account.go
@@ -1182,6 +1182,18 @@ func (a *Account) IsOpenAIOAuth() bool {
return a.IsOpenAI() && a.Type == AccountTypeOAuth
}
+func (a *Account) IsOpenAIChatGPTSubscription() bool {
+ if !a.IsOpenAIOAuth() {
+ return false
+ }
+ switch strings.ToLower(strings.TrimSpace(a.GetCredential("plan_type"))) {
+ case "", "free", "abnormal":
+ return false
+ default:
+ return true
+ }
+}
+
func (a *Account) IsOpenAIPersonalAccessToken() bool {
if !a.IsOpenAIOAuth() {
return false
diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go
index 7685174f3b..ce59c34475 100644
--- a/backend/internal/service/admin_service.go
+++ b/backend/internal/service/admin_service.go
@@ -2618,6 +2618,29 @@ func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int,
return accounts, result.Total, nil
}
+func (s *adminServiceImpl) ListAccountsForSchedulerScoreFilter(ctx context.Context, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, error) {
+ if s == nil || s.accountRepo == nil {
+ return nil, nil
+ }
+ lister, ok := s.accountRepo.(interface {
+ ListAllWithFilters(ctx context.Context, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, error)
+ })
+ if !ok {
+ return nil, nil
+ }
+ return lister.ListAllWithFilters(ctx, platform, accountType, status, search, groupID, privacyMode)
+}
+
+func (s *adminServiceImpl) ListOpenAISchedulableAccountsForSchedulerScore(ctx context.Context, groupID *int64) ([]Account, error) {
+ if s == nil || s.accountRepo == nil {
+ return nil, nil
+ }
+ if groupID != nil {
+ return s.accountRepo.ListSchedulableByGroupIDAndPlatform(ctx, *groupID, PlatformOpenAI)
+ }
+ return s.accountRepo.ListSchedulableUngroupedByPlatform(ctx, PlatformOpenAI)
+}
+
func (s *adminServiceImpl) GetAccount(ctx context.Context, id int64) (*Account, error) {
return s.accountRepo.GetByID(ctx, id)
}
diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go
index ab853c280a..bc1db19105 100644
--- a/backend/internal/service/domain_constants.go
+++ b/backend/internal/service/domain_constants.go
@@ -431,6 +431,20 @@ const (
// SettingKeyAllowUngroupedKeyScheduling 允许未分组 API Key 调度(默认 false:未分组 Key 返回 403)
SettingKeyAllowUngroupedKeyScheduling = "allow_ungrouped_key_scheduling"
+ // SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled OpenAI 高级调度下是否启用粘性加权。
+ SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled = "openai_advanced_scheduler_sticky_weighted_enabled"
+ // SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled OpenAI 高级调度下是否优先使用订阅账号池。
+ SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled = "openai_advanced_scheduler_subscription_priority_enabled"
+ SettingKeyOpenAIAdvancedSchedulerLBTopK = "openai_advanced_scheduler_lb_top_k"
+ SettingKeyOpenAIAdvancedSchedulerWeightPriority = "openai_advanced_scheduler_weight_priority"
+ SettingKeyOpenAIAdvancedSchedulerWeightLoad = "openai_advanced_scheduler_weight_load"
+ SettingKeyOpenAIAdvancedSchedulerWeightQueue = "openai_advanced_scheduler_weight_queue"
+ SettingKeyOpenAIAdvancedSchedulerWeightErrorRate = "openai_advanced_scheduler_weight_error_rate"
+ SettingKeyOpenAIAdvancedSchedulerWeightTTFT = "openai_advanced_scheduler_weight_ttft"
+ SettingKeyOpenAIAdvancedSchedulerWeightReset = "openai_advanced_scheduler_weight_reset"
+ SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom = "openai_advanced_scheduler_weight_quota_headroom"
+ SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse = "openai_advanced_scheduler_weight_previous_response"
+ SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky = "openai_advanced_scheduler_weight_session_sticky"
// SettingKeyBackendModeEnabled Backend 模式:禁用用户注册和自助服务,仅管理员可登录
SettingKeyBackendModeEnabled = "backend_mode_enabled"
diff --git a/backend/internal/service/openai_account_scheduler.go b/backend/internal/service/openai_account_scheduler.go
index ab298d4521..dd65163abc 100644
--- a/backend/internal/service/openai_account_scheduler.go
+++ b/backend/internal/service/openai_account_scheduler.go
@@ -36,8 +36,20 @@ const (
)
type cachedOpenAIAdvancedSchedulerSetting struct {
- enabled bool
- expiresAt int64
+ enabled bool
+ stickyWeightedEnabled bool
+ subscriptionPriorityEnabled bool
+ lbTopKOverride int
+ weightOverrides map[string]float64
+ expiresAt int64
+}
+
+type openAIAdvancedSchedulerRuntimeSettings struct {
+ enabled bool
+ stickyWeightedEnabled bool
+ subscriptionPriorityEnabled bool
+ lbTopKOverride int
+ weightOverrides map[string]float64
}
var openAIAdvancedSchedulerSettingCache atomic.Value // *cachedOpenAIAdvancedSchedulerSetting
@@ -48,8 +60,12 @@ type OpenAIAccountScheduleRequest struct {
Platform string
SessionHash string
StickyAccountID int64
+ StickyPreviousAccountID int64
+ StickyWeighted bool
+ SubscriptionPriority bool
PreserveStickyBinding bool
PreviousResponseID string
+ PreviousResponseCanMove bool
RequestedModel string
RequiredTransport OpenAIUpstreamTransport
RequiredCapability OpenAIEndpointCapability
@@ -111,6 +127,17 @@ type openAIAccountLoadPlan struct {
loadSkew float64
}
+type openAIAccountLoadSelectionAttempt struct {
+ result *AccountSelectionResult
+ selectionOrder []openAIAccountCandidateScore
+ candidateCount int
+ topK int
+ loadSkew float64
+ compactBlocked bool
+ noCompactCandidates bool
+ err error
+}
+
func (m *openAIAccountSchedulerMetrics) recordSelect(decision OpenAIAccountScheduleDecision) {
if m == nil {
return
@@ -277,7 +304,8 @@ func (s *defaultOpenAIAccountScheduler) Select(
}()
previousResponseID := strings.TrimSpace(req.PreviousResponseID)
- if previousResponseID != "" && normalizeOpenAICompatiblePlatform(req.Platform) == PlatformOpenAI {
+ if previousResponseID != "" && normalizeOpenAICompatiblePlatform(req.Platform) == PlatformOpenAI &&
+ (!req.StickyWeighted || !req.PreviousResponseCanMove) {
selection, err := s.service.selectAccountByPreviousResponseIDForCapability(
ctx,
req.GroupID,
@@ -310,19 +338,21 @@ func (s *defaultOpenAIAccountScheduler) Select(
}
}
- selection, escapedSticky, err := s.selectBySessionHash(ctx, req)
- if err != nil {
- return nil, decision, err
- }
- if selection != nil && selection.Account != nil {
- decision.Layer = openAIAccountScheduleLayerSessionSticky
- decision.StickySessionHit = true
- decision.SelectedAccountID = selection.Account.ID
- decision.SelectedAccountType = selection.Account.Type
- return selection, decision, nil
- }
- if escapedSticky {
- req.PreserveStickyBinding = true
+ if !req.StickyWeighted {
+ selection, escapedSticky, err := s.selectBySessionHash(ctx, req)
+ if err != nil {
+ return nil, decision, err
+ }
+ if selection != nil && selection.Account != nil {
+ decision.Layer = openAIAccountScheduleLayerSessionSticky
+ decision.StickySessionHit = true
+ decision.SelectedAccountID = selection.Account.ID
+ decision.SelectedAccountType = selection.Account.Type
+ return selection, decision, nil
+ }
+ if escapedSticky {
+ req.PreserveStickyBinding = true
+ }
}
selection, candidateCount, topK, loadSkew, err := s.selectByLoadBalance(ctx, req)
@@ -336,6 +366,14 @@ func (s *defaultOpenAIAccountScheduler) Select(
if selection != nil && selection.Account != nil {
decision.SelectedAccountID = selection.Account.ID
decision.SelectedAccountType = selection.Account.Type
+ if req.StickyWeighted {
+ if req.StickyPreviousAccountID > 0 && selection.Account.ID == req.StickyPreviousAccountID {
+ decision.StickyPreviousHit = true
+ }
+ if req.StickyAccountID > 0 && selection.Account.ID == req.StickyAccountID {
+ decision.StickySessionHit = true
+ }
+ }
}
return selection, decision, nil
}
@@ -453,6 +491,13 @@ func openAIStickyAccountMatchesGroup(account *Account, groupID *int64) bool {
return false
}
+func openAIAccountSchedulingPriority(account *Account) int {
+ if account == nil {
+ return 0
+ }
+ return account.Priority
+}
+
func (s *defaultOpenAIAccountScheduler) shouldEscapeStickyAccount(accountID int64, cfg openAIStickyEscapeConfig) (reason string, errorRate float64, ttft float64, shouldEscape bool) {
if !cfg.enabled || s == nil || s.stats == nil || accountID <= 0 {
return "", 0, 0, false
@@ -471,6 +516,7 @@ type openAIAccountCandidateScore struct {
account *Account
loadInfo *AccountLoadInfo
score float64
+ priority int
errorRate float64
ttft float64
hasTTFT bool
@@ -669,6 +715,7 @@ func buildOpenAIWeightedSelectionOrder(
}
func (s *defaultOpenAIAccountScheduler) buildOpenAIAccountLoadPlan(
+ ctx context.Context,
req OpenAIAccountScheduleRequest,
filtered []*Account,
loadMap map[int64]*AccountLoadInfo,
@@ -716,18 +763,20 @@ func (s *defaultOpenAIAccountScheduler) buildOpenAIAccountLoadPlan(
return plan
}
- minPriority, maxPriority := candidates[0].account.Priority, candidates[0].account.Priority
+ minPriority, maxPriority := openAIAccountSchedulingPriority(candidates[0].account), openAIAccountSchedulingPriority(candidates[0].account)
maxWaiting := 1
loadRateSum := 0.0
loadRateSumSquares := 0.0
minTTFT, maxTTFT := 0.0, 0.0
hasTTFTSample := false
- for _, candidate := range candidates {
- if candidate.account.Priority < minPriority {
- minPriority = candidate.account.Priority
+ for i := range candidates {
+ candidate := &candidates[i]
+ candidate.priority = openAIAccountSchedulingPriority(candidate.account)
+ if candidate.priority < minPriority {
+ minPriority = candidate.priority
}
- if candidate.account.Priority > maxPriority {
- maxPriority = candidate.account.Priority
+ if candidate.priority > maxPriority {
+ maxPriority = candidate.priority
}
if candidate.loadInfo.WaitingCount > maxWaiting {
maxWaiting = candidate.loadInfo.WaitingCount
@@ -751,7 +800,7 @@ func (s *defaultOpenAIAccountScheduler) buildOpenAIAccountLoadPlan(
}
plan.loadSkew = calcLoadSkewByMoments(loadRateSum, loadRateSumSquares, len(candidates))
- weights := s.service.openAIWSSchedulerWeights()
+ weights := s.service.openAIWSSchedulerWeightsForRequest(ctx)
// Reset 因子(use-it-or-lose-it):在拥有「未来会话窗口结束时间」的账号中,
// 剩余时间越短 → 因子越接近 1(越早重置越优先用尽)。无活跃窗口的账号因子为 0。
@@ -785,7 +834,7 @@ func (s *defaultOpenAIAccountScheduler) buildOpenAIAccountLoadPlan(
item := &candidates[i]
priorityFactor := 1.0
if maxPriority > minPriority {
- priorityFactor = 1 - float64(item.account.Priority-minPriority)/float64(maxPriority-minPriority)
+ priorityFactor = 1 - float64(item.priority-minPriority)/float64(maxPriority-minPriority)
}
loadFactor := 1 - clamp01(float64(item.loadInfo.LoadRate)/100.0)
queueFactor := 1 - clamp01(float64(item.loadInfo.WaitingCount)/float64(maxWaiting))
@@ -817,10 +866,18 @@ func (s *defaultOpenAIAccountScheduler) buildOpenAIAccountLoadPlan(
weights.TTFT*ttftFactor +
weights.Reset*resetFactor +
weights.QuotaHeadroom*quotaHeadroomFactor
+ if req.StickyWeighted {
+ if req.PreviousResponseCanMove && req.StickyPreviousAccountID > 0 && item.account.ID == req.StickyPreviousAccountID {
+ item.score += weights.Previous
+ }
+ if req.StickyAccountID > 0 && item.account.ID == req.StickyAccountID {
+ item.score += weights.SessionSticky
+ }
+ }
}
plan.candidates = candidates
- plan.topK = s.service.openAIWSLBTopK()
+ plan.topK = s.service.openAIWSLBTopKForRequest(ctx)
if plan.topK > len(candidates) {
plan.topK = len(candidates)
}
@@ -845,6 +902,20 @@ func (s *defaultOpenAIAccountScheduler) buildOpenAISelectionOrder(
groupTopK = len(pool)
}
ranked := selectTopKOpenAICandidates(pool, groupTopK)
+ if req.StickyWeighted {
+ for _, stickyID := range []int64{req.StickyPreviousAccountID, req.StickyAccountID} {
+ if stickyID <= 0 {
+ continue
+ }
+ for i, candidate := range ranked {
+ if candidate.account != nil && candidate.account.ID == stickyID {
+ ordered := append([]openAIAccountCandidateScore{candidate}, ranked[:i]...)
+ ordered = append(ordered, ranked[i+1:]...)
+ return ordered
+ }
+ }
+ }
+ }
return buildOpenAIWeightedSelectionOrder(ranked, req)
}
@@ -939,6 +1010,66 @@ func (s *defaultOpenAIAccountScheduler) tryAcquireOpenAISelectionOrder(
return nil, compactBlocked, nil
}
+func (s *defaultOpenAIAccountScheduler) tryFallbackToWeightedSticky(
+ ctx context.Context,
+ req OpenAIAccountScheduleRequest,
+) (*AccountSelectionResult, error) {
+ if !req.StickyWeighted {
+ return nil, nil
+ }
+ for _, accountID := range []int64{req.StickyPreviousAccountID, req.StickyAccountID} {
+ if accountID <= 0 {
+ continue
+ }
+ if req.ExcludedIDs != nil {
+ if _, excluded := req.ExcludedIDs[accountID]; excluded {
+ continue
+ }
+ }
+ account, err := s.service.getSchedulableAccount(ctx, accountID)
+ if err != nil || account == nil {
+ continue
+ }
+ if !s.isAccountRequestCompatible(ctx, account, req) || !s.isAccountTransportCompatible(account, req.RequiredTransport) {
+ continue
+ }
+ account = s.service.recheckSelectedOpenAIAccountFromDB(ctx, account, req.Platform, req.RequestedModel, req.RequireCompact, req.RequiredCapability)
+ if account == nil || !s.isAccountRequestCompatible(ctx, account, req) || !s.isAccountTransportCompatible(account, req.RequiredTransport) {
+ continue
+ }
+ if req.RequireCompact && openAICompactSupportTier(account) == 0 {
+ continue
+ }
+ result, acquireErr := s.service.tryAcquireAccountSlot(ctx, account.ID, account.Concurrency)
+ if acquireErr != nil {
+ return nil, acquireErr
+ }
+ if result != nil && result.Acquired {
+ if req.SessionHash != "" && !req.PreserveStickyBinding {
+ _ = s.service.BindStickySession(ctx, req.GroupID, req.SessionHash, account.ID)
+ }
+ return &AccountSelectionResult{
+ Account: account,
+ Acquired: true,
+ ReleaseFunc: result.ReleaseFunc,
+ }, nil
+ }
+ if s.service.concurrencyService != nil {
+ cfg := s.service.schedulingConfig()
+ return &AccountSelectionResult{
+ Account: account,
+ WaitPlan: &AccountWaitPlan{
+ AccountID: account.ID,
+ MaxConcurrency: account.Concurrency,
+ Timeout: cfg.StickySessionWaitTimeout,
+ MaxWaiting: cfg.StickySessionMaxWaiting,
+ },
+ }, nil
+ }
+ }
+ return nil, nil
+}
+
func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
ctx context.Context,
req OpenAIAccountScheduleRequest,
@@ -1002,52 +1133,159 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
}
}
- plan := s.buildOpenAIAccountLoadPlan(req, filtered, loadMap)
- candidateCount := plan.candidateCount
- topK := plan.topK
- loadSkew := plan.loadSkew
- selectionOrder := plan.selectionOrder
- if req.RequireCompact && len(plan.candidates) == 0 && len(plan.staleSnapshotCompactRetry) == 0 {
- return nil, 0, 0, 0, ErrNoAvailableCompactAccounts
- }
- if req.RequireCompact && len(selectionOrder) == 0 && s.service.schedulerSnapshot == nil {
- return nil, candidateCount, topK, loadSkew, ErrNoAvailableCompactAccounts
- }
- if len(selectionOrder) == 0 {
- return nil, candidateCount, topK, loadSkew, noAvailableOpenAISelectionError(req.RequestedModel, req.RequireCompact && len(plan.allCandidates) > 0)
+ if req.SubscriptionPriority {
+ subscriptionAccounts, regularAccounts := partitionOpenAIChatGPTSubscriptionAccounts(filtered)
+ if len(subscriptionAccounts) > 0 {
+ attempt := s.trySelectByLoadBalancePool(ctx, req, subscriptionAccounts, loadMap)
+ if attempt.err != nil && (!attempt.noCompactCandidates || len(regularAccounts) <= 0) {
+ return nil, attempt.candidateCount, attempt.topK, attempt.loadSkew, attempt.err
+ }
+ if attempt.result != nil {
+ return attempt.result, attempt.candidateCount, attempt.topK, attempt.loadSkew, nil
+ }
+ if len(regularAccounts) > 0 {
+ regularAttempt := s.trySelectByLoadBalancePool(ctx, req, regularAccounts, loadMap)
+ if regularAttempt.err != nil {
+ return nil, regularAttempt.candidateCount, regularAttempt.topK, regularAttempt.loadSkew, regularAttempt.err
+ }
+ if regularAttempt.result != nil {
+ return regularAttempt.result, regularAttempt.candidateCount, regularAttempt.topK, regularAttempt.loadSkew, nil
+ }
+ return s.finishLoadBalanceSelectionFallback(ctx, req, regularAttempt)
+ }
+ return s.finishLoadBalanceSelectionFallback(ctx, req, attempt)
+ }
}
- result, compactBlocked, acquireErr := s.tryAcquireOpenAISelectionOrder(ctx, req, selectionOrder)
+ attempt := s.trySelectByLoadBalancePool(ctx, req, filtered, loadMap)
+ if attempt.err != nil {
+ return nil, attempt.candidateCount, attempt.topK, attempt.loadSkew, attempt.err
+ }
+ if attempt.result != nil {
+ return attempt.result, attempt.candidateCount, attempt.topK, attempt.loadSkew, nil
+ }
+ return s.finishLoadBalanceSelectionFallback(ctx, req, attempt)
+}
+
+func partitionOpenAIChatGPTSubscriptionAccounts(accounts []*Account) ([]*Account, []*Account) {
+ subscriptionAccounts := make([]*Account, 0, len(accounts))
+ regularAccounts := make([]*Account, 0, len(accounts))
+ for _, account := range accounts {
+ if account != nil && account.IsOpenAIChatGPTSubscription() {
+ subscriptionAccounts = append(subscriptionAccounts, account)
+ continue
+ }
+ regularAccounts = append(regularAccounts, account)
+ }
+ return subscriptionAccounts, regularAccounts
+}
+
+func (s *defaultOpenAIAccountScheduler) trySelectByLoadBalancePool(
+ ctx context.Context,
+ req OpenAIAccountScheduleRequest,
+ filtered []*Account,
+ loadMap map[int64]*AccountLoadInfo,
+) openAIAccountLoadSelectionAttempt {
+ plan := s.buildOpenAIAccountLoadPlan(ctx, req, filtered, loadMap)
+ attempt := openAIAccountLoadSelectionAttempt{
+ selectionOrder: plan.selectionOrder,
+ candidateCount: plan.candidateCount,
+ topK: plan.topK,
+ loadSkew: plan.loadSkew,
+ }
+ if req.RequireCompact && len(plan.candidates) == 0 && len(plan.staleSnapshotCompactRetry) == 0 {
+ attempt.noCompactCandidates = true
+ attempt.err = ErrNoAvailableCompactAccounts
+ return attempt
+ }
+ if req.RequireCompact && len(attempt.selectionOrder) == 0 && s.service.schedulerSnapshot == nil {
+ attempt.noCompactCandidates = true
+ attempt.err = ErrNoAvailableCompactAccounts
+ return attempt
+ }
+ if len(attempt.selectionOrder) == 0 {
+ attempt.compactBlocked = req.RequireCompact && len(plan.allCandidates) > 0
+ return attempt
+ }
+
+ result, compactBlocked, acquireErr := s.tryAcquireOpenAISelectionOrder(ctx, req, attempt.selectionOrder)
+ attempt.compactBlocked = compactBlocked
if acquireErr != nil {
- return nil, candidateCount, topK, loadSkew, acquireErr
+ attempt.err = acquireErr
+ return attempt
}
if result != nil {
- return result, candidateCount, topK, loadSkew, nil
+ attempt.result = result
+ return attempt
}
if s.service.concurrencyService != nil {
+ loadReq := buildOpenAIAccountLoadRequest(filtered)
if freshLoadMap, loadErr := s.service.concurrencyService.GetAccountsLoadBatchFresh(ctx, loadReq); loadErr == nil {
- freshPlan := s.buildOpenAIAccountLoadPlan(req, filtered, freshLoadMap)
+ freshPlan := s.buildOpenAIAccountLoadPlan(ctx, req, filtered, freshLoadMap)
if len(freshPlan.selectionOrder) > 0 {
freshResult, freshCompactBlocked, freshAcquireErr := s.tryAcquireOpenAISelectionOrder(ctx, req, freshPlan.selectionOrder)
if freshAcquireErr != nil {
- return nil, candidateCount, topK, loadSkew, freshAcquireErr
+ attempt.err = freshAcquireErr
+ return attempt
}
if freshResult != nil {
- return freshResult, freshPlan.candidateCount, freshPlan.topK, freshPlan.loadSkew, nil
+ attempt.result = freshResult
+ attempt.selectionOrder = freshPlan.selectionOrder
+ attempt.candidateCount = freshPlan.candidateCount
+ attempt.topK = freshPlan.topK
+ attempt.loadSkew = freshPlan.loadSkew
+ return attempt
}
- compactBlocked = compactBlocked || freshCompactBlocked
- selectionOrder = freshPlan.selectionOrder
- candidateCount = freshPlan.candidateCount
- topK = freshPlan.topK
- loadSkew = freshPlan.loadSkew
+ attempt.compactBlocked = attempt.compactBlocked || freshCompactBlocked
+ attempt.selectionOrder = freshPlan.selectionOrder
+ attempt.candidateCount = freshPlan.candidateCount
+ attempt.topK = freshPlan.topK
+ attempt.loadSkew = freshPlan.loadSkew
}
}
}
+ return attempt
+}
+
+func buildOpenAIAccountLoadRequest(accounts []*Account) []AccountWithConcurrency {
+ loadReq := make([]AccountWithConcurrency, 0, len(accounts))
+ for _, account := range accounts {
+ if account == nil {
+ continue
+ }
+ loadReq = append(loadReq, AccountWithConcurrency{
+ ID: account.ID,
+ MaxConcurrency: account.EffectiveLoadFactor(),
+ })
+ }
+ return loadReq
+}
+
+func (s *defaultOpenAIAccountScheduler) finishLoadBalanceSelectionFallback(
+ ctx context.Context,
+ req OpenAIAccountScheduleRequest,
+ attempt openAIAccountLoadSelectionAttempt,
+) (*AccountSelectionResult, int, int, float64, error) {
+ candidateCount := attempt.candidateCount
+ topK := attempt.topK
+ loadSkew := attempt.loadSkew
+
+ if len(attempt.selectionOrder) == 0 {
+ return nil, candidateCount, topK, loadSkew, noAvailableOpenAISelectionError(req.RequestedModel, attempt.compactBlocked)
+ }
+
+ if stickyFallback, stickyErr := s.tryFallbackToWeightedSticky(ctx, req); stickyErr != nil {
+ return nil, candidateCount, topK, loadSkew, stickyErr
+ } else if stickyFallback != nil {
+ return stickyFallback, candidateCount, topK, loadSkew, nil
+ }
+
cfg := s.service.schedulingConfig()
+ compactBlocked := attempt.compactBlocked
// WaitPlan.MaxConcurrency 使用 Concurrency(非 EffectiveLoadFactor),因为 WaitPlan 控制的是 Redis 实际并发槽位等待。
- for _, candidate := range selectionOrder {
+ for _, candidate := range attempt.selectionOrder {
fresh := s.service.resolveFreshSchedulableOpenAIAccount(ctx, candidate.account, req.Platform, req.RequestedModel, false, req.RequiredCapability)
if fresh == nil || !s.isAccountTransportCompatible(fresh, req.RequiredTransport) || !s.isAccountRequestCompatible(ctx, fresh, req) {
continue
@@ -1184,40 +1422,164 @@ func (s *OpenAIGatewayService) openAIAdvancedSchedulerSettingRepo() SettingRepos
return s.rateLimitService.settingService.settingRepo
}
-func (s *OpenAIGatewayService) isOpenAIAdvancedSchedulerEnabled(ctx context.Context) bool {
+func (s *OpenAIGatewayService) openAIAdvancedSchedulerRuntimeSettings(ctx context.Context) openAIAdvancedSchedulerRuntimeSettings {
if cached, ok := openAIAdvancedSchedulerSettingCache.Load().(*cachedOpenAIAdvancedSchedulerSetting); ok && cached != nil {
if time.Now().UnixNano() < cached.expiresAt {
- return cached.enabled
+ return openAIAdvancedSchedulerRuntimeSettings{
+ enabled: cached.enabled,
+ stickyWeightedEnabled: cached.stickyWeightedEnabled,
+ subscriptionPriorityEnabled: cached.subscriptionPriorityEnabled,
+ lbTopKOverride: cached.lbTopKOverride,
+ weightOverrides: cloneOpenAIAdvancedSchedulerWeightOverrides(cached.weightOverrides),
+ }
}
}
result, _, _ := openAIAdvancedSchedulerSettingSF.Do(openAIAdvancedSchedulerSettingKey, func() (any, error) {
if cached, ok := openAIAdvancedSchedulerSettingCache.Load().(*cachedOpenAIAdvancedSchedulerSetting); ok && cached != nil {
if time.Now().UnixNano() < cached.expiresAt {
- return cached.enabled, nil
+ return openAIAdvancedSchedulerRuntimeSettings{
+ enabled: cached.enabled,
+ stickyWeightedEnabled: cached.stickyWeightedEnabled,
+ subscriptionPriorityEnabled: cached.subscriptionPriorityEnabled,
+ lbTopKOverride: cached.lbTopKOverride,
+ weightOverrides: cloneOpenAIAdvancedSchedulerWeightOverrides(cached.weightOverrides),
+ }, nil
}
}
enabled := false
+ stickyWeightedEnabled := false
+ subscriptionPriorityEnabled := false
+ lbTopKOverride := 0
+ weightOverrides := map[string]float64{}
if repo := s.openAIAdvancedSchedulerSettingRepo(); repo != nil {
dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), openAIAdvancedSchedulerSettingDBTimeout)
defer cancel()
- value, err := repo.GetValue(dbCtx, openAIAdvancedSchedulerSettingKey)
- if err == nil {
- enabled = strings.EqualFold(strings.TrimSpace(value), "true")
+ if values, err := repo.GetMultiple(dbCtx, openAIAdvancedSchedulerRuntimeSettingKeys()); err == nil {
+ enabled = strings.EqualFold(strings.TrimSpace(values[openAIAdvancedSchedulerSettingKey]), "true")
+ stickyWeightedEnabled = strings.EqualFold(strings.TrimSpace(values[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled]), "true")
+ subscriptionPriorityEnabled = strings.EqualFold(strings.TrimSpace(values[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled]), "true")
+ lbTopKOverride = parsePositiveIntOverride(values[SettingKeyOpenAIAdvancedSchedulerLBTopK])
+ weightOverrides = parseOpenAIAdvancedSchedulerWeightOverrides(values)
+ } else {
+ if value, err := repo.GetValue(dbCtx, openAIAdvancedSchedulerSettingKey); err == nil {
+ enabled = strings.EqualFold(strings.TrimSpace(value), "true")
+ }
+ if value, err := repo.GetValue(dbCtx, SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled); err == nil {
+ stickyWeightedEnabled = strings.EqualFold(strings.TrimSpace(value), "true")
+ }
+ if value, err := repo.GetValue(dbCtx, SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled); err == nil {
+ subscriptionPriorityEnabled = strings.EqualFold(strings.TrimSpace(value), "true")
+ }
}
}
openAIAdvancedSchedulerSettingCache.Store(&cachedOpenAIAdvancedSchedulerSetting{
- enabled: enabled,
- expiresAt: time.Now().Add(openAIAdvancedSchedulerSettingCacheTTL).UnixNano(),
+ enabled: enabled,
+ stickyWeightedEnabled: stickyWeightedEnabled,
+ subscriptionPriorityEnabled: subscriptionPriorityEnabled,
+ lbTopKOverride: lbTopKOverride,
+ weightOverrides: cloneOpenAIAdvancedSchedulerWeightOverrides(weightOverrides),
+ expiresAt: time.Now().Add(openAIAdvancedSchedulerSettingCacheTTL).UnixNano(),
})
- return enabled, nil
+ return openAIAdvancedSchedulerRuntimeSettings{
+ enabled: enabled,
+ stickyWeightedEnabled: stickyWeightedEnabled,
+ subscriptionPriorityEnabled: subscriptionPriorityEnabled,
+ lbTopKOverride: lbTopKOverride,
+ weightOverrides: weightOverrides,
+ }, nil
})
- enabled, _ := result.(bool)
- return enabled
+ settings, _ := result.(openAIAdvancedSchedulerRuntimeSettings)
+ return settings
+}
+
+func (s *OpenAIGatewayService) isOpenAIAdvancedSchedulerEnabled(ctx context.Context) bool {
+ return s.openAIAdvancedSchedulerRuntimeSettings(ctx).enabled
+}
+
+func (s *OpenAIGatewayService) isOpenAIAdvancedSchedulerStickyWeightedEnabled(ctx context.Context) bool {
+ settings := s.openAIAdvancedSchedulerRuntimeSettings(ctx)
+ return settings.enabled && settings.stickyWeightedEnabled
+}
+
+func (s *OpenAIGatewayService) isOpenAIAdvancedSchedulerSubscriptionPriorityEnabled(ctx context.Context) bool {
+ settings := s.openAIAdvancedSchedulerRuntimeSettings(ctx)
+ return settings.enabled && settings.subscriptionPriorityEnabled
+}
+
+func openAIAdvancedSchedulerRuntimeSettingKeys() []string {
+ keys := []string{
+ openAIAdvancedSchedulerSettingKey,
+ SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled,
+ SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
+ SettingKeyOpenAIAdvancedSchedulerLBTopK,
+ }
+ for _, spec := range openAIAdvancedSchedulerWeightOverrideSpecs() {
+ keys = append(keys, spec.key)
+ }
+ return keys
+}
+
+type openAIAdvancedSchedulerWeightOverrideSpec struct {
+ key string
+ name string
+}
+
+func openAIAdvancedSchedulerWeightOverrideSpecs() []openAIAdvancedSchedulerWeightOverrideSpec {
+ return []openAIAdvancedSchedulerWeightOverrideSpec{
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightPriority, name: "priority"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightLoad, name: "load"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightQueue, name: "queue"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightErrorRate, name: "error_rate"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightTTFT, name: "ttft"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightReset, name: "reset"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom, name: "quota_headroom"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse, name: "previous_response"},
+ {key: SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky, name: "session_sticky"},
+ }
+}
+
+func parsePositiveIntOverride(raw string) int {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return 0
+ }
+ value, err := strconv.Atoi(raw)
+ if err != nil || value <= 0 {
+ return 0
+ }
+ return value
+}
+
+func parseOpenAIAdvancedSchedulerWeightOverrides(values map[string]string) map[string]float64 {
+ overrides := map[string]float64{}
+ for _, spec := range openAIAdvancedSchedulerWeightOverrideSpecs() {
+ raw := strings.TrimSpace(values[spec.key])
+ if raw == "" {
+ continue
+ }
+ value, err := strconv.ParseFloat(raw, 64)
+ if err != nil || value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
+ continue
+ }
+ overrides[spec.name] = value
+ }
+ return overrides
+}
+
+func cloneOpenAIAdvancedSchedulerWeightOverrides(in map[string]float64) map[string]float64 {
+ if len(in) == 0 {
+ return nil
+ }
+ out := make(map[string]float64, len(in))
+ for key, value := range in {
+ out[key] = value
+ }
+ return out
}
func (s *OpenAIGatewayService) getOpenAIAccountScheduler(ctx context.Context) OpenAIAccountScheduler {
@@ -1253,7 +1615,7 @@ func (s *OpenAIGatewayService) SelectAccountWithScheduler(
requiredTransport OpenAIUpstreamTransport,
requireCompact bool,
) (*AccountSelectionResult, OpenAIAccountScheduleDecision, error) {
- return s.selectAccountWithScheduler(ctx, groupID, previousResponseID, sessionHash, requestedModel, excludedIDs, requiredTransport, "", "", requireCompact, PlatformOpenAI)
+ return s.selectAccountWithScheduler(ctx, groupID, previousResponseID, sessionHash, requestedModel, excludedIDs, requiredTransport, "", "", requireCompact, PlatformOpenAI, false)
}
func (s *OpenAIGatewayService) SelectAccountWithSchedulerForCapability(
@@ -1269,10 +1631,14 @@ func (s *OpenAIGatewayService) SelectAccountWithSchedulerForCapability(
platformOverride ...string,
) (*AccountSelectionResult, OpenAIAccountScheduleDecision, error) {
platform := PlatformOpenAI
+ previousResponseCanMove := false
if len(platformOverride) > 0 {
platform = platformOverride[0]
}
- return s.selectAccountWithScheduler(ctx, groupID, previousResponseID, sessionHash, requestedModel, excludedIDs, requiredTransport, requiredCapability, "", requireCompact, platform)
+ if len(platformOverride) > 1 {
+ previousResponseCanMove = strings.EqualFold(platformOverride[1], "previous_response_can_move")
+ }
+ return s.selectAccountWithScheduler(ctx, groupID, previousResponseID, sessionHash, requestedModel, excludedIDs, requiredTransport, requiredCapability, "", requireCompact, platform, previousResponseCanMove)
}
func (s *OpenAIGatewayService) SelectAccountWithSchedulerForImages(
@@ -1283,13 +1649,13 @@ func (s *OpenAIGatewayService) SelectAccountWithSchedulerForImages(
excludedIDs map[int64]struct{},
requiredCapability OpenAIImagesCapability,
) (*AccountSelectionResult, OpenAIAccountScheduleDecision, error) {
- selection, decision, err := s.selectAccountWithScheduler(ctx, groupID, "", sessionHash, requestedModel, excludedIDs, OpenAIUpstreamTransportHTTPSSE, "", requiredCapability, false, PlatformOpenAI)
+ selection, decision, err := s.selectAccountWithScheduler(ctx, groupID, "", sessionHash, requestedModel, excludedIDs, OpenAIUpstreamTransportHTTPSSE, "", requiredCapability, false, PlatformOpenAI, false)
if err == nil && selection != nil && selection.Account != nil {
return selection, decision, nil
}
// 如果要求 native 能力(如指定了模型)但没有可用的 APIKey 账号,回退到 basic(OAuth 账号)
if requiredCapability == OpenAIImagesCapabilityNative {
- return s.selectAccountWithScheduler(ctx, groupID, "", sessionHash, requestedModel, excludedIDs, OpenAIUpstreamTransportHTTPSSE, "", OpenAIImagesCapabilityBasic, false, PlatformOpenAI)
+ return s.selectAccountWithScheduler(ctx, groupID, "", sessionHash, requestedModel, excludedIDs, OpenAIUpstreamTransportHTTPSSE, "", OpenAIImagesCapabilityBasic, false, PlatformOpenAI, false)
}
return selection, decision, err
}
@@ -1306,6 +1672,7 @@ func (s *OpenAIGatewayService) selectAccountWithScheduler(
requiredImageCapability OpenAIImagesCapability,
requireCompact bool,
platform string,
+ previousResponseCanMove bool,
) (*AccountSelectionResult, OpenAIAccountScheduleDecision, error) {
ctx = s.withOpenAIQuotaAutoPauseContext(ctx)
platform = normalizeOpenAICompatiblePlatform(platform)
@@ -1378,13 +1745,23 @@ func (s *OpenAIGatewayService) selectAccountWithScheduler(
stickyAccountID = accountID
}
}
+ stickyWeighted := s.isOpenAIAdvancedSchedulerStickyWeightedEnabled(ctx)
+ subscriptionPriority := s.isOpenAIAdvancedSchedulerSubscriptionPriorityEnabled(ctx)
+ stickyPreviousAccountID := int64(0)
+ if stickyWeighted && previousResponseCanMove && strings.TrimSpace(previousResponseID) != "" && platform == PlatformOpenAI {
+ stickyPreviousAccountID = s.ResolveAccountIDByPreviousResponseIDForScheduler(ctx, groupID, previousResponseID, requestedModel, excludedIDs, requiredCapability, requireCompact)
+ }
return scheduler.Select(ctx, OpenAIAccountScheduleRequest{
GroupID: groupID,
Platform: platform,
SessionHash: sessionHash,
StickyAccountID: stickyAccountID,
+ StickyPreviousAccountID: stickyPreviousAccountID,
+ StickyWeighted: stickyWeighted,
+ SubscriptionPriority: subscriptionPriority,
PreviousResponseID: previousResponseID,
+ PreviousResponseCanMove: previousResponseCanMove,
RequestedModel: requestedModel,
RequiredTransport: requiredTransport,
RequiredCapability: requiredCapability,
@@ -1473,6 +1850,15 @@ func (s *OpenAIGatewayService) openAIWSLBTopK() int {
return 7
}
+func (s *OpenAIGatewayService) openAIWSLBTopKForRequest(ctx context.Context) int {
+ base := s.openAIWSLBTopK()
+ settings := s.openAIAdvancedSchedulerRuntimeSettings(ctx)
+ if settings.lbTopKOverride > 0 {
+ return settings.lbTopKOverride
+ }
+ return base
+}
+
func (s *OpenAIGatewayService) openAIStickyEscapeConfig() openAIStickyEscapeConfig {
if s != nil && s.cfg != nil {
cfg := s.cfg.Gateway.OpenAIScheduler
@@ -1514,6 +1900,8 @@ func (s *OpenAIGatewayService) openAIWSSchedulerWeights() GatewayOpenAIWSSchedul
TTFT: s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT,
Reset: s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Reset,
QuotaHeadroom: s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights.QuotaHeadroom,
+ Previous: s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights.PreviousResponse,
+ SessionSticky: s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights.SessionSticky,
}
}
return GatewayOpenAIWSSchedulerScoreWeightsView{
@@ -1524,9 +1912,46 @@ func (s *OpenAIGatewayService) openAIWSSchedulerWeights() GatewayOpenAIWSSchedul
TTFT: 0.5,
Reset: 0.0,
QuotaHeadroom: 0.0,
+ Previous: 5.0,
+ SessionSticky: 3.0,
}
}
+func (s *OpenAIGatewayService) openAIWSSchedulerWeightsForRequest(ctx context.Context) GatewayOpenAIWSSchedulerScoreWeightsView {
+ weights := s.openAIWSSchedulerWeights()
+ settings := s.openAIAdvancedSchedulerRuntimeSettings(ctx)
+ return applyOpenAIAdvancedSchedulerWeightOverrides(weights, settings.weightOverrides)
+}
+
+func applyOpenAIAdvancedSchedulerWeightOverrides(
+ weights GatewayOpenAIWSSchedulerScoreWeightsView,
+ overrides map[string]float64,
+) GatewayOpenAIWSSchedulerScoreWeightsView {
+ for key, value := range overrides {
+ switch key {
+ case "priority":
+ weights.Priority = value
+ case "load":
+ weights.Load = value
+ case "queue":
+ weights.Queue = value
+ case "error_rate":
+ weights.ErrorRate = value
+ case "ttft":
+ weights.TTFT = value
+ case "reset":
+ weights.Reset = value
+ case "quota_headroom":
+ weights.QuotaHeadroom = value
+ case "previous_response":
+ weights.Previous = value
+ case "session_sticky":
+ weights.SessionSticky = value
+ }
+ }
+ return weights
+}
+
type GatewayOpenAIWSSchedulerScoreWeightsView struct {
Priority float64
Load float64
@@ -1536,6 +1961,149 @@ type GatewayOpenAIWSSchedulerScoreWeightsView struct {
// Reset 倾向「会话窗口最早重置」的账号;0 表示关闭(默认)。
Reset float64
QuotaHeadroom float64
+ Previous float64
+ SessionSticky float64
+}
+
+type OpenAIAccountSchedulerScoreSnapshot struct {
+ BaseScore float64
+ StickyScore float64
+ StickyScoreInfinity bool
+ StickyWeightedEnabled bool
+}
+
+func (s *RateLimitService) BuildOpenAIAccountSchedulerScoreSnapshot(
+ ctx context.Context,
+ accounts []*Account,
+ loadMap map[int64]*AccountLoadInfo,
+) map[int64]OpenAIAccountSchedulerScoreSnapshot {
+ gateway := &OpenAIGatewayService{cfg: nil, rateLimitService: s}
+ if s != nil {
+ gateway.cfg = s.cfg
+ }
+ return buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, gateway.openAIWSSchedulerWeightsForRequest(ctx), gateway.isOpenAIAdvancedSchedulerStickyWeightedEnabled(ctx))
+}
+
+func BuildOpenAIAccountSchedulerScoreSnapshot(
+ accounts []*Account,
+ loadMap map[int64]*AccountLoadInfo,
+) map[int64]OpenAIAccountSchedulerScoreSnapshot {
+ gateway := &OpenAIGatewayService{}
+ return buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, gateway.openAIWSSchedulerWeights(), false)
+}
+
+func buildOpenAIAccountSchedulerScoreSnapshot(
+ accounts []*Account,
+ loadMap map[int64]*AccountLoadInfo,
+ weights GatewayOpenAIWSSchedulerScoreWeightsView,
+ stickyWeightedEnabled bool,
+) map[int64]OpenAIAccountSchedulerScoreSnapshot {
+ if len(accounts) == 0 {
+ return nil
+ }
+ candidates := make([]openAIAccountCandidateScore, 0, len(accounts))
+ for _, account := range accounts {
+ if account == nil {
+ continue
+ }
+ loadInfo := loadMap[account.ID]
+ if loadInfo == nil {
+ loadInfo = &AccountLoadInfo{AccountID: account.ID}
+ }
+ candidates = append(candidates, openAIAccountCandidateScore{
+ account: account,
+ loadInfo: loadInfo,
+ errorRate: 0,
+ ttft: 0,
+ hasTTFT: false,
+ })
+ }
+ if len(candidates) == 0 {
+ return nil
+ }
+
+ minPriority, maxPriority := openAIAccountSchedulingPriority(candidates[0].account), openAIAccountSchedulingPriority(candidates[0].account)
+ maxWaiting := 1
+ for i := range candidates {
+ candidate := &candidates[i]
+ candidate.priority = openAIAccountSchedulingPriority(candidate.account)
+ if candidate.priority < minPriority {
+ minPriority = candidate.priority
+ }
+ if candidate.priority > maxPriority {
+ maxPriority = candidate.priority
+ }
+ if candidate.loadInfo.WaitingCount > maxWaiting {
+ maxWaiting = candidate.loadInfo.WaitingCount
+ }
+ }
+
+ minResetRemaining, maxResetRemaining := 0.0, 0.0
+ hasResetSample := false
+ now := time.Now()
+ if weights.Reset > 0 {
+ for _, candidate := range candidates {
+ end := candidate.account.SessionWindowEnd
+ if end == nil || !now.Before(*end) {
+ continue
+ }
+ remaining := end.Sub(now).Seconds()
+ if !hasResetSample {
+ minResetRemaining, maxResetRemaining = remaining, remaining
+ hasResetSample = true
+ continue
+ }
+ if remaining < minResetRemaining {
+ minResetRemaining = remaining
+ }
+ if remaining > maxResetRemaining {
+ maxResetRemaining = remaining
+ }
+ }
+ }
+
+ result := make(map[int64]OpenAIAccountSchedulerScoreSnapshot, len(candidates))
+ for _, candidate := range candidates {
+ priorityFactor := 1.0
+ if maxPriority > minPriority {
+ priorityFactor = 1 - float64(candidate.priority-minPriority)/float64(maxPriority-minPriority)
+ }
+ loadFactor := 1 - clamp01(float64(candidate.loadInfo.LoadRate)/100.0)
+ queueFactor := 1 - clamp01(float64(candidate.loadInfo.WaitingCount)/float64(maxWaiting))
+ errorFactor := 1.0
+ ttftFactor := 0.5
+ resetFactor := 0.0
+ if weights.Reset > 0 && hasResetSample {
+ if end := candidate.account.SessionWindowEnd; end != nil && now.Before(*end) {
+ if maxResetRemaining > minResetRemaining {
+ resetFactor = 1 - clamp01((end.Sub(now).Seconds()-minResetRemaining)/(maxResetRemaining-minResetRemaining))
+ } else {
+ resetFactor = 1
+ }
+ }
+ }
+ quotaHeadroomFactor := 0.0
+ if weights.QuotaHeadroom > 0 {
+ quotaHeadroomFactor = openAIQuotaHeadroomFactor(candidate.account, now)
+ }
+ baseScore := weights.Priority*priorityFactor +
+ weights.Load*loadFactor +
+ weights.Queue*queueFactor +
+ weights.ErrorRate*errorFactor +
+ weights.TTFT*ttftFactor +
+ weights.Reset*resetFactor +
+ weights.QuotaHeadroom*quotaHeadroomFactor
+ score := OpenAIAccountSchedulerScoreSnapshot{
+ BaseScore: baseScore,
+ StickyWeightedEnabled: stickyWeightedEnabled,
+ StickyScoreInfinity: !stickyWeightedEnabled,
+ }
+ if stickyWeightedEnabled {
+ score.StickyScore = baseScore + weights.Previous + weights.SessionSticky
+ }
+ result[candidate.account.ID] = score
+ }
+ return result
}
func openAIQuotaHeadroomFactor(account *Account, now time.Time) float64 {
diff --git a/backend/internal/service/openai_account_scheduler_reset_test.go b/backend/internal/service/openai_account_scheduler_reset_test.go
index 50155f20d6..f1270c8caa 100644
--- a/backend/internal/service/openai_account_scheduler_reset_test.go
+++ b/backend/internal/service/openai_account_scheduler_reset_test.go
@@ -1,6 +1,7 @@
package service
import (
+ "context"
"testing"
"time"
@@ -49,7 +50,7 @@ func TestBuildOpenAIAccountLoadPlan_ResetWeightPrefersSoonestReset(t *testing.T)
}
sched := openAIResetTestScheduler(5.0)
- plan := sched.buildOpenAIAccountLoadPlan(OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
+ plan := sched.buildOpenAIAccountLoadPlan(context.Background(), OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
scores := openAIPlanScores(plan)
require.Greater(t, scores[2], scores[1], "重置时间最早的账号(ID=2)得分更高")
}
@@ -65,7 +66,7 @@ func TestBuildOpenAIAccountLoadPlan_ResetWeightZeroNoEffect(t *testing.T) {
}
sched := openAIResetTestScheduler(0.0)
- plan := sched.buildOpenAIAccountLoadPlan(OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
+ plan := sched.buildOpenAIAccountLoadPlan(context.Background(), OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
scores := openAIPlanScores(plan)
require.Equal(t, scores[1], scores[2], "Reset 权重为 0 时两账号得分相同")
}
@@ -80,7 +81,7 @@ func TestBuildOpenAIAccountLoadPlan_ResetWeightIgnoresNilWindow(t *testing.T) {
}
sched := openAIResetTestScheduler(5.0)
- plan := sched.buildOpenAIAccountLoadPlan(OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
+ plan := sched.buildOpenAIAccountLoadPlan(context.Background(), OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
scores := openAIPlanScores(plan)
require.Greater(t, scores[2], scores[1], "拥有活跃窗口的账号得分高于无窗口账号")
}
@@ -161,7 +162,7 @@ func TestBuildOpenAIAccountLoadPlan_QuotaHeadroomPrefersHigher7dRemaining(t *tes
}
sched := openAIQuotaHeadroomTestScheduler(1.0)
- plan := sched.buildOpenAIAccountLoadPlan(OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
+ plan := sched.buildOpenAIAccountLoadPlan(context.Background(), OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
scores := openAIPlanScores(plan)
require.Greater(t, scores[2], scores[1], "7d 剩余额度更高的账号得分应更高")
}
@@ -190,7 +191,7 @@ func TestBuildOpenAIAccountLoadPlan_QuotaHeadroomZeroNoEffect(t *testing.T) {
}
sched := openAIResetTestScheduler(0)
- plan := sched.buildOpenAIAccountLoadPlan(OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
+ plan := sched.buildOpenAIAccountLoadPlan(context.Background(), OpenAIAccountScheduleRequest{}, filtered, map[int64]*AccountLoadInfo{})
scores := openAIPlanScores(plan)
require.Equal(t, scores[1], scores[2], "quota_headroom 权重为 0 时不应影响打分")
}
diff --git a/backend/internal/service/openai_account_scheduler_test.go b/backend/internal/service/openai_account_scheduler_test.go
index 0255dbbde3..a61a923053 100644
--- a/backend/internal/service/openai_account_scheduler_test.go
+++ b/backend/internal/service/openai_account_scheduler_test.go
@@ -183,6 +183,17 @@ func newSchedulerTestOpenAIWSV2Config() *config.Config {
return cfg
}
+func newSchedulerTestSubscriptionPriorityConfig() *config.Config {
+ cfg := &config.Config{}
+ cfg.Gateway.OpenAIWS.LBTopK = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0
+ return cfg
+}
+
type openAIAdvancedSchedulerSettingRepoStub struct {
values map[string]string
}
@@ -210,8 +221,14 @@ func (s *openAIAdvancedSchedulerSettingRepoStub) Set(context.Context, string, st
panic("unexpected call to Set")
}
-func (s *openAIAdvancedSchedulerSettingRepoStub) GetMultiple(context.Context, []string) (map[string]string, error) {
- panic("unexpected call to GetMultiple")
+func (s *openAIAdvancedSchedulerSettingRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
+ result := make(map[string]string, len(keys))
+ for _, key := range keys {
+ if value, err := s.GetValue(context.Background(), key); err == nil {
+ result[key] = value
+ }
+ }
+ return result, nil
}
func (s *openAIAdvancedSchedulerSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
@@ -226,7 +243,7 @@ func (s *openAIAdvancedSchedulerSettingRepoStub) Delete(context.Context, string)
panic("unexpected call to Delete")
}
-func newOpenAIAdvancedSchedulerRateLimitService(enabled string) *RateLimitService {
+func newOpenAIAdvancedSchedulerRateLimitService(enabled string, values ...string) *RateLimitService {
resetOpenAIAdvancedSchedulerSettingCacheForTest()
repo := &openAIAdvancedSchedulerSettingRepoStub{
values: map[string]string{},
@@ -234,6 +251,12 @@ func newOpenAIAdvancedSchedulerRateLimitService(enabled string) *RateLimitServic
if enabled != "" {
repo.values[openAIAdvancedSchedulerSettingKey] = enabled
}
+ if len(values) > 0 && values[0] != "" {
+ repo.values[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] = values[0]
+ }
+ if len(values) > 1 && values[1] != "" {
+ repo.values[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] = values[1]
+ }
return &RateLimitService{
settingService: NewSettingService(repo, &config.Config{}),
}
@@ -266,6 +289,45 @@ func (s *openAISnapshotCacheStub) GetAccount(ctx context.Context, accountID int6
return &cloned, nil
}
+func TestOpenAIGatewayService_OpenAIAdvancedSchedulerRuntimeSettings_DBOverridesConfig(t *testing.T) {
+ resetOpenAIAdvancedSchedulerSettingCacheForTest()
+ defer resetOpenAIAdvancedSchedulerSettingCacheForTest()
+
+ cfg := &config.Config{}
+ cfg.Gateway.OpenAIWS.LBTopK = 11
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights = config.GatewayOpenAIWSSchedulerScoreWeights{
+ Priority: 1,
+ Load: 2,
+ Queue: 3,
+ ErrorRate: 4,
+ TTFT: 5,
+ Reset: 6,
+ QuotaHeadroom: 7,
+ PreviousResponse: 8,
+ SessionSticky: 9,
+ }
+ repo := &openAIAdvancedSchedulerSettingRepoStub{
+ values: map[string]string{
+ openAIAdvancedSchedulerSettingKey: "true",
+ SettingKeyOpenAIAdvancedSchedulerLBTopK: "3",
+ SettingKeyOpenAIAdvancedSchedulerWeightPriority: "2.5",
+ SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse: "12",
+ },
+ }
+ svc := &OpenAIGatewayService{
+ cfg: cfg,
+ rateLimitService: &RateLimitService{settingService: NewSettingService(repo, cfg)},
+ }
+
+ ctx := context.Background()
+ require.Equal(t, 3, svc.openAIWSLBTopKForRequest(ctx))
+ weights := svc.openAIWSSchedulerWeightsForRequest(ctx)
+ require.Equal(t, 2.5, weights.Priority)
+ require.Equal(t, 2.0, weights.Load)
+ require.Equal(t, 12.0, weights.Previous)
+ require.Equal(t, 9.0, weights.SessionSticky)
+}
+
func TestOpenAIGatewayService_SelectAccountWithScheduler_DefaultDisabledUsesLegacyLoadAwareness(t *testing.T) {
resetOpenAIAdvancedSchedulerSettingCacheForTest()
@@ -584,6 +646,247 @@ func TestOpenAIGatewayService_SelectAccountWithScheduler_EnabledUsesAdvancedPrev
require.True(t, decision.StickyPreviousHit)
}
+func TestOpenAIGatewayService_SelectAccountWithScheduler_StickyWeightedSessionInTopKUsesStickyFirst(t *testing.T) {
+ resetOpenAIAdvancedSchedulerSettingCacheForTest()
+
+ ctx := context.Background()
+ groupID := int64(101071)
+ accounts := []Account{
+ {
+ ID: 37101,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 100,
+ GroupIDs: []int64{groupID},
+ },
+ {
+ ID: 37102,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ GroupIDs: []int64{groupID},
+ },
+ }
+ cfg := &config.Config{}
+ cfg.Gateway.OpenAIWS.LBTopK = 2
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0.7
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0.8
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0.5
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.SessionSticky = 3
+ cache := &schedulerTestGatewayCache{sessionBindings: map[string]int64{
+ "openai:session_hash_weighted_topk": 37101,
+ }}
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: cache,
+ cfg: cfg,
+ rateLimitService: newOpenAIAdvancedSchedulerRateLimitService("true", "true"),
+ concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
+ }
+
+ selection, decision, err := svc.SelectAccountWithScheduler(
+ ctx,
+ &groupID,
+ "",
+ "session_hash_weighted_topk",
+ "gpt-5.1",
+ nil,
+ OpenAIUpstreamTransportAny,
+ false,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(37101), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+ require.True(t, decision.StickySessionHit)
+ require.Equal(t, 2, decision.TopK)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+}
+
+func TestOpenAIGatewayService_SelectAccountWithScheduler_StickyWeightedPreviousRequiresMovableContext(t *testing.T) {
+ resetOpenAIAdvancedSchedulerSettingCacheForTest()
+
+ ctx := context.Background()
+ groupID := int64(101072)
+ accounts := []Account{
+ {
+ ID: 37111,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 100,
+ GroupIDs: []int64{groupID},
+ Extra: map[string]any{
+ "openai_apikey_responses_websockets_v2_enabled": true,
+ },
+ },
+ {
+ ID: 37112,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ GroupIDs: []int64{groupID},
+ Extra: map[string]any{
+ "openai_apikey_responses_websockets_v2_enabled": true,
+ },
+ },
+ }
+ cfg := newSchedulerTestOpenAIWSV2Config()
+ cfg.Gateway.OpenAIWS.LBTopK = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0.7
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0.8
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0.5
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: &schedulerTestGatewayCache{},
+ cfg: cfg,
+ rateLimitService: newOpenAIAdvancedSchedulerRateLimitService("true", "true"),
+ concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
+ }
+ store := svc.getOpenAIWSStateStore()
+ require.NoError(t, store.BindResponseAccount(ctx, groupID, "resp_weighted_unmovable", 37111, time.Hour))
+
+ selection, decision, err := svc.SelectAccountWithSchedulerForCapability(
+ ctx,
+ &groupID,
+ "resp_weighted_unmovable",
+ "",
+ "gpt-5.1",
+ nil,
+ OpenAIUpstreamTransportAny,
+ OpenAIEndpointCapabilityChatCompletions,
+ false,
+ PlatformOpenAI,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(37111), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerPreviousResponse, decision.Layer)
+ require.True(t, decision.StickyPreviousHit)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+
+ selection, decision, err = svc.SelectAccountWithSchedulerForCapability(
+ ctx,
+ &groupID,
+ "resp_weighted_unmovable",
+ "",
+ "gpt-5.1",
+ nil,
+ OpenAIUpstreamTransportAny,
+ OpenAIEndpointCapabilityChatCompletions,
+ false,
+ PlatformOpenAI,
+ "previous_response_can_move",
+ )
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(37112), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+ require.False(t, decision.StickyPreviousHit)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+}
+
+func TestOpenAIGatewayService_SelectAccountWithScheduler_PreviousResponseCompactUnsupportedDeletesBinding(t *testing.T) {
+ resetOpenAIAdvancedSchedulerSettingCacheForTest()
+
+ ctx := context.Background()
+ groupID := int64(101073)
+ accounts := []Account{
+ {
+ ID: 37121,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ GroupIDs: []int64{groupID},
+ Extra: map[string]any{
+ "openai_apikey_responses_websockets_v2_enabled": true,
+ "openai_compact_mode": OpenAICompactModeForceOff,
+ },
+ },
+ {
+ ID: 37122,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 10,
+ GroupIDs: []int64{groupID},
+ Extra: map[string]any{
+ "openai_apikey_responses_websockets_v2_enabled": true,
+ "openai_compact_mode": OpenAICompactModeForceOn,
+ },
+ },
+ }
+ cfg := newSchedulerTestOpenAIWSV2Config()
+ cfg.Gateway.OpenAIWS.LBTopK = 2
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 1
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0.7
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.ErrorRate = 0.8
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.TTFT = 0.5
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: &schedulerTestGatewayCache{},
+ cfg: cfg,
+ rateLimitService: newOpenAIAdvancedSchedulerRateLimitService("true"),
+ concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
+ }
+ store := svc.getOpenAIWSStateStore()
+ require.NoError(t, store.BindResponseAccount(ctx, groupID, "resp_compact_unsupported", 37121, time.Hour))
+
+ selection, decision, err := svc.SelectAccountWithScheduler(
+ ctx,
+ &groupID,
+ "resp_compact_unsupported",
+ "",
+ "gpt-5.1",
+ nil,
+ OpenAIUpstreamTransportAny,
+ true,
+ )
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(37122), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+ require.False(t, decision.StickyPreviousHit)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+
+ accountID, err := store.GetResponseAccount(ctx, groupID, "resp_compact_unsupported")
+ require.NoError(t, err)
+ require.Zero(t, accountID)
+}
+
func TestOpenAIGatewayService_SelectAccountWithScheduler_Enabled_EmbeddingsSkipsChatOnlyAccount(t *testing.T) {
resetOpenAIAdvancedSchedulerSettingCacheForTest()
@@ -1560,6 +1863,217 @@ func TestOpenAIGatewayService_SelectAccountWithScheduler_SessionStickyEscapeDisa
require.True(t, decision.StickySessionHit)
}
+func TestOpenAIGatewayService_SelectAccountWithScheduler_SubscriptionPriorityChoosesSubscriptionPoolFirst(t *testing.T) {
+ ctx := context.Background()
+ groupID := int64(10120)
+ accounts := []Account{
+ {
+ ID: 21601,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeOAuth,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 10,
+ GroupIDs: []int64{groupID},
+ Credentials: map[string]any{"plan_type": "plus"},
+ },
+ {
+ ID: 21602,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ GroupIDs: []int64{groupID},
+ },
+ }
+ concurrencyCache := schedulerTestConcurrencyCache{
+ acquireResults: map[int64]bool{21601: true, 21602: true},
+ loadMap: map[int64]*AccountLoadInfo{
+ 21601: {AccountID: 21601, LoadRate: 90, WaitingCount: 1},
+ 21602: {AccountID: 21602, LoadRate: 0, WaitingCount: 0},
+ },
+ }
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: &schedulerTestGatewayCache{},
+ cfg: newSchedulerTestSubscriptionPriorityConfig(),
+ rateLimitService: newOpenAIAdvancedSchedulerRateLimitService("true", "", "true"),
+ concurrencyService: NewConcurrencyService(concurrencyCache),
+ }
+
+ selection, decision, err := svc.SelectAccountWithScheduler(ctx, &groupID, "", "session_subscription_first", "gpt-5.1", nil, OpenAIUpstreamTransportAny, false)
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(21601), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+ require.Equal(t, 1, decision.TopK)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+}
+
+func TestOpenAIGatewayService_SelectAccountWithScheduler_SubscriptionPriorityFallsBackWhenSubscriptionFull(t *testing.T) {
+ ctx := context.Background()
+ groupID := int64(10121)
+ accounts := []Account{
+ {
+ ID: 21611,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeOAuth,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ GroupIDs: []int64{groupID},
+ Credentials: map[string]any{"plan_type": "team"},
+ },
+ {
+ ID: 21612,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 9,
+ GroupIDs: []int64{groupID},
+ },
+ }
+ concurrencyCache := schedulerTestConcurrencyCache{
+ acquireResults: map[int64]bool{21611: false, 21612: true},
+ loadMap: map[int64]*AccountLoadInfo{
+ 21611: {AccountID: 21611, LoadRate: 0, WaitingCount: 0},
+ 21612: {AccountID: 21612, LoadRate: 90, WaitingCount: 1},
+ },
+ }
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: &schedulerTestGatewayCache{},
+ cfg: newSchedulerTestSubscriptionPriorityConfig(),
+ rateLimitService: newOpenAIAdvancedSchedulerRateLimitService("true", "", "true"),
+ concurrencyService: NewConcurrencyService(concurrencyCache),
+ }
+
+ selection, decision, err := svc.SelectAccountWithScheduler(ctx, &groupID, "", "session_subscription_fallback", "gpt-5.1", nil, OpenAIUpstreamTransportAny, false)
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(21612), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+ require.True(t, selection.Acquired)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+}
+
+func TestOpenAIGatewayService_SelectAccountWithScheduler_SubscriptionPriorityDisabledUsesScore(t *testing.T) {
+ ctx := context.Background()
+ groupID := int64(10122)
+ accounts := []Account{
+ {
+ ID: 21621,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeOAuth,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 10,
+ GroupIDs: []int64{groupID},
+ Credentials: map[string]any{"plan_type": "pro"},
+ },
+ {
+ ID: 21622,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 0,
+ GroupIDs: []int64{groupID},
+ },
+ }
+ concurrencyCache := schedulerTestConcurrencyCache{
+ acquireResults: map[int64]bool{21621: true, 21622: true},
+ loadMap: map[int64]*AccountLoadInfo{
+ 21621: {AccountID: 21621, LoadRate: 90, WaitingCount: 1},
+ 21622: {AccountID: 21622, LoadRate: 0, WaitingCount: 0},
+ },
+ }
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts},
+ cache: &schedulerTestGatewayCache{},
+ cfg: newSchedulerTestSubscriptionPriorityConfig(),
+ rateLimitService: newOpenAIAdvancedSchedulerRateLimitService("true", "", "false"),
+ concurrencyService: NewConcurrencyService(concurrencyCache),
+ }
+
+ selection, decision, err := svc.SelectAccountWithScheduler(ctx, &groupID, "", "session_subscription_disabled", "gpt-5.1", nil, OpenAIUpstreamTransportAny, false)
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(21622), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+}
+
+func TestOpenAIGatewayService_SelectAccountWithScheduler_UsesAccountPriorityWithinGroupPool(t *testing.T) {
+ ctx := context.Background()
+ groupID := int64(10123)
+ accounts := []Account{
+ {
+ ID: 21631,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 1,
+ AccountGroups: []AccountGroup{
+ {AccountID: 21631, GroupID: groupID, Priority: 100},
+ },
+ GroupIDs: []int64{groupID},
+ },
+ {
+ ID: 21632,
+ Platform: PlatformOpenAI,
+ Type: AccountTypeAPIKey,
+ Status: StatusActive,
+ Schedulable: true,
+ Concurrency: 1,
+ Priority: 100000,
+ AccountGroups: []AccountGroup{
+ {AccountID: 21632, GroupID: groupID, Priority: 1},
+ },
+ GroupIDs: []int64{groupID},
+ },
+ }
+ cfg := newSchedulerTestSubscriptionPriorityConfig()
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Load = 0
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Queue = 0
+ svc := &OpenAIGatewayService{
+ accountRepo: schedulerGroupAwareOpenAIAccountRepo{schedulerTestOpenAIAccountRepo{accounts: accounts}},
+ cache: &schedulerTestGatewayCache{},
+ cfg: cfg,
+ rateLimitService: newOpenAIAdvancedSchedulerRateLimitService("true"),
+ concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
+ }
+
+ selection, decision, err := svc.SelectAccountWithScheduler(ctx, &groupID, "", "session_group_priority", "gpt-5.1", nil, OpenAIUpstreamTransportAny, false)
+ require.NoError(t, err)
+ require.NotNil(t, selection)
+ require.NotNil(t, selection.Account)
+ require.Equal(t, int64(21631), selection.Account.ID)
+ require.Equal(t, openAIAccountScheduleLayerLoadBalance, decision.Layer)
+ if selection.ReleaseFunc != nil {
+ selection.ReleaseFunc()
+ }
+}
+
func TestDefaultOpenAIAccountScheduler_ShouldEscapeStickyAccount_ThresholdBoundary(t *testing.T) {
stats := newOpenAIAccountRuntimeStats()
accountID := int64(21501)
diff --git a/backend/internal/service/openai_ws_forwarder.go b/backend/internal/service/openai_ws_forwarder.go
index 9261db5544..fcc4b98064 100644
--- a/backend/internal/service/openai_ws_forwarder.go
+++ b/backend/internal/service/openai_ws_forwarder.go
@@ -4325,87 +4325,8 @@ func (s *OpenAIGatewayService) selectAccountByPreviousResponseIDForCapability(
if s == nil {
return nil, nil
}
- responseID := strings.TrimSpace(previousResponseID)
- if responseID == "" {
- return nil, nil
- }
- store := s.getOpenAIWSStateStore()
- if store == nil {
- return nil, nil
- }
-
- accountID, err := store.GetResponseAccount(ctx, derefGroupID(groupID), responseID)
- if err != nil || accountID <= 0 {
- return nil, nil
- }
- if excludedIDs != nil {
- if _, excluded := excludedIDs[accountID]; excluded {
- return nil, nil
- }
- }
-
- account, err := s.getSchedulableAccount(ctx, accountID)
- if err != nil || account == nil {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
- return nil, nil
- }
- // 非 WSv2 场景(如 force_http/全局关闭)不应使用 previous_response_id 粘连,
- // 以保持“回滚到 HTTP”后的历史行为一致性。
- if s.getOpenAIWSProtocolResolver().Resolve(account).Transport != OpenAIUpstreamTransportResponsesWebsocketV2 {
- return nil, nil
- }
- if shouldClearStickySession(account, requestedModel) || !account.IsOpenAI() || !account.IsSchedulable() {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
- return nil, nil
- }
- if !parentHealthyForShadow(account, s.parentAccountLookup(ctx)) {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
- return nil, nil
- }
- if requestedModel != "" && !account.IsModelSupported(requestedModel) {
- return nil, nil
- }
- if !account.SupportsOpenAIEndpointCapability(requiredCapability) {
- return nil, nil
- }
- // Quota auto-pause must also gate the previous_response_id sticky path; otherwise an
- // account over its 5h/7d threshold keeps serving the same response chain even though
- // normal scheduling skips it. Pause is transient, so fall through to normal scheduling
- // without deleting the binding (the window may reset before the next turn).
- if paused, _ := shouldAutoPauseOpenAIAccountByQuota(ctx, account); paused {
- return nil, nil
- }
- if s.schedulerSnapshot != nil && s.accountRepo != nil {
- latest, latestErr := s.accountRepo.GetByID(ctx, account.ID)
- if latestErr != nil || latest == nil {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
- return nil, nil
- }
- if shouldClearStickySession(latest, requestedModel) || !latest.IsOpenAI() || !latest.IsSchedulable() {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
- return nil, nil
- }
- if !parentHealthyForShadow(latest, s.parentAccountLookup(ctx)) {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
- return nil, nil
- }
- if requestedModel != "" && !latest.IsModelSupported(requestedModel) {
- return nil, nil
- }
- if !latest.SupportsOpenAIEndpointCapability(requiredCapability) {
- return nil, nil
- }
- if paused, _ := shouldAutoPauseOpenAIAccountByQuota(ctx, latest); paused {
- return nil, nil
- }
- if s.isOpenAIAccountRuntimeBlocked(latest) {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
- return nil, nil
- }
- account = latest
- }
- if requireCompact && openAICompactSupportTier(account) == 0 {
- _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ accountID, account, responseID, store := s.resolveAccountByPreviousResponseIDForCapability(ctx, groupID, previousResponseID, requestedModel, excludedIDs, requiredCapability, requireCompact)
+ if accountID <= 0 || account == nil || store == nil {
return nil, nil
}
@@ -4439,6 +4360,117 @@ func (s *OpenAIGatewayService) selectAccountByPreviousResponseIDForCapability(
return nil, nil
}
+func (s *OpenAIGatewayService) ResolveAccountIDByPreviousResponseIDForScheduler(
+ ctx context.Context,
+ groupID *int64,
+ previousResponseID string,
+ requestedModel string,
+ excludedIDs map[int64]struct{},
+ requiredCapability OpenAIEndpointCapability,
+ requireCompact bool,
+) int64 {
+ accountID, _, _, _ := s.resolveAccountByPreviousResponseIDForCapability(ctx, groupID, previousResponseID, requestedModel, excludedIDs, requiredCapability, requireCompact)
+ return accountID
+}
+
+func (s *OpenAIGatewayService) resolveAccountByPreviousResponseIDForCapability(
+ ctx context.Context,
+ groupID *int64,
+ previousResponseID string,
+ requestedModel string,
+ excludedIDs map[int64]struct{},
+ requiredCapability OpenAIEndpointCapability,
+ requireCompact bool,
+) (int64, *Account, string, OpenAIWSStateStore) {
+ if s == nil {
+ return 0, nil, "", nil
+ }
+ responseID := strings.TrimSpace(previousResponseID)
+ if responseID == "" {
+ return 0, nil, "", nil
+ }
+ store := s.getOpenAIWSStateStore()
+ if store == nil {
+ return 0, nil, "", nil
+ }
+
+ accountID, err := store.GetResponseAccount(ctx, derefGroupID(groupID), responseID)
+ if err != nil || accountID <= 0 {
+ return 0, nil, "", nil
+ }
+ if excludedIDs != nil {
+ if _, excluded := excludedIDs[accountID]; excluded {
+ return 0, nil, "", nil
+ }
+ }
+
+ account, err := s.getSchedulableAccount(ctx, accountID)
+ if err != nil || account == nil {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ // 非 WSv2 场景(如 force_http/全局关闭)不应使用 previous_response_id 粘连,
+ // 以保持“回滚到 HTTP”后的历史行为一致性。
+ if s.getOpenAIWSProtocolResolver().Resolve(account).Transport != OpenAIUpstreamTransportResponsesWebsocketV2 {
+ return 0, nil, "", nil
+ }
+ if shouldClearStickySession(account, requestedModel) || !account.IsOpenAI() || !account.IsSchedulable() {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ if !parentHealthyForShadow(account, s.parentAccountLookup(ctx)) {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ if requestedModel != "" && !account.IsModelSupported(requestedModel) {
+ return 0, nil, "", nil
+ }
+ if !account.SupportsOpenAIEndpointCapability(requiredCapability) {
+ return 0, nil, "", nil
+ }
+ // Quota auto-pause must also gate the previous_response_id sticky path; otherwise an
+ // account over its 5h/7d threshold keeps serving the same response chain even though
+ // normal scheduling skips it. Pause is transient, so fall through to normal scheduling
+ // without deleting the binding (the window may reset before the next turn).
+ if paused, _ := shouldAutoPauseOpenAIAccountByQuota(ctx, account); paused {
+ return 0, nil, "", nil
+ }
+ if s.schedulerSnapshot != nil && s.accountRepo != nil {
+ latest, latestErr := s.accountRepo.GetByID(ctx, account.ID)
+ if latestErr != nil || latest == nil {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ if shouldClearStickySession(latest, requestedModel) || !latest.IsOpenAI() || !latest.IsSchedulable() {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ if !parentHealthyForShadow(latest, s.parentAccountLookup(ctx)) {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ if requestedModel != "" && !latest.IsModelSupported(requestedModel) {
+ return 0, nil, "", nil
+ }
+ if !latest.SupportsOpenAIEndpointCapability(requiredCapability) {
+ return 0, nil, "", nil
+ }
+ if paused, _ := shouldAutoPauseOpenAIAccountByQuota(ctx, latest); paused {
+ return 0, nil, "", nil
+ }
+ if s.isOpenAIAccountRuntimeBlocked(latest) {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ account = latest
+ }
+ if requireCompact && openAICompactSupportTier(account) == 0 {
+ _ = store.DeleteResponseAccount(ctx, derefGroupID(groupID), responseID)
+ return 0, nil, "", nil
+ }
+ return accountID, account, responseID, store
+}
+
func classifyOpenAIWSAcquireError(err error) string {
if err == nil {
return "acquire_conn"
diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go
index 2b9c85a2fa..c84192368f 100644
--- a/backend/internal/service/ratelimit_service.go
+++ b/backend/internal/service/ratelimit_service.go
@@ -116,6 +116,14 @@ func (s *RateLimitService) SetAccountRuntimeBlocker(blocker AccountRuntimeBlocke
s.runtimeBlocker = blocker
}
+func (s *RateLimitService) IsOpenAIAdvancedSchedulerStickyWeightedEnabled(ctx context.Context) bool {
+ if s == nil || s.settingService == nil {
+ return false
+ }
+ gateway := &OpenAIGatewayService{rateLimitService: s}
+ return gateway.isOpenAIAdvancedSchedulerStickyWeightedEnabled(ctx)
+}
+
func (s *RateLimitService) notifyAccountSchedulingBlocked(account *Account, until time.Time, reason string) {
if s == nil || s.runtimeBlocker == nil || account == nil {
return
diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go
index 1a5e677a12..1024243bea 100644
--- a/backend/internal/service/setting_service.go
+++ b/backend/internal/service/setting_service.go
@@ -1921,6 +1921,9 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
if err != nil {
return nil, err
}
+ if err := normalizeOpenAIAdvancedSchedulerOverrides(settings); err != nil {
+ return nil, err
+ }
settings.PaymentVisibleMethodAlipaySource = alipaySource
settings.PaymentVisibleMethodWxpaySource = wxpaySource
settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID)
@@ -2229,6 +2232,18 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
updates[SettingPaymentVisibleMethodWxpayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodWxpayEnabled)
updates[openAIAdvancedSchedulerSettingKey] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerEnabled)
+ updates[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerStickyWeightedEnabled)
+ updates[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled)
+ updates[SettingKeyOpenAIAdvancedSchedulerLBTopK] = settings.OpenAIAdvancedSchedulerLBTopK
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightPriority] = settings.OpenAIAdvancedSchedulerWeightPriority
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightLoad] = settings.OpenAIAdvancedSchedulerWeightLoad
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightQueue] = settings.OpenAIAdvancedSchedulerWeightQueue
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate] = settings.OpenAIAdvancedSchedulerWeightErrorRate
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightTTFT] = settings.OpenAIAdvancedSchedulerWeightTTFT
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightReset] = settings.OpenAIAdvancedSchedulerWeightReset
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom] = settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse] = settings.OpenAIAdvancedSchedulerWeightPreviousResponse
+ updates[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky] = settings.OpenAIAdvancedSchedulerWeightSessionSticky
// 余额、订阅到期与账号限额通知
updates[SettingKeyBalanceLowNotifyEnabled] = strconv.FormatBool(settings.BalanceLowNotifyEnabled)
@@ -2376,7 +2391,21 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
})
openAIAdvancedSchedulerSettingSF.Forget(openAIAdvancedSchedulerSettingKey)
openAIAdvancedSchedulerSettingCache.Store(&cachedOpenAIAdvancedSchedulerSetting{
- enabled: settings.OpenAIAdvancedSchedulerEnabled,
+ enabled: settings.OpenAIAdvancedSchedulerEnabled,
+ stickyWeightedEnabled: settings.OpenAIAdvancedSchedulerStickyWeightedEnabled,
+ subscriptionPriorityEnabled: settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
+ lbTopKOverride: parsePositiveIntOverride(settings.OpenAIAdvancedSchedulerLBTopK),
+ weightOverrides: parseOpenAIAdvancedSchedulerWeightOverrides(map[string]string{
+ SettingKeyOpenAIAdvancedSchedulerWeightPriority: settings.OpenAIAdvancedSchedulerWeightPriority,
+ SettingKeyOpenAIAdvancedSchedulerWeightLoad: settings.OpenAIAdvancedSchedulerWeightLoad,
+ SettingKeyOpenAIAdvancedSchedulerWeightQueue: settings.OpenAIAdvancedSchedulerWeightQueue,
+ SettingKeyOpenAIAdvancedSchedulerWeightErrorRate: settings.OpenAIAdvancedSchedulerWeightErrorRate,
+ SettingKeyOpenAIAdvancedSchedulerWeightTTFT: settings.OpenAIAdvancedSchedulerWeightTTFT,
+ SettingKeyOpenAIAdvancedSchedulerWeightReset: settings.OpenAIAdvancedSchedulerWeightReset,
+ SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom: settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
+ SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse: settings.OpenAIAdvancedSchedulerWeightPreviousResponse,
+ SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: settings.OpenAIAdvancedSchedulerWeightSessionSticky,
+ }),
expiresAt: time.Now().Add(openAIAdvancedSchedulerSettingCacheTTL).UnixNano(),
})
// Invalidate the quota auto-pause cache and let the next read trigger a fresh load.
@@ -3196,17 +3225,29 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyCodexCLIOnlyEngineFingerprintSignals: openai.DefaultEngineFingerprintSignalsJSON(),
// 分组隔离(默认不允许未分组 Key 调度)
- SettingKeyAllowUngroupedKeyScheduling: "false",
- SettingKeyEnableAnthropicCacheTTL1hInjection: "false",
- SettingKeyRewriteMessageCacheControl: strconv.FormatBool(s.defaultRewriteMessageCacheControl()),
- SettingKeyEnableClientDatelineNormalization: "true",
- SettingKeyAntigravityUserAgentVersion: "",
- SettingKeyOpenAICodexUserAgent: "",
- SettingPaymentVisibleMethodAlipaySource: "",
- SettingPaymentVisibleMethodWxpaySource: "",
- SettingPaymentVisibleMethodAlipayEnabled: "false",
- SettingPaymentVisibleMethodWxpayEnabled: "false",
- openAIAdvancedSchedulerSettingKey: "false",
+ SettingKeyAllowUngroupedKeyScheduling: "false",
+ SettingKeyEnableAnthropicCacheTTL1hInjection: "false",
+ SettingKeyRewriteMessageCacheControl: strconv.FormatBool(s.defaultRewriteMessageCacheControl()),
+ SettingKeyEnableClientDatelineNormalization: "true",
+ SettingKeyAntigravityUserAgentVersion: "",
+ SettingKeyOpenAICodexUserAgent: "",
+ SettingPaymentVisibleMethodAlipaySource: "",
+ SettingPaymentVisibleMethodWxpaySource: "",
+ SettingPaymentVisibleMethodAlipayEnabled: "false",
+ SettingPaymentVisibleMethodWxpayEnabled: "false",
+ openAIAdvancedSchedulerSettingKey: "false",
+ SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled: "false",
+ SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled: "false",
+ SettingKeyOpenAIAdvancedSchedulerLBTopK: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightPriority: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightLoad: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightQueue: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightErrorRate: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightTTFT: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightReset: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse: "",
+ SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: "",
SettingKeyAllowUserViewErrorRequests: "false",
}
@@ -3769,6 +3810,29 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
result.PaymentVisibleMethodAlipayEnabled = settings[SettingPaymentVisibleMethodAlipayEnabled] == "true"
result.PaymentVisibleMethodWxpayEnabled = settings[SettingPaymentVisibleMethodWxpayEnabled] == "true"
result.OpenAIAdvancedSchedulerEnabled = settings[openAIAdvancedSchedulerSettingKey] == "true"
+ result.OpenAIAdvancedSchedulerStickyWeightedEnabled = settings[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] == "true"
+ result.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled = settings[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] == "true"
+ result.OpenAIAdvancedSchedulerLBTopK = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerLBTopK])
+ result.OpenAIAdvancedSchedulerWeightPriority = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightPriority])
+ result.OpenAIAdvancedSchedulerWeightLoad = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightLoad])
+ result.OpenAIAdvancedSchedulerWeightQueue = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightQueue])
+ result.OpenAIAdvancedSchedulerWeightErrorRate = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate])
+ result.OpenAIAdvancedSchedulerWeightTTFT = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightTTFT])
+ result.OpenAIAdvancedSchedulerWeightReset = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightReset])
+ result.OpenAIAdvancedSchedulerWeightQuotaHeadroom = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom])
+ result.OpenAIAdvancedSchedulerWeightPreviousResponse = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse])
+ result.OpenAIAdvancedSchedulerWeightSessionSticky = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky])
+ result.OpenAIAdvancedSchedulerEffectiveLBTopK = s.openAIAdvancedSchedulerEffectiveLBTopK()
+ effectiveWeights := s.openAIAdvancedSchedulerEffectiveWeights()
+ result.OpenAIAdvancedSchedulerEffectiveWeightPriority = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Priority)
+ result.OpenAIAdvancedSchedulerEffectiveWeightLoad = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Load)
+ result.OpenAIAdvancedSchedulerEffectiveWeightQueue = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Queue)
+ result.OpenAIAdvancedSchedulerEffectiveWeightErrorRate = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.ErrorRate)
+ result.OpenAIAdvancedSchedulerEffectiveWeightTTFT = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.TTFT)
+ result.OpenAIAdvancedSchedulerEffectiveWeightReset = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Reset)
+ result.OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.QuotaHeadroom)
+ result.OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.PreviousResponse)
+ result.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.SessionSticky)
// 余额、订阅到期与账号限额通知
result.BalanceLowNotifyEnabled = settings[SettingKeyBalanceLowNotifyEnabled] == "true"
@@ -3841,6 +3905,93 @@ func normalizeVisibleMethodSettingSource(method, source string, enabled bool) (s
return normalized, nil
}
+func (s *SettingService) openAIAdvancedSchedulerEffectiveLBTopK() string {
+ if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.LBTopK > 0 {
+ return strconv.Itoa(s.cfg.Gateway.OpenAIWS.LBTopK)
+ }
+ return "7"
+}
+
+func (s *SettingService) openAIAdvancedSchedulerEffectiveWeights() config.GatewayOpenAIWSSchedulerScoreWeights {
+ defaults := config.GatewayOpenAIWSSchedulerScoreWeights{
+ Priority: 1.0,
+ Load: 1.0,
+ Queue: 0.7,
+ ErrorRate: 0.8,
+ TTFT: 0.5,
+ Reset: 0.0,
+ QuotaHeadroom: 0.0,
+ PreviousResponse: 5.0,
+ SessionSticky: 3.0,
+ }
+ if s == nil || s.cfg == nil {
+ return defaults
+ }
+
+ weights := s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights
+ baseSum := weights.Priority + weights.Load + weights.Queue + weights.ErrorRate + weights.TTFT + weights.QuotaHeadroom
+ if baseSum <= 0 {
+ return defaults
+ }
+ return weights
+}
+
+func formatOpenAIAdvancedSchedulerFloat(value float64) string {
+ return strconv.FormatFloat(value, 'f', -1, 64)
+}
+
+func normalizeOpenAIAdvancedSchedulerOverrides(settings *SystemSettings) error {
+ lbTopK, err := normalizeOptionalPositiveIntString(settings.OpenAIAdvancedSchedulerLBTopK)
+ if err != nil {
+ return infraerrors.BadRequest("INVALID_OPENAI_ADVANCED_SCHEDULER_LB_TOP_K", "openai advanced scheduler TopK must be a positive integer or empty")
+ }
+ settings.OpenAIAdvancedSchedulerLBTopK = lbTopK
+
+ weights := []*string{
+ &settings.OpenAIAdvancedSchedulerWeightPriority,
+ &settings.OpenAIAdvancedSchedulerWeightLoad,
+ &settings.OpenAIAdvancedSchedulerWeightQueue,
+ &settings.OpenAIAdvancedSchedulerWeightErrorRate,
+ &settings.OpenAIAdvancedSchedulerWeightTTFT,
+ &settings.OpenAIAdvancedSchedulerWeightReset,
+ &settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
+ &settings.OpenAIAdvancedSchedulerWeightPreviousResponse,
+ &settings.OpenAIAdvancedSchedulerWeightSessionSticky,
+ }
+ for _, target := range weights {
+ normalized, err := normalizeOptionalNonNegativeFloatString(*target)
+ if err != nil {
+ return infraerrors.BadRequest("INVALID_OPENAI_ADVANCED_SCHEDULER_WEIGHT", "openai advanced scheduler weights must be non-negative numbers or empty")
+ }
+ *target = normalized
+ }
+ return nil
+}
+
+func normalizeOptionalPositiveIntString(raw string) (string, error) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return "", nil
+ }
+ value, err := strconv.Atoi(raw)
+ if err != nil || value <= 0 {
+ return "", fmt.Errorf("invalid positive integer")
+ }
+ return strconv.Itoa(value), nil
+}
+
+func normalizeOptionalNonNegativeFloatString(raw string) (string, error) {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return "", nil
+ }
+ value, err := strconv.ParseFloat(raw, 64)
+ if err != nil || value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
+ return "", fmt.Errorf("invalid non-negative float")
+ }
+ return strconv.FormatFloat(value, 'f', -1, 64), nil
+}
+
func parseDefaultSubscriptions(raw string) []DefaultSubscriptionSetting {
raw = strings.TrimSpace(raw)
if raw == "" {
diff --git a/backend/internal/service/setting_service_update_test.go b/backend/internal/service/setting_service_update_test.go
index 379bf9bc04..a47e1379f7 100644
--- a/backend/internal/service/setting_service_update_test.go
+++ b/backend/internal/service/setting_service_update_test.go
@@ -49,6 +49,42 @@ func (s *settingUpdateRepoStub) Delete(ctx context.Context, key string) error {
panic("unexpected Delete call")
}
+type settingGetAllRepoStub struct {
+ values map[string]string
+}
+
+func (s *settingGetAllRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
+ panic("unexpected Get call")
+}
+
+func (s *settingGetAllRepoStub) GetValue(ctx context.Context, key string) (string, error) {
+ panic("unexpected GetValue call")
+}
+
+func (s *settingGetAllRepoStub) Set(ctx context.Context, key, value string) error {
+ panic("unexpected Set call")
+}
+
+func (s *settingGetAllRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
+ panic("unexpected GetMultiple call")
+}
+
+func (s *settingGetAllRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
+ panic("unexpected SetMultiple call")
+}
+
+func (s *settingGetAllRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
+ out := make(map[string]string, len(s.values))
+ for key, value := range s.values {
+ out[key] = value
+ }
+ return out, nil
+}
+
+func (s *settingGetAllRepoStub) Delete(ctx context.Context, key string) error {
+ panic("unexpected Delete call")
+}
+
type settingAntigravityUARepoStub struct {
values map[string]string
}
@@ -261,15 +297,30 @@ func TestSettingService_UpdateSettings_TablePreferences(t *testing.T) {
}
func TestSettingService_UpdateSettings_PaymentVisibleMethodsAndAdvancedScheduler(t *testing.T) {
+ resetOpenAIAdvancedSchedulerSettingCacheForTest()
+ defer resetOpenAIAdvancedSchedulerSettingCacheForTest()
+
repo := &settingUpdateRepoStub{}
svc := NewSettingService(repo, &config.Config{})
err := svc.UpdateSettings(context.Background(), &SystemSettings{
- PaymentVisibleMethodAlipaySource: "alipay",
- PaymentVisibleMethodWxpaySource: "easypay",
- PaymentVisibleMethodAlipayEnabled: true,
- PaymentVisibleMethodWxpayEnabled: false,
- OpenAIAdvancedSchedulerEnabled: true,
+ PaymentVisibleMethodAlipaySource: "alipay",
+ PaymentVisibleMethodWxpaySource: "easypay",
+ PaymentVisibleMethodAlipayEnabled: true,
+ PaymentVisibleMethodWxpayEnabled: false,
+ OpenAIAdvancedSchedulerEnabled: true,
+ OpenAIAdvancedSchedulerStickyWeightedEnabled: true,
+ OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: true,
+ OpenAIAdvancedSchedulerLBTopK: " 3 ",
+ OpenAIAdvancedSchedulerWeightPriority: "2.50",
+ OpenAIAdvancedSchedulerWeightLoad: "0",
+ OpenAIAdvancedSchedulerWeightQueue: "0.75",
+ OpenAIAdvancedSchedulerWeightErrorRate: "1.25",
+ OpenAIAdvancedSchedulerWeightTTFT: "0.5",
+ OpenAIAdvancedSchedulerWeightReset: "",
+ OpenAIAdvancedSchedulerWeightQuotaHeadroom: "0.2",
+ OpenAIAdvancedSchedulerWeightPreviousResponse: "8",
+ OpenAIAdvancedSchedulerWeightSessionSticky: "4",
})
require.NoError(t, err)
require.Equal(t, VisibleMethodSourceOfficialAlipay, repo.updates[SettingPaymentVisibleMethodAlipaySource])
@@ -277,6 +328,49 @@ func TestSettingService_UpdateSettings_PaymentVisibleMethodsAndAdvancedScheduler
require.Equal(t, "true", repo.updates[SettingPaymentVisibleMethodAlipayEnabled])
require.Equal(t, "false", repo.updates[SettingPaymentVisibleMethodWxpayEnabled])
require.Equal(t, "true", repo.updates[openAIAdvancedSchedulerSettingKey])
+ require.Equal(t, "true", repo.updates[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled])
+ require.Equal(t, "true", repo.updates[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled])
+ require.Equal(t, "3", repo.updates[SettingKeyOpenAIAdvancedSchedulerLBTopK])
+ require.Equal(t, "2.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightPriority])
+ require.Equal(t, "0", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightLoad])
+ require.Equal(t, "0.75", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightQueue])
+ require.Equal(t, "1.25", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate])
+ require.Equal(t, "0.5", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightTTFT])
+ require.Equal(t, "", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightReset])
+ require.Equal(t, "0.2", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom])
+ require.Equal(t, "8", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse])
+ require.Equal(t, "4", repo.updates[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky])
+}
+
+func TestSettingService_GetAllSettings_OpenAIAdvancedSchedulerEffectiveValuesUseConfig(t *testing.T) {
+ cfg := &config.Config{}
+ cfg.Gateway.OpenAIWS.LBTopK = 13
+ cfg.Gateway.OpenAIWS.SchedulerScoreWeights = config.GatewayOpenAIWSSchedulerScoreWeights{
+ Priority: 2,
+ Load: 3,
+ Queue: 4,
+ ErrorRate: 5,
+ TTFT: 6,
+ Reset: 7,
+ QuotaHeadroom: 8,
+ PreviousResponse: 9,
+ SessionSticky: 10,
+ }
+ svc := NewSettingService(&settingGetAllRepoStub{values: map[string]string{
+ SettingKeyOpenAIAdvancedSchedulerLBTopK: "3",
+ SettingKeyOpenAIAdvancedSchedulerWeightPriority: "99",
+ SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: "88",
+ }}, cfg)
+
+ settings, err := svc.GetAllSettings(context.Background())
+ require.NoError(t, err)
+ require.Equal(t, "3", settings.OpenAIAdvancedSchedulerLBTopK)
+ require.Equal(t, "99", settings.OpenAIAdvancedSchedulerWeightPriority)
+ require.Equal(t, "88", settings.OpenAIAdvancedSchedulerWeightSessionSticky)
+ require.Equal(t, "13", settings.OpenAIAdvancedSchedulerEffectiveLBTopK)
+ require.Equal(t, "2", settings.OpenAIAdvancedSchedulerEffectiveWeightPriority)
+ require.Equal(t, "3", settings.OpenAIAdvancedSchedulerEffectiveWeightLoad)
+ require.Equal(t, "10", settings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky)
}
func TestSettingService_UpdateSettings_AntigravityUserAgentVersion(t *testing.T) {
diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go
index ac225e1b14..465760a445 100644
--- a/backend/internal/service/settings_view.go
+++ b/backend/internal/service/settings_view.go
@@ -218,7 +218,29 @@ type SystemSettings struct {
PaymentVisibleMethodWxpayEnabled bool
// OpenAI 账号调度
- OpenAIAdvancedSchedulerEnabled bool
+ OpenAIAdvancedSchedulerEnabled bool
+ OpenAIAdvancedSchedulerStickyWeightedEnabled bool
+ OpenAIAdvancedSchedulerSubscriptionPriorityEnabled bool
+ OpenAIAdvancedSchedulerLBTopK string
+ OpenAIAdvancedSchedulerWeightPriority string
+ OpenAIAdvancedSchedulerWeightLoad string
+ OpenAIAdvancedSchedulerWeightQueue string
+ OpenAIAdvancedSchedulerWeightErrorRate string
+ OpenAIAdvancedSchedulerWeightTTFT string
+ OpenAIAdvancedSchedulerWeightReset string
+ OpenAIAdvancedSchedulerWeightQuotaHeadroom string
+ OpenAIAdvancedSchedulerWeightPreviousResponse string
+ OpenAIAdvancedSchedulerWeightSessionSticky string
+ OpenAIAdvancedSchedulerEffectiveLBTopK string
+ OpenAIAdvancedSchedulerEffectiveWeightPriority string
+ OpenAIAdvancedSchedulerEffectiveWeightLoad string
+ OpenAIAdvancedSchedulerEffectiveWeightQueue string
+ OpenAIAdvancedSchedulerEffectiveWeightErrorRate string
+ OpenAIAdvancedSchedulerEffectiveWeightTTFT string
+ OpenAIAdvancedSchedulerEffectiveWeightReset string
+ OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom string
+ OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse string
+ OpenAIAdvancedSchedulerEffectiveWeightSessionSticky string
// 余额不足提醒
BalanceLowNotifyEnabled bool
diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts
index 44fbe29187..457775870d 100644
--- a/frontend/src/api/admin/settings.ts
+++ b/frontend/src/api/admin/settings.ts
@@ -606,6 +606,28 @@ export interface SystemSettings {
payment_visible_method_alipay_enabled?: boolean;
payment_visible_method_wxpay_enabled?: boolean;
openai_advanced_scheduler_enabled?: boolean;
+ openai_advanced_scheduler_sticky_weighted_enabled?: boolean;
+ openai_advanced_scheduler_subscription_priority_enabled?: boolean;
+ openai_advanced_scheduler_lb_top_k?: string;
+ openai_advanced_scheduler_weight_priority?: string;
+ openai_advanced_scheduler_weight_load?: string;
+ openai_advanced_scheduler_weight_queue?: string;
+ openai_advanced_scheduler_weight_error_rate?: string;
+ openai_advanced_scheduler_weight_ttft?: string;
+ openai_advanced_scheduler_weight_reset?: string;
+ openai_advanced_scheduler_weight_quota_headroom?: string;
+ openai_advanced_scheduler_weight_previous_response?: string;
+ openai_advanced_scheduler_weight_session_sticky?: string;
+ openai_advanced_scheduler_effective_lb_top_k?: string;
+ openai_advanced_scheduler_effective_weight_priority?: string;
+ openai_advanced_scheduler_effective_weight_load?: string;
+ openai_advanced_scheduler_effective_weight_queue?: string;
+ openai_advanced_scheduler_effective_weight_error_rate?: string;
+ openai_advanced_scheduler_effective_weight_ttft?: string;
+ openai_advanced_scheduler_effective_weight_reset?: string;
+ openai_advanced_scheduler_effective_weight_quota_headroom?: string;
+ openai_advanced_scheduler_effective_weight_previous_response?: string;
+ openai_advanced_scheduler_effective_weight_session_sticky?: string;
// 余额、订阅到期与账号限额通知
balance_low_notify_enabled: boolean;
@@ -855,6 +877,18 @@ export interface UpdateSettingsRequest {
payment_visible_method_alipay_enabled?: boolean;
payment_visible_method_wxpay_enabled?: boolean;
openai_advanced_scheduler_enabled?: boolean;
+ openai_advanced_scheduler_sticky_weighted_enabled?: boolean;
+ openai_advanced_scheduler_subscription_priority_enabled?: boolean;
+ openai_advanced_scheduler_lb_top_k?: string;
+ openai_advanced_scheduler_weight_priority?: string;
+ openai_advanced_scheduler_weight_load?: string;
+ openai_advanced_scheduler_weight_queue?: string;
+ openai_advanced_scheduler_weight_error_rate?: string;
+ openai_advanced_scheduler_weight_ttft?: string;
+ openai_advanced_scheduler_weight_reset?: string;
+ openai_advanced_scheduler_weight_quota_headroom?: string;
+ openai_advanced_scheduler_weight_previous_response?: string;
+ openai_advanced_scheduler_weight_session_sticky?: string;
// 余额、订阅到期与账号限额通知
balance_low_notify_enabled?: boolean;
balance_low_notify_threshold?: number;
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index ac831278bd..b42acc9c91 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -3265,6 +3265,7 @@ export default {
priority: 'Priority',
billingRateMultiplier: 'Billing Rate',
weight: 'Weight',
+ schedulerScore: 'Scheduler Score',
status: 'Status',
schedulable: 'Schedulable',
todayStats: 'Today Stats',
@@ -3276,6 +3277,12 @@ export default {
expiresAt: 'Expires At',
actions: 'Actions'
},
+ schedulerScore: {
+ baseShort: 'Base',
+ stickyShort: 'Sticky',
+ ungrouped: 'Ungrouped',
+ hint: 'Displayed as "group / base score / sticky bonus". The base score is computed within the current filtered candidate set and includes priority, load, queue depth, error rate, first-token latency, reset window, quota headroom, and related factors. The sticky bonus applies only when sticky weighting is enabled for previous_response_id or session_hash. Higher scores are preferred.'
+ },
usageWindowsHint: '"5h / 7d" are the upstream account\'s official rolling usage windows (e.g. OpenAI ChatGPT, Claude). They are imposed by the upstream provider on the account itself — not configured by sub2api, and unrelated to the models you map. Usage resets automatically once each window rolls over, and the limit cannot be lifted from within sub2api.',
allPrivacyModes: 'All Privacy States',
privacyUnset: 'Unset',
@@ -6724,7 +6731,24 @@ export default {
},
openaiExperimentalScheduler: {
title: 'OpenAI experimental scheduler policy',
- description: "Disabled by default. When enabled, this only changes the gateway's experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability."
+ description: "Disabled by default. When enabled, this only changes the gateway's experimental account-selection policy for OpenAI traffic; it does not indicate an upstream OpenAI capability.",
+ stickyWeightedTitle: 'Sticky weighting',
+ stickyWeightedDescription: 'When enabled, previous_response_id and session_hash affinity are scored by the advanced scheduler. When disabled, sticky accounts keep the legacy hard-hit behavior.',
+ subscriptionPriorityTitle: 'Subscription priority',
+ subscriptionPriorityDescription: 'When enabled, the scheduler scores ChatGPT subscription accounts first and falls back to non-subscription accounts only if no subscription slot can be acquired.',
+ weightsTitle: 'Scheduler weight overrides',
+ weightsDescription: 'Blank values use config/environment values; when config is not set, built-in defaults apply. Non-blank page settings take priority.',
+ defaultPlaceholder: 'config/default: {value}',
+ topKLabel: 'TopK',
+ priorityWeight: 'Priority',
+ loadWeight: 'Load',
+ queueWeight: 'Queue',
+ errorRateWeight: 'Error rate',
+ ttftWeight: 'TTFT',
+ resetWeight: 'Reset window',
+ quotaHeadroomWeight: 'Quota headroom',
+ previousResponseWeight: 'previous_response sticky',
+ sessionStickyWeight: 'session_hash sticky'
},
usageRecords: {
title: 'Usage Records',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index dc090a458f..44194168d9 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -3296,6 +3296,7 @@ export default {
priority: '优先级',
billingRateMultiplier: '账号倍率',
weight: '权重',
+ schedulerScore: '调度权值',
status: '状态',
schedulable: '调度',
todayStats: '今日统计',
@@ -3307,6 +3308,12 @@ export default {
expiresAt: '过期时间',
actions: '操作'
},
+ schedulerScore: {
+ baseShort: '普通',
+ stickyShort: '粘性',
+ ungrouped: '未分组',
+ hint: '显示格式为“分组名 / 基础分 / 粘性加分”。基础分按当前筛选条件限定的候选账号计算,包含优先级、负载、排队、错误率、首包延迟、重置窗口、额度余量等因子;粘性加分只在开启粘性加权时用于 previous_response_id 或 session_hash。分数越大越优先。'
+ },
usageWindowsHint: '“5h / 7d”是上游账号(如 OpenAI ChatGPT、Claude)官方的滚动用量窗口限制,由上游对账号设定,并非 sub2api 配置,也与你映射的模型无关。窗口滚动到期后用量会自动重置,无法在 sub2api 端解除该限制。',
allPrivacyModes: '全部Privacy状态',
privacyUnset: '未设置',
@@ -6878,7 +6885,24 @@ export default {
},
openaiExperimentalScheduler: {
title: 'OpenAI 实验调度策略',
- description: '默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。'
+ description: '默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。',
+ stickyWeightedTitle: '粘性加权',
+ stickyWeightedDescription: '开启后 previous_response_id 和 session_hash 粘性进入高级调度打分;关闭时仍按旧逻辑硬命中粘性账号。',
+ subscriptionPriorityTitle: '订阅优先',
+ subscriptionPriorityDescription: '开启后先在 ChatGPT 订阅账号池中按权值选取;订阅池拿不到席位时再回退到非订阅账号池。',
+ weightsTitle: '调度权值覆盖',
+ weightsDescription: '留空时使用配置/环境变量值;配置未设置时使用内置默认值。页面非空设置优先。',
+ defaultPlaceholder: '配置/默认:{value}',
+ topKLabel: 'TopK',
+ priorityWeight: '优先级',
+ loadWeight: '负载',
+ queueWeight: '排队',
+ errorRateWeight: '错误率',
+ ttftWeight: '首包延迟',
+ resetWeight: '重置窗口',
+ quotaHeadroomWeight: '额度余量',
+ previousResponseWeight: 'previous_response 粘性',
+ sessionStickyWeight: 'session_hash 粘性'
},
usageRecords: {
title: '使用记录',
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 315db76224..cff836d041 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -857,6 +857,13 @@ export interface Account {
concurrency: number
load_factor?: number | null
current_concurrency?: number // Real-time concurrency count from Redis
+ scheduler_score?: {
+ base_score: number
+ sticky_score?: number
+ sticky_score_infinity?: boolean
+ sticky_weighted_enabled: boolean
+ } | null
+ scheduler_scores?: AccountSchedulerGroupScore[] | null
priority: number
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
status: 'active' | 'inactive' | 'error'
@@ -947,6 +954,16 @@ export interface Account {
parent_chatgpt_account_id?: string
}
+export interface AccountSchedulerGroupScore {
+ group_id?: number | null
+ group_name?: string
+ group_priority?: number | null
+ base_score: number
+ sticky_score?: number
+ sticky_score_infinity?: boolean
+ sticky_weighted_enabled: boolean
+}
+
// Account Usage types
export interface WindowStats {
requests: number
diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue
index e8cd05cb2b..bd8fd6067a 100644
--- a/frontend/src/views/admin/AccountsView.vue
+++ b/frontend/src/views/admin/AccountsView.vue
@@ -322,6 +322,29 @@
{{ value }}
+
+
+ {{ + t("admin.settings.openaiExperimentalScheduler.stickyWeightedDescription") + }} +
++ {{ + t("admin.settings.openaiExperimentalScheduler.subscriptionPriorityDescription") + }} +
++ {{ + t("admin.settings.openaiExperimentalScheduler.weightsDescription") + }} +
+