From c826f1a479d80cfa9ece366c6b695728e8bb153c Mon Sep 17 00:00:00 2001 From: Resham Joshi <65915470+iamtoruk@users.noreply.github.com> Date: Thu, 8 Oct 2026 00:02:38 +0200 Subject: [PATCH] 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. --- src/daily-cache.ts | 8 ++- src/models.ts | 52 ++++++++++++++--- src/parser.ts | 4 +- src/providers/kimi.ts | 4 +- src/providers/kimicode.ts | 4 +- tests/kimi-stand-in-estimated.test.ts | 57 +++++++++++++++++++ tests/menubar-json.test.ts | 2 +- tests/models-hoist.test.ts | 2 +- tests/models.test.ts | 80 +++++++++++++++++++++++++-- tests/providers/kimi.test.ts | 26 +++++++++ 10 files changed, 218 insertions(+), 21 deletions(-) create mode 100644 tests/kimi-stand-in-estimated.test.ts diff --git a/src/daily-cache.ts b/src/daily-cache.ts index b162b9a2..0fa47095 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -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 diff --git a/src/models.ts b/src/models.ts index eefac44b..31c646b8 100644 --- a/src/models.ts +++ b/src/models.ts @@ -131,6 +131,9 @@ const BUILTIN_PRICE_OVERRIDES: Record = { '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 = { '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 = { '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', } diff --git a/src/parser.ts b/src/parser.ts index e76a3942..df98994d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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, diff --git a/src/providers/kimi.ts b/src/providers/kimi.ts index ceb98519..ae6ddb00 100644 --- a/src/providers/kimi.ts +++ b/src/providers/kimi.ts @@ -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 @@ -298,7 +298,7 @@ function createParser(source: SessionSource, shareDir: string, seenKeys: Set): SessionPars reasoningTokens: 0, webSearchRequests: 0, costUSD: calculateCost( - realModel, + pricingModelAt(realModel, timestamp), inputTokens, outputTokens, cacheCreationInputTokens, diff --git a/tests/kimi-stand-in-estimated.test.ts b/tests/kimi-stand-in-estimated.test.ts new file mode 100644 index 00000000..bfd9599c --- /dev/null +++ b/tests/kimi-stand-in-estimated.test.ts @@ -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>) => 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]]) +}) diff --git a/tests/menubar-json.test.ts b/tests/menubar-json.test.ts index 4e1e9279..2ad2862a 100644 --- a/tests/menubar-json.test.ts +++ b/tests/menubar-json.test.ts @@ -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() diff --git a/tests/models-hoist.test.ts b/tests/models-hoist.test.ts index 324e6ff6..68c28772 100644 --- a/tests/models-hoist.test.ts +++ b/tests/models-hoist.test.ts @@ -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) diff --git a/tests/models.test.ts b/tests/models.test.ts index 8c125e83..e5d4acf1 100644 --- a/tests/models.test.ts +++ b/tests/models.test.ts @@ -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) diff --git a/tests/providers/kimi.test.ts b/tests/providers/kimi.test.ts index 486a03ee..eab0de6c 100644 --- a/tests/providers/kimi.test.ts +++ b/tests/providers/kimi.test.ts @@ -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', [