diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 554295f97e..339e07bcf1 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -134,6 +134,7 @@ :utilization="usageInfo.seven_day.utilization" :resets-at="usageInfo.seven_day.resets_at" :window-stats="usageInfo.seven_day.window_stats" + :estimated-total-cost="openAISevenDayEstimatedTotalCost" :show-now-when-idle="true" color="emerald" /> @@ -784,6 +785,25 @@ const hasOpenAIUsageFallback = computed(() => { return !!usageInfo.value?.five_hour || !!usageInfo.value?.seven_day }) +const openAISevenDayEstimatedTotalCost = computed(() => { + const sevenDay = usageInfo.value?.seven_day + const utilization = sevenDay?.utilization + const currentCost = sevenDay?.window_stats?.cost + if ( + typeof utilization !== 'number' || + typeof currentCost !== 'number' || + !Number.isFinite(utilization) || + !Number.isFinite(currentCost) || + utilization <= 0 || + currentCost <= 0 + ) { + return null + } + + const estimate = (currentCost * 100) / utilization + return Number.isFinite(estimate) && estimate > 0 ? estimate : null +}) + const openAIUsageRefreshKey = computed(() => buildOpenAIUsageRefreshKey(props.account)) const shouldAutoLoadUsageOnMount = computed(() => { diff --git a/frontend/src/components/account/UsageProgressBar.vue b/frontend/src/components/account/UsageProgressBar.vue index 75c84a8305..1f7cde1645 100644 --- a/frontend/src/components/account/UsageProgressBar.vue +++ b/frontend/src/components/account/UsageProgressBar.vue @@ -22,6 +22,14 @@ > U ${{ formatUserCost }} + + {{ t('admin.accounts.usageWindow.estimatedTotalCost', { cost: estimatedTotalCost.toFixed(2) }) }} + @@ -67,6 +75,7 @@ const props = withDefaults( resetsAt?: string | null color: 'indigo' | 'emerald' | 'purple' | 'amber' windowStats?: WindowStats | null + estimatedTotalCost?: number | null showNowWhenIdle?: boolean remainingCapacity?: boolean /** fixed: 定宽居中徽章(账号页纵向对齐);auto: 限宽截断左对齐(监控页组合标签) */ diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts index 5ee8e5805a..ead39a7013 100644 --- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts +++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts @@ -441,6 +441,84 @@ describe('AccountUsageCell', () => { expect(wrapper.text()).toContain('7d|36|900') }) + it('仅为 OpenAI OAuth 7d 窗口计算预计总费用', async () => { + getUsage.mockResolvedValue({ + five_hour: { + utilization: 25, + resets_at: null, + remaining_seconds: 0, + window_stats: { requests: 1, tokens: 100, cost: 2 } + }, + seven_day: { + utilization: 40, + resets_at: null, + remaining_seconds: 0, + window_stats: { requests: 2, tokens: 200, cost: 12 } + } + }) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ id: 6752, platform: 'openai', type: 'oauth' }) + }, + global: { + stubs: { + UsageProgressBar: { + props: ['label', 'estimatedTotalCost'], + template: '
' + }, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + + expect(wrapper.text()).toContain('5h|none') + expect(wrapper.text()).toContain('7d|30') + }) + + it.each([ + { id: 6801, utilization: 0, cost: 12 }, + { id: 6802, utilization: -1, cost: 12 }, + { id: 6803, utilization: Number.NaN, cost: 12 }, + { id: 6804, utilization: 40, cost: 0 }, + { id: 6805, utilization: 40, cost: Number.POSITIVE_INFINITY } + ])('OpenAI OAuth 7d 输入无效时不显示预计总费用 (%o)', async ({ id, utilization, cost }) => { + getUsage.mockResolvedValue({ + seven_day: { + utilization, + resets_at: null, + remaining_seconds: 0, + window_stats: { requests: 1, tokens: 100, cost } + } + }) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ + id, + platform: 'openai', + type: 'oauth' + }) + }, + global: { + stubs: { + UsageProgressBar: { + props: ['label', 'estimatedTotalCost'], + template: '' + }, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + + expect(wrapper.text()).toContain('7d|none') + expect(wrapper.text()).not.toMatch(/Infinity|NaN/) + }) + it('OpenAI OAuth 有现成快照时,手动刷新信号会触发 usage 重拉', async () => { getUsage.mockResolvedValue({ five_hour: { diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index fb7a81a330..8a86a367cc 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -1534,7 +1534,9 @@ export default { grokLastProbe: 'Probe {time}', grokLastHeadersSeen: 'Headers {time}', passiveSampled: 'Passive', - activeQuery: 'Query' + activeQuery: 'Query', + estimatedTotalCost: 'Est. total ${cost}', + estimatedTotalCostTooltip: 'Estimated total cost at 100% utilization, based on current window cost and utilization' }, openaiQuotaReset: { count: 'Credits', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 64f5e96e5f..872d5b1a26 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -479,7 +479,9 @@ export default { grokLastProbe: '探测 {time}', grokLastHeadersSeen: '响应头 {time}', passiveSampled: '被动采样', - activeQuery: '查询' + activeQuery: '查询', + estimatedTotalCost: '预计总费用 ${cost}', + estimatedTotalCostTooltip: '根据当前窗口费用和使用率估算达到 100% 使用率时的总费用' }, openaiQuotaReset: { count: '次数',