Skip to content

Commit 70d4584

Browse files
authored
fix(kimi): price kimi-for-coding by timestamp
Fixes #1106.
1 parent 63cf17e commit 70d4584

3 files changed

Lines changed: 117 additions & 7 deletions

File tree

‎docs/guide/kimi/index.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ Expected files are discovered under:
5151

5252
Only `StatusUpdate` messages with non-zero token usage are included.
5353

54+
## Cost Calculation
55+
56+
Kimi rows do not store recorded USD cost, so ccusage estimates cost from token counts and LiteLLM pricing. The default `kimi-for-coding` model is kept as the displayed model name, but pricing resolves to Moonshot K2.5 before `2026-04-20T15:28:10.072Z` and Moonshot K2.6 at or after that timestamp.
57+
5458
## Environment Variables
5559

5660
| Variable | Description |

‎rust/crates/ccusage/src/adapter/kimi/parser.rs‎

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{
1515

1616
const DEFAULT_MODEL: &str = "kimi-for-coding";
1717
const DEFAULT_PROVIDER: &str = "moonshot";
18+
const KIMI_FOR_CODING_K2_6_CUTOFF_MS: i64 = 1_776_698_890_072;
1819

1920
#[derive(Debug, Clone)]
2021
pub(super) struct KimiUsageEntry {
@@ -212,7 +213,7 @@ fn calculate_kimi_cost(
212213
match mode {
213214
CostMode::Display => 0.0,
214215
CostMode::Auto | CostMode::Calculate => {
215-
for candidate in model_candidates(&entry.model) {
216+
for candidate in model_candidates(entry) {
216217
if pricing.find(&candidate).is_some() {
217218
return calculate_cost_for_usage(
218219
Some(&candidate),
@@ -228,17 +229,29 @@ fn calculate_kimi_cost(
228229
}
229230
}
230231

231-
fn model_candidates(model: &str) -> Vec<String> {
232-
let mut candidates = vec![
233-
format!("{DEFAULT_PROVIDER}/{model}"),
234-
format!("kimi/{model}"),
235-
model.to_string(),
236-
];
232+
fn model_candidates(entry: &KimiUsageEntry) -> Vec<String> {
233+
let mut candidates = Vec::new();
234+
if entry.model == DEFAULT_MODEL {
235+
candidates.push(kimi_for_coding_pricing_model(entry.timestamp).to_string());
236+
}
237+
candidates.extend([
238+
format!("{DEFAULT_PROVIDER}/{}", entry.model),
239+
format!("kimi/{}", entry.model),
240+
entry.model.clone(),
241+
]);
237242
let mut seen = std::collections::HashSet::new();
238243
candidates.retain(|candidate| seen.insert(candidate.clone()));
239244
candidates
240245
}
241246

247+
fn kimi_for_coding_pricing_model(timestamp: TimestampMs) -> &'static str {
248+
if timestamp.as_millis() < KIMI_FOR_CODING_K2_6_CUTOFF_MS {
249+
"moonshot/kimi-k2.5"
250+
} else {
251+
"moonshot/kimi-k2.6"
252+
}
253+
}
254+
242255
#[cfg(test)]
243256
mod tests {
244257
use std::{fs, path::PathBuf};
@@ -279,4 +292,43 @@ mod tests {
279292
assert_eq!(entry.output_tokens, 432);
280293
assert_eq!(entry.extra_total_tokens, 0);
281294
}
295+
296+
#[test]
297+
fn prices_default_kimi_model_by_timestamp_without_changing_display_model() {
298+
let pricing = PricingMap::load_embedded();
299+
let usage = TokenUsageRaw {
300+
input_tokens: 100,
301+
output_tokens: 50,
302+
cache_creation_input_tokens: 20,
303+
cache_read_input_tokens: 10,
304+
speed: None,
305+
};
306+
let before_cutoff = KimiUsageEntry {
307+
timestamp: TimestampMs::from_millis(1_776_698_890_071),
308+
timestamp_text: "2026-04-20T15:28:10.071Z".to_string(),
309+
session_id: "session-a".to_string(),
310+
model: DEFAULT_MODEL.to_string(),
311+
message_id: Some("msg-before".to_string()),
312+
input_tokens: usage.input_tokens,
313+
output_tokens: usage.output_tokens,
314+
cache_creation_tokens: usage.cache_creation_input_tokens,
315+
cache_read_tokens: usage.cache_read_input_tokens,
316+
extra_total_tokens: 0,
317+
};
318+
let at_cutoff = KimiUsageEntry {
319+
timestamp: TimestampMs::from_millis(1_776_698_890_072),
320+
timestamp_text: "2026-04-20T15:28:10.072Z".to_string(),
321+
message_id: Some("msg-at".to_string()),
322+
..before_cutoff.clone()
323+
};
324+
325+
let before_cost = calculate_kimi_cost(&before_cutoff, CostMode::Calculate, &pricing, usage);
326+
let at_cost = calculate_kimi_cost(&at_cutoff, CostMode::Calculate, &pricing, usage);
327+
let loaded = kimi_entry_to_loaded(at_cutoff, None, CostMode::Calculate, &pricing);
328+
329+
assert!((before_cost - 0.000226).abs() < f64::EPSILON);
330+
assert!((at_cost - 0.00032035).abs() < f64::EPSILON);
331+
assert_eq!(loaded.model.as_deref(), Some(DEFAULT_MODEL));
332+
assert_eq!(loaded.data.message.model.as_deref(), Some(DEFAULT_MODEL));
333+
}
282334
}

‎rust/crates/ccusage/src/pricing.rs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,38 @@ impl PricingMap {
421421
fast_multiplier: 1.0,
422422
},
423423
);
424+
// Source: https://platform.kimi.ai/docs/pricing/chat-k25
425+
self.entries.insert(
426+
"moonshot/kimi-k2.5".to_string(),
427+
Pricing {
428+
input: 0.6e-6,
429+
output: 3e-6,
430+
cache_create: 0.75e-6,
431+
cache_read: 0.1e-6,
432+
cache_read_explicit: true,
433+
input_above_200k: None,
434+
output_above_200k: None,
435+
cache_create_above_200k: None,
436+
cache_read_above_200k: None,
437+
fast_multiplier: 1.0,
438+
},
439+
);
440+
// Source: https://platform.kimi.ai/docs/pricing/chat-k26
441+
self.entries.insert(
442+
"moonshot/kimi-k2.6".to_string(),
443+
Pricing {
444+
input: 0.95e-6,
445+
output: 4e-6,
446+
cache_create: 1.1875e-6,
447+
cache_read: 0.16e-6,
448+
cache_read_explicit: true,
449+
input_above_200k: None,
450+
output_above_200k: None,
451+
cache_create_above_200k: None,
452+
cache_read_above_200k: None,
453+
fast_multiplier: 1.0,
454+
},
455+
);
424456
let gpt_5_1_pricing = Pricing {
425457
input: 1.25e-6,
426458
output: 10e-6,
@@ -512,6 +544,10 @@ impl PricingMap {
512544
self.context_limits
513545
.insert("grok-4.3".to_string(), 1_000_000);
514546
self.context_limits.insert("gpt-5.4".to_string(), 1_050_000);
547+
self.context_limits
548+
.insert("moonshot/kimi-k2.5".to_string(), 262_144);
549+
self.context_limits
550+
.insert("moonshot/kimi-k2.6".to_string(), 262_144);
515551

516552
for model in [
517553
"claude-opus-4-5",
@@ -597,6 +633,24 @@ mod tests {
597633
assert_eq!(pricing.context_limit("grok-4.3"), Some(1_000_000));
598634
}
599635

636+
#[test]
637+
fn embedded_pricing_includes_moonshot_kimi_for_offline_reports() {
638+
let pricing = PricingMap::load_embedded();
639+
let kimi_k25 = pricing.find("moonshot/kimi-k2.5").unwrap();
640+
let kimi_k26 = pricing.find("moonshot/kimi-k2.6").unwrap();
641+
642+
assert_eq!(kimi_k25.input, 0.6e-6);
643+
assert_eq!(kimi_k25.output, 3e-6);
644+
assert_eq!(kimi_k25.cache_read, 0.1e-6);
645+
assert!(kimi_k25.cache_read_explicit);
646+
assert_eq!(kimi_k26.input, 0.95e-6);
647+
assert_eq!(kimi_k26.output, 4e-6);
648+
assert_eq!(kimi_k26.cache_read, 0.16e-6);
649+
assert!(kimi_k26.cache_read_explicit);
650+
assert_eq!(pricing.context_limit("moonshot/kimi-k2.5"), Some(262_144));
651+
assert_eq!(pricing.context_limit("moonshot/kimi-k2.6"), Some(262_144));
652+
}
653+
600654
#[test]
601655
fn records_whether_cache_read_rate_came_from_litellm_pricing() {
602656
let mut pricing = PricingMap::default();

0 commit comments

Comments
 (0)