fix(models): correct fast pricing and online sync

This commit is contained in:
elky
2026-07-24 01:45:38 +08:00
parent a0767d957c
commit 387134ca87
11 changed files with 1269 additions and 420 deletions
@@ -63,6 +63,14 @@ describe('buildModelsDevTieredPricing', () => {
})
})
it('fails closed when a legacy long-context copy has no authoritative tier boundary', () => {
expect(buildModelsDevTieredPricing({
input: 5,
output: 30,
context_over_200k: { input: 10, output: 45 },
})).toBeNull()
})
it('allows special token dimensions only when they use the base token price', () => {
expect(buildModelsDevTieredPricing({
input: 1,
@@ -214,7 +222,7 @@ describe('resolveModelsDevTieredPricing', () => {
expect(resolveModelsDevTieredPricing('openai', 'gpt-5.6-sol', undefined)).toBeNull()
})
it('keeps a models.dev fast cost as an explicit Priority catalog when bands differ', () => {
it('collapses a flat models.dev Fast cost to the Standard first band multiplier', () => {
expect(resolveModelsDevTieredPricing('openai', 'gpt-5.6-sol', {
input: 5,
output: 30,
@@ -235,12 +243,81 @@ describe('resolveModelsDevTieredPricing', () => {
],
processing_tiers: {
priority: {
tiers: [{ up_to: null, input_price_per_1m: 10, output_price_per_1m: 60 }],
price_multiplier: 2,
},
},
})
})
it('maps the GPT-5.6 Fast cost and ignores the legacy context copy', () => {
expect(resolveModelsDevTieredPricing('openai', 'gpt-5.6-sol', {
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 },
}],
context_over_200k: {
input: 10,
output: 45,
cache_read: 1,
cache_write: 12.5,
},
}, {
fast: {
cost: {
input: 10,
output: 60,
cache_read: 1,
cache_write: 12.5,
},
provider: { body: { service_tier: 'priority' } },
},
})).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,
},
],
processing_tiers: {
priority: { price_multiplier: 2 },
},
})
})
it('fails closed when an experimental mode tries to supply context tiers', () => {
expect(resolveModelsDevTieredPricing('openai', 'gpt-5.6-sol', {
input: 5,
output: 30,
tiers: [{ input: 10, output: 45, tier: { type: 'context', size: 272_000 } }],
}, {
fast: {
cost: {
input: 10,
output: 60,
tiers: [{ input: 20, output: 90, tier: { type: 'context', size: 272_000 } }],
},
provider: { body: { service_tier: 'priority' } },
},
})?.processing_tiers).toBeUndefined()
})
it('uses a multiplier only when every fast price has the same ratio', () => {
expect(resolveModelsDevTieredPricing('anthropic', 'claude-opus-4.8', {
input: 5,
@@ -269,6 +346,27 @@ describe('resolveModelsDevTieredPricing', () => {
})
})
it('keeps a non-uniform Fast cost as one explicit unbounded band', () => {
expect(resolveModelsDevTieredPricing('openai', 'future-model', {
input: 5,
output: 30,
tiers: [{
input: 10,
output: 45,
tier: { type: 'context', size: 272_000 },
}],
}, {
fast: {
cost: { input: 10, output: 75 },
provider: { body: { service_tier: 'priority' } },
},
})?.processing_tiers).toEqual({
priority: {
tiers: [{ up_to: null, input_price_per_1m: 10, output_price_per_1m: 75 }],
},
})
})
it('uses the standard catalog when an imported tier has a zero default ratio', () => {
expect(resolveModelsDevTieredPricing('openai', 'gpt-5.6-sol', {
input: 5,
+39 -3
View File
@@ -2,18 +2,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const apiMocks = vi.hoisted(() => ({
get: vi.fn(),
delete: vi.fn(),
}))
vi.mock('@/api/client', () => ({
default: { get: apiMocks.get },
default: { get: apiMocks.get, delete: apiMocks.delete },
}))
import { clearModelsDevCache, getModelsDevList } from '@/api/models-dev'
import { clearModelsDevCache, getModelsDevList, refreshModelsDevList } from '@/api/models-dev'
beforeEach(() => {
clearModelsDevCache()
localStorage.clear()
apiMocks.get.mockReset()
apiMocks.delete.mockReset()
apiMocks.delete.mockResolvedValue({ data: { cleared: true } })
})
describe('getModelsDevList', () => {
@@ -34,7 +37,15 @@ describe('getModelsDevList', () => {
input: ['text', 'image'],
output: ['text', 'image'],
},
cost: { input: 2, output: 4 },
cost: {
input: 2,
output: 4,
tiers: [{
input: 4,
output: 8,
tier: { type: 'context', size: 100_000 },
}],
},
experimental: {
modes: {
fast: {
@@ -86,4 +97,29 @@ describe('getModelsDevList', () => {
})
expect(audioPriced?.tieredPricing).toBeUndefined()
})
it('clears the gateway cache before rebuilding the online model list', async () => {
apiMocks.get.mockResolvedValue({
data: {
openai: {
name: 'OpenAI',
official: true,
models: {
'gpt-test': {
id: 'gpt-test',
name: 'GPT Test',
cost: { input: 2, output: 4 },
},
},
},
},
})
await getModelsDevList(false)
await refreshModelsDevList(false)
expect(apiMocks.delete).toHaveBeenCalledOnce()
expect(apiMocks.delete).toHaveBeenCalledWith('/api/admin/models/external/cache')
expect(apiMocks.get).toHaveBeenCalledTimes(2)
})
})