mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3692 from linshuboy/codex/openai-advanced-scheduler-controls-squashed
[codex] add OpenAI advanced scheduler controls
This commit is contained in:
@@ -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 +
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
// 添加窗口费用(仅当启用时)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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": []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 时不应影响打分")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '使用记录',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -322,6 +322,29 @@
|
||||
<template #cell-priority="{ value }">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value }}</span>
|
||||
</template>
|
||||
<template #header-scheduler_score="{ column }">
|
||||
<div class="flex items-center">
|
||||
<span>{{ column.label }}</span>
|
||||
<HelpTooltip :content="t('admin.accounts.schedulerScore.hint')" width-class="w-80" />
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-scheduler_score="{ row }">
|
||||
<div v-if="getSchedulerScoreRows(row).length" class="flex min-w-[7rem] flex-col gap-0.5 font-mono text-[11px] leading-4">
|
||||
<div
|
||||
v-for="score in getSchedulerScoreRows(row)"
|
||||
:key="String(score.group_id)"
|
||||
class="flex items-center gap-1 whitespace-nowrap text-gray-700 dark:text-gray-300"
|
||||
:title="`${formatSchedulerScoreGroup(score)} / ${formatSchedulerScore(score.base_score)} / ${formatStickySchedulerScore(score)}`"
|
||||
>
|
||||
<span class="max-w-[4.75rem] truncate text-gray-500 dark:text-dark-400">{{ formatSchedulerScoreGroup(score) }}</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">/</span>
|
||||
<span>{{ formatSchedulerScore(score.base_score) }}</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">/</span>
|
||||
<span class="text-primary-700 dark:text-primary-300">{{ formatStickySchedulerScore(score) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
|
||||
</template>
|
||||
<template #cell-last_used_at="{ value }">
|
||||
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatRelativeTime(value) }}</span>
|
||||
</template>
|
||||
@@ -441,7 +464,7 @@ import TLSFingerprintProfilesModal from '@/components/admin/TLSFingerprintProfil
|
||||
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
|
||||
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||
import { proxyExpiryBadgeClass, proxyExpiryLabelKey } from '@/utils/proxyExpiry'
|
||||
import type { Account, AccountPlatform, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
|
||||
import type { Account, AccountPlatform, AccountSchedulerGroupScore, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
@@ -643,6 +666,29 @@ const autoRefreshIntervalLabel = (sec: number) => {
|
||||
return `${sec}s`
|
||||
}
|
||||
|
||||
const formatSchedulerScore = (value: unknown): string => {
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num)) return '-'
|
||||
return num.toFixed(6).replace(/\.?0+$/, '')
|
||||
}
|
||||
|
||||
const formatStickySchedulerScore = (score: AccountSchedulerGroupScore): string => {
|
||||
if (!score) return '-'
|
||||
if (score.sticky_score_infinity) return '+∞'
|
||||
return formatSchedulerScore(score.sticky_score)
|
||||
}
|
||||
|
||||
const getSchedulerScoreRows = (account: Account): AccountSchedulerGroupScore[] => {
|
||||
if (!Array.isArray(account.scheduler_scores)) return []
|
||||
return account.scheduler_scores.filter(score => score.group_id != null)
|
||||
}
|
||||
|
||||
const formatSchedulerScoreGroup = (score: AccountSchedulerGroupScore): string => {
|
||||
if ('group_name' in score && score.group_name) return score.group_name
|
||||
if ('group_id' in score && score.group_id != null) return `#${score.group_id}`
|
||||
return '-'
|
||||
}
|
||||
|
||||
const loadSavedColumns = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem(HIDDEN_COLUMNS_KEY)
|
||||
@@ -1166,6 +1212,7 @@ const allColumns = computed(() => {
|
||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||
{ key: 'proxy', label: t('admin.accounts.columns.proxy'), sortable: false },
|
||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||
{ key: 'scheduler_score', label: t('admin.accounts.columns.schedulerScore'), sortable: false },
|
||||
{ key: 'rate_multiplier', label: t('admin.accounts.columns.billingRateMultiplier'), sortable: true },
|
||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||
{ key: 'created_at', label: t('admin.accounts.columns.createdAt'), sortable: true },
|
||||
|
||||
@@ -4014,6 +4014,81 @@
|
||||
</div>
|
||||
<Toggle v-model="form.openai_advanced_scheduler_enabled" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.openai_advanced_scheduler_enabled"
|
||||
class="flex items-center justify-between border-t border-gray-100 pt-5 dark:border-dark-700"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.openaiExperimentalScheduler.stickyWeightedTitle") }}
|
||||
</label>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t("admin.settings.openaiExperimentalScheduler.stickyWeightedDescription")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.openai_advanced_scheduler_sticky_weighted_enabled" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.openai_advanced_scheduler_enabled"
|
||||
class="flex items-center justify-between border-t border-gray-100 pt-5 dark:border-dark-700"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.openaiExperimentalScheduler.subscriptionPriorityTitle") }}
|
||||
</label>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t("admin.settings.openaiExperimentalScheduler.subscriptionPriorityDescription")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle v-model="form.openai_advanced_scheduler_subscription_priority_enabled" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.openai_advanced_scheduler_enabled"
|
||||
class="border-t border-gray-100 pt-5 dark:border-dark-700"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.openaiExperimentalScheduler.weightsTitle") }}
|
||||
</label>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t("admin.settings.openaiExperimentalScheduler.weightsDescription")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
<label
|
||||
v-for="field in openAIAdvancedSchedulerWeightFields"
|
||||
:key="field.key"
|
||||
class="block"
|
||||
>
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ field.label }}
|
||||
</span>
|
||||
<input
|
||||
v-model="form[field.key]"
|
||||
class="input mt-1"
|
||||
inputmode="decimal"
|
||||
:placeholder="field.placeholder"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7903,6 +7978,18 @@ type SettingsForm = Omit<
|
||||
google_oauth_client_secret: string;
|
||||
force_email_on_third_party_signup: 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;
|
||||
// 系统全局平台限额 map;form 内始终归一化为全 4 平台对象(模板非空绑定依赖此不变量)
|
||||
default_platform_quotas: DefaultPlatformQuotasMap;
|
||||
};
|
||||
@@ -8094,6 +8181,18 @@ const form = reactive<SettingsForm>({
|
||||
// 分组隔离
|
||||
allow_ungrouped_key_scheduling: 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: "",
|
||||
// Gateway forwarding behavior
|
||||
enable_fingerprint_unification: true,
|
||||
enable_metadata_passthrough: false,
|
||||
@@ -8131,6 +8230,103 @@ const form = reactive<SettingsForm>({
|
||||
allow_user_view_error_requests: false,
|
||||
});
|
||||
|
||||
type OpenAIAdvancedSchedulerOverrideKey =
|
||||
| "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";
|
||||
|
||||
type OpenAIAdvancedSchedulerEffectiveKey =
|
||||
| "openai_advanced_scheduler_effective_lb_top_k"
|
||||
| "openai_advanced_scheduler_effective_weight_priority"
|
||||
| "openai_advanced_scheduler_effective_weight_load"
|
||||
| "openai_advanced_scheduler_effective_weight_queue"
|
||||
| "openai_advanced_scheduler_effective_weight_error_rate"
|
||||
| "openai_advanced_scheduler_effective_weight_ttft"
|
||||
| "openai_advanced_scheduler_effective_weight_reset"
|
||||
| "openai_advanced_scheduler_effective_weight_quota_headroom"
|
||||
| "openai_advanced_scheduler_effective_weight_previous_response"
|
||||
| "openai_advanced_scheduler_effective_weight_session_sticky";
|
||||
|
||||
const openAIAdvancedSchedulerWeightFields = computed<
|
||||
Array<{
|
||||
key: OpenAIAdvancedSchedulerOverrideKey;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
}>
|
||||
>(() => {
|
||||
const placeholder = (
|
||||
effectiveKey: OpenAIAdvancedSchedulerEffectiveKey,
|
||||
fallbackValue: string,
|
||||
) => {
|
||||
const effectiveValue = String(
|
||||
(form as Record<string, unknown>)[effectiveKey] ?? "",
|
||||
).trim();
|
||||
return t("admin.settings.openaiExperimentalScheduler.defaultPlaceholder", {
|
||||
value: effectiveValue || fallbackValue,
|
||||
});
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
key: "openai_advanced_scheduler_lb_top_k",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.topKLabel"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_lb_top_k", "7"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_priority",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.priorityWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_priority", "1"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_load",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.loadWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_load", "1"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_queue",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.queueWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_queue", "0.7"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_error_rate",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.errorRateWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_error_rate", "0.8"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_ttft",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.ttftWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_ttft", "0.5"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_reset",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.resetWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_reset", "0"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_quota_headroom",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.quotaHeadroomWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_quota_headroom", "0"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_previous_response",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.previousResponseWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_previous_response", "5"),
|
||||
},
|
||||
{
|
||||
key: "openai_advanced_scheduler_weight_session_sticky",
|
||||
label: t("admin.settings.openaiExperimentalScheduler.sessionStickyWeight"),
|
||||
placeholder: placeholder("openai_advanced_scheduler_effective_weight_session_sticky", "3"),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const authSourceDefaults = reactive<AuthSourceDefaultsState>(
|
||||
buildAuthSourceDefaultsState({}),
|
||||
);
|
||||
@@ -9359,6 +9555,30 @@ async function saveSettings() {
|
||||
form.payment_cancel_rate_limit_window_mode,
|
||||
payment_alipay_force_qrcode: form.payment_alipay_force_qrcode,
|
||||
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
|
||||
openai_advanced_scheduler_sticky_weighted_enabled:
|
||||
form.openai_advanced_scheduler_sticky_weighted_enabled,
|
||||
openai_advanced_scheduler_subscription_priority_enabled:
|
||||
form.openai_advanced_scheduler_subscription_priority_enabled,
|
||||
openai_advanced_scheduler_lb_top_k:
|
||||
form.openai_advanced_scheduler_lb_top_k.trim(),
|
||||
openai_advanced_scheduler_weight_priority:
|
||||
form.openai_advanced_scheduler_weight_priority.trim(),
|
||||
openai_advanced_scheduler_weight_load:
|
||||
form.openai_advanced_scheduler_weight_load.trim(),
|
||||
openai_advanced_scheduler_weight_queue:
|
||||
form.openai_advanced_scheduler_weight_queue.trim(),
|
||||
openai_advanced_scheduler_weight_error_rate:
|
||||
form.openai_advanced_scheduler_weight_error_rate.trim(),
|
||||
openai_advanced_scheduler_weight_ttft:
|
||||
form.openai_advanced_scheduler_weight_ttft.trim(),
|
||||
openai_advanced_scheduler_weight_reset:
|
||||
form.openai_advanced_scheduler_weight_reset.trim(),
|
||||
openai_advanced_scheduler_weight_quota_headroom:
|
||||
form.openai_advanced_scheduler_weight_quota_headroom.trim(),
|
||||
openai_advanced_scheduler_weight_previous_response:
|
||||
form.openai_advanced_scheduler_weight_previous_response.trim(),
|
||||
openai_advanced_scheduler_weight_session_sticky:
|
||||
form.openai_advanced_scheduler_weight_session_sticky.trim(),
|
||||
// 余额、订阅到期与账号限额通知
|
||||
balance_low_notify_enabled: form.balance_low_notify_enabled,
|
||||
balance_low_notify_threshold:
|
||||
|
||||
@@ -161,6 +161,23 @@ vi.mock("vue-i18n", async () => {
|
||||
"admin.settings.payment.findProvider": "查看支持的支付方式",
|
||||
"admin.settings.openaiExperimentalScheduler.title": "OpenAI 实验调度策略",
|
||||
"admin.settings.openaiExperimentalScheduler.description": "默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。",
|
||||
"admin.settings.openaiExperimentalScheduler.stickyWeightedTitle": "粘性加权",
|
||||
"admin.settings.openaiExperimentalScheduler.stickyWeightedDescription": "开启后 previous_response_id 和 session_hash 粘性进入高级调度打分;关闭时仍按旧逻辑硬命中粘性账号。",
|
||||
"admin.settings.openaiExperimentalScheduler.subscriptionPriorityTitle": "订阅优先",
|
||||
"admin.settings.openaiExperimentalScheduler.subscriptionPriorityDescription": "开启后先在 ChatGPT 订阅账号池中按权值选取;订阅池拿不到席位时再回退到非订阅账号池。",
|
||||
"admin.settings.openaiExperimentalScheduler.weightsTitle": "调度权值覆盖",
|
||||
"admin.settings.openaiExperimentalScheduler.weightsDescription": "留空时使用配置/环境变量值;配置未设置时使用内置默认值。页面非空设置优先。",
|
||||
"admin.settings.openaiExperimentalScheduler.defaultPlaceholder": "配置/默认:{value}",
|
||||
"admin.settings.openaiExperimentalScheduler.topKLabel": "TopK",
|
||||
"admin.settings.openaiExperimentalScheduler.priorityWeight": "优先级",
|
||||
"admin.settings.openaiExperimentalScheduler.loadWeight": "负载",
|
||||
"admin.settings.openaiExperimentalScheduler.queueWeight": "排队",
|
||||
"admin.settings.openaiExperimentalScheduler.errorRateWeight": "错误率",
|
||||
"admin.settings.openaiExperimentalScheduler.ttftWeight": "首包延迟",
|
||||
"admin.settings.openaiExperimentalScheduler.resetWeight": "重置窗口",
|
||||
"admin.settings.openaiExperimentalScheduler.quotaHeadroomWeight": "额度余量",
|
||||
"admin.settings.openaiExperimentalScheduler.previousResponseWeight": "previous_response 粘性",
|
||||
"admin.settings.openaiExperimentalScheduler.sessionStickyWeight": "session_hash 粘性",
|
||||
"admin.settings.site.uploadImage": "上传图片",
|
||||
"admin.settings.site.remove": "移除",
|
||||
"admin.settings.platformQuota.platform": "平台",
|
||||
@@ -411,6 +428,28 @@ const baseSettingsResponse = {
|
||||
payment_visible_method_alipay_enabled: true,
|
||||
payment_visible_method_wxpay_enabled: true,
|
||||
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",
|
||||
balance_low_notify_enabled: false,
|
||||
balance_low_notify_threshold: 0,
|
||||
balance_low_notify_recharge_url: "",
|
||||
|
||||
Reference in New Issue
Block a user