#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::str_to_string
)]
use std::collections::BTreeSet;
use embacle::pricing::{
calculate_cost, calculate_cost_for, lookup_pricing, ModelPricing, TokenCounts, PRICING_TABLE,
};
#[test]
fn test_known_model_cost() {
let cost = calculate_cost("gemini", "gemini-2.0-flash", 1000, 500);
let expected = 1000.0_f64.mul_add(0.075, 500.0 * 0.30) / 1_000_000.0;
assert!(
(cost - expected).abs() < f64::EPSILON,
"Expected {expected}, got {cost}"
);
}
#[test]
fn test_unknown_model_returns_zero() {
let cost = calculate_cost("unknown_provider", "unknown_model", 1000, 500);
assert!(
cost.abs() < f64::EPSILON,
"Expected 0.0 for unknown model, got {cost}"
);
}
#[test]
fn test_zero_tokens_returns_zero() {
let cost = calculate_cost("gemini", "gemini-2.0-flash", 0, 0);
assert!(
cost.abs() < f64::EPSILON,
"Expected 0.0 for zero tokens, got {cost}"
);
}
#[test]
fn test_prefix_matching() {
let cost_exact = calculate_cost("gemini", "gemini-2.0-flash", 1000, 500);
let cost_variant = calculate_cost("gemini", "gemini-2.0-flash-exp", 1000, 500);
assert!(
(cost_exact - cost_variant).abs() < f64::EPSILON,
"Variant model should match base pricing"
);
}
#[test]
fn test_gemini_25_pro_pricing() {
let cost = calculate_cost("gemini", "gemini-2.5-pro", 1_000_000, 500_000);
let expected = 1.25 + 5.0; assert!(
(cost - expected).abs() < 1e-10,
"Expected {expected}, got {cost}"
);
}
#[test]
fn test_groq_llama_pricing() {
let cost = calculate_cost("groq", "llama-3.3-70b-versatile", 2000, 1000);
let expected = 2000.0_f64.mul_add(0.59, 1000.0 * 0.79) / 1_000_000.0;
assert!(
(cost - expected).abs() < f64::EPSILON,
"Expected {expected}, got {cost}"
);
}
#[test]
fn test_groq_mixtral_pricing() {
let cost = calculate_cost("groq", "mixtral-8x7b-32768", 5000, 3000);
let expected = 5000.0_f64.mul_add(0.24, 3000.0 * 0.24) / 1_000_000.0;
assert!(
(cost - expected).abs() < f64::EPSILON,
"Expected {expected}, got {cost}"
);
}
#[test]
fn test_gemini_flash_lite_latest_pricing() {
let cost = calculate_cost("gemini", "gemini-flash-lite-latest", 1_000_000, 100_000);
let expected = 0.10 + 0.04; assert!(
(cost - expected).abs() < 1e-10,
"Expected {expected}, got {cost}"
);
}
#[test]
fn test_every_default_model_has_pricing() {
let production_models = [
("gemini", "gemini-flash-lite-latest"),
("gemini", "gemini-2.5-flash"),
("gemini", "gemini-2.0-flash"),
("groq", "llama-3.3-70b-versatile"),
("anthropic", "claude-haiku-5-5"),
("anthropic", "claude-sonnet-5-5"),
("anthropic", "claude-opus-5-5"),
];
for (provider, model) in production_models {
let cost = calculate_cost(provider, model, 1000, 1000);
assert!(
cost > 0.0,
"Model {provider}/{model} has ZERO pricing — every turn on it bills $0.00"
);
}
}
#[test]
fn anthropic_cache_read_bills_at_ten_percent() {
let counts = TokenCounts::new(1_000_000, 0).with_cache(1_000_000, 0);
let cost = calculate_cost_for("copilot_headless", "claude-opus-4", &counts);
assert!(
(cost - 1.50).abs() < 1e-9,
"expected $1.50 for 1M Anthropic cache-read tokens, got {cost}"
);
}
#[test]
fn anthropic_cache_write_bills_above_fresh_input() {
let written = TokenCounts::new(1_000_000, 0).with_cache(0, 1_000_000);
let fresh = TokenCounts::new(1_000_000, 0);
let write_cost = calculate_cost_for("copilot_headless", "claude-opus-4", &written);
let fresh_cost = calculate_cost_for("copilot_headless", "claude-opus-4", &fresh);
assert!(
(write_cost - 18.75).abs() < 1e-9,
"expected $18.75 for 1M Anthropic cache-write tokens, got {write_cost}"
);
assert!(
write_cost > fresh_cost,
"a cache write is a premium, not a discount: write={write_cost} fresh={fresh_cost}"
);
}
#[test]
fn reasoning_tokens_bill_at_the_output_rate() {
let without = TokenCounts::new(0, 1_000_000);
let with = TokenCounts::new(0, 1_000_000).with_reasoning(1_000_000);
let cost_without = calculate_cost_for("copilot_headless", "claude-opus-4", &without);
let cost_with = calculate_cost_for("copilot_headless", "claude-opus-4", &with);
assert!(
(cost_without - 75.0).abs() < 1e-9,
"expected $75 for 1M completion tokens, got {cost_without}"
);
assert!(
(cost_with - 150.0).abs() < 1e-9,
"reasoning tokens must bill at the output rate; got {cost_with}"
);
}
#[test]
fn cache_read_rate_is_per_provider() {
let counts = TokenCounts::new(1_000_000, 0).with_cache(1_000_000, 0);
let anthropic = calculate_cost_for("copilot_headless", "claude-opus-4", &counts);
let openai = calculate_cost_for("openai_api", "gpt-4o", &counts);
let gemini = calculate_cost_for("gemini", "gemini-2.0-flash", &counts);
assert!((anthropic - 1.50).abs() < 1e-9, "anthropic={anthropic}");
assert!((openai - 1.25).abs() < 1e-9, "openai={openai}");
assert!((gemini - 0.018_75).abs() < 1e-9, "gemini={gemini}");
}
#[test]
fn real_acp_turn_splits_prompt_across_three_rates() {
let counts = TokenCounts::new(27_862, 4).with_cache(15_320, 12_540);
let cost = calculate_cost_for("copilot_headless", "claude-opus-4", &counts);
let input = 15.0 / 1_000_000.0;
let fresh = f64::from(27_862 - 15_320 - 12_540) * input;
let read = 15_320.0 * input * 0.10;
let write = 12_540.0 * input * 1.25;
let output = 4.0 * 75.0 / 1_000_000.0;
let expected = fresh + read + write + output;
assert!(
(cost - expected).abs() < 1e-12,
"expected {expected}, got {cost}"
);
let naive = calculate_cost_for(
"copilot_headless",
"claude-opus-4",
&TokenCounts::new(27_862, 4),
);
assert!(
(cost - naive).abs() > 1e-9,
"cache-aware and all-fresh imputation must differ; both were {cost}"
);
}
#[test]
fn cache_counts_are_clamped_to_the_prompt() {
let counts = TokenCounts::new(1_000, 0).with_cache(900, 900);
let cost = calculate_cost_for("copilot_headless", "claude-opus-4", &counts);
let input = 15.0 / 1_000_000.0;
let expected = 900.0f64.mul_add(input * 0.10, 100.0 * input * 1.25);
assert!(
(cost - expected).abs() < 1e-12,
"expected {expected}, got {cost}"
);
}
const ANTHROPIC_5_5_RATES: &[(&str, f64, f64, f64, f64)] = &[
("claude-haiku-5-5", 0.10, 0.50, 0.01, 0.125),
("claude-sonnet-5-5", 2.0, 10.0, 0.20, 2.50),
("claude-opus-5-5", 4.0, 20.0, 0.20, 5.0),
];
#[test]
fn anthropic_5_5_models_price_every_rate() {
for &(model, input, output, read, write) in ANTHROPIC_5_5_RATES {
let tokens = 100_000;
let tenth = |rate: f64| rate / 10.0;
let fresh = calculate_cost("anthropic", model, tokens, 0);
assert!(
(fresh - tenth(input)).abs() < 1e-9,
"{model} input: {fresh}"
);
let out = calculate_cost("anthropic", model, 0, tokens);
assert!((out - tenth(output)).abs() < 1e-9, "{model} output: {out}");
let cached = calculate_cost_for(
"anthropic",
model,
&TokenCounts::new(tokens, 0).with_cache(tokens, 0),
);
assert!(
(cached - tenth(read)).abs() < 1e-9,
"{model} cache read: {cached}"
);
let written = calculate_cost_for(
"anthropic",
model,
&TokenCounts::new(tokens, 0).with_cache(0, tokens),
);
assert!(
(written - tenth(write)).abs() < 1e-9,
"{model} cache write: {written}"
);
}
}
#[test]
fn haiku_5_5_bills_a_long_prompt_on_its_second_rate_card() {
let at_threshold = calculate_cost("anthropic", "claude-haiku-5-5", 100_000, 1_000);
let base = 0.0105;
assert!((at_threshold - base).abs() < 1e-12, "{at_threshold}");
let above = calculate_cost("anthropic", "claude-haiku-5-5", 100_001, 1_000);
let long = 0.052_500_5;
assert!((above - long).abs() < 1e-12, "{above}");
let cached = calculate_cost_for(
"anthropic#2",
"claude-haiku-5-5",
&TokenCounts::new(150_000, 0).with_cache(140_000, 0),
);
let expected = 0.012;
assert!((cached - expected).abs() < 1e-12, "{cached}");
let sonnet = calculate_cost("anthropic", "claude-sonnet-5-5", 1_000_000, 0);
assert!((sonnet - 2.0).abs() < 1e-9, "{sonnet}");
}
#[test]
fn anthropic_pool_accounts_price_as_their_family() {
let counts = TokenCounts::new(40_000, 900).with_cache(30_000, 5_000);
let first = calculate_cost_for("anthropic", "claude-haiku-5-5", &counts);
assert!(first > 0.0);
for account in ["anthropic#2", "anthropic#10"] {
let cost = calculate_cost_for(account, "claude-haiku-5-5", &counts);
assert!((cost - first).abs() < 1e-12, "{account}: {cost} vs {first}");
}
}
fn resolved(provider: &str, model: &str) -> ModelPricing {
lookup_pricing(provider, model)
.unwrap_or_else(|| panic!("{provider}/{model} resolves to no price row"))
}
fn assert_rates(provider: &str, model: &str, rates: (f64, f64, f64, f64), long: bool) {
let pricing = resolved(provider, model);
let actual = (
pricing.input_per_million,
pricing.output_per_million,
pricing.cache_read_multiplier,
pricing.cache_write_multiplier,
);
let close = |a: f64, b: f64| (a - b).abs() < 1e-12;
assert!(
close(actual.0, rates.0)
&& close(actual.1, rates.1)
&& close(actual.2, rates.2)
&& close(actual.3, rates.3),
"{provider}/{model} resolved to (input, output, read, write) = {actual:?}, \
expected {rates:?}"
);
assert_eq!(
pricing.long_prompt.is_some(),
long,
"{provider}/{model} long-prompt card: {:?}",
pricing.long_prompt
);
}
#[test]
fn the_longest_matching_prefix_wins_whatever_the_table_order() {
let mini = (0.15, 0.60, 0.50, 1.0);
assert_rates("openai_api", "gpt-4o-mini", mini, false);
assert_rates("openai_api", "gpt-4o-mini-2024-07-18", mini, false);
assert_rates(
"openai_api",
"gpt-4o-2024-08-06",
(2.50, 10.0, 0.50, 1.0),
false,
);
}
#[test]
fn no_provider_lists_the_same_prefix_twice() {
let mut seen = BTreeSet::new();
for (provider, prefix, _) in PRICING_TABLE {
assert!(
seen.insert((*provider, *prefix)),
"PRICING_TABLE lists ({provider}, {prefix}) more than once"
);
}
}
const PASS_THROUGH_RUNNERS: [&str; 3] = ["copilot_headless", "copilot_sdk", "claude-code"];
const HAIKU_5_5: (f64, f64, f64, f64) = (0.10, 0.50, 0.10, 1.25);
const SONNET_5_5: (f64, f64, f64, f64) = (2.0, 10.0, 0.10, 1.25);
#[test]
fn haiku_5_5_is_priced_under_every_id_a_runner_reports() {
for provider in PASS_THROUGH_RUNNERS {
for model in ["claude-haiku-5.5", "claude-haiku-5-5"] {
assert_rates(provider, model, HAIKU_5_5, false);
}
}
assert_rates("anthropic", "claude-haiku-5-5", HAIKU_5_5, true);
assert_rates("claude-code#2", "claude-haiku-5-5", HAIKU_5_5, false);
}
#[test]
fn sonnet_5_5_wins_over_the_version_agnostic_sonnet_row() {
for provider in PASS_THROUGH_RUNNERS {
for model in ["claude-sonnet-5.5", "claude-sonnet-5-5"] {
assert_rates(provider, model, SONNET_5_5, false);
}
assert_rates(
provider,
"claude-sonnet-4.5",
(3.0, 15.0, 0.10, 1.25),
false,
);
}
assert_rates("anthropic", "claude-sonnet-5-5", SONNET_5_5, false);
}
#[test]
fn no_other_haiku_5_inherits_the_haiku_5_5_card() {
for provider in [
"anthropic",
"copilot_headless",
"copilot_sdk",
"claude-code",
] {
for model in [
"claude-haiku-5",
"claude-haiku-5-1",
"claude-haiku-5.1",
"claude-haiku-5-0",
] {
assert!(
lookup_pricing(provider, model).is_none(),
"{provider}/{model} resolved to {:?}; only Haiku 5.5 is priced",
lookup_pricing(provider, model)
);
}
}
}
#[test]
fn haiku_4_5_bills_at_its_list_price() {
for provider in PASS_THROUGH_RUNNERS {
for model in [
"claude-haiku-4.5",
"claude-haiku-4-5",
"claude-haiku-4-5-20251001",
] {
assert_rates(provider, model, (1.0, 5.0, 0.10, 1.25), false);
}
}
}
#[test]
fn haiku_5_5_prices_a_cached_tool_turn() {
let counts = TokenCounts::new(43_538, 163)
.with_cache(21_678, 21_854)
.with_reasoning(113);
for provider in PASS_THROUGH_RUNNERS {
let cost = calculate_cost_for(provider, "claude-haiku-5.5", &counts);
let expected = 0.000_000_6 + 0.000_216_78 + 0.002_731_75 + 0.000_138;
assert!(
(cost - expected).abs() < 1e-12,
"{provider}: expected {expected}, got {cost}"
);
}
}
#[test]
fn a_long_summed_turn_stays_on_the_base_card_off_the_anthropic_api() {
let turn = calculate_cost("copilot_sdk", "claude-haiku-5.5", 150_000, 0);
assert!((turn - 0.015).abs() < 1e-12, "{turn}");
let request = calculate_cost("anthropic", "claude-haiku-5-5", 150_000, 0);
assert!((request - 0.075).abs() < 1e-12, "{request}");
}
const OPUS_5_5: (f64, f64, f64, f64) = (4.0, 20.0, 0.05, 1.25);
const OPUS_5_25: (f64, f64, f64, f64) = (5.0, 25.0, 0.10, 1.25);
const OPUS_4_FAMILY: (f64, f64, f64, f64) = (15.0, 75.0, 0.10, 1.25);
const SONNET_5: (f64, f64, f64, f64) = (2.0, 10.0, 0.10, 1.25);
const SONNET_4: (f64, f64, f64, f64) = (3.0, 15.0, 0.10, 1.25);
#[test]
fn opus_5_5_is_priced_under_both_ids() {
for provider in PASS_THROUGH_RUNNERS {
for model in ["claude-opus-5.5", "claude-opus-5-5"] {
assert_rates(provider, model, OPUS_5_5, false);
}
}
}
#[test]
fn opus_5_and_4_6_through_4_8_bill_five_and_twenty_five() {
for provider in PASS_THROUGH_RUNNERS {
for model in [
"claude-opus-5",
"claude-opus-4.8",
"claude-opus-4-8",
"claude-opus-4.7",
"claude-opus-4-7",
"claude-opus-4.6",
"claude-opus-4-6",
] {
assert_rates(provider, model, OPUS_5_25, false);
}
}
}
#[test]
fn opus_4_6_and_4_7_fast_tiers_do_not_fall_to_their_base_rows() {
for provider in PASS_THROUGH_RUNNERS {
for model in [
"claude-opus-4.7-fast",
"claude-opus-4-7-fast",
"claude-opus-4.6-fast",
"claude-opus-4-6-fast",
] {
assert_rates(provider, model, OPUS_4_FAMILY, false);
}
}
}
#[test]
fn opus_4_5_and_older_keep_the_opus_4_family_row() {
for provider in PASS_THROUGH_RUNNERS {
for model in [
"claude-opus-4",
"claude-opus-4.5",
"claude-opus-4-5",
"claude-opus-4-5-20251101",
"claude-opus-4-1",
"claude-opus-4-0",
] {
assert_rates(provider, model, OPUS_4_FAMILY, false);
}
}
}
#[test]
fn sonnet_5_wins_over_the_version_agnostic_sonnet_row() {
for provider in PASS_THROUGH_RUNNERS {
assert_rates(provider, "claude-sonnet-5", SONNET_5, false);
for model in [
"claude-sonnet-4.6",
"claude-sonnet-4-6",
"claude-sonnet-4.5",
"claude-sonnet-4",
"claude-sonnet-4-0",
] {
assert_rates(provider, model, SONNET_4, false);
}
}
}