mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
fix(frontend): import structured models.dev pricing
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildModelsDevTieredPricing } from '@/api/models-dev-pricing'
|
||||
|
||||
describe('buildModelsDevTieredPricing', () => {
|
||||
it('maps context bands and cache prices without flattening them', () => {
|
||||
expect(buildModelsDevTieredPricing({
|
||||
input: 5,
|
||||
output: 30,
|
||||
cache_read: 0.5,
|
||||
cache_write: 6.25,
|
||||
tiers: [{
|
||||
input: 10,
|
||||
output: 45,
|
||||
cache_read: 1,
|
||||
cache_write: 12.5,
|
||||
tier: { type: 'context', size: 272_000 },
|
||||
}],
|
||||
})).toEqual({
|
||||
tiers: [
|
||||
{
|
||||
up_to: 271_999,
|
||||
input_price_per_1m: 5,
|
||||
output_price_per_1m: 30,
|
||||
cache_creation_price_per_1m: 6.25,
|
||||
cache_read_price_per_1m: 0.5,
|
||||
},
|
||||
{
|
||||
up_to: null,
|
||||
input_price_per_1m: 10,
|
||||
output_price_per_1m: 45,
|
||||
cache_creation_price_per_1m: 12.5,
|
||||
cache_read_price_per_1m: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('sorts multiple context boundaries into contiguous Aether bands', () => {
|
||||
const cost = {
|
||||
input: 1,
|
||||
output: 2,
|
||||
tiers: [
|
||||
{ input: 5, output: 6, tier: { type: 'context' as const, size: 200_000 } },
|
||||
{ input: 3, output: 4, tier: { type: 'context' as const, size: 100_000 } },
|
||||
],
|
||||
}
|
||||
|
||||
expect(buildModelsDevTieredPricing(cost)?.tiers).toEqual([
|
||||
{ up_to: 99_999, input_price_per_1m: 1, output_price_per_1m: 2 },
|
||||
{ up_to: 199_999, input_price_per_1m: 3, output_price_per_1m: 4 },
|
||||
{ up_to: null, input_price_per_1m: 5, output_price_per_1m: 6 },
|
||||
])
|
||||
expect(cost.tiers.map(tier => tier.tier.size)).toEqual([200_000, 100_000])
|
||||
})
|
||||
|
||||
it('keeps flat token pricing as one unbounded band', () => {
|
||||
expect(buildModelsDevTieredPricing({ input: 0, output: 0.1 })).toEqual({
|
||||
tiers: [{ up_to: null, input_price_per_1m: 0, output_price_per_1m: 0.1 }],
|
||||
})
|
||||
})
|
||||
|
||||
it('omits an empty base band when context pricing starts at zero', () => {
|
||||
expect(buildModelsDevTieredPricing({
|
||||
input: 1,
|
||||
output: 2,
|
||||
tiers: [
|
||||
{ input: 3, output: 4, tier: { type: 'context', size: 0 } },
|
||||
{ input: 5, output: 6, tier: { type: 'context', size: 100_000 } },
|
||||
],
|
||||
})?.tiers).toEqual([
|
||||
{ up_to: 99_999, input_price_per_1m: 3, output_price_per_1m: 4 },
|
||||
{ up_to: null, input_price_per_1m: 5, output_price_per_1m: 6 },
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ input: -1, output: 2 },
|
||||
{ input: 1, output: Number.POSITIVE_INFINITY },
|
||||
{
|
||||
input: 1,
|
||||
output: 2,
|
||||
tiers: [{ input: 3, output: 4, tier: { type: 'context', size: Number.MAX_SAFE_INTEGER + 1 } }],
|
||||
},
|
||||
{
|
||||
input: 1,
|
||||
output: 2,
|
||||
tiers: [{ input: 3, output: 4, tier: { type: 'context', size: -1 } }],
|
||||
},
|
||||
{
|
||||
input: 1,
|
||||
output: 2,
|
||||
tiers: [
|
||||
{ input: 3, output: 4, tier: { type: 'context', size: 100 } },
|
||||
{ input: 5, output: 6, tier: { type: 'context', size: 100 } },
|
||||
],
|
||||
},
|
||||
])('fails closed for malformed structured pricing', (cost) => {
|
||||
expect(buildModelsDevTieredPricing(cost)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { PricingTier, TieredPricingConfig } from './endpoints/types'
|
||||
|
||||
export interface ModelsDevTokenCost {
|
||||
input: number
|
||||
output: number
|
||||
reasoning?: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
input_audio?: number
|
||||
output_audio?: number
|
||||
}
|
||||
|
||||
export interface ModelsDevCostTier extends ModelsDevTokenCost {
|
||||
tier: {
|
||||
type: 'context'
|
||||
size: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ModelsDevCost extends ModelsDevTokenCost {
|
||||
tiers?: ModelsDevCostTier[]
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isPrice(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
||||
}
|
||||
|
||||
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
|
||||
if (value.cache_read !== undefined && !isPrice(value.cache_read)) return null
|
||||
|
||||
return {
|
||||
input_price_per_1m: value.input,
|
||||
output_price_per_1m: value.output,
|
||||
...(value.cache_write === undefined
|
||||
? {}
|
||||
: { cache_creation_price_per_1m: value.cache_write }),
|
||||
...(value.cache_read === undefined
|
||||
? {}
|
||||
: { cache_read_price_per_1m: value.cache_read }),
|
||||
}
|
||||
}
|
||||
|
||||
function parseContextTier(value: unknown): { size: number; prices: Omit<PricingTier, 'up_to'> } | null {
|
||||
if (!isRecord(value) || !isRecord(value.tier)) return null
|
||||
if (
|
||||
value.tier.type !== 'context'
|
||||
|| typeof value.tier.size !== 'number'
|
||||
|| !Number.isSafeInteger(value.tier.size)
|
||||
|| value.tier.size < 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const prices = parseTokenPrices(value)
|
||||
return prices ? { size: value.tier.size, prices } : null
|
||||
}
|
||||
|
||||
export function buildModelsDevTieredPricing(cost: unknown): TieredPricingConfig | null {
|
||||
const basePrices = parseTokenPrices(cost)
|
||||
if (!basePrices || !isRecord(cost)) return null
|
||||
|
||||
const rawTiers = cost.tiers
|
||||
if (rawTiers !== undefined && !Array.isArray(rawTiers)) return null
|
||||
const contextTiers = (rawTiers ?? []).map(parseContextTier)
|
||||
if (contextTiers.some(tier => tier === null)) return null
|
||||
|
||||
const sortedTiers = contextTiers
|
||||
.filter((tier): tier is NonNullable<typeof tier> => tier !== null)
|
||||
.sort((a, b) => a.size - b.size)
|
||||
if (sortedTiers.some((tier, index) => index > 0 && tier.size === sortedTiers[index - 1].size)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tiers: PricingTier[] = []
|
||||
if (sortedTiers[0]?.size !== 0) {
|
||||
tiers.push({
|
||||
...basePrices,
|
||||
up_to: sortedTiers[0] ? sortedTiers[0].size - 1 : null,
|
||||
})
|
||||
}
|
||||
tiers.push(...sortedTiers.map((tier, index) => ({
|
||||
...tier.prices,
|
||||
up_to: sortedTiers[index + 1] ? sortedTiers[index + 1].size - 1 : null,
|
||||
})))
|
||||
|
||||
return { tiers }
|
||||
}
|
||||
@@ -4,19 +4,19 @@
|
||||
*/
|
||||
|
||||
import api from './client'
|
||||
import {
|
||||
buildModelsDevTieredPricing,
|
||||
type ModelsDevCost,
|
||||
} from './models-dev-pricing'
|
||||
import type { TieredPricingConfig } from './endpoints/types'
|
||||
|
||||
export type { ModelsDevCost, ModelsDevCostTier, ModelsDevTokenCost } from './models-dev-pricing'
|
||||
|
||||
// 缓存配置
|
||||
const CACHE_KEY = 'models_dev_cache'
|
||||
const CACHE_DURATION = 15 * 60 * 1000 // 15 分钟
|
||||
|
||||
// Models.dev API 数据结构
|
||||
export interface ModelsDevCost {
|
||||
input?: number
|
||||
output?: number
|
||||
reasoning?: number
|
||||
cache_read?: number
|
||||
}
|
||||
|
||||
export interface ModelsDevLimit {
|
||||
context?: number
|
||||
output?: number
|
||||
@@ -64,6 +64,7 @@ export interface ModelsDevModelItem {
|
||||
family?: string
|
||||
inputPrice?: number
|
||||
outputPrice?: number
|
||||
tieredPricing?: TieredPricingConfig
|
||||
contextLimit?: number
|
||||
outputLimit?: number
|
||||
supportsVision?: boolean
|
||||
@@ -165,6 +166,7 @@ export async function getModelsDevList(officialOnly: boolean = true): Promise<Mo
|
||||
if (!provider.models) continue
|
||||
|
||||
for (const [modelId, model] of Object.entries(provider.models)) {
|
||||
const tieredPricing = buildModelsDevTieredPricing(model.cost)
|
||||
items.push({
|
||||
providerId,
|
||||
providerName: provider.name,
|
||||
@@ -173,6 +175,7 @@ export async function getModelsDevList(officialOnly: boolean = true): Promise<Mo
|
||||
family: model.family,
|
||||
inputPrice: model.cost?.input,
|
||||
outputPrice: model.cost?.output,
|
||||
tieredPricing: tieredPricing ?? undefined,
|
||||
contextLimit: model.limit?.context,
|
||||
outputLimit: model.limit?.output,
|
||||
supportsVision: model.input?.includes('image'),
|
||||
|
||||
@@ -1105,17 +1105,9 @@ function selectModel(model: ModelsDevModelItem) {
|
||||
}
|
||||
loadVideoPricingFromConfig()
|
||||
|
||||
if (model.inputPrice !== undefined || model.outputPrice !== undefined) {
|
||||
tieredPricing.value = {
|
||||
tiers: [{
|
||||
up_to: null,
|
||||
input_price_per_1m: model.inputPrice || 0,
|
||||
output_price_per_1m: model.outputPrice || 0,
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
tieredPricing.value = null
|
||||
}
|
||||
tieredPricing.value = model.tieredPricing
|
||||
? structuredClone(model.tieredPricing)
|
||||
: null
|
||||
|
||||
presetPanelCollapsed.value = true
|
||||
scrollToBasicInformation()
|
||||
|
||||
Reference in New Issue
Block a user