mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
fix(codex): support dynamic quota windows
This commit is contained in:
@@ -135,10 +135,10 @@
|
||||
:title="row.statusBadgeTitle"
|
||||
>{{ row.statusBadgeLabel }}</Badge>
|
||||
<Badge
|
||||
v-if="row.key.oauth_plan_type"
|
||||
v-if="row.planLabel"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
>{{ row.key.oauth_plan_type }}</Badge>
|
||||
>{{ row.planLabel }}</Badge>
|
||||
<Badge
|
||||
v-if="row.oauthOrgBadge"
|
||||
variant="secondary"
|
||||
@@ -499,6 +499,7 @@ import { exportKey, refreshProviderQuota } from '@/api/endpoints/keys'
|
||||
import { refreshProviderOAuth } from '@/api/endpoints/provider_oauth'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
|
||||
import { formatOAuthPlanType } from '@/utils/oauthPlanType'
|
||||
import {
|
||||
canExportOAuthCredential,
|
||||
canRefreshOAuthCredential,
|
||||
@@ -552,6 +553,7 @@ type BatchActionOption = {
|
||||
|
||||
type PageKeyRow = {
|
||||
key: PoolKeyDetail
|
||||
planLabel: string
|
||||
authTypeLabel: string
|
||||
statusBadgeLabel: string | null
|
||||
statusBadgeTitle: string
|
||||
@@ -650,6 +652,7 @@ const pageKeyRows = computed<PageKeyRow[]>(() => pageKeys.value.map((key) => {
|
||||
|
||||
return {
|
||||
key,
|
||||
planLabel: formatOAuthPlanType(key.oauth_plan_type),
|
||||
authTypeLabel: normalizeAuthTypeLabel(key),
|
||||
statusBadgeLabel,
|
||||
statusBadgeTitle: statusBadgeLabel ? getStatusBadgeTitle(key) : '',
|
||||
|
||||
@@ -109,7 +109,10 @@ const QuotaProgressRows = defineComponent({
|
||||
: 'flex flex-col gap-1 min-w-[140px] max-w-[208px]',
|
||||
}, [
|
||||
h('div', { class: 'flex items-center justify-between text-[10px] leading-none' }, [
|
||||
h('span', { class: 'text-muted-foreground font-medium shrink-0' }, item.label),
|
||||
h('span', {
|
||||
'data-testid': 'pool-quota-period-label',
|
||||
class: 'text-muted-foreground font-medium shrink-0',
|
||||
}, item.label),
|
||||
item.resetText
|
||||
? h('span', {
|
||||
'data-testid': 'pool-quota-reset-text',
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="cycle"
|
||||
v-if="cycle && cycleMetricRows.length > 0"
|
||||
:class="cycleContainerClass"
|
||||
:data-testid="variant === 'desktop' ? 'pool-stats-cycle-groups' : undefined"
|
||||
:data-testid="variant === 'desktop' ? 'pool-stats-cycle-text' : 'pool-mobile-stats-cycle-text'"
|
||||
>
|
||||
<div
|
||||
:class="cycleGridClass"
|
||||
:data-testid="variant === 'desktop' ? 'pool-stats-cycle-grid' : 'pool-mobile-stats-cycle-grid'"
|
||||
v-for="row in cycleMetricRows"
|
||||
:key="`${row.key}-${variant}-cycle-row`"
|
||||
class="flex items-center justify-between gap-2"
|
||||
>
|
||||
<span aria-hidden="true" />
|
||||
<span class="shrink-0 text-muted-foreground">{{ row.label }}</span>
|
||||
<span
|
||||
:class="cycleGroupLabelClass"
|
||||
:data-testid="variant === 'desktop' ? 'pool-stats-cycle-group-5h' : 'pool-mobile-stats-cycle-group-5h'"
|
||||
>5H</span>
|
||||
<span class="text-center text-muted-foreground/50">|</span>
|
||||
<span
|
||||
:class="cycleGroupLabelClass"
|
||||
:data-testid="variant === 'desktop' ? 'pool-stats-cycle-group-weekly' : 'pool-mobile-stats-cycle-group-weekly'"
|
||||
>{{ legacyT('周') }}</span>
|
||||
|
||||
<template
|
||||
v-for="row in cycleRows"
|
||||
:key="`${row.key}-${variant}-cycle-row`"
|
||||
class="min-w-0 truncate text-right font-medium tabular-nums text-foreground/90"
|
||||
:data-testid="variant === 'desktop' ? `pool-stats-cycle-${row.key}` : undefined"
|
||||
:title="row.valueText"
|
||||
>
|
||||
<span class="text-muted-foreground truncate">{{ row.label }}</span>
|
||||
<span
|
||||
:class="[cycleValueClass, row.fiveH.missing ? 'text-muted-foreground/80' : '']"
|
||||
:data-testid="variant === 'desktop' ? `pool-stats-5h-${row.key}` : undefined"
|
||||
:title="row.fiveH.value"
|
||||
>{{ row.fiveH.value }}</span>
|
||||
<span class="text-center text-muted-foreground/50">|</span>
|
||||
<span
|
||||
:class="[cycleValueClass, row.weekly.missing ? 'text-muted-foreground/80' : '']"
|
||||
:data-testid="variant === 'desktop' ? `pool-stats-weekly-${row.key}` : undefined"
|
||||
:title="row.weekly.value"
|
||||
>{{ row.weekly.value }}</span>
|
||||
</template>
|
||||
{{ row.valueText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="cycle"
|
||||
:class="cycleContainerClass"
|
||||
:data-testid="variant === 'desktop' ? 'pool-stats-cycle-empty' : 'pool-mobile-stats-cycle-empty'"
|
||||
>
|
||||
<div class="flex min-h-16 items-center justify-center text-muted-foreground">
|
||||
—
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,47 +59,67 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from '@/i18n'
|
||||
import type { PoolStatsMetric } from '@/features/pool/utils/poolStatsDisplay'
|
||||
|
||||
export interface PoolKeyCycleStatsRow {
|
||||
key: PoolStatsMetric['key']
|
||||
label: string
|
||||
fiveH: PoolStatsMetric
|
||||
weekly: PoolStatsMetric
|
||||
}
|
||||
import type {
|
||||
PoolCodexCycleStatsGroup,
|
||||
PoolStatsMetric,
|
||||
PoolStatsMetricKey,
|
||||
} from '@/features/pool/utils/poolStatsDisplay'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
cycle: boolean
|
||||
cycleRows: PoolKeyCycleStatsRow[]
|
||||
cycleGroups: PoolCodexCycleStatsGroup[]
|
||||
accountMetrics: PoolStatsMetric[]
|
||||
variant?: 'desktop' | 'mobile'
|
||||
}>(), {
|
||||
variant: 'desktop',
|
||||
})
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
const CYCLE_METRIC_KEYS: PoolStatsMetricKey[] = ['request_count', 'total_tokens', 'total_cost_usd']
|
||||
const CYCLE_METRIC_LABELS: Record<PoolStatsMetricKey, string> = {
|
||||
request_count: '请求',
|
||||
total_tokens: 'Token',
|
||||
total_cost_usd: '费用',
|
||||
}
|
||||
|
||||
const cycleContainerClass = computed(() => props.variant === 'desktop'
|
||||
? 'mx-auto w-[188px] text-[10px] leading-4'
|
||||
: ''
|
||||
)
|
||||
function missingMetric(key: PoolStatsMetricKey): PoolStatsMetric {
|
||||
return {
|
||||
key,
|
||||
label: CYCLE_METRIC_LABELS[key],
|
||||
value: '—',
|
||||
missing: true,
|
||||
numericValue: null,
|
||||
}
|
||||
}
|
||||
|
||||
const cycleGridClass = computed(() => [
|
||||
'grid min-h-16 w-[188px] grid-cols-[38px_64px_10px_64px] items-center gap-x-1',
|
||||
props.variant === 'mobile' ? 'text-left' : '',
|
||||
function metricForGroup(
|
||||
group: PoolCodexCycleStatsGroup | undefined,
|
||||
key: PoolStatsMetricKey,
|
||||
): PoolStatsMetric {
|
||||
return group?.metrics.find(metric => metric.key === key) ?? missingMetric(key)
|
||||
}
|
||||
|
||||
const cycleMetricRows = computed(() => {
|
||||
const smallGroup = props.cycleGroups.length > 1 ? props.cycleGroups[0] : undefined
|
||||
const largeGroup = props.cycleGroups.at(-1)
|
||||
if (!largeGroup) return []
|
||||
|
||||
return CYCLE_METRIC_KEYS.map((key) => {
|
||||
const smallMetric = metricForGroup(smallGroup, key)
|
||||
const largeMetric = metricForGroup(largeGroup, key)
|
||||
const hasComparison = Boolean(smallGroup)
|
||||
return {
|
||||
key,
|
||||
label: CYCLE_METRIC_LABELS[key],
|
||||
valueText: hasComparison ? `${smallMetric.value}/${largeMetric.value}` : largeMetric.value,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const cycleContainerClass = computed(() => [
|
||||
'mx-auto w-[132px] space-y-1 text-[9px] leading-3',
|
||||
props.variant === 'mobile' ? 'py-0.5' : '',
|
||||
].filter(Boolean).join(' '))
|
||||
|
||||
const cycleGroupLabelClass = computed(() => props.variant === 'desktop'
|
||||
? 'text-center text-[9px] font-semibold text-muted-foreground/80'
|
||||
: 'text-center text-[10px] font-semibold text-foreground'
|
||||
)
|
||||
|
||||
const cycleValueClass = computed(() => [
|
||||
'min-w-0 truncate text-center text-foreground/90',
|
||||
props.variant === 'desktop' ? 'tabular-nums' : 'font-medium tabular-nums',
|
||||
].join(' '))
|
||||
|
||||
const accountContainerClass = computed(() => props.variant === 'desktop'
|
||||
? 'grid min-h-16 w-[188px] grid-rows-4 gap-0 mx-auto text-[10px] leading-4'
|
||||
: ''
|
||||
|
||||
@@ -11,20 +11,30 @@ describe('pool key display panels', () => {
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(PoolKeyStatsPanel, {
|
||||
cycle: true,
|
||||
cycleRows: [{
|
||||
key: 'request_count',
|
||||
label: '请求',
|
||||
fiveH: { key: 'request_count', label: '请求', value: '12', missing: false },
|
||||
weekly: { key: 'request_count', label: '请求', value: '88', missing: false },
|
||||
}],
|
||||
cycleGroups: [
|
||||
{
|
||||
code: '5h',
|
||||
label: '5H',
|
||||
metrics: [{ key: 'request_count', label: '请求', value: '12', missing: false, numericValue: 12 }],
|
||||
},
|
||||
{
|
||||
code: 'weekly',
|
||||
label: '周',
|
||||
metrics: [{ key: 'request_count', label: '请求', value: '88', missing: false, numericValue: 88 }],
|
||||
},
|
||||
],
|
||||
accountMetrics: [],
|
||||
})
|
||||
app.use(createI18n())
|
||||
app.mount(root)
|
||||
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-grid"]')).toBeTruthy()
|
||||
expect(root.querySelector('[data-testid="pool-stats-5h-request_count"]')?.textContent).toBe('12')
|
||||
expect(root.querySelector('[data-testid="pool-stats-weekly-request_count"]')?.textContent).toBe('88')
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')).toBeTruthy()
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')?.className).toContain('w-[132px]')
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('12/88')
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-small-overlay"]')).toBeNull()
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-large-base"]')).toBeNull()
|
||||
expect(root.textContent).not.toContain('5H')
|
||||
expect(root.textContent).not.toContain('周')
|
||||
|
||||
app.unmount()
|
||||
root.remove()
|
||||
@@ -68,4 +78,32 @@ describe('pool key display panels', () => {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
})
|
||||
|
||||
it('renders single-cycle stats as plain text', () => {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(PoolKeyStatsPanel, {
|
||||
cycle: true,
|
||||
cycleGroups: [{
|
||||
code: 'monthly',
|
||||
label: '月',
|
||||
metrics: [
|
||||
{ key: 'request_count', label: '请求', value: '31', missing: false, numericValue: 31 },
|
||||
{ key: 'total_tokens', label: 'Token', value: '38.8K', missing: false, numericValue: 38_800 },
|
||||
{ key: 'total_cost_usd', label: '费用', value: '$0.077', missing: false, numericValue: 0.077 },
|
||||
],
|
||||
}],
|
||||
accountMetrics: [],
|
||||
})
|
||||
app.use(createI18n())
|
||||
app.mount(root)
|
||||
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('31')
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-single-marker"]')).toBeNull()
|
||||
expect(root.querySelector('[data-testid="pool-stats-cycle-bar-request_count"]')).toBeNull()
|
||||
expect(root.textContent).not.toContain('月')
|
||||
|
||||
app.unmount()
|
||||
root.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ function createCodexKey(overrides: Partial<PoolStatsKeyInput> = {}): PoolStatsKe
|
||||
windows: [
|
||||
{
|
||||
code: '5h',
|
||||
window_minutes: 300,
|
||||
usage: {
|
||||
request_count: 5,
|
||||
total_tokens: 2500,
|
||||
@@ -27,10 +28,11 @@ function createCodexKey(overrides: Partial<PoolStatsKeyInput> = {}): PoolStatsKe
|
||||
},
|
||||
{
|
||||
code: 'weekly',
|
||||
window_minutes: 10_080,
|
||||
usage: {
|
||||
request_count: 0,
|
||||
total_tokens: 0,
|
||||
total_cost_usd: '0.00000000',
|
||||
request_count: 8,
|
||||
total_tokens: 5000,
|
||||
total_cost_usd: '0.012',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -54,9 +56,9 @@ describe('poolStatsDisplay', () => {
|
||||
total_cost_usd: '$0.0045',
|
||||
})
|
||||
expect(metricValues(display.groups[1].metrics)).toEqual({
|
||||
request_count: '0',
|
||||
total_tokens: '0',
|
||||
total_cost_usd: '0',
|
||||
request_count: '8',
|
||||
total_tokens: '5K',
|
||||
total_cost_usd: '$0.012',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -65,7 +67,7 @@ describe('poolStatsDisplay', () => {
|
||||
createCodexKey({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
windows: [{ code: '5h', usage: null }],
|
||||
windows: [{ code: '5h', window_minutes: 300, usage: null }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -81,10 +83,48 @@ describe('poolStatsDisplay', () => {
|
||||
total_tokens: '—',
|
||||
total_cost_usd: '—',
|
||||
})
|
||||
expect(metricValues(display.groups[1].metrics)).toEqual({
|
||||
request_count: '—',
|
||||
total_tokens: '—',
|
||||
total_cost_usd: '—',
|
||||
expect(display.groups).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('builds monthly stats from the actual quota window and ignores zero placeholders', () => {
|
||||
const display = buildPoolStatsDisplay(
|
||||
createCodexKey({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
windows: [
|
||||
{
|
||||
code: 'monthly',
|
||||
label: '月',
|
||||
window_minutes: 43_800,
|
||||
usage: {
|
||||
request_count: 12,
|
||||
total_tokens: 3456,
|
||||
total_cost_usd: '0.125',
|
||||
},
|
||||
},
|
||||
{
|
||||
code: 'weekly',
|
||||
label: '周',
|
||||
window_minutes: 0,
|
||||
usage: {
|
||||
request_count: 99,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
'codex',
|
||||
'current_cycle',
|
||||
)
|
||||
|
||||
expect(display.kind).toBe('codex_cycle')
|
||||
if (display.kind !== 'codex_cycle') throw new Error('expected codex cycle display')
|
||||
expect(display.groups.map(group => group.label)).toEqual(['月'])
|
||||
expect(metricValues(display.groups[0].metrics)).toEqual({
|
||||
request_count: '12',
|
||||
total_tokens: '3.5K',
|
||||
total_cost_usd: '$0.125',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { QuotaWindowUsageSnapshot } from '@/api/endpoints/types/statusSnapshot'
|
||||
import type { PoolManagementStatsMode } from '@/features/pool/utils/poolManagementState'
|
||||
import { getCodexQuotaWindowPresentation } from '@/utils/codexQuotaWindow'
|
||||
import { formatCompactNumber } from '@/utils/format'
|
||||
|
||||
export type PoolStatsMetricKey = 'request_count' | 'total_tokens' | 'total_cost_usd'
|
||||
export type PoolStatsDisplayKind = 'account_total' | 'codex_cycle'
|
||||
export type PoolCodexCycleWindowCode = '5h' | 'weekly'
|
||||
export type PoolCodexCycleWindowCode = string
|
||||
|
||||
export interface PoolStatsKeyInput {
|
||||
request_count?: number | null
|
||||
@@ -14,6 +15,9 @@ export interface PoolStatsKeyInput {
|
||||
quota?: {
|
||||
windows?: Array<{
|
||||
code?: string | null
|
||||
label?: string | null
|
||||
scope?: string | null
|
||||
window_minutes?: number | null
|
||||
usage?: QuotaWindowUsageSnapshot | null
|
||||
} | null> | null
|
||||
} | null
|
||||
@@ -25,6 +29,7 @@ export interface PoolStatsMetric {
|
||||
label: string
|
||||
value: string
|
||||
missing: boolean
|
||||
numericValue?: number | null
|
||||
}
|
||||
|
||||
export interface PoolAccountTotalStatsDisplay {
|
||||
@@ -46,10 +51,6 @@ export interface PoolCodexCycleStatsDisplay {
|
||||
export type PoolStatsDisplay = PoolAccountTotalStatsDisplay | PoolCodexCycleStatsDisplay
|
||||
|
||||
const MISSING_STAT_VALUE = '—'
|
||||
const CODEX_CYCLE_WINDOWS: Array<{ code: PoolCodexCycleWindowCode, label: string }> = [
|
||||
{ code: '5h', label: '5H' },
|
||||
{ code: 'weekly', label: '周' },
|
||||
]
|
||||
|
||||
export function isCodexProviderType(providerType: string | null | undefined): boolean {
|
||||
return String(providerType || '').trim().toLowerCase() === 'codex'
|
||||
@@ -103,12 +104,14 @@ function createMetric(
|
||||
key: PoolStatsMetricKey,
|
||||
label: string,
|
||||
value: string | null,
|
||||
numericValue?: number | null,
|
||||
): PoolStatsMetric {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
value: value ?? MISSING_STAT_VALUE,
|
||||
missing: value == null,
|
||||
numericValue: numericValue ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,17 +119,40 @@ function normalizeWindowCode(value: unknown): string {
|
||||
return String(value || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function getQuotaWindowUsage(
|
||||
key: PoolStatsKeyInput,
|
||||
code: PoolCodexCycleWindowCode,
|
||||
): QuotaWindowUsageSnapshot | null {
|
||||
function getCodexCycleStatsGroups(key: PoolStatsKeyInput): PoolCodexCycleStatsGroup[] {
|
||||
const windows = key.status_snapshot?.quota?.windows
|
||||
if (!Array.isArray(windows)) return null
|
||||
if (!Array.isArray(windows)) return []
|
||||
|
||||
const window = windows.find(item => normalizeWindowCode(item?.code) === code)
|
||||
return window?.usage ?? null
|
||||
const seenCodes = new Set<string>()
|
||||
return windows
|
||||
.map((window) => {
|
||||
if (!window) return null
|
||||
const code = normalizeWindowCode(window.code)
|
||||
const scope = String(window.scope || 'account').trim().toLowerCase()
|
||||
if (!code || scope !== 'account' || code.startsWith('spark_') || seenCodes.has(code)) {
|
||||
return null
|
||||
}
|
||||
const presentation = getCodexQuotaWindowPresentation({
|
||||
code,
|
||||
label: window.label,
|
||||
scope,
|
||||
window_minutes: window.window_minutes,
|
||||
})
|
||||
if (!presentation) return null
|
||||
seenCodes.add(code)
|
||||
return {
|
||||
code,
|
||||
label: presentation.label,
|
||||
sortOrder: presentation.sortOrder,
|
||||
metrics: buildCycleMetrics(window.usage ?? null),
|
||||
}
|
||||
})
|
||||
.filter((group): group is PoolCodexCycleStatsGroup & { sortOrder: number } => group != null)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.map(({ sortOrder: _sortOrder, ...group }) => group)
|
||||
}
|
||||
|
||||
|
||||
function buildAccountTotalMetrics(key: PoolStatsKeyInput): PoolStatsMetric[] {
|
||||
return [
|
||||
createMetric('request_count', '请求', formatPoolStatInteger(key.request_count)),
|
||||
@@ -136,10 +162,28 @@ function buildAccountTotalMetrics(key: PoolStatsKeyInput): PoolStatsMetric[] {
|
||||
}
|
||||
|
||||
function buildCycleMetrics(usage: QuotaWindowUsageSnapshot | null): PoolStatsMetric[] {
|
||||
const requestCount = usage?.request_count == null ? null : Number(usage.request_count)
|
||||
const totalTokens = usage?.total_tokens == null ? null : Number(usage.total_tokens)
|
||||
const totalCostUsd = usage?.total_cost_usd == null ? null : Number(usage.total_cost_usd)
|
||||
return [
|
||||
createMetric('request_count', '请求', formatCycleInteger(usage?.request_count)),
|
||||
createMetric('total_tokens', 'Token', formatCycleTokenCount(usage?.total_tokens)),
|
||||
createMetric('total_cost_usd', '费用', formatCycleUsd(usage?.total_cost_usd)),
|
||||
createMetric(
|
||||
'request_count',
|
||||
'请求',
|
||||
formatCycleInteger(usage?.request_count),
|
||||
Number.isFinite(requestCount) ? Math.max(requestCount ?? 0, 0) : null,
|
||||
),
|
||||
createMetric(
|
||||
'total_tokens',
|
||||
'Token',
|
||||
formatCycleTokenCount(usage?.total_tokens),
|
||||
Number.isFinite(totalTokens) ? Math.max(totalTokens ?? 0, 0) : null,
|
||||
),
|
||||
createMetric(
|
||||
'total_cost_usd',
|
||||
'费用',
|
||||
formatCycleUsd(usage?.total_cost_usd),
|
||||
Number.isFinite(totalCostUsd) ? Math.max(totalCostUsd ?? 0, 0) : null,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -157,10 +201,7 @@ export function buildCodexCycleStatsDisplay(
|
||||
): PoolCodexCycleStatsDisplay {
|
||||
return {
|
||||
kind: 'codex_cycle',
|
||||
groups: CODEX_CYCLE_WINDOWS.map(window => ({
|
||||
...window,
|
||||
metrics: buildCycleMetrics(getQuotaWindowUsage(key, window.code)),
|
||||
})),
|
||||
groups: getCodexCycleStatsGroups(key),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user