feat(frontend): expose processing tier pricing

This commit is contained in:
MMEXA
2026-07-11 12:27:09 +08:00
parent 0b30cc6b0f
commit 8f1070a451
25 changed files with 2488 additions and 340 deletions
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import type { TieredPricingConfig } from '@/api/endpoints/types'
import {
tieredPricingHasCacheTtl,
tieredPricingHasImageOutputPricing,
} from '../tiered-pricing'
function pricingWithProcessingTier(
processingTier: NonNullable<TieredPricingConfig['processing_tiers']>[string],
): TieredPricingConfig {
return {
tiers: [{ up_to: null, input_price_per_1m: 1, output_price_per_1m: 2 }],
processing_tiers: { priority: processingTier },
}
}
describe('tiered pricing capabilities', () => {
it('detects image prices in the base and processing-tier catalogs', () => {
expect(tieredPricingHasImageOutputPricing({
tiers: [],
image_output_price_default: 0,
})).toBe(true)
expect(tieredPricingHasImageOutputPricing(pricingWithProcessingTier({
image_output_prices: { '1024x1024': { high: 0.08 } },
}))).toBe(true)
expect(tieredPricingHasImageOutputPricing(pricingWithProcessingTier({
image_output_price_ranges: [{
up_to_pixels: null,
prices: { medium: 0.04 },
}],
}))).toBe(true)
expect(tieredPricingHasImageOutputPricing(pricingWithProcessingTier({}))).toBe(false)
})
it('detects cache TTL prices in the base and processing-tier catalogs', () => {
expect(tieredPricingHasCacheTtl({
tiers: [{
up_to: null,
input_price_per_1m: 1,
output_price_per_1m: 2,
cache_ttl_pricing: [{ ttl_minutes: 60, cache_creation_price_per_1m: 3 }],
}],
}, 60)).toBe(true)
expect(tieredPricingHasCacheTtl(pricingWithProcessingTier({
tiers: [{
up_to: null,
input_price_per_1m: 1,
output_price_per_1m: 2,
cache_ttl_pricing: [{ ttl_minutes: 60, cache_creation_price_per_1m: 3 }],
}],
}), 60)).toBe(true)
expect(tieredPricingHasCacheTtl(pricingWithProcessingTier({}), 60)).toBe(false)
})
})
@@ -0,0 +1,67 @@
import type {
ProcessingTierPricingConfig,
TieredPricingConfig,
} from '@/api/endpoints/types'
type PricingCatalog = TieredPricingConfig | ProcessingTierPricingConfig
export function comparePricingUpperBounds(
left: number | null,
right: number | null,
): number {
if (left === null && right === null) return 0
if (left === null) return 1
if (right === null) return -1
return left - right
}
function pricingCatalogs(pricing: TieredPricingConfig | null | undefined): PricingCatalog[] {
if (!pricing) return []
const processingTiers = pricing.processing_tiers
? Object.values(pricing.processing_tiers).filter(isRecord)
: []
return [pricing, ...processingTiers]
}
export function tieredPricingHasImageOutputPricing(
pricing: TieredPricingConfig | null | undefined,
): boolean {
return pricingCatalogs(pricing).some((catalog) => {
if (toFinitePrice(catalog.image_output_price_default) !== null) return true
if (Object.values(catalog.image_output_prices || {}).some(prices => (
isRecord(prices)
&& Object.values(prices).some(price => toFinitePrice(price) !== null)
))) return true
return (catalog.image_output_price_ranges || []).some(range => (
isRecord(range)
&& isRecord(range.prices)
&& Object.values(range.prices).some(price => toFinitePrice(price) !== null)
))
})
}
export function tieredPricingHasCacheTtl(
pricing: TieredPricingConfig | null | undefined,
ttlMinutes: number,
): boolean {
return pricingCatalogs(pricing).some(catalog => (
Array.isArray(catalog.tiers)
&& catalog.tiers.some(tier => (
Array.isArray(tier.cache_ttl_pricing)
&& tier.cache_ttl_pricing.some(entry => entry.ttl_minutes === ttlMinutes)
))
))
}
function toFinitePrice(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}