mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
fix(codex): support dynamic quota windows
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
const PLAN_TYPE_LABELS: Record<string, string> = {
|
||||
self_serve_business_usage_based: 'Codex',
|
||||
free: 'Free',
|
||||
plus: 'Plus',
|
||||
team: 'Team',
|
||||
|
||||
@@ -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(' | ')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user