feat(models): track online pricing sources and unsupported fields

This commit is contained in:
ZheFox
2026-07-23 15:18:08 +08:00
parent 323273ff30
commit 1d2655432d
11 changed files with 549 additions and 242 deletions
@@ -63,6 +63,36 @@ describe('buildModelsDevTieredPricing', () => {
})
})
it('allows special token dimensions only when they use the base token price', () => {
expect(buildModelsDevTieredPricing({
input: 1,
output: 2,
input_audio: 1,
output_audio: 2,
reasoning: 2,
})).toEqual({
tiers: [{ up_to: null, input_price_per_1m: 1, output_price_per_1m: 2 }],
})
})
it.each([
{ input: 1, output: 2, reasoning: 4 },
{ input: 1, output: 2, input_audio: 3 },
{ input: 1, output: 2, output_audio: 5 },
{
input: 1,
output: 2,
tiers: [{
input: 3,
output: 4,
input_audio: 9,
tier: { type: 'context', size: 100_000 },
}],
},
])('rejects pricing dimensions the billing engine cannot settle independently', (cost) => {
expect(buildModelsDevTieredPricing(cost)).toBeNull()
})
it('omits an empty base band when context pricing starts at zero', () => {
expect(buildModelsDevTieredPricing({
input: 1,
@@ -51,6 +51,11 @@ describe('getModelsDevList', () => {
output: ['text'],
cost: { input: 1, output: 2 },
},
'audio-priced': {
id: 'audio-priced',
name: 'Audio Priced',
cost: { input: 1, output: 2, input_audio: 4 },
},
},
},
},
@@ -59,6 +64,7 @@ describe('getModelsDevList', () => {
const models = await getModelsDevList()
const current = models.find(model => model.modelId === 'gpt-test')
const legacy = models.find(model => model.modelId === 'legacy')
const audioPriced = models.find(model => model.modelId === 'audio-priced')
expect(current).toMatchObject({
supportsVision: true,
@@ -73,5 +79,11 @@ describe('getModelsDevList', () => {
inputModalities: ['text', 'image'],
outputModalities: ['text'],
})
expect(audioPriced).toMatchObject({
inputPrice: 1,
outputPrice: 2,
pricingUnsupportedFields: ['input_audio'],
})
expect(audioPriced?.tieredPricing).toBeUndefined()
})
})
+37
View File
@@ -21,6 +21,8 @@ export interface ModelsDevCost extends ModelsDevTokenCost {
tiers?: ModelsDevCostTier[]
}
export type ModelsDevUnsupportedPricingField = 'reasoning' | 'input_audio' | 'output_audio'
const TOKEN_PRICE_FIELDS = [
'input_price_per_1m',
'output_price_per_1m',
@@ -38,6 +40,40 @@ function isPrice(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0
}
const SPECIAL_PRICE_BASE_FIELDS: Array<{
field: ModelsDevUnsupportedPricingField
baseField: 'input' | 'output'
}> = [
{ field: 'reasoning', baseField: 'output' },
{ field: 'input_audio', baseField: 'input' },
{ field: 'output_audio', baseField: 'output' },
]
export function getModelsDevUnsupportedPricingFields(
cost: unknown,
): ModelsDevUnsupportedPricingField[] {
if (!isRecord(cost)) return []
const unsupportedFields = new Set<ModelsDevUnsupportedPricingField>()
const inspectPrices = (prices: Record<string, unknown>) => {
for (const { field, baseField } of SPECIAL_PRICE_BASE_FIELDS) {
const specialPrice = prices[field]
if (specialPrice === undefined) continue
const basePrice = prices[baseField]
if (!isPrice(specialPrice) || !isPrice(basePrice) || specialPrice !== basePrice) {
unsupportedFields.add(field)
}
}
}
inspectPrices(cost)
if (Array.isArray(cost.tiers)) {
for (const tier of cost.tiers) {
if (isRecord(tier)) inspectPrices(tier)
}
}
return [...unsupportedFields]
}
function parseTokenPrices(value: unknown): Omit<PricingTier, 'up_to'> | null {
if (!isRecord(value) || !isPrice(value.input) || !isPrice(value.output)) return null
if (value.cache_write !== undefined && !isPrice(value.cache_write)) return null
@@ -70,6 +106,7 @@ function parseContextTier(value: unknown): { size: number; prices: Omit<PricingT
}
export function buildModelsDevTieredPricing(cost: unknown): TieredPricingConfig | null {
if (getModelsDevUnsupportedPricingFields(cost).length > 0) return null
const basePrices = parseTokenPrices(cost)
if (!basePrices || !isRecord(cost)) return null
+11
View File
@@ -5,8 +5,10 @@
import api from './client'
import {
getModelsDevUnsupportedPricingFields,
resolveModelsDevTieredPricing,
type ModelsDevCost,
type ModelsDevUnsupportedPricingField,
} from './models-dev-pricing'
import type { TieredPricingConfig } from './endpoints/types'
@@ -78,6 +80,7 @@ export interface ModelsDevModelItem {
inputPrice?: number
outputPrice?: number
tieredPricing?: TieredPricingConfig
pricingUnsupportedFields?: ModelsDevUnsupportedPricingField[]
contextLimit?: number
outputLimit?: number
supportsVision?: boolean
@@ -187,6 +190,11 @@ export async function getModelsDevList(officialOnly: boolean = true): Promise<Mo
model.cost,
model.experimental?.modes,
)
const pricingUnsupportedFields = [...new Set([
...getModelsDevUnsupportedPricingFields(model.cost),
...Object.values(model.experimental?.modes ?? {})
.flatMap(mode => getModelsDevUnsupportedPricingFields(mode.cost)),
])]
const basePricingTier = tieredPricing?.tiers[0]
items.push({
providerId,
@@ -197,6 +205,9 @@ export async function getModelsDevList(officialOnly: boolean = true): Promise<Mo
inputPrice: basePricingTier?.input_price_per_1m ?? model.cost?.input,
outputPrice: basePricingTier?.output_price_per_1m ?? model.cost?.output,
tieredPricing: tieredPricing ?? undefined,
pricingUnsupportedFields: pricingUnsupportedFields.length > 0
? pricingUnsupportedFields
: undefined,
contextLimit: model.limit?.context,
outputLimit: model.limit?.output,
supportsVision: inputModalities?.includes('image'),