mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(anthropic): treat 7d_oi (Fable) window 429 as model-level rate limit
Anthropic OAuth/setup-token accounts now expose a Fable-only 7d window in 429 responses (anthropic-ratelimit-unified-7d_oi-*). Previously, a 429 triggered solely by this window fell through to the legacy path and marked the whole account rate limited (picking the sooner 5h/7d reset) even though 5h/7d were still "allowed". Backend: - HandleUpstreamError: when only 7d_oi is rejected/exceeded, set a model-level rate limit (scope "claude-fable-5") and skip account-level SetRateLimited / session-window rewrite / temp-unsched entirely. Real 5h/7d exhaustion keeps the exact legacy account-level behavior. - modelRateLimitKeysForRequest: append the Fable family scope for any mapped Anthropic model containing "fable", so claude-fable-5[1m] and other variants are excluded from scheduling together. - Passive sampling: collect 7d_oi utilization/reset into Extra (passive_usage_7d_oi_*), including from the 429 response itself so the usage bar doesn't freeze at a stale value during the limit window; cleared on 5h window reset like the existing 7d keys. - Usage API: new seven_day_fable window in UsageInfo. GetPassiveUsage builds it from passive samples; active queries parse the upstream seven_day_overage_included field (claim name from the representative-claim header) and fall back to passive data when the upstream usage API doesn't return it, so the active-query button keeps the bar populated. seven_day_sonnet handling is preserved unchanged. Frontend: - /admin/accounts usage cell renders a "7d F" (amber) progress bar next to the existing 5h / 7d / 7d S bars for Anthropic OAuth/setup-token accounts.
This commit is contained in:
@@ -68,6 +68,15 @@
|
||||
color="purple"
|
||||
/>
|
||||
|
||||
<!-- 7d Fable Window (7d_oi) -->
|
||||
<UsageProgressBar
|
||||
v-if="usageInfo.seven_day_fable"
|
||||
label="7d F"
|
||||
:utilization="usageInfo.seven_day_fable.utilization"
|
||||
:resets-at="usageInfo.seven_day_fable.resets_at"
|
||||
color="amber"
|
||||
/>
|
||||
|
||||
<!-- Passive sampling label + active query button -->
|
||||
<div class="flex items-center gap-1.5 mt-0.5">
|
||||
<span
|
||||
|
||||
@@ -655,4 +655,102 @@ describe('AccountUsageCell', () => {
|
||||
expect(wrapper.text()).toContain('A $0.00')
|
||||
expect(wrapper.text()).toContain('U $0.00')
|
||||
})
|
||||
|
||||
it('Anthropic OAuth 会渲染 7d F (Fable) 进度条,且 7d S 逻辑保留', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
source: 'passive',
|
||||
five_hour: {
|
||||
utilization: 41,
|
||||
resets_at: '2026-07-03T10:00:00Z',
|
||||
remaining_seconds: 3600
|
||||
},
|
||||
seven_day: {
|
||||
utilization: 56,
|
||||
resets_at: '2026-07-06T22:00:00Z',
|
||||
remaining_seconds: 300000
|
||||
},
|
||||
seven_day_sonnet: {
|
||||
utilization: 30,
|
||||
resets_at: '2026-07-06T22:00:00Z',
|
||||
remaining_seconds: 300000
|
||||
},
|
||||
seven_day_fable: {
|
||||
utilization: 100,
|
||||
resets_at: '2026-07-06T22:00:00Z',
|
||||
remaining_seconds: 300000
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
id: 3001,
|
||||
platform: 'anthropic',
|
||||
type: 'oauth',
|
||||
extra: {}
|
||||
})
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization', 'resetsAt', 'color'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('5h|41')
|
||||
expect(wrapper.text()).toContain('7d|56')
|
||||
expect(wrapper.text()).toContain('7d S|30')
|
||||
expect(wrapper.text()).toContain('7d F|100')
|
||||
})
|
||||
|
||||
it('Anthropic OAuth 无 Fable 数据时不渲染 7d F 进度条', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
source: 'passive',
|
||||
five_hour: {
|
||||
utilization: 41,
|
||||
resets_at: '2026-07-03T10:00:00Z',
|
||||
remaining_seconds: 3600
|
||||
},
|
||||
seven_day: {
|
||||
utilization: 56,
|
||||
resets_at: '2026-07-06T22:00:00Z',
|
||||
remaining_seconds: 300000
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({
|
||||
id: 3002,
|
||||
platform: 'anthropic',
|
||||
type: 'oauth',
|
||||
extra: {}
|
||||
})
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization', 'resetsAt', 'color'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('5h|41')
|
||||
expect(wrapper.text()).toContain('7d|56')
|
||||
expect(wrapper.text()).not.toContain('7d S')
|
||||
expect(wrapper.text()).not.toContain('7d F')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -984,6 +984,7 @@ export interface AccountUsageInfo {
|
||||
five_hour: UsageProgress | null
|
||||
seven_day: UsageProgress | null
|
||||
seven_day_sonnet: UsageProgress | null
|
||||
seven_day_fable?: UsageProgress | null
|
||||
gemini_shared_daily?: UsageProgress | null
|
||||
gemini_pro_daily?: UsageProgress | null
|
||||
gemini_flash_daily?: UsageProgress | null
|
||||
|
||||
Reference in New Issue
Block a user