fix: 修复会话限制功能并在创建账号时支持配额控制

This commit is contained in:
shaw
2026-01-18 16:41:15 +08:00
parent 32fff3798c
commit a07174c191
2 changed files with 292 additions and 76 deletions

View File

@@ -410,11 +410,9 @@ func (s *GatewayService) SelectAccountForModelWithExclusions(ctx context.Context
} }
// SelectAccountWithLoadAwareness selects account with load-awareness and wait plan. // SelectAccountWithLoadAwareness selects account with load-awareness and wait plan.
// metadataUserID: 原始 metadata.user_id 字段(用于提取会话 UUID 进行会话数量限制) // metadataUserID: 已废弃参数,会话限制现在统一使用 sessionHash
func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, groupID *int64, sessionHash string, requestedModel string, excludedIDs map[int64]struct{}, metadataUserID string) (*AccountSelectionResult, error) { func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, groupID *int64, sessionHash string, requestedModel string, excludedIDs map[int64]struct{}, metadataUserID string) (*AccountSelectionResult, error) {
cfg := s.schedulingConfig() cfg := s.schedulingConfig()
// 提取会话 UUID用于会话数量限制
sessionUUID := extractSessionUUID(metadataUserID)
var stickyAccountID int64 var stickyAccountID int64
if sessionHash != "" && s.cache != nil { if sessionHash != "" && s.cache != nil {
@@ -440,41 +438,63 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
} }
if s.concurrencyService == nil || !cfg.LoadBatchEnabled { if s.concurrencyService == nil || !cfg.LoadBatchEnabled {
account, err := s.SelectAccountForModelWithExclusions(ctx, groupID, sessionHash, requestedModel, excludedIDs) // 复制排除列表,用于会话限制拒绝时的重试
if err != nil { localExcluded := make(map[int64]struct{})
return nil, err for k, v := range excludedIDs {
localExcluded[k] = v
} }
result, err := s.tryAcquireAccountSlot(ctx, account.ID, account.Concurrency)
if err == nil && result.Acquired { for {
return &AccountSelectionResult{ account, err := s.SelectAccountForModelWithExclusions(ctx, groupID, sessionHash, requestedModel, localExcluded)
Account: account, if err != nil {
Acquired: true, return nil, err
ReleaseFunc: result.ReleaseFunc, }
}, nil
} result, err := s.tryAcquireAccountSlot(ctx, account.ID, account.Concurrency)
if stickyAccountID > 0 && stickyAccountID == account.ID && s.concurrencyService != nil { if err == nil && result.Acquired {
waitingCount, _ := s.concurrencyService.GetAccountWaitingCount(ctx, account.ID) // 获取槽位后检查会话限制(使用 sessionHash 作为会话标识符)
if waitingCount < cfg.StickySessionMaxWaiting { if !s.checkAndRegisterSession(ctx, account, sessionHash) {
result.ReleaseFunc() // 释放槽位
localExcluded[account.ID] = struct{}{} // 排除此账号
continue // 重新选择
}
return &AccountSelectionResult{ return &AccountSelectionResult{
Account: account, Account: account,
WaitPlan: &AccountWaitPlan{ Acquired: true,
AccountID: account.ID, ReleaseFunc: result.ReleaseFunc,
MaxConcurrency: account.Concurrency,
Timeout: cfg.StickySessionWaitTimeout,
MaxWaiting: cfg.StickySessionMaxWaiting,
},
}, nil }, nil
} }
// 对于等待计划的情况,也需要先检查会话限制
if !s.checkAndRegisterSession(ctx, account, sessionHash) {
localExcluded[account.ID] = struct{}{}
continue
}
if stickyAccountID > 0 && stickyAccountID == account.ID && s.concurrencyService != nil {
waitingCount, _ := s.concurrencyService.GetAccountWaitingCount(ctx, account.ID)
if waitingCount < cfg.StickySessionMaxWaiting {
return &AccountSelectionResult{
Account: account,
WaitPlan: &AccountWaitPlan{
AccountID: account.ID,
MaxConcurrency: account.Concurrency,
Timeout: cfg.StickySessionWaitTimeout,
MaxWaiting: cfg.StickySessionMaxWaiting,
},
}, nil
}
}
return &AccountSelectionResult{
Account: account,
WaitPlan: &AccountWaitPlan{
AccountID: account.ID,
MaxConcurrency: account.Concurrency,
Timeout: cfg.FallbackWaitTimeout,
MaxWaiting: cfg.FallbackMaxWaiting,
},
}, nil
} }
return &AccountSelectionResult{
Account: account,
WaitPlan: &AccountWaitPlan{
AccountID: account.ID,
MaxConcurrency: account.Concurrency,
Timeout: cfg.FallbackWaitTimeout,
MaxWaiting: cfg.FallbackMaxWaiting,
},
}, nil
} }
platform, hasForcePlatform, err := s.resolvePlatform(ctx, groupID, group) platform, hasForcePlatform, err := s.resolvePlatform(ctx, groupID, group)
@@ -590,7 +610,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
result, err := s.tryAcquireAccountSlot(ctx, stickyAccountID, stickyAccount.Concurrency) result, err := s.tryAcquireAccountSlot(ctx, stickyAccountID, stickyAccount.Concurrency)
if err == nil && result.Acquired { if err == nil && result.Acquired {
// 会话数量限制检查 // 会话数量限制检查
if !s.checkAndRegisterSession(ctx, stickyAccount, sessionUUID) { if !s.checkAndRegisterSession(ctx, stickyAccount, sessionHash) {
result.ReleaseFunc() // 释放槽位 result.ReleaseFunc() // 释放槽位
// 继续到负载感知选择 // 继续到负载感知选择
} else { } else {
@@ -608,15 +628,20 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
waitingCount, _ := s.concurrencyService.GetAccountWaitingCount(ctx, stickyAccountID) waitingCount, _ := s.concurrencyService.GetAccountWaitingCount(ctx, stickyAccountID)
if waitingCount < cfg.StickySessionMaxWaiting { if waitingCount < cfg.StickySessionMaxWaiting {
return &AccountSelectionResult{ // 会话数量限制检查(等待计划也需要占用会话配额)
Account: stickyAccount, if !s.checkAndRegisterSession(ctx, stickyAccount, sessionHash) {
WaitPlan: &AccountWaitPlan{ // 会话限制已满,继续到负载感知选择
AccountID: stickyAccountID, } else {
MaxConcurrency: stickyAccount.Concurrency, return &AccountSelectionResult{
Timeout: cfg.StickySessionWaitTimeout, Account: stickyAccount,
MaxWaiting: cfg.StickySessionMaxWaiting, WaitPlan: &AccountWaitPlan{
}, AccountID: stickyAccountID,
}, nil MaxConcurrency: stickyAccount.Concurrency,
Timeout: cfg.StickySessionWaitTimeout,
MaxWaiting: cfg.StickySessionMaxWaiting,
},
}, nil
}
} }
// 粘性账号槽位满且等待队列已满,继续使用负载感知选择 // 粘性账号槽位满且等待队列已满,继续使用负载感知选择
} }
@@ -677,7 +702,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
result, err := s.tryAcquireAccountSlot(ctx, item.account.ID, item.account.Concurrency) result, err := s.tryAcquireAccountSlot(ctx, item.account.ID, item.account.Concurrency)
if err == nil && result.Acquired { if err == nil && result.Acquired {
// 会话数量限制检查 // 会话数量限制检查
if !s.checkAndRegisterSession(ctx, item.account, sessionUUID) { if !s.checkAndRegisterSession(ctx, item.account, sessionHash) {
result.ReleaseFunc() // 释放槽位,继续尝试下一个账号 result.ReleaseFunc() // 释放槽位,继续尝试下一个账号
continue continue
} }
@@ -695,20 +720,26 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
} }
} }
// 5. 所有路由账号槽位满,返回等待计划(选择负载最低的) // 5. 所有路由账号槽位满,尝试返回等待计划(选择负载最低的)
acc := routingAvailable[0].account // 遍历找到第一个满足会话限制的账号
if s.debugModelRoutingEnabled() { for _, item := range routingAvailable {
log.Printf("[ModelRoutingDebug] routed wait: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), acc.ID) if !s.checkAndRegisterSession(ctx, item.account, sessionHash) {
continue // 会话限制已满,尝试下一个
}
if s.debugModelRoutingEnabled() {
log.Printf("[ModelRoutingDebug] routed wait: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), item.account.ID)
}
return &AccountSelectionResult{
Account: item.account,
WaitPlan: &AccountWaitPlan{
AccountID: item.account.ID,
MaxConcurrency: item.account.Concurrency,
Timeout: cfg.StickySessionWaitTimeout,
MaxWaiting: cfg.StickySessionMaxWaiting,
},
}, nil
} }
return &AccountSelectionResult{ // 所有路由账号会话限制都已满,继续到 Layer 2 回退
Account: acc,
WaitPlan: &AccountWaitPlan{
AccountID: acc.ID,
MaxConcurrency: acc.Concurrency,
Timeout: cfg.StickySessionWaitTimeout,
MaxWaiting: cfg.StickySessionMaxWaiting,
},
}, nil
} }
// 路由列表中的账号都不可用(负载率 >= 100继续到 Layer 2 回退 // 路由列表中的账号都不可用(负载率 >= 100继续到 Layer 2 回退
log.Printf("[ModelRouting] All routed accounts unavailable for model=%s, falling back to normal selection", requestedModel) log.Printf("[ModelRouting] All routed accounts unavailable for model=%s, falling back to normal selection", requestedModel)
@@ -728,7 +759,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
result, err := s.tryAcquireAccountSlot(ctx, accountID, account.Concurrency) result, err := s.tryAcquireAccountSlot(ctx, accountID, account.Concurrency)
if err == nil && result.Acquired { if err == nil && result.Acquired {
// 会话数量限制检查 // 会话数量限制检查
if !s.checkAndRegisterSession(ctx, account, sessionUUID) { if !s.checkAndRegisterSession(ctx, account, sessionHash) {
result.ReleaseFunc() // 释放槽位,继续到 Layer 2 result.ReleaseFunc() // 释放槽位,继续到 Layer 2
} else { } else {
_ = s.cache.RefreshSessionTTL(ctx, derefGroupID(groupID), sessionHash, stickySessionTTL) _ = s.cache.RefreshSessionTTL(ctx, derefGroupID(groupID), sessionHash, stickySessionTTL)
@@ -742,15 +773,20 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
waitingCount, _ := s.concurrencyService.GetAccountWaitingCount(ctx, accountID) waitingCount, _ := s.concurrencyService.GetAccountWaitingCount(ctx, accountID)
if waitingCount < cfg.StickySessionMaxWaiting { if waitingCount < cfg.StickySessionMaxWaiting {
return &AccountSelectionResult{ // 会话数量限制检查(等待计划也需要占用会话配额)
Account: account, if !s.checkAndRegisterSession(ctx, account, sessionHash) {
WaitPlan: &AccountWaitPlan{ // 会话限制已满,继续到 Layer 2
AccountID: accountID, } else {
MaxConcurrency: account.Concurrency, return &AccountSelectionResult{
Timeout: cfg.StickySessionWaitTimeout, Account: account,
MaxWaiting: cfg.StickySessionMaxWaiting, WaitPlan: &AccountWaitPlan{
}, AccountID: accountID,
}, nil MaxConcurrency: account.Concurrency,
Timeout: cfg.StickySessionWaitTimeout,
MaxWaiting: cfg.StickySessionMaxWaiting,
},
}, nil
}
} }
} }
} }
@@ -799,7 +835,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
loadMap, err := s.concurrencyService.GetAccountsLoadBatch(ctx, accountLoads) loadMap, err := s.concurrencyService.GetAccountsLoadBatch(ctx, accountLoads)
if err != nil { if err != nil {
if result, ok := s.tryAcquireByLegacyOrder(ctx, candidates, groupID, sessionHash, preferOAuth, sessionUUID); ok { if result, ok := s.tryAcquireByLegacyOrder(ctx, candidates, groupID, sessionHash, preferOAuth); ok {
return result, nil return result, nil
} }
} else { } else {
@@ -849,7 +885,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
result, err := s.tryAcquireAccountSlot(ctx, item.account.ID, item.account.Concurrency) result, err := s.tryAcquireAccountSlot(ctx, item.account.ID, item.account.Concurrency)
if err == nil && result.Acquired { if err == nil && result.Acquired {
// 会话数量限制检查 // 会话数量限制检查
if !s.checkAndRegisterSession(ctx, item.account, sessionUUID) { if !s.checkAndRegisterSession(ctx, item.account, sessionHash) {
result.ReleaseFunc() // 释放槽位,继续尝试下一个账号 result.ReleaseFunc() // 释放槽位,继续尝试下一个账号
continue continue
} }
@@ -869,6 +905,10 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
// ============ Layer 3: 兜底排队 ============ // ============ Layer 3: 兜底排队 ============
sortAccountsByPriorityAndLastUsed(candidates, preferOAuth) sortAccountsByPriorityAndLastUsed(candidates, preferOAuth)
for _, acc := range candidates { for _, acc := range candidates {
// 会话数量限制检查(等待计划也需要占用会话配额)
if !s.checkAndRegisterSession(ctx, acc, sessionHash) {
continue // 会话限制已满,尝试下一个账号
}
return &AccountSelectionResult{ return &AccountSelectionResult{
Account: acc, Account: acc,
WaitPlan: &AccountWaitPlan{ WaitPlan: &AccountWaitPlan{
@@ -882,7 +922,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
return nil, errors.New("no available accounts") return nil, errors.New("no available accounts")
} }
func (s *GatewayService) tryAcquireByLegacyOrder(ctx context.Context, candidates []*Account, groupID *int64, sessionHash string, preferOAuth bool, sessionUUID string) (*AccountSelectionResult, bool) { func (s *GatewayService) tryAcquireByLegacyOrder(ctx context.Context, candidates []*Account, groupID *int64, sessionHash string, preferOAuth bool) (*AccountSelectionResult, bool) {
ordered := append([]*Account(nil), candidates...) ordered := append([]*Account(nil), candidates...)
sortAccountsByPriorityAndLastUsed(ordered, preferOAuth) sortAccountsByPriorityAndLastUsed(ordered, preferOAuth)
@@ -890,7 +930,7 @@ func (s *GatewayService) tryAcquireByLegacyOrder(ctx context.Context, candidates
result, err := s.tryAcquireAccountSlot(ctx, acc.ID, acc.Concurrency) result, err := s.tryAcquireAccountSlot(ctx, acc.ID, acc.Concurrency)
if err == nil && result.Acquired { if err == nil && result.Acquired {
// 会话数量限制检查 // 会话数量限制检查
if !s.checkAndRegisterSession(ctx, acc, sessionUUID) { if !s.checkAndRegisterSession(ctx, acc, sessionHash) {
result.ReleaseFunc() // 释放槽位,继续尝试下一个账号 result.ReleaseFunc() // 释放槽位,继续尝试下一个账号
continue continue
} }
@@ -1188,15 +1228,16 @@ checkSchedulability:
// checkAndRegisterSession 检查并注册会话,用于会话数量限制 // checkAndRegisterSession 检查并注册会话,用于会话数量限制
// 仅适用于 Anthropic OAuth/SetupToken 账号 // 仅适用于 Anthropic OAuth/SetupToken 账号
// sessionID: 会话标识符(使用粘性会话的 hash
// 返回 true 表示允许在限制内或会话已存在false 表示拒绝(超出限制且是新会话) // 返回 true 表示允许在限制内或会话已存在false 表示拒绝(超出限制且是新会话)
func (s *GatewayService) checkAndRegisterSession(ctx context.Context, account *Account, sessionUUID string) bool { func (s *GatewayService) checkAndRegisterSession(ctx context.Context, account *Account, sessionID string) bool {
// 只检查 Anthropic OAuth/SetupToken 账号 // 只检查 Anthropic OAuth/SetupToken 账号
if !account.IsAnthropicOAuthOrSetupToken() { if !account.IsAnthropicOAuthOrSetupToken() {
return true return true
} }
maxSessions := account.GetMaxSessions() maxSessions := account.GetMaxSessions()
if maxSessions <= 0 || sessionUUID == "" { if maxSessions <= 0 || sessionID == "" {
return true // 未启用会话限制或无会话ID return true // 未启用会话限制或无会话ID
} }
@@ -1206,7 +1247,7 @@ func (s *GatewayService) checkAndRegisterSession(ctx context.Context, account *A
idleTimeout := time.Duration(account.GetSessionIdleTimeoutMinutes()) * time.Minute idleTimeout := time.Duration(account.GetSessionIdleTimeoutMinutes()) * time.Minute
allowed, err := s.sessionLimitCache.RegisterSession(ctx, account.ID, sessionUUID, maxSessions, idleTimeout) allowed, err := s.sessionLimitCache.RegisterSession(ctx, account.ID, sessionID, maxSessions, idleTimeout)
if err != nil { if err != nil {
// 失败开放:缓存错误时允许通过 // 失败开放:缓存错误时允许通过
return true return true

View File

@@ -1191,6 +1191,136 @@
</div> </div>
</div> </div>
<!-- Quota Control Section (Anthropic OAuth/SetupToken only) -->
<div
v-if="form.platform === 'anthropic' && accountCategory === 'oauth-based'"
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
>
<div class="mb-3">
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaControl.title') }}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaControl.hint') }}
</p>
</div>
<!-- Window Cost Limit -->
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.quotaControl.windowCost.label') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaControl.windowCost.hint') }}
</p>
</div>
<button
type="button"
@click="windowCostEnabled = !windowCostEnabled"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
windowCostEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
windowCostEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button>
</div>
<div v-if="windowCostEnabled" class="grid grid-cols-2 gap-4">
<div>
<label class="input-label">{{ t('admin.accounts.quotaControl.windowCost.limit') }}</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400">$</span>
<input
v-model.number="windowCostLimit"
type="number"
min="0"
step="1"
class="input pl-7"
:placeholder="t('admin.accounts.quotaControl.windowCost.limitPlaceholder')"
/>
</div>
<p class="input-hint">{{ t('admin.accounts.quotaControl.windowCost.limitHint') }}</p>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.quotaControl.windowCost.stickyReserve') }}</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400">$</span>
<input
v-model.number="windowCostStickyReserve"
type="number"
min="0"
step="1"
class="input pl-7"
:placeholder="t('admin.accounts.quotaControl.windowCost.stickyReservePlaceholder')"
/>
</div>
<p class="input-hint">{{ t('admin.accounts.quotaControl.windowCost.stickyReserveHint') }}</p>
</div>
</div>
</div>
<!-- Session Limit -->
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.quotaControl.sessionLimit.label') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.quotaControl.sessionLimit.hint') }}
</p>
</div>
<button
type="button"
@click="sessionLimitEnabled = !sessionLimitEnabled"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
sessionLimitEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
sessionLimitEnabled ? 'translate-x-5' : 'translate-x-0'
]"
/>
</button>
</div>
<div v-if="sessionLimitEnabled" class="grid grid-cols-2 gap-4">
<div>
<label class="input-label">{{ t('admin.accounts.quotaControl.sessionLimit.maxSessions') }}</label>
<input
v-model.number="maxSessions"
type="number"
min="1"
step="1"
class="input"
:placeholder="t('admin.accounts.quotaControl.sessionLimit.maxSessionsPlaceholder')"
/>
<p class="input-hint">{{ t('admin.accounts.quotaControl.sessionLimit.maxSessionsHint') }}</p>
</div>
<div>
<label class="input-label">{{ t('admin.accounts.quotaControl.sessionLimit.idleTimeout') }}</label>
<div class="relative">
<input
v-model.number="sessionIdleTimeout"
type="number"
min="1"
step="1"
class="input pr-12"
:placeholder="t('admin.accounts.quotaControl.sessionLimit.idleTimeoutPlaceholder')"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400">{{ t('common.minutes') }}</span>
</div>
<p class="input-hint">{{ t('admin.accounts.quotaControl.sessionLimit.idleTimeoutHint') }}</p>
</div>
</div>
</div>
</div>
<div> <div>
<label class="input-label">{{ t('admin.accounts.proxy') }}</label> <label class="input-label">{{ t('admin.accounts.proxy') }}</label>
<ProxySelector v-model="form.proxy_id" :proxies="proxies" /> <ProxySelector v-model="form.proxy_id" :proxies="proxies" />
@@ -1763,6 +1893,14 @@ const geminiAIStudioOAuthEnabled = ref(false)
const showAdvancedOAuth = ref(false) const showAdvancedOAuth = ref(false)
const showGeminiHelpDialog = ref(false) const showGeminiHelpDialog = ref(false)
// Quota control state (Anthropic OAuth/SetupToken only)
const windowCostEnabled = ref(false)
const windowCostLimit = ref<number | null>(null)
const windowCostStickyReserve = ref<number | null>(null)
const sessionLimitEnabled = ref(false)
const maxSessions = ref<number | null>(null)
const sessionIdleTimeout = ref<number | null>(null)
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails) // Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
const geminiTierGoogleOne = ref<'google_one_free' | 'google_ai_pro' | 'google_ai_ultra'>('google_one_free') const geminiTierGoogleOne = ref<'google_one_free' | 'google_ai_pro' | 'google_ai_ultra'>('google_one_free')
const geminiTierGcp = ref<'gcp_standard' | 'gcp_enterprise'>('gcp_standard') const geminiTierGcp = ref<'gcp_standard' | 'gcp_enterprise'>('gcp_standard')
@@ -2140,6 +2278,13 @@ const resetForm = () => {
customErrorCodeInput.value = null customErrorCodeInput.value = null
interceptWarmupRequests.value = false interceptWarmupRequests.value = false
autoPauseOnExpired.value = true autoPauseOnExpired.value = true
// Reset quota control state
windowCostEnabled.value = false
windowCostLimit.value = null
windowCostStickyReserve.value = null
sessionLimitEnabled.value = false
maxSessions.value = null
sessionIdleTimeout.value = null
tempUnschedEnabled.value = false tempUnschedEnabled.value = false
tempUnschedRules.value = [] tempUnschedRules.value = []
geminiOAuthType.value = 'code_assist' geminiOAuthType.value = 'code_assist'
@@ -2407,7 +2552,22 @@ const handleAnthropicExchange = async (authCode: string) => {
...proxyConfig ...proxyConfig
}) })
const extra = oauth.buildExtraInfo(tokenInfo) // Build extra with quota control settings
const baseExtra = oauth.buildExtraInfo(tokenInfo) || {}
const extra: Record<string, unknown> = { ...baseExtra }
// Add window cost limit settings
if (windowCostEnabled.value && windowCostLimit.value != null && windowCostLimit.value > 0) {
extra.window_cost_limit = windowCostLimit.value
extra.window_cost_sticky_reserve = windowCostStickyReserve.value ?? 10
}
// Add session limit settings
if (sessionLimitEnabled.value && maxSessions.value != null && maxSessions.value > 0) {
extra.max_sessions = maxSessions.value
extra.session_idle_timeout_minutes = sessionIdleTimeout.value ?? 5
}
const credentials = { const credentials = {
...tokenInfo, ...tokenInfo,
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {}) ...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
@@ -2475,7 +2635,22 @@ const handleCookieAuth = async (sessionKey: string) => {
...proxyConfig ...proxyConfig
}) })
const extra = oauth.buildExtraInfo(tokenInfo) // Build extra with quota control settings
const baseExtra = oauth.buildExtraInfo(tokenInfo) || {}
const extra: Record<string, unknown> = { ...baseExtra }
// Add window cost limit settings
if (windowCostEnabled.value && windowCostLimit.value != null && windowCostLimit.value > 0) {
extra.window_cost_limit = windowCostLimit.value
extra.window_cost_sticky_reserve = windowCostStickyReserve.value ?? 10
}
// Add session limit settings
if (sessionLimitEnabled.value && maxSessions.value != null && maxSessions.value > 0) {
extra.max_sessions = maxSessions.value
extra.session_idle_timeout_minutes = sessionIdleTimeout.value ?? 5
}
const accountName = keys.length > 1 ? `${form.name} #${i + 1}` : form.name const accountName = keys.length > 1 ? `${form.name} #${i + 1}` : form.name
// Merge interceptWarmupRequests into credentials // Merge interceptWarmupRequests into credentials