feat(pricing): support processing tier multipliers

This commit is contained in:
elky
2026-07-16 23:30:42 +08:00
parent 312583f055
commit 0be380243b
40 changed files with 5012 additions and 565 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
formatServiceTierFact,
hasServiceTierFact,
normalizeServiceTierFact,
resolveServiceTierFacts,
@@ -38,4 +39,16 @@ describe('service tier facts', () => {
expect(normalizeServiceTierFact(' ')).toBeNull()
expect(normalizeServiceTierFact(0)).toBeNull()
})
it.each(['priority', 'fast', ' Priority ', 'FAST'])(
'displays the raw %s tier as Fast',
(tier) => {
expect(formatServiceTierFact(tier)).toBe('Fast')
},
)
it('keeps non-fast tier labels unchanged', () => {
expect(formatServiceTierFact(' Batch ')).toBe('Batch')
expect(formatServiceTierFact(' ')).toBeNull()
})
})
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest'
import {
formatPricePerMillion,
resolveProcessingTierPriceMultiplier,
resolveSettlementPricingSnapshot,
resolveSettlementPricingSourceLabel,
resolveSettlementPricingTiers,
} from '../settlement-pricing'
function buildSource(overrides: Record<string, unknown> = {}) {
return {
settlement: {
rate_multiplier: 9,
settlement_snapshot: {
pricing_snapshot: {
billing_processing_tier: 'priority',
pricing_source: 'provider_override',
tiered_pricing_source: 'global_default',
processing_tier_price_multiplier: 2.5,
tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 5, output_price_per_1m: 30 }],
},
...overrides,
},
},
},
tiered_pricing: {
source: 'global',
tiers: [{ up_to: null, input_price_per_1m: 1 }],
},
}
}
describe('settlement pricing presentation', () => {
it('reads the resolved pricing snapshot and prefers its catalog over legacy tiers', () => {
const source = buildSource()
expect(resolveSettlementPricingSnapshot(source)?.billing_processing_tier).toBe('priority')
expect(resolveSettlementPricingTiers(source)).toEqual([
{ up_to: null, input_price_per_1m: 5, output_price_per_1m: 30 },
])
})
it.each([
['provider_override', '提供商定价'],
['global_default', '全局定价'],
['mixed', '混合定价'],
])('maps the resolved %s pricing source', (pricingSource, label) => {
expect(resolveSettlementPricingSourceLabel(buildSource({
pricing_source: pricingSource,
}))).toBe(label)
})
it('falls back from pricing_source to tiered_pricing_source and then legacy source', () => {
expect(resolveSettlementPricingSourceLabel(buildSource({
pricing_source: null,
tiered_pricing_source: 'global_default',
}))).toBe('全局定价')
expect(resolveSettlementPricingSourceLabel({
tiered_pricing: { source: 'provider' },
})).toBe('提供商定价')
})
it('uses only processing_tier_price_multiplier, never settlement.rate_multiplier', () => {
expect(resolveProcessingTierPriceMultiplier(buildSource())).toBe(2.5)
expect(resolveProcessingTierPriceMultiplier(buildSource({
processing_tier_price_multiplier: null,
}))).toBeNull()
expect(resolveProcessingTierPriceMultiplier({
settlement: { rate_multiplier: 9 },
})).toBeNull()
})
it('falls back to legacy tiers when the resolved snapshot has no catalog', () => {
expect(resolveSettlementPricingTiers(buildSource({ tiered_pricing: null }))).toEqual([
{ up_to: null, input_price_per_1m: 1 },
])
})
it('formats an input-only embedding tier without inventing an output price', () => {
const tiers = resolveSettlementPricingTiers(buildSource({
tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 0.1 }],
},
}))
expect(formatPricePerMillion(tiers?.[0]?.input_price_per_1m)).toBe('$0.1/M')
expect(formatPricePerMillion(tiers?.[0]?.output_price_per_1m)).toBe('-')
expect(formatPricePerMillion(null)).toBe('-')
expect(formatPricePerMillion(0)).toBe('$0/M')
})
})
@@ -33,6 +33,21 @@ export function normalizeServiceTierFact(value: unknown): string | null {
return normalized || null
}
/**
* Provider contracts use both `priority` (OpenAI) and `fast` (Claude) for the
* same user-facing processing mode. Keep the raw fact in data structures and
* normalize only at the presentation boundary.
*/
export function formatServiceTierFact(value: unknown): string | null {
const normalized = normalizeServiceTierFact(value)
if (normalized === null) return null
const canonical = normalized.toLowerCase()
return canonical === 'priority' || canonical === 'fast'
? 'Fast'
: normalized
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
@@ -0,0 +1,85 @@
interface SettlementPricingSource {
settlement?: unknown
tiered_pricing?: unknown
}
type JsonRecord = Record<string, unknown>
const PRICING_SOURCE_LABELS: Record<string, string> = {
provider_override: '提供商定价',
global_default: '全局定价',
mixed: '混合定价',
provider: '提供商定价',
global: '全局定价',
unpriced: '未定价',
}
export function resolveSettlementPricingSnapshot(
source: SettlementPricingSource | null | undefined,
): JsonRecord | null {
const settlement = asRecord(source?.settlement)
const settlementSnapshot = asRecord(settlement?.settlement_snapshot)
return asRecord(settlementSnapshot?.pricing_snapshot)
}
export function resolveSettlementPricingSourceLabel(
source: SettlementPricingSource | null | undefined,
): string | null {
const snapshot = resolveSettlementPricingSnapshot(source)
const legacyPricing = asRecord(source?.tiered_pricing)
for (const value of [
snapshot?.pricing_source,
snapshot?.tiered_pricing_source,
legacyPricing?.source,
]) {
const key = normalizeString(value)?.toLowerCase()
if (key && PRICING_SOURCE_LABELS[key]) return PRICING_SOURCE_LABELS[key]
}
return null
}
export function resolveSettlementPricingTiers(
source: SettlementPricingSource | null | undefined,
): JsonRecord[] | null {
const snapshot = resolveSettlementPricingSnapshot(source)
const snapshotPricing = asRecord(snapshot?.tiered_pricing)
const snapshotTiers = nonEmptyRecordArray(snapshotPricing?.tiers)
if (snapshotTiers) return snapshotTiers
const legacyPricing = asRecord(source?.tiered_pricing)
return nonEmptyRecordArray(legacyPricing?.tiers)
}
export function resolveProcessingTierPriceMultiplier(
source: SettlementPricingSource | null | undefined,
): number | null {
const value = resolveSettlementPricingSnapshot(source)?.processing_tier_price_multiplier
return typeof value === 'number' && Number.isFinite(value) && value >= 0
? value
: null
}
export function formatPricePerMillion(value: unknown): string {
if (typeof value !== 'number' || !Number.isFinite(value)) return '-'
const fixed = value.toFixed(4)
return `$${Number.parseFloat(fixed).toString()}/M`
}
function normalizeString(value: unknown): string | null {
if (typeof value !== 'string') return null
const normalized = value.trim()
return normalized || null
}
function nonEmptyRecordArray(value: unknown): JsonRecord[] | null {
if (!Array.isArray(value) || value.length === 0) return null
const records = value.filter((item): item is JsonRecord => asRecord(item) !== null)
return records.length > 0 ? records : null
}
function asRecord(value: unknown): JsonRecord | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as JsonRecord
: null
}