mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-20 11:44:39 +00:00
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.
This commit is contained in:
parent
d049b38255
commit
c826f1a479
10 changed files with 218 additions and 21 deletions
|
|
@ -308,7 +308,13 @@ import type { DateRange, ProjectSummary } from './types.js'
|
|||
// billed again at the cache rates. Only input tokens and estimated cost fall;
|
||||
// call counts are unchanged, so no PENDING_REDERIVE_PROVIDER_VERSIONS entry is
|
||||
// needed.
|
||||
export const DAILY_CACHE_VERSION = 63
|
||||
// v64: Kimi Code's `kimi-for-coding` prices by the model it served on the call's
|
||||
// date (K2.5, K2.6, K2.7 Code) instead of retired K2 Thinking, and
|
||||
// `kimi-for-coding-highspeed` prices at K2.7 Code HighSpeed instead of $0.
|
||||
// From 11 Sep 2026 the alias served K2.8 Preview, which has no published rate;
|
||||
// those calls price as K2.7 Code and are marked estimated.
|
||||
// Calls are unchanged, so no PENDING_REDERIVE_PROVIDER_VERSIONS entry is needed.
|
||||
export const DAILY_CACHE_VERSION = 64
|
||||
const MIN_SUPPORTED_VERSION = 28
|
||||
|
||||
/// Providers whose per-day CALL COUNT means something different at
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ const BUILTIN_PRICE_OVERRIDES: Record<string, SnapshotEntry> = {
|
|||
'composer-2': [0.5e-6, 2.5e-6, 0.5e-6, 0.2e-6],
|
||||
'composer-1.5': [3.5e-6, 17.5e-6, 3.5e-6, 0.35e-6],
|
||||
'composer-1': [1.25e-6, 10e-6, 1.25e-6, 0.125e-6],
|
||||
// Moonshot's published rate (platform.kimi.ai/docs/pricing/chat): $1.90 miss,
|
||||
// $0.38 hit, $8.00 output. LiteLLM only carries a reseller row for it.
|
||||
'kimi-k2.7-code-highspeed': [1.9e-6, 8e-6, null, 0.38e-6],
|
||||
}
|
||||
|
||||
// Assemble a ModelCosts, applying the cache-cost heuristics (write = 1.25x
|
||||
|
|
@ -676,8 +679,14 @@ const BUILTIN_ALIASES: Record<string, string> = {
|
|||
'orcarouter/fusion-flash': 'openai/gpt-oss-120b',
|
||||
'orcarouter/fusion-mini': 'openai/gpt-oss-120b',
|
||||
'kimi-auto': 'kimi-k2-thinking',
|
||||
'kimi-code': 'kimi-k2-thinking',
|
||||
'kimi-for-coding': 'kimi-k2-thinking',
|
||||
// `kimi-for-coding` is Kimi Code's moving alias; `kimi-code` is kimi-cli's
|
||||
// spelling of the same SKU. pricingModelAt prices older calls by the model
|
||||
// the alias served then. K2.8 Preview (11 Sep 2026 on) has no Open Platform
|
||||
// price, so it stays on K2.7 Code's.
|
||||
'kimi-code': 'kimi-k2.7-code',
|
||||
'kimi-for-coding': 'kimi-k2.7-code',
|
||||
// HighSpeed has been K2.7 Code HighSpeed since it launched on 9 Jul 2026.
|
||||
'kimi-for-coding-highspeed': 'kimi-k2.7-code-highspeed',
|
||||
// Kimi Code wires report the bare `k3` id in llm.request.model; without an
|
||||
// alias those calls priced at $0 and the provider looked absent in the UI.
|
||||
'k3': 'kimi-k3',
|
||||
|
|
@ -989,7 +998,6 @@ export function isBuiltInFlatRateModel(model: string): boolean {
|
|||
if (
|
||||
leaf === 'auto'
|
||||
|| leaf === 'auto-genius'
|
||||
|| leaf === 'kimi-for-coding-highspeed'
|
||||
) return true
|
||||
if (leaf.startsWith('grok-composer-')) return true
|
||||
if (leaf.startsWith('warp-auto-')) return true
|
||||
|
|
@ -1244,13 +1252,39 @@ function stripKnownPricingVariantSuffix(model: string): string | null {
|
|||
}
|
||||
|
||||
const AUTO_REVIEW_LUNA_FROM = Date.parse('2026-07-30T00:00:00Z')
|
||||
// kimi-cli labelled the alias "powered by kimi-k2.5" from 27 Jan 2026 (1.2) and
|
||||
// dropped that on 13 Apr 2026 (#1860) as K2.6 rolled out; Kimi Code's What's
|
||||
// New dates K2.7 Code to 12 Jun 2026. What it served before K2.5 is unsourced,
|
||||
// so those calls keep the K2 Thinking rate they always had.
|
||||
const KIMI_CODING_K2_5_FROM = Date.parse('2026-01-27T00:00:00Z')
|
||||
const KIMI_CODING_K2_6_FROM = Date.parse('2026-04-13T00:00:00Z')
|
||||
const KIMI_CODING_K2_7_FROM = Date.parse('2026-06-12T00:00:00Z')
|
||||
const KIMI_CODING_K2_8_FROM = Date.parse('2026-09-11T00:00:00Z')
|
||||
|
||||
/// The model a call is priced by. Only `codex-auto-review` depends on the
|
||||
/// call's date (see BUILTIN_ALIASES); a user alias for it still wins, and a
|
||||
/// missing or unparseable timestamp keeps the forward default.
|
||||
/// The model a call is priced by. Only `codex-auto-review` and the Kimi Code
|
||||
/// alias depend on the call's date (see BUILTIN_ALIASES); a user alias for
|
||||
/// them still wins, and a missing or unparseable timestamp keeps the forward
|
||||
/// default.
|
||||
export function pricingModelAt(model: string, timestamp: string | undefined): string {
|
||||
if (model.toLowerCase() !== 'codex-auto-review' || Object.hasOwn(userAliases, model) || userPriceOverrides.has(model)) return model
|
||||
return Date.parse(timestamp ?? '') < AUTO_REVIEW_LUNA_FROM ? 'gpt-5.4' : model
|
||||
const id = model.toLowerCase()
|
||||
if (id !== 'codex-auto-review' && id !== 'kimi-for-coding' && id !== 'kimi-code') return model
|
||||
if (Object.hasOwn(userAliases, model) || userPriceOverrides.has(model)) return model
|
||||
const at = Date.parse(timestamp ?? '')
|
||||
if (id === 'codex-auto-review') return at < AUTO_REVIEW_LUNA_FROM ? 'gpt-5.4' : model
|
||||
if (at < KIMI_CODING_K2_5_FROM) return 'kimi-k2-thinking'
|
||||
if (at < KIMI_CODING_K2_6_FROM) return 'kimi-k2.5'
|
||||
return at < KIMI_CODING_K2_7_FROM ? 'kimi-k2.6' : model
|
||||
}
|
||||
|
||||
/// True when pricingModelAt stands in for a model with no published rate:
|
||||
/// Kimi Code's alias served K2.8 Preview from 11 Sep 2026, priced as K2.7 Code.
|
||||
/// A missing or unparseable timestamp gets the forward default, so it counts.
|
||||
export function isStandInPricedAt(model: string, timestamp: string | undefined): boolean {
|
||||
const id = model.toLowerCase()
|
||||
if (id !== 'kimi-for-coding' && id !== 'kimi-code') return false
|
||||
if (Object.hasOwn(userAliases, model) || userPriceOverrides.has(model)) return false
|
||||
const at = Date.parse(timestamp ?? '')
|
||||
return !(at < KIMI_CODING_K2_8_FROM)
|
||||
}
|
||||
|
||||
export function getModelCosts(model: string): ModelCosts | null {
|
||||
|
|
@ -1548,6 +1582,8 @@ const autoModelNames: Record<string, string> = {
|
|||
'openclaw-auto': 'OpenClaw (auto)',
|
||||
'qwen-auto': 'Qwen (auto)',
|
||||
'kimi-auto': 'Kimi (auto)',
|
||||
'kimi-for-coding': 'Kimi for Coding',
|
||||
'kimi-for-coding-highspeed': 'Kimi for Coding HighSpeed',
|
||||
'codex-auto-review': 'Codex Auto Review',
|
||||
'gpt-reserve': 'Luna Reserve',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { createHash } from 'crypto'
|
|||
import { performance } from 'node:perf_hooks'
|
||||
import { basename, dirname, join, resolve, sep } from 'path'
|
||||
import { FS_SCAN_CONCURRENCY, mapWithConcurrency, readSessionLines } from './fs-utils.js'
|
||||
import { billableOutputTokens, calculateCost, calculateLocalModelSavings, getShortModelName, modelRowKey, pricingModelAt, isProxiedPath, getProxyPathsConfigHash, getModelAliasesConfigHash, getPriceOverridesConfigHash, getLocalModelSavingsConfigHash, recordedCostFallback, getModelRoute } from './models.js'
|
||||
import { billableOutputTokens, calculateCost, calculateLocalModelSavings, getShortModelName, modelRowKey, pricingModelAt, isProxiedPath, getProxyPathsConfigHash, getModelAliasesConfigHash, getPriceOverridesConfigHash, getLocalModelSavingsConfigHash, recordedCostFallback, getModelRoute, isStandInPricedAt } from './models.js'
|
||||
import { resolveSubagentAttribution, sessionIdentity } from './sessions-report.js'
|
||||
import { normalizeContentBlocks, flatSlice, flatString } from './content-utils.js'
|
||||
import { discoverAllSessions, discoverAllSessionsWithFailures, getProvider } from './providers/index.js'
|
||||
|
|
@ -2869,7 +2869,7 @@ function cachedCallToApiCall(call: CachedCall): ParsedApiCall {
|
|||
webSearchRequests: u.webSearchRequests,
|
||||
},
|
||||
costUSD: call.costUSD ?? recordedCostFallback(call.model, costUSD, call.fallbackCostUSD) ?? costUSD,
|
||||
isEstimated: call.isEstimated,
|
||||
isEstimated: call.isEstimated || (call.costUSD === undefined && isStandInPricedAt(call.model, call.timestamp)) || undefined,
|
||||
tools: call.tools,
|
||||
mcpTools: extractMcpTools(call.tools),
|
||||
skills: call.skills,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { homedir } from 'os'
|
|||
|
||||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import { readSessionLines } from '../fs-utils.js'
|
||||
import { calculateCost, getShortModelName } from '../models.js'
|
||||
import { calculateCost, getShortModelName, pricingModelAt } from '../models.js'
|
||||
import type { ProbeRoot, ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
|
@ -298,7 +298,7 @@ function createParser(source: SessionSource, shareDir: string, seenKeys: Set<str
|
|||
|
||||
const model = stringField(envelope.payload, 'model') ?? stringField(envelope.payload, 'model_name') ?? configuredModel
|
||||
const costUSD = calculateCost(
|
||||
model,
|
||||
pricingModelAt(model, envelope.timestamp),
|
||||
usage.inputTokens,
|
||||
usage.outputTokens,
|
||||
usage.cacheCreationInputTokens,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { homedir } from 'node:os'
|
|||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
|
||||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import { calculateCost } from '../models.js'
|
||||
import { calculateCost, pricingModelAt } from '../models.js'
|
||||
import { FS_SCAN_CONCURRENCY, mapWithConcurrency } from '../fs-utils.js'
|
||||
import type { ParsedProviderCall, ProbeRoot, Provider, SessionParser, SessionSource } from './types.js'
|
||||
|
||||
|
|
@ -377,7 +377,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
reasoningTokens: 0,
|
||||
webSearchRequests: 0,
|
||||
costUSD: calculateCost(
|
||||
realModel,
|
||||
pricingModelAt(realModel, timestamp),
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheCreationInputTokens,
|
||||
|
|
|
|||
57
tests/kimi-stand-in-estimated.test.ts
Normal file
57
tests/kimi-stand-in-estimated.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// The K2.8 Preview stand-in flag is set where every call is read
|
||||
// (cachedCallToApiCall), so it reaches calls from any provider, cold or warm.
|
||||
// Own file because the codex provider captures CODEX_HOME at import.
|
||||
|
||||
import { afterAll, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { mkdir, rm, writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
const testRoot = vi.hoisted(() => {
|
||||
const root = `${process.env['TMPDIR'] || '/tmp'}/kimi-stand-in-${process.pid}-${Date.now()}`
|
||||
process.env['HOME'] = `${root}/home`
|
||||
process.env['USERPROFILE'] = `${root}/home`
|
||||
process.env['CODEX_HOME'] = `${root}/codex`
|
||||
return root
|
||||
})
|
||||
|
||||
const CODEX_HOME = join(testRoot, 'codex')
|
||||
const CACHE_DIR = join(testRoot, 'cache')
|
||||
|
||||
beforeEach(() => {
|
||||
process.env['HOME'] = join(testRoot, 'home')
|
||||
process.env['USERPROFILE'] = join(testRoot, 'home')
|
||||
process.env['CODEX_HOME'] = CODEX_HOME
|
||||
process.env['CODEBURN_CACHE_DIR'] = CACHE_DIR
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(testRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function rollout(id: string, day: string): string {
|
||||
const usage = { input_tokens: 1000, cached_input_tokens: 0, output_tokens: 100, reasoning_output_tokens: 0, total_tokens: 1100 }
|
||||
return [
|
||||
JSON.stringify({ type: 'session_meta', timestamp: `${day}T10:00:00Z`, payload: { session_id: id, model: 'kimi-for-coding', cwd: '/Users/test/proj', originator: 'codex_cli_rs' } }),
|
||||
JSON.stringify({ type: 'response_item', timestamp: `${day}T10:00:10Z`, payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hi' }] } }),
|
||||
JSON.stringify({ type: 'event_msg', timestamp: `${day}T10:01:00Z`, payload: { type: 'token_count', info: { model: 'kimi-for-coding', last_token_usage: usage, total_token_usage: usage } } }),
|
||||
].join('\n') + '\n'
|
||||
}
|
||||
|
||||
it('marks kimi-for-coding calls from 11 Sep 2026 estimated on a cold parse and a cache-rehydrated read', async () => {
|
||||
await mkdir(join(CODEX_HOME, 'sessions', '2026', '09', '10'), { recursive: true })
|
||||
await mkdir(join(CODEX_HOME, 'sessions', '2026', '09', '12'), { recursive: true })
|
||||
await mkdir(CACHE_DIR, { recursive: true })
|
||||
await writeFile(join(CODEX_HOME, 'sessions', '2026', '09', '10', 'rollout-k27.jsonl'), rollout('k27', '2026-09-10'))
|
||||
await writeFile(join(CODEX_HOME, 'sessions', '2026', '09', '12', 'rollout-k28.jsonl'), rollout('k28', '2026-09-12'))
|
||||
|
||||
const { clearSessionCache, parseAllSessions } = await import('../src/parser.js')
|
||||
const flags = (projects: Awaited<ReturnType<typeof parseAllSessions>>) => projects
|
||||
.flatMap(p => p.sessions).flatMap(s => s.turns).flatMap(t => t.assistantCalls)
|
||||
.map(c => [c.timestamp.slice(0, 10), c.isEstimated ?? false])
|
||||
.sort()
|
||||
|
||||
clearSessionCache()
|
||||
expect(flags(await parseAllSessions(undefined, 'codex'))).toEqual([['2026-09-10', false], ['2026-09-12', true]])
|
||||
clearSessionCache()
|
||||
expect(flags(await parseAllSessions(undefined, 'codex'))).toEqual([['2026-09-10', false], ['2026-09-12', true]])
|
||||
})
|
||||
|
|
@ -208,7 +208,7 @@ describe('buildMenubarPayload', () => {
|
|||
const kimiK3 = payload.current.topModels.find(m => m.name === 'Kimi K3')!
|
||||
expect(kimiK3.cost).toBeCloseTo(2.5 + 0.5 + 1.2)
|
||||
expect(kimiK3.calls).toBe(78 + 2 + 40)
|
||||
const k2 = payload.current.topModels.find(m => m.name === 'Kimi K2 Thinking')!
|
||||
const k2 = payload.current.topModels.find(m => m.name === 'Kimi for Coding')!
|
||||
expect(k2.cost).toBeCloseTo(0.06)
|
||||
expect(payload.current.topModels.find(m => m.name === 'k3')).toBeUndefined()
|
||||
expect(payload.current.topModels.find(m => m.name === 'k3-agent')).toBeUndefined()
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ describe('post-hoist resolution stability', () => {
|
|||
|
||||
it('kimi managed aliases resolve to priced Kimi models', () => {
|
||||
expect(getShortModelName('kimi-auto')).toBe('Kimi (auto)')
|
||||
expect(getShortModelName('kimi-for-coding')).toBe('Kimi K2 Thinking')
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
snapshotPricingState,
|
||||
restorePricingState,
|
||||
pricingModelAt,
|
||||
isStandInPricedAt,
|
||||
} from '../src/models.js'
|
||||
import { getDailyCacheConfigHash } from '../src/usage-aggregator.js'
|
||||
import snapshotData from '../src/data/litellm-snapshot.json' with { type: 'json' }
|
||||
|
|
@ -572,7 +573,7 @@ describe('resolveCanonicalModelId', () => {
|
|||
expect(resolveCanonicalModelId('gpt-5-fast')).toBe('gpt-5')
|
||||
expect(resolveCanonicalModelId('gpt-5-untracked-xyz')).toBe('gpt-5-untracked-xyz')
|
||||
expect(resolveCanonicalModelId('claude-opus-4.6')).toBe('claude-opus-4-6')
|
||||
expect(resolveCanonicalModelId('kimi-code')).toBe('kimi-k2-thinking')
|
||||
expect(resolveCanonicalModelId('kimi-code')).toBe('kimi-k2.7-code')
|
||||
expect(resolveCanonicalModelId('cline-pass/kimi-k3')).toBe('kimi-k3')
|
||||
expect(resolveCanonicalModelId('orcarouter/auto')).not.toBe(resolveCanonicalModelId('claude-sonnet-4-5'))
|
||||
expect(resolveCanonicalModelId('orcarouter/fusion')).toBe(resolveCanonicalModelId('openai/gpt-oss-120b'))
|
||||
|
|
@ -1196,6 +1197,79 @@ describe('Cursor model variants resolve to pricing', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Kimi Code moving alias', () => {
|
||||
const rates = (model: string) => {
|
||||
const c = getModelCosts(model)!
|
||||
return [c.inputCostPerToken * 1e6, c.cacheReadCostPerToken * 1e6, c.outputCostPerToken * 1e6].map(v => +v.toFixed(4))
|
||||
}
|
||||
|
||||
it('prices kimi-for-coding by the model it served on the call date', () => {
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-01-26T23:59:59.999Z')).toBe('kimi-k2-thinking')
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-01-27T00:00:00.000Z')).toBe('kimi-k2.5')
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-04-12T23:59:59.999Z')).toBe('kimi-k2.5')
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-04-13T00:00:00.000Z')).toBe('kimi-k2.6')
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-06-11T23:59:59.999Z')).toBe('kimi-k2.6')
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-06-12T00:00:00.000Z')).toBe('kimi-for-coding')
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-09-27T10:00:00Z')).toBe('kimi-for-coding')
|
||||
expect(pricingModelAt('kimi-code', '2026-03-01T00:00:00Z')).toBe('kimi-k2.5')
|
||||
expect(pricingModelAt('kimi-for-coding', undefined)).toBe('kimi-for-coding')
|
||||
expect(pricingModelAt('kimi-for-coding', 'not a date')).toBe('kimi-for-coding')
|
||||
expect(pricingModelAt('kimi-for-coding-highspeed', '2026-03-01T00:00:00Z')).toBe('kimi-for-coding-highspeed')
|
||||
expect(pricingModelAt('k3', '2026-03-01T00:00:00Z')).toBe('k3')
|
||||
})
|
||||
|
||||
it('resolves each period to Moonshot list prices ($/M input, cache hit, output)', () => {
|
||||
expect(rates('kimi-k2.5')).toEqual([0.6, 0.1, 3])
|
||||
expect(rates('kimi-k2.6')).toEqual([0.95, 0.16, 4])
|
||||
expect(rates('kimi-for-coding')).toEqual([0.95, 0.19, 4])
|
||||
expect(rates('kimi-code')).toEqual([0.95, 0.19, 4])
|
||||
expect(rates('kimi-for-coding-highspeed')).toEqual([1.9, 0.38, 8])
|
||||
expect(rates('k3')).toEqual([3, 0.3, 15])
|
||||
})
|
||||
|
||||
it('treats highspeed as priced, not a flat-rate SKU', () => {
|
||||
expect(isFlatRateModel('kimi-for-coding-highspeed')).toBe(false)
|
||||
expect(calculateCost('kimi-for-coding-highspeed', 1_000_000, 1_000_000, 0, 1_000_000, 0)).toBeCloseTo(10.28)
|
||||
})
|
||||
|
||||
it('marks only the K2.8 Preview period (from 11 Sep 2026) as stand-in priced', () => {
|
||||
expect(isStandInPricedAt('kimi-for-coding', '2026-09-10T23:59:59.999Z')).toBe(false)
|
||||
expect(isStandInPricedAt('kimi-for-coding', '2026-09-11T00:00:00.000Z')).toBe(true)
|
||||
expect(isStandInPricedAt('kimi-code', '2026-09-27T10:00:00Z')).toBe(true)
|
||||
expect(isStandInPricedAt('kimi-for-coding', '2026-07-01T00:00:00Z')).toBe(false)
|
||||
expect(isStandInPricedAt('kimi-for-coding', '2026-03-01T00:00:00Z')).toBe(false)
|
||||
expect(isStandInPricedAt('kimi-for-coding', undefined)).toBe(true)
|
||||
expect(isStandInPricedAt('kimi-for-coding-highspeed', '2026-09-27T10:00:00Z')).toBe(false)
|
||||
expect(isStandInPricedAt('k3', '2026-09-27T10:00:00Z')).toBe(false)
|
||||
setModelAliases({ 'kimi-for-coding': 'kimi-k3' })
|
||||
try {
|
||||
expect(isStandInPricedAt('kimi-for-coding', '2026-09-27T10:00:00Z')).toBe(false)
|
||||
} finally {
|
||||
setModelAliases({})
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the alias name for display', () => {
|
||||
expect(getShortModelName('kimi-for-coding')).toBe('Kimi for Coding')
|
||||
expect(getShortModelName('kimi-for-coding-highspeed')).toBe('Kimi for Coding HighSpeed')
|
||||
})
|
||||
|
||||
it('lets a user alias or price override win over the date rule', () => {
|
||||
setModelAliases({ 'kimi-for-coding': 'kimi-k3' })
|
||||
try {
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-03-01T00:00:00Z')).toBe('kimi-for-coding')
|
||||
} finally {
|
||||
setModelAliases({})
|
||||
}
|
||||
setPriceOverrides({ 'kimi-for-coding': { input: 1, output: 2 } })
|
||||
try {
|
||||
expect(pricingModelAt('kimi-for-coding', '2026-03-01T00:00:00Z')).toBe('kimi-for-coding')
|
||||
} finally {
|
||||
setPriceOverrides({})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Codex activity ids (#1047)', () => {
|
||||
it('keeps the activity label instead of collapsing to the underlying model name', () => {
|
||||
expect(getShortModelName('codex-auto-review')).toBe('Codex Auto Review')
|
||||
|
|
@ -1786,8 +1860,6 @@ describe('findUnpricedModels', () => {
|
|||
{ model: 'auto-genius', calls: 898, cost: 0, tokens: 35_300_000 },
|
||||
{ model: 'cline-pass/auto-genius', calls: 4, cost: 0, tokens: 33_900 },
|
||||
{ model: 'auto', calls: 449, cost: 0, tokens: 17_700_000 },
|
||||
{ model: 'kimi-for-coding-highspeed', calls: 12, cost: 0, tokens: 3_400_000 },
|
||||
{ model: 'moonshot/kimi-for-coding-highspeed', calls: 2, cost: 0, tokens: 80_000 },
|
||||
{ model: 'grok-composer-2.5-fast', calls: 10, cost: 0, tokens: 1_900_000 },
|
||||
{ model: 'Grok Composer 2.5 Fast', calls: 10, cost: 0, tokens: 1_900_000 },
|
||||
{ model: 'Warp Auto (efficient)', calls: 3, cost: 0, tokens: 50_000 },
|
||||
|
|
@ -1827,7 +1899,7 @@ describe('findUnpricedModels', () => {
|
|||
expect(isExpectedFreeModel('warp-auto-efficient')).toBe(false)
|
||||
expect(isExpectedFreeModel('auto-genius')).toBe(true)
|
||||
expect(isExpectedFreeModel('auto')).toBe(true)
|
||||
expect(isExpectedFreeModel('kimi-for-coding-highspeed')).toBe(true)
|
||||
expect(isExpectedFreeModel('kimi-for-coding-highspeed')).toBe(false)
|
||||
expect(isExpectedFreeModel('warp')).toBe(false)
|
||||
expect(isExpectedFreeModel('codex-auto-review')).toBe(false)
|
||||
expect(isExpectedFreeModel('zz-mystery-paid-model-999')).toBe(false)
|
||||
|
|
|
|||
|
|
@ -125,6 +125,32 @@ describe('Kimi provider', () => {
|
|||
expect(calls[0]!.costUSD).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('prices kimi-for-coding by call date', async () => {
|
||||
const wirePath = await writeSession('/Users/test/work/app', 'sess-k28', [
|
||||
record(1789084799, 'StatusUpdate', {
|
||||
message_id: 'before',
|
||||
model_name: 'kimi-for-coding',
|
||||
token_usage: { input_other: 1_000_000, output: 1_000_000 },
|
||||
}),
|
||||
record(1789084800, 'StatusUpdate', {
|
||||
message_id: 'from',
|
||||
model_name: 'kimi-for-coding',
|
||||
token_usage: { input_other: 1_000_000, output: 1_000_000 },
|
||||
}),
|
||||
record(1777593600, 'StatusUpdate', {
|
||||
message_id: 'k26',
|
||||
model_name: 'kimi-for-coding',
|
||||
token_usage: { input_other: 1_000_000, output: 0 },
|
||||
}),
|
||||
])
|
||||
|
||||
const calls = await collect(createKimiProvider(tmpDir), wirePath)
|
||||
|
||||
expect(calls[0]!.costUSD).toBeCloseTo(4.95)
|
||||
expect(calls[1]!.costUSD).toBeCloseTo(4.95)
|
||||
expect(calls[2]!.costUSD).toBeCloseTo(0.95)
|
||||
})
|
||||
|
||||
it('uses content parts, model payload overrides, and message-id deduplication', async () => {
|
||||
process.env.KIMI_MODEL_NAME = 'kimi-k2-thinking'
|
||||
const wirePath = await writeSession('/Users/test/work/app', 'sess-2', [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue