fix(codex): support dynamic quota windows

This commit is contained in:
elky
2026-07-17 20:18:04 +08:00
parent 664c063a06
commit f65ed2795c
22 changed files with 1234 additions and 365 deletions
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest'
import { getCodexQuotaWindowPresentation } from '../codexQuotaWindow'
describe('getCodexQuotaWindowPresentation', () => {
it.each([
[300, '5H'],
[10_080, '周'],
[43_200, '月'],
[43_800, '月'],
[44_640, '月'],
])('labels a %i-minute window as %s', (windowMinutes, expectedLabel) => {
expect(getCodexQuotaWindowPresentation({
code: 'primary',
window_minutes: windowMinutes,
})?.label).toBe(expectedLabel)
})
it('supports simultaneous 5H and weekly windows', () => {
const windows = [
getCodexQuotaWindowPresentation({ code: 'secondary', window_minutes: 10_080 }),
getCodexQuotaWindowPresentation({ code: 'primary', window_minutes: 300 }),
].filter((item): item is NonNullable<typeof item> => item != null)
expect(windows.sort((a, b) => a.sortOrder - b.sortOrder).map(item => item.label)).toEqual(['5H', '周'])
})
it('drops zero-minute placeholder windows', () => {
expect(getCodexQuotaWindowPresentation({
code: 'weekly',
label: '周',
window_minutes: 0,
})).toBeNull()
})
it('keeps legacy labels when old snapshots have no window duration', () => {
expect(getCodexQuotaWindowPresentation({ code: '5h' })?.label).toBe('5H')
expect(getCodexQuotaWindowPresentation({ code: 'weekly' })?.label).toBe('周')
})
})
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { formatOAuthPlanType } from '../oauthPlanType'
describe('formatOAuthPlanType', () => {
it('uses the compact Codex label for the usage-based business plan', () => {
expect(formatOAuthPlanType('self_serve_business_usage_based')).toBe('Codex')
expect(formatOAuthPlanType(' SELF_SERVE_BUSINESS_USAGE_BASED ')).toBe('Codex')
})
it('keeps existing known plan labels intact', () => {
expect(formatOAuthPlanType('plus')).toBe('Plus')
expect(formatOAuthPlanType('team')).toBe('Team')
})
})
@@ -43,6 +43,34 @@ describe('providerKeyQuota', () => {
}, 'codex')).toBe('周剩余 90.0% | 5H剩余 80.0% | Spark5H剩余 60.0% | Spark周剩余 95.0%')
})
it('uses actual Codex window durations and ignores zero placeholders', () => {
expect(getQuotaDisplayText({
status_snapshot: {
oauth: { code: 'valid' },
account: { code: 'ok', blocked: false },
quota: {
provider_type: 'codex',
code: 'ok',
exhausted: false,
windows: [
{
code: 'weekly',
label: '周',
window_minutes: 0,
remaining_ratio: 1,
},
{
code: '5h',
label: '5H',
window_minutes: 43_800,
remaining_ratio: 0.86,
},
],
},
},
}, 'codex')).toBe('月剩余 86.0%')
})
it('formats Grok account quota from structured quota windows', () => {
expect(getQuotaDisplayText({
status_snapshot: {
+62
View File
@@ -0,0 +1,62 @@
import type { QuotaWindowSnapshot } from '@/api/endpoints/types'
const MINUTES_PER_HOUR = 60
const MINUTES_PER_DAY = 24 * MINUTES_PER_HOUR
const MINUTES_PER_WEEK = 7 * MINUTES_PER_DAY
const MIN_MONTH_MINUTES = 28 * MINUTES_PER_DAY
const MAX_MONTH_MINUTES = 31 * MINUTES_PER_DAY
export interface CodexQuotaWindowPresentation {
label: string
sortOrder: number
}
function formatCodexQuotaPeriod(windowMinutes: number): string {
if (windowMinutes === 5 * MINUTES_PER_HOUR) return '5H'
if (windowMinutes === MINUTES_PER_WEEK) return '周'
if (windowMinutes >= MIN_MONTH_MINUTES && windowMinutes <= MAX_MONTH_MINUTES) return '月'
if (windowMinutes % MINUTES_PER_WEEK === 0) {
return `${windowMinutes / MINUTES_PER_WEEK}周`
}
if (windowMinutes % MINUTES_PER_DAY === 0) {
return `${windowMinutes / MINUTES_PER_DAY}天`
}
if (windowMinutes % MINUTES_PER_HOUR === 0) {
return `${windowMinutes / MINUTES_PER_HOUR}H`
}
return `${windowMinutes}分钟`
}
function getLegacyCodexQuotaPeriod(code: string, label: string): string | null {
if (code === '5h') return '5H'
if (code === 'weekly') return '周'
if (code === 'monthly') return '月'
return label || null
}
export function getCodexQuotaWindowPresentation(
window: QuotaWindowSnapshot,
): CodexQuotaWindowPresentation | null {
const code = String(window.code || '').trim().toLowerCase()
const isSpark = code.startsWith('spark_')
const baseCode = isSpark ? code.slice('spark_'.length) : code
const rawLabel = String(window.label || '').trim().replace(/^Spark\s*/i, '')
const hasExplicitWindowMinutes = window.window_minutes != null
const windowMinutes = Number(window.window_minutes)
if (hasExplicitWindowMinutes && (!Number.isFinite(windowMinutes) || windowMinutes <= 0)) {
return null
}
const period = hasExplicitWindowMinutes
? formatCodexQuotaPeriod(windowMinutes)
: getLegacyCodexQuotaPeriod(baseCode, rawLabel)
if (!period) return null
const fallbackOrder = baseCode === '5h' ? 300 : baseCode === 'weekly' ? 10_080 : 1_000_000
return {
label: isSpark ? `Spark${period}` : period,
sortOrder: (isSpark ? 10_000_000 : 0) + (hasExplicitWindowMinutes ? windowMinutes : fallbackOrder),
}
}
+1
View File
@@ -1,4 +1,5 @@
const PLAN_TYPE_LABELS: Record<string, string> = {
self_serve_business_usage_based: 'Codex',
free: 'Free',
plus: 'Plus',
team: 'Team',
+6 -9
View File
@@ -4,6 +4,7 @@ import type {
QuotaWindowSnapshot,
} from '@/api/endpoints/types/statusSnapshot'
import type { UpstreamMetadata } from '@/api/endpoints/types/provider'
import { getCodexQuotaWindowPresentation } from '@/utils/codexQuotaWindow'
export interface ProviderKeyQuotaCarrier {
account_quota?: string | null
@@ -222,15 +223,11 @@ function getGrokQuotaWindowLabel(window: QuotaWindowSnapshot): string {
function getCodexQuotaText(quota: QuotaStatusSnapshot): string | null {
const parts: string[] = []
for (const [label, code] of [
['周', 'weekly'],
['5H', '5h'],
['Spark5H', 'spark_5h'],
['Spark周', 'spark_weekly'],
] as const) {
const remainingPercent = getQuotaWindowRemainingPercent(getQuotaWindow(quota, code))
if (remainingPercent == null) continue
parts.push(`${label}剩余 ${formatPercent(remainingPercent)}`)
for (const window of getQuotaWindows(quota)) {
const presentation = getCodexQuotaWindowPresentation(window)
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (!presentation || remainingPercent == null) continue
parts.push(`${presentation.label}剩余 ${formatPercent(remainingPercent)}`)
}
if (parts.length > 0) return parts.join(' | ')