mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(account): address second-round review on quota auto-pause
- TopK initial filter now drops quota-paused accounts: fold the quota check into isAccountRequestCompatible so session-hash, TopK pool, and per-candidate rechecks all skip paused accounts. Previously the candidate pool was built without the quota check, so paused accounts could fill TopK and leave the scheduler returning "no available accounts" even with healthy ones available. - Add per-account explicit disable flags auto_pause_5h_disabled / auto_pause_7d_disabled with toggles in EditAccountModal. Without these, leaving the account threshold blank silently falls back to the global default, so admins could not exempt a single account once a global default existed. Disable is per-window: an account can opt out of 5h auto-pause while still honoring 7d. Schedule snapshot whitelist includes the new fields, i18n EN/ZH updated, threshold-hint text revised to explain "blank = global default". - Move quota auto-pause settings off the request hot path: replace the per-repo TTL+singleflight sync DB read with a per-SettingService stale-while-revalidate in-memory snapshot. Get is non-blocking (atomic.Pointer load + async refresh on staleness); writes via UpdateOpsAdvancedSettings push directly into the cache through an injected sink; wire warms the cache at startup. Adds Warm (sync) for tests/init and SetOpenAIQuotaAutoPauseSettings (sink target). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
8b7a822706
commit
c9caadb378
@@ -1791,6 +1791,28 @@
|
||||
v-if="account?.platform === 'openai'"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.autoPause5hDisabled') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="autoPause5hDisabled = !autoPause5hDisabled"
|
||||
: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',
|
||||
autoPause5hDisabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
data-testid="auto-pause-5h-disabled"
|
||||
>
|
||||
<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',
|
||||
autoPause5hDisabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="input-hint">{{ t('admin.accounts.autoPauseDisabledHint') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.autoPause5hThreshold') }}</label>
|
||||
<input
|
||||
@@ -1800,10 +1822,33 @@
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
:disabled="autoPause5hDisabled"
|
||||
data-testid="auto-pause-5h-threshold"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.autoPauseThresholdHint') }}</p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.autoPause7dDisabled') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="autoPause7dDisabled = !autoPause7dDisabled"
|
||||
: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',
|
||||
autoPause7dDisabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
data-testid="auto-pause-7d-disabled"
|
||||
>
|
||||
<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',
|
||||
autoPause7dDisabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="input-hint">{{ t('admin.accounts.autoPauseDisabledHint') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.autoPause7dThreshold') }}</label>
|
||||
<input
|
||||
@@ -1813,6 +1858,7 @@
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
:disabled="autoPause7dDisabled"
|
||||
data-testid="auto-pause-7d-threshold"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.autoPauseThresholdHint') }}</p>
|
||||
@@ -2481,6 +2527,8 @@ const interceptWarmupRequests = ref(false)
|
||||
const autoPauseOnExpired = ref(false)
|
||||
const autoPause5hThreshold = ref<number | null>(null)
|
||||
const autoPause7dThreshold = ref<number | null>(null)
|
||||
const autoPause5hDisabled = ref(false)
|
||||
const autoPause7dDisabled = ref(false)
|
||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||
const allowOverages = ref(false) // For antigravity accounts: enable AI Credits overages
|
||||
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||
@@ -2901,6 +2949,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
allowOverages.value = extra?.allow_overages === true
|
||||
autoPause5hThreshold.value = typeof extra?.auto_pause_5h_threshold === 'number' ? extra.auto_pause_5h_threshold * 100 : null
|
||||
autoPause7dThreshold.value = typeof extra?.auto_pause_7d_threshold === 'number' ? extra.auto_pause_7d_threshold * 100 : null
|
||||
autoPause5hDisabled.value = extra?.auto_pause_5h_disabled === true
|
||||
autoPause7dDisabled.value = extra?.auto_pause_7d_disabled === true
|
||||
|
||||
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
|
||||
openaiPassthroughEnabled.value = false
|
||||
@@ -4064,6 +4114,16 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
delete newExtra.auto_pause_7d_threshold
|
||||
}
|
||||
if (autoPause5hDisabled.value) {
|
||||
newExtra.auto_pause_5h_disabled = true
|
||||
} else {
|
||||
delete newExtra.auto_pause_5h_disabled
|
||||
}
|
||||
if (autoPause7dDisabled.value) {
|
||||
newExtra.auto_pause_7d_disabled = true
|
||||
} else {
|
||||
delete newExtra.auto_pause_7d_disabled
|
||||
}
|
||||
|
||||
delete newExtra.codex_image_generation_bridge_enabled
|
||||
if (codexImageGenerationBridgeMode.value === 'inherit') {
|
||||
|
||||
@@ -352,6 +352,27 @@ describe('EditAccountModal', () => {
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.auto_pause_7d_threshold).toBe(0.96)
|
||||
})
|
||||
|
||||
it('submits OpenAI quota auto-pause disable flag in extra', async () => {
|
||||
// Toggling the per-account disable flag must persist as auto_pause_5h_disabled
|
||||
// so an admin can exempt one account from auto-pause even when a global default
|
||||
// threshold is configured (otherwise leaving the threshold blank would silently
|
||||
// fall back to the global default).
|
||||
const account = buildAccount()
|
||||
updateAccountMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||
updateAccountMock.mockResolvedValue(account)
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
|
||||
await wrapper.get('[data-testid="auto-pause-5h-disabled"]').trigger('click')
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.auto_pause_5h_disabled).toBe(true)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.auto_pause_7d_disabled).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps at least one OpenAI APIKey endpoint capability selected', async () => {
|
||||
const account = buildAccount()
|
||||
updateAccountMock.mockReset()
|
||||
|
||||
@@ -3477,7 +3477,10 @@ export default {
|
||||
autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires',
|
||||
autoPause5hThreshold: '5h Usage Threshold (%)',
|
||||
autoPause7dThreshold: '7d Usage Threshold (%)',
|
||||
autoPauseThresholdHint: 'Leave empty or set 0 to disable. Reaching the threshold only skips the account during scheduling and does not modify schedulable.',
|
||||
autoPauseThresholdHint: 'Leave empty or set 0 to use the global default threshold (configured in Ops settings); set a value to override the global default. Reaching the threshold only skips the account during scheduling and does not modify schedulable.',
|
||||
autoPause5hDisabled: 'Disable 5h auto-pause',
|
||||
autoPause7dDisabled: 'Disable 7d auto-pause',
|
||||
autoPauseDisabledHint: 'When enabled, this account is never auto-paused (even if a global default threshold is configured).',
|
||||
// Quota control (Anthropic OAuth/SetupToken only)
|
||||
quotaControl: {
|
||||
title: 'Quota Control',
|
||||
|
||||
@@ -3615,7 +3615,10 @@ export default {
|
||||
autoPauseOnExpiredDesc: '启用后,账号过期将自动暂停调度',
|
||||
autoPause5hThreshold: '5h 用量阈值(%)',
|
||||
autoPause7dThreshold: '7d 用量阈值(%)',
|
||||
autoPauseThresholdHint: '填 0 或留空表示不启用;达到阈值后仅在调度时跳过账号,不修改 schedulable。',
|
||||
autoPauseThresholdHint: '留空或填 0 表示使用全局默认阈值(在运维设置中配置);填具体值则覆盖全局默认。达到阈值后仅在调度时跳过账号,不修改 schedulable。',
|
||||
autoPause5hDisabled: '禁用 5h 自动暂停',
|
||||
autoPause7dDisabled: '禁用 7d 自动暂停',
|
||||
autoPauseDisabledHint: '开启后该账号永不进入自动暂停(即使全局默认阈值已配置)。',
|
||||
// Quota control (Anthropic OAuth/SetupToken only)
|
||||
quotaControl: {
|
||||
title: '配额控制',
|
||||
|
||||
Reference in New Issue
Block a user