From 4c62dd9aaf9e2502cf8370d6f66c4456dc245406 Mon Sep 17 00:00:00 2001 From: twtrubiks Date: Wed, 14 Oct 2026 18:39:47 +0800 Subject: [PATCH 1/2] fix(runtime): update model pricing table to current rates Align pricing_for_model with the published Anthropic pricing table: - default_sonnet_tier was $15/$75 (the old Opus rate); it is now the Sonnet 4.x rate of $3/$15, which is what the `sonnet` alias resolves to. - Add Sonnet 5 / 5.5, Haiku 5.5, Haiku 3.5, and Fable / Mythos rates. - Opus 4.5+ is $5/$25 and Opus 5.5 is $4/$20; Opus 4/4.1 keep the $15/$75 tier they are still billed at on Bedrock and Google Cloud. Versions are parsed from the model id so dated, provider-prefixed, and Vertex-style ids resolve to the right tier. Haiku 5.5's >100K-token tier is not modeled because usage is aggregated across requests. --- rust/crates/api/src/types.rs | 2 +- rust/crates/api/tests/client_integration.rs | 6 +- rust/crates/runtime/src/usage.rs | 132 +++++++++++++++----- 3 files changed, 107 insertions(+), 33 deletions(-) diff --git a/rust/crates/api/src/types.rs b/rust/crates/api/src/types.rs index 3ec7f879e5..a2d01d82a8 100644 --- a/rust/crates/api/src/types.rs +++ b/rust/crates/api/src/types.rs @@ -320,7 +320,7 @@ mod tests { }; let cost = response.usage.estimated_cost_usd(&response.model); - assert_eq!(format_usd(cost.total_cost_usd()), "$54.6750"); + assert_eq!(format_usd(cost.total_cost_usd()), "$10.9350"); assert_eq!(response.total_tokens(), 1_800_000); } diff --git a/rust/crates/api/tests/client_integration.rs b/rust/crates/api/tests/client_integration.rs index 3fd1fea02d..20661f1398 100644 --- a/rust/crates/api/tests/client_integration.rs +++ b/rust/crates/api/tests/client_integration.rs @@ -208,7 +208,7 @@ async fn send_message_applies_request_profile_and_records_telemetry() { "\"model\":\"claude-3-7-sonnet-latest\",", "\"stop_reason\":\"end_turn\",", "\"stop_sequence\":null,", - "\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":2,\"cache_read_input_tokens\":3,\"output_tokens\":1}", + "\"usage\":{\"input_tokens\":100,\"cache_creation_input_tokens\":200,\"cache_read_input_tokens\":300,\"output_tokens\":100}", "}" ), &[("request-id", "req_profile_123")], @@ -283,8 +283,8 @@ async fn send_message_applies_request_profile_and_records_telemetry() { if event.namespace == "api" && event.action == "message_usage" && event.properties.get("request_id") == Some(&json!("req_profile_123")) - && event.properties.get("total_tokens") == Some(&json!(7)) - && event.properties.get("estimated_cost_usd") == Some(&json!("$0.0001")) + && event.properties.get("total_tokens") == Some(&json!(700)) + && event.properties.get("estimated_cost_usd") == Some(&json!("$0.0026")) )); assert!(matches!( &events[5], diff --git a/rust/crates/runtime/src/usage.rs b/rust/crates/runtime/src/usage.rs index 9241f7c2d7..e108369297 100644 --- a/rust/crates/runtime/src/usage.rs +++ b/rust/crates/runtime/src/usage.rs @@ -1,9 +1,9 @@ use crate::session::Session; -const DEFAULT_INPUT_COST_PER_MILLION: f64 = 15.0; -const DEFAULT_OUTPUT_COST_PER_MILLION: f64 = 75.0; -const DEFAULT_CACHE_CREATION_COST_PER_MILLION: f64 = 18.75; -const DEFAULT_CACHE_READ_COST_PER_MILLION: f64 = 1.5; +const DEFAULT_INPUT_COST_PER_MILLION: f64 = 3.0; +const DEFAULT_OUTPUT_COST_PER_MILLION: f64 = 15.0; +const DEFAULT_CACHE_CREATION_COST_PER_MILLION: f64 = 3.75; +const DEFAULT_CACHE_READ_COST_PER_MILLION: f64 = 0.3; /// Per-million-token pricing used for cost estimation. #[derive(Debug, Clone, Copy, PartialEq)] @@ -17,11 +17,20 @@ pub struct ModelPricing { impl ModelPricing { #[must_use] pub const fn default_sonnet_tier() -> Self { + Self::per_million( + DEFAULT_INPUT_COST_PER_MILLION, + DEFAULT_OUTPUT_COST_PER_MILLION, + DEFAULT_CACHE_CREATION_COST_PER_MILLION, + DEFAULT_CACHE_READ_COST_PER_MILLION, + ) + } + + const fn per_million(input: f64, output: f64, cache_creation: f64, cache_read: f64) -> Self { Self { - input_cost_per_million: DEFAULT_INPUT_COST_PER_MILLION, - output_cost_per_million: DEFAULT_OUTPUT_COST_PER_MILLION, - cache_creation_cost_per_million: DEFAULT_CACHE_CREATION_COST_PER_MILLION, - cache_read_cost_per_million: DEFAULT_CACHE_READ_COST_PER_MILLION, + input_cost_per_million: input, + output_cost_per_million: output, + cache_creation_cost_per_million: cache_creation, + cache_read_cost_per_million: cache_read, } } } @@ -55,31 +64,61 @@ impl UsageCostEstimate { } /// Returns pricing metadata for a known model alias or family. +/// +/// Rates follow , using +/// 5-minute cache writes. Unversioned ids fall back to the tier the bare alias resolves to. #[must_use] pub fn pricing_for_model(model: &str) -> Option { let normalized = model.to_ascii_lowercase(); + if normalized.contains("fable") || normalized.contains("mythos") { + let version = + family_version(&normalized, "fable").or_else(|| family_version(&normalized, "mythos")); + return Some(match version { + Some((5, 1)) => ModelPricing::per_million(10.0, 50.0, 12.5, 0.25), + _ => ModelPricing::per_million(10.0, 50.0, 12.5, 1.0), + }); + } if normalized.contains("haiku") { - return Some(ModelPricing { - input_cost_per_million: 1.0, - output_cost_per_million: 5.0, - cache_creation_cost_per_million: 1.25, - cache_read_cost_per_million: 0.1, + if normalized.contains("3-5-haiku") { + return Some(ModelPricing::per_million(0.8, 4.0, 1.0, 0.08)); + } + return Some(match family_version(&normalized, "haiku") { + // Haiku 5.5 bills prompts over 100K tokens at 5x; usage totals are not + // tracked per request, so estimates use the base tier. + Some((5, 5)) => ModelPricing::per_million(0.1, 0.5, 0.125, 0.01), + _ => ModelPricing::per_million(1.0, 5.0, 1.25, 0.1), }); } if normalized.contains("opus") { - return Some(ModelPricing { - input_cost_per_million: 15.0, - output_cost_per_million: 75.0, - cache_creation_cost_per_million: 18.75, - cache_read_cost_per_million: 1.5, + return Some(match family_version(&normalized, "opus") { + Some((5, 5)) => ModelPricing::per_million(4.0, 20.0, 5.0, 0.2), + // Opus 4/4.1 remain on Bedrock and Google Cloud at the original tier. + Some((4, 0 | 1)) => ModelPricing::per_million(15.0, 75.0, 18.75, 1.5), + _ => ModelPricing::per_million(5.0, 25.0, 6.25, 0.5), }); } if normalized.contains("sonnet") { - return Some(ModelPricing::default_sonnet_tier()); + return Some(match family_version(&normalized, "sonnet") { + Some((5, 5)) => ModelPricing::per_million(2.0, 10.0, 2.5, 0.1), + Some((5, 0)) => ModelPricing::per_million(2.0, 10.0, 2.5, 0.2), + _ => ModelPricing::default_sonnet_tier(), + }); } None } +/// Parses the `(major, minor)` version after `family` in ids such as +/// `claude-opus-4-1-20250805`, `anthropic/claude-sonnet-5-5`, or `claude-opus-4@20250514`. +/// Date suffixes are not treated as a minor version. +fn family_version(model: &str, family: &str) -> Option<(u32, u32)> { + let rest = model[model.find(family)? + family.len()..].strip_prefix('-')?; + let mut parts = rest.split(|c: char| !c.is_ascii_digit()); + let parse = |part: &str| part.parse::().ok().filter(|_| part.len() <= 2); + let major = parse(parts.next()?)?; + let minor = parts.next().and_then(parse).unwrap_or(0); + Some((major, minor)) +} + impl TokenUsage { #[must_use] pub fn total_tokens(self) -> u32 { @@ -253,12 +292,12 @@ mod tests { }; let cost = usage.estimate_cost_usd(); - assert_eq!(format_usd(cost.input_cost_usd), "$15.0000"); - assert_eq!(format_usd(cost.output_cost_usd), "$37.5000"); + assert_eq!(format_usd(cost.input_cost_usd), "$3.0000"); + assert_eq!(format_usd(cost.output_cost_usd), "$7.5000"); let lines = usage.summary_lines_for_model("usage", Some("claude-sonnet-4-20250514")); - assert!(lines[0].contains("estimated_cost=$54.6750")); + assert!(lines[0].contains("estimated_cost=$10.9350")); assert!(lines[0].contains("model=claude-sonnet-4-20250514")); - assert!(lines[1].contains("cache_read=$0.3000")); + assert!(lines[1].contains("cache_read=$0.0600")); } #[test] @@ -269,13 +308,48 @@ mod tests { cache_creation_input_tokens: 0, cache_read_input_tokens: 0, }; + let total_for = |model: &str| { + let pricing = pricing_for_model(model).expect("known model pricing"); + format_usd( + usage + .estimate_cost_usd_with_pricing(pricing) + .total_cost_usd(), + ) + }; + + assert_eq!(total_for("claude-fable-5-1"), "$35.0000"); + assert_eq!(total_for("claude-opus-4-6"), "$17.5000"); + assert_eq!(total_for("anthropic/claude-opus-5-5"), "$14.0000"); + assert_eq!(total_for("claude-opus-4-1-20250805"), "$52.5000"); + assert_eq!(total_for("claude-opus-4@20250514"), "$52.5000"); + assert_eq!(total_for("claude-sonnet-5-5"), "$7.0000"); + assert_eq!(total_for("claude-sonnet-4-6"), "$10.5000"); + assert_eq!(total_for("claude-haiku-5-5"), "$0.3500"); + assert_eq!(total_for("claude-haiku-4-5-20251001"), "$3.5000"); + assert_eq!(total_for("claude-3-5-haiku-20241022"), "$2.8000"); + } + + #[test] + fn distinguishes_cache_read_rates_across_versions() { + let usage = TokenUsage { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 1_000_000, + }; + let cache_read_for = |model: &str| { + let pricing = pricing_for_model(model).expect("known model pricing"); + format_usd( + usage + .estimate_cost_usd_with_pricing(pricing) + .cache_read_cost_usd, + ) + }; - let haiku = pricing_for_model("claude-haiku-4-5-20251001").expect("haiku pricing"); - let opus = pricing_for_model("claude-opus-4-6").expect("opus pricing"); - let haiku_cost = usage.estimate_cost_usd_with_pricing(haiku); - let opus_cost = usage.estimate_cost_usd_with_pricing(opus); - assert_eq!(format_usd(haiku_cost.total_cost_usd()), "$3.5000"); - assert_eq!(format_usd(opus_cost.total_cost_usd()), "$52.5000"); + assert_eq!(cache_read_for("claude-fable-5-1"), "$0.2500"); + assert_eq!(cache_read_for("claude-fable-5"), "$1.0000"); + assert_eq!(cache_read_for("claude-sonnet-5-5"), "$0.1000"); + assert_eq!(cache_read_for("claude-sonnet-5"), "$0.2000"); } #[test] From 78989ba330c02ef212fef0725075a3bf72b2aebe Mon Sep 17 00:00:00 2001 From: twtrubiks Date: Wed, 14 Oct 2026 19:03:49 +0800 Subject: [PATCH 2/2] fix(runtime): add DeepSeek API pricing DeepSeek models fell back to default_sonnet_tier ($3/$15 per MTok), overestimating V4 Flash by about 10x on input and 12x on output. Price the ids served by the DeepSeek API at peak rates: deepseek-v4-pro at $1.32/$3.96, and deepseek-flash, deepseek-v4-flash and the retired deepseek-chat / deepseek-reasoner aliases at $0.30/$1.20. Off-peak usage is billed at half. Other DeepSeek models on third-party hosts keep the default because their prices vary by host. --- rust/crates/runtime/src/usage.rs | 46 +++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/rust/crates/runtime/src/usage.rs b/rust/crates/runtime/src/usage.rs index e108369297..83d880fea9 100644 --- a/rust/crates/runtime/src/usage.rs +++ b/rust/crates/runtime/src/usage.rs @@ -65,7 +65,7 @@ impl UsageCostEstimate { /// Returns pricing metadata for a known model alias or family. /// -/// Rates follow , using +/// Anthropic rates follow , using /// 5-minute cache writes. Unversioned ids fall back to the tier the bare alias resolves to. #[must_use] pub fn pricing_for_model(model: &str) -> Option { @@ -104,6 +104,24 @@ pub fn pricing_for_model(model: &str) -> Option { _ => ModelPricing::default_sonnet_tier(), }); } + // DeepSeek API peak rates (); + // off-peak usage is billed at half. Cache misses are billed as regular input, + // with no separate cache-write charge. The retired `deepseek-chat` and + // `deepseek-reasoner` ids were served by V4 Flash. + if normalized.contains("deepseek-v4-pro") { + return Some(ModelPricing::per_million(1.32, 3.96, 1.32, 0.044)); + } + if [ + "deepseek-flash", + "deepseek-v4-flash", + "deepseek-chat", + "deepseek-reasoner", + ] + .iter() + .any(|id| normalized.contains(id)) + { + return Some(ModelPricing::per_million(0.3, 1.2, 0.3, 0.006)); + } None } @@ -352,6 +370,32 @@ mod tests { assert_eq!(cache_read_for("claude-sonnet-5"), "$0.2000"); } + #[test] + fn prices_deepseek_api_models_at_peak_rates() { + let usage = TokenUsage { + input_tokens: 1_000_000, + output_tokens: 500_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 1_000_000, + }; + let total_for = |model: &str| { + let pricing = pricing_for_model(model).expect("known model pricing"); + format_usd( + usage + .estimate_cost_usd_with_pricing(pricing) + .total_cost_usd(), + ) + }; + + assert_eq!(total_for("deepseek-v4-pro"), "$3.3440"); + assert_eq!(total_for("openai/deepseek-v4-pro"), "$3.3440"); + assert_eq!(total_for("deepseek-flash"), "$0.9060"); + assert_eq!(total_for("deepseek/deepseek-v4-flash"), "$0.9060"); + assert_eq!(total_for("deepseek-reasoner"), "$0.9060"); + // Third-party hosts set their own prices for other DeepSeek models. + assert_eq!(pricing_for_model("deepseek-r1"), None); + } + #[test] fn marks_unknown_model_pricing_as_fallback() { let usage = TokenUsage {