mirror of
https://github.com/instructkr/claw-code.git
synced 2026-10-31 00:04:39 +08:00
fix(runtime): update model pricing for Anthropic and DeepSeek APIs
* 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. * 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.
This commit is contained in:
1 parent
78ffd13d19
commit
4b8c79e73c
3 files changed
+151
-33
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,79 @@ impl UsageCostEstimate {
|
||||
}
|
||||
|
||||
/// Returns pricing metadata for a known model alias or family.
|
||||
///
|
||||
/// Anthropic rates follow <https://platform.claude.com/docs/en/about-claude/pricing>, 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<ModelPricing> {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
// DeepSeek API peak rates (<https://api-docs.deepseek.com/quick_start/pricing>);
|
||||
// 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
|
||||
}
|
||||
|
||||
/// 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::<u32>().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 +310,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 +326,74 @@ 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(),
|
||||
)
|
||||
};
|
||||
|
||||
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!(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,
|
||||
)
|
||||
};
|
||||
|
||||
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]
|
||||
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]
|
||||
|
||||
Reference in new issue
Block a user