mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-04 23:42:13 +08:00
fix(billing): 修复 OpenAI fast 档位计费并补齐展示
- 打通 service_tier 在 OpenAI HTTP、WS、passthrough 与 usage 记录中的传递 - 修正 priority/flex 计费逻辑,并将 fast 归一化为 priority - 在用户端和管理端补齐服务档位与计费明细展示 - 补齐前后端测试,并修复 WS 限流信号重复持久化导致的全量回归失败 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
39
frontend/src/utils/__tests__/usageServiceTier.spec.ts
Normal file
39
frontend/src/utils/__tests__/usageServiceTier.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { formatUsageServiceTier, getUsageServiceTierLabel, normalizeUsageServiceTier } from '@/utils/usageServiceTier'
|
||||
|
||||
describe('usageServiceTier utils', () => {
|
||||
it('normalizes fast/default aliases', () => {
|
||||
expect(normalizeUsageServiceTier('fast')).toBe('priority')
|
||||
expect(normalizeUsageServiceTier(' default ')).toBe('standard')
|
||||
expect(normalizeUsageServiceTier('STANDARD')).toBe('standard')
|
||||
})
|
||||
|
||||
it('preserves supported tiers', () => {
|
||||
expect(normalizeUsageServiceTier('priority')).toBe('priority')
|
||||
expect(normalizeUsageServiceTier('flex')).toBe('flex')
|
||||
})
|
||||
|
||||
it('formats empty values as standard', () => {
|
||||
expect(formatUsageServiceTier()).toBe('standard')
|
||||
expect(formatUsageServiceTier('')).toBe('standard')
|
||||
})
|
||||
|
||||
it('passes through unknown non-empty tiers for display fallback', () => {
|
||||
expect(normalizeUsageServiceTier('custom-tier')).toBe('custom-tier')
|
||||
expect(formatUsageServiceTier('custom-tier')).toBe('custom-tier')
|
||||
})
|
||||
|
||||
it('maps tiers to translated labels', () => {
|
||||
const translate = (key: string) => ({
|
||||
'usage.serviceTierPriority': 'Fast',
|
||||
'usage.serviceTierFlex': 'Flex',
|
||||
'usage.serviceTierStandard': 'Standard',
|
||||
})[key] ?? key
|
||||
|
||||
expect(getUsageServiceTierLabel('fast', translate)).toBe('Fast')
|
||||
expect(getUsageServiceTierLabel('flex', translate)).toBe('Flex')
|
||||
expect(getUsageServiceTierLabel(undefined, translate)).toBe('Standard')
|
||||
expect(getUsageServiceTierLabel('custom-tier', translate)).toBe('custom-tier')
|
||||
})
|
||||
})
|
||||
49
frontend/src/utils/usagePricing.ts
Normal file
49
frontend/src/utils/usagePricing.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export const TOKENS_PER_MILLION = 1_000_000
|
||||
|
||||
interface TokenPriceFormatOptions {
|
||||
fractionDigits?: number
|
||||
withCurrencySymbol?: boolean
|
||||
emptyValue?: string
|
||||
}
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
}
|
||||
|
||||
export function calculateTokenUnitPrice(
|
||||
cost: number | null | undefined,
|
||||
tokens: number | null | undefined
|
||||
): number | null {
|
||||
if (!isFiniteNumber(cost) || !isFiniteNumber(tokens) || tokens <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return cost / tokens
|
||||
}
|
||||
|
||||
export function calculateTokenPricePerMillion(
|
||||
cost: number | null | undefined,
|
||||
tokens: number | null | undefined
|
||||
): number | null {
|
||||
const unitPrice = calculateTokenUnitPrice(cost, tokens)
|
||||
if (unitPrice == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return unitPrice * TOKENS_PER_MILLION
|
||||
}
|
||||
|
||||
export function formatTokenPricePerMillion(
|
||||
cost: number | null | undefined,
|
||||
tokens: number | null | undefined,
|
||||
options: TokenPriceFormatOptions = {}
|
||||
): string {
|
||||
const pricePerMillion = calculateTokenPricePerMillion(cost, tokens)
|
||||
if (pricePerMillion == null) {
|
||||
return options.emptyValue ?? '-'
|
||||
}
|
||||
|
||||
const fractionDigits = options.fractionDigits ?? 4
|
||||
const formatted = pricePerMillion.toFixed(fractionDigits)
|
||||
return options.withCurrencySymbol == false ? formatted : `$${formatted}`
|
||||
}
|
||||
25
frontend/src/utils/usageServiceTier.ts
Normal file
25
frontend/src/utils/usageServiceTier.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export function normalizeUsageServiceTier(serviceTier?: string | null): string | null {
|
||||
const value = serviceTier?.trim().toLowerCase()
|
||||
if (!value) return null
|
||||
if (value === 'fast') return 'priority'
|
||||
if (value === 'default' || value === 'standard') return 'standard'
|
||||
if (value === 'priority' || value === 'flex') return value
|
||||
return value
|
||||
}
|
||||
|
||||
export function formatUsageServiceTier(serviceTier?: string | null): string {
|
||||
const normalized = normalizeUsageServiceTier(serviceTier)
|
||||
if (!normalized) return 'standard'
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function getUsageServiceTierLabel(
|
||||
serviceTier: string | null | undefined,
|
||||
translate: (key: string) => string,
|
||||
): string {
|
||||
const tier = formatUsageServiceTier(serviceTier)
|
||||
if (tier === 'priority') return translate('usage.serviceTierPriority')
|
||||
if (tier === 'flex') return translate('usage.serviceTierFlex')
|
||||
if (tier === 'standard') return translate('usage.serviceTierStandard')
|
||||
return tier
|
||||
}
|
||||
Reference in New Issue
Block a user