mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
feat(pricing): support processing tier multipliers
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user