codeburn/tests/models-hoist.test.ts
Resham Joshi c826f1a479
fix(models): price kimi-for-coding by the model it served on the call date (#1676)
* fix(models): price kimi-for-coding by the model it served on the call date

kimi-for-coding is a moving alias. It priced as retired K2 Thinking for
every call. It now prices as K2.5 from 27 Jan 2026, K2.6 from 13 Apr and
K2.7 Code from 12 Jun (K2.8 Preview from 11 Sep has no published price,
so it stays on K2.7 Code). kimi-for-coding-highspeed prices at Moonshot's
K2.7 Code HighSpeed rate instead of $0. Daily cache v64.

* fix(models): mark K2.8 Preview kimi-for-coding calls as estimated

From 11 Sep 2026 kimi-for-coding served K2.8 Preview, which has no
published rate and prices as K2.7 Code. Those calls now carry the
estimated flag. The legacy kimi provider prices by the dated model too.

* fix(models): flag the K2.8 Preview stand-in where every call is read

cachedCallToApiCall now marks kimi-for-coding calls from 11 Sep 2026
estimated, so the flag reaches every provider and cached reads without a
parse bump. Calls with a reported cost are left alone. The kimi provider's
own flag and its parse version entry go, as they only duplicated this.
2026-10-08 00:02:38 +02:00

132 lines
4.3 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { calculateCost, getModelCosts, getShortModelName } from '../src/models.js'
// Lock down the post-hoist refactor: every model name a real user has
// emitted in the last year should resolve to the same display name and
// the same costs as before. If this list grows or shrinks, the refactor
// is fine — it's the per-name resolution that must stay stable.
const KNOWN_NAMES = [
'claude-opus-4-7',
'claude-opus-4-6',
'claude-opus-4-5',
'claude-sonnet-4-6',
'claude-sonnet-4-5',
'claude-haiku-4-5',
'claude-3-5-sonnet',
'claude-3-5-haiku',
'claude-opus-4-7-20250101',
'claude-sonnet-4-6-20250929',
'anthropic/claude-opus-4-7',
'anthropic--claude-4.6-opus',
'anthropic--claude-4.6-sonnet',
'claude-4.6-sonnet',
'gpt-5',
'gpt-5-mini',
'gpt-5-nano',
'gpt-5-pro',
'gpt-5.1',
'gpt-5.1-codex',
'gpt-5.1-codex-mini',
'gpt-5.2',
'gpt-5.2-low',
'gpt-5.3-codex',
'gpt-5.4',
'gpt-5.4-mini',
'gpt-4o',
'gpt-4o-mini',
'gpt-4.1',
'gpt-4.1-mini',
'gpt-4.1-nano',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-3.1-pro-preview',
'gemini-3-flash-preview',
'gemini-3.1-pro',
'gemini-3-flash',
'cursor-auto',
'cursor-agent-auto',
'copilot-auto',
'copilot-openai-auto',
'kiro-auto',
'cline-auto',
'qwen-auto',
'kimi-auto',
'kimi-for-coding',
'kimi-k2-thinking-turbo',
'kimi-k2.6',
'o3',
'o4-mini',
'deepseek-coder',
'deepseek-coder-max',
'deepseek-r1',
'MiniMax-M2.7',
'MiniMax-M2.7-highspeed',
]
describe('post-hoist resolution stability', () => {
it('every known model resolves to a non-empty short name', () => {
for (const name of KNOWN_NAMES) {
const short = getShortModelName(name)
expect(short, `short name for ${name}`).toBeTruthy()
expect(typeof short, `short name for ${name}`).toBe('string')
}
})
it('gpt-5-mini does NOT collide with gpt-5 (longest-prefix wins)', () => {
expect(getShortModelName('gpt-5-mini')).toBe('GPT-5 Mini')
expect(getShortModelName('gpt-5')).toBe('GPT-5')
expect(getShortModelName('gpt-5-nano')).toBe('GPT-5 Nano')
expect(getShortModelName('gpt-5-pro')).toBe('GPT-5 Pro')
})
it('gpt-5.1-codex-mini does NOT collapse to gpt-5.1-codex or gpt-5', () => {
expect(getShortModelName('gpt-5.1-codex-mini')).toBe('GPT-5.1 Codex Mini')
expect(getShortModelName('gpt-5.1-codex')).toBe('GPT-5.1 Codex')
expect(getShortModelName('gpt-5.1')).toBe('GPT-5.1')
})
it('claude-haiku-4-5 does NOT collapse to claude-haiku-4 or claude-3-5-haiku', () => {
expect(getShortModelName('claude-haiku-4-5')).toBe('Haiku 4.5')
expect(getShortModelName('claude-3-5-haiku')).toBe('Haiku 3.5')
})
it('kimi managed aliases resolve to priced Kimi models', () => {
expect(getShortModelName('kimi-auto')).toBe('Kimi (auto)')
expect(getShortModelName('kimi-for-coding')).toBe('Kimi for Coding')
expect(getShortModelName('kimi-k2-thinking-turbo')).toBe('Kimi K2 Thinking Turbo')
expect(getShortModelName('kimi-k2.6')).toBe('Kimi K2.6')
expect(getModelCosts('kimi-auto')?.inputCostPerToken).toBeGreaterThan(0)
})
it('getModelCosts returns positive token costs for every known name', () => {
for (const name of KNOWN_NAMES) {
const c = getModelCosts(name)
expect(c, `costs for ${name}`).not.toBeNull()
expect(c!.inputCostPerToken).toBeGreaterThan(0)
expect(c!.outputCostPerToken).toBeGreaterThan(0)
}
})
it('calculateCost is stable for a typical Sonnet 4.6 turn', () => {
// 1k input, 2k output, 50k cache read — common Claude Code shape.
const cost = calculateCost('claude-sonnet-4-6', 1000, 2000, 0, 50_000, 0)
expect(cost).toBeGreaterThan(0)
expect(Number.isFinite(cost)).toBe(true)
})
it('calculateCost clamps NaN/negative inputs to 0', () => {
const c1 = calculateCost('claude-sonnet-4-6', NaN, 1000, 0, 0, 0)
const c2 = calculateCost('claude-sonnet-4-6', 0, 1000, 0, 0, 0)
expect(c1).toBe(c2)
const c3 = calculateCost('claude-sonnet-4-6', -1000, 1000, 0, 0, 0)
expect(c3).toBe(c2)
})
it('repeated calls return the same cost (memoized sort cache is consistent)', () => {
const a = getModelCosts('gpt-5-mini')
const b = getModelCosts('gpt-5-mini')
const c = getModelCosts('gpt-5-mini')
expect(a).toEqual(b)
expect(b).toEqual(c)
})
})