fix(dashboard): k3-256k 按 k3 牌价计费

- 命中零价订阅条目(kimi-for-coding/k3-256k)时,剥掉上下文长度后缀重查基础模型(k3-256k → moonshotai/kimi-k3)
- 无命中时同样先尝试剥后缀再回落旧表
- cache_write 改按解析后的 price_id 取值
- 新增回归测试 context_suffix_variant_bills_at_base_model
This commit is contained in:
KimiSwitch Dev committed 2026-08-01 19:04:35 +08:00
1 parent 3fbd70932c
commit 9fd22ef55a
1 file changed
+60 -2
+60 -2
View File
@@ -581,6 +581,26 @@ fn models_dev_lookup(model_name: &str) -> Option<(String, ModelsDevCost)> {
Some((key.clone(), *cost))
}
/// Strip a trailing context-size suffix ("-256k", "-128k", "-1m") from a model
/// id: context variants like "k3-256k" bill at the base model's ("k3") price.
/// Returns the stripped id (provider prefix preserved), or None when there is
/// no such suffix.
fn strip_context_suffix(model_name: &str) -> Option<String> {
let (prefix, bare) = match model_name.rsplit_once('/') {
Some((p, b)) => (Some(p), b),
None => (None, model_name),
};
let (stem, suffix) = bare.rsplit_once('-')?;
let digits = suffix.strip_suffix(|c| matches!(c, 'k' | 'K' | 'm' | 'M'))?;
if stem.is_empty() || digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some(match prefix {
Some(p) => format!("{p}/{stem}"),
None => stem.to_string(),
})
}
fn match_price(model_name: &str) -> (String, f64, f64, f64, bool) {
let bare = match model_name.rsplit_once('/') {
Some((_, b)) => b,
@@ -588,8 +608,27 @@ fn match_price(model_name: &str) -> (String, f64, f64, f64, bool) {
};
// 1) models.dev snapshot first (authoritative, all providers).
if let Some((id, c)) = models_dev_lookup(model_name) {
if c.input > 0.0 || c.output > 0.0 {
return (id, c.cache_read, c.input, c.output, false);
}
// Zero-priced listing (subscription-plan entries such as
// kimi-for-coding/k3-256k): bill at the base model's list price.
if let Some(base) = strip_context_suffix(model_name) {
if let Some((base_id, base_c)) = models_dev_lookup(&base) {
if base_c.input > 0.0 || base_c.output > 0.0 {
return (base_id, base_c.cache_read, base_c.input, base_c.output, false);
}
}
}
return (id, c.cache_read, c.input, c.output, false);
}
// 1b) no direct match: retry with the context-size suffix stripped
// ("k3-256k" → "k3") before falling back to legacy tables.
if let Some(base) = strip_context_suffix(model_name) {
if let Some((id, c)) = models_dev_lookup(&base) {
return (id, c.cache_read, c.input, c.output, false);
}
}
// 2) legacy Kimi table (kept as a fallback for names not in models.dev).
let bare_l = bare.to_ascii_lowercase();
for p in &list_prices() {
@@ -603,10 +642,12 @@ fn match_price(model_name: &str) -> (String, f64, f64, f64, bool) {
}
fn cost_for_usage(input_other: u64, output: u64, cache_read: u64, cache_create: u64, model: &str) -> (f64, bool) {
let (_price_id, cache_hit, input_price, output_price, est) = match_price(model);
let (price_id, cache_hit, input_price, output_price, est) = match_price(model);
// models.dev reports a separate cache_write price; fall back to input when
// absent (models without a cache_write field bill cache creation at input).
let cache_write_price = models_dev_lookup(model)
// Resolve via the matched price id so context-suffix fallbacks (k3-256k →
// k3) also pick up the base model's cache_write price.
let cache_write_price = models_dev_lookup(&price_id)
.and_then(|(_, c)| c.cache_write)
.unwrap_or(input_price);
let cost = (input_other as f64 / 1e6) * input_price
@@ -1581,6 +1622,23 @@ mod pricing_tests {
assert_eq!(input, 0.6);
}
#[test]
fn context_suffix_variant_bills_at_base_model() {
// kimi-code/k3-256k only exists as a zero-priced subscription entry
// (kimi-for-coding/k3-256k); it must bill at the k3 list price.
let (id, ch, input, output, est) = match_price("kimi-code/k3-256k");
assert!(!est);
assert_eq!(id, "moonshotai/kimi-k3");
assert_eq!(input, 3.0);
assert_eq!(output, 15.0);
assert_eq!(ch, 0.3);
// The strip helper leaves normal ids alone.
assert_eq!(strip_context_suffix("kimi-code/k3-256k").as_deref(), Some("kimi-code/k3"));
assert_eq!(strip_context_suffix("glm-4.6"), None);
assert_eq!(strip_context_suffix("kimi-k2-thinking"), None);
}
#[test]
fn cache_write_price_preferred_when_present() {
// glm-4.6 has cache_write: 0 → cache creation billed at 0, not input.