#[derive(Debug, Clone, Copy)]
pub struct OpenAIPricing {
pub input: f64,
pub cached_input: f64,
pub output: f64,
}
impl OpenAIPricing {
pub const GPT_56_SOL: Self = Self { input: 4.00, cached_input: 0.40, output: 20.00 };
pub const GPT_56_SOL_LONG: Self = Self { input: 8.00, cached_input: 0.80, output: 30.00 };
pub const GPT_56_TERRA: Self = Self { input: 2.00, cached_input: 0.20, output: 12.00 };
pub const GPT_56_TERRA_LONG: Self = Self { input: 4.00, cached_input: 0.40, output: 18.00 };
pub const GPT_56_LUNA: Self = Self { input: 0.20, cached_input: 0.02, output: 1.20 };
pub const GPT_56_LUNA_LONG: Self = Self { input: 0.40, cached_input: 0.04, output: 1.80 };
pub const GPT_56_CYBER: Self = Self { input: 12.50, cached_input: 1.25, output: 75.00 };
pub const GPT_55: Self = Self { input: 5.00, cached_input: 0.50, output: 30.00 };
pub const GPT_55_LONG: Self = Self { input: 10.00, cached_input: 1.00, output: 45.00 };
pub const GPT_55_PRO: Self = Self { input: 30.00, cached_input: 30.00, output: 180.00 };
pub const GPT_55_PRO_LONG: Self = Self { input: 60.00, cached_input: 60.00, output: 270.00 };
pub const GPT_55_CYBER: Self = Self { input: 12.50, cached_input: 1.25, output: 75.00 };
#[deprecated(note = "no published OpenAI rate as of 2026-08-23; lookup_pricing returns None")]
pub const GPT_55_INSTANT: Self = Self { input: 0.50, cached_input: 0.05, output: 3.00 };
pub const GPT_54: Self = Self { input: 2.50, cached_input: 0.25, output: 15.00 };
pub const GPT_54_LONG: Self = Self { input: 5.00, cached_input: 0.50, output: 22.50 };
pub const GPT_54_MINI: Self = Self { input: 0.75, cached_input: 0.075, output: 4.50 };
pub const GPT_54_NANO: Self = Self { input: 0.20, cached_input: 0.02, output: 1.25 };
pub const GPT_54_PRO: Self = Self { input: 30.00, cached_input: 30.00, output: 180.00 };
pub const GPT_54_PRO_LONG: Self = Self { input: 60.00, cached_input: 60.00, output: 270.00 };
pub const GPT_53_CODEX: Self = Self { input: 1.75, cached_input: 0.175, output: 14.00 };
pub const GPT_53_CODEX_FAST: Self = Self { input: 3.50, cached_input: 0.35, output: 28.00 };
#[deprecated(note = "OpenAI prices this endpoint as `chat-latest`; use Self::CHAT_LATEST")]
pub const GPT_53_CHAT_LATEST: Self = Self { input: 1.50, cached_input: 0.15, output: 12.00 };
pub const CHAT_LATEST: Self = Self { input: 5.00, cached_input: 0.50, output: 30.00 };
pub const GPT_52: Self = Self { input: 1.75, cached_input: 0.175, output: 14.00 };
pub const GPT_52_PRO: Self = Self { input: 21.00, cached_input: 21.00, output: 168.00 };
#[deprecated(note = "no published OpenAI rate as of 2026-08-23; lookup_pricing returns None")]
pub const GPT_52_CODEX: Self = Self { input: 1.25, cached_input: 0.125, output: 10.00 };
pub const GPT_51: Self = Self { input: 1.25, cached_input: 0.125, output: 10.00 };
#[deprecated(note = "no published OpenAI rate as of 2026-08-23; lookup_pricing returns None")]
pub const GPT_51_CODEX: Self = Self { input: 1.00, cached_input: 0.10, output: 8.00 };
#[deprecated(note = "no published OpenAI rate as of 2026-08-23; lookup_pricing returns None")]
pub const GPT_51_CODEX_MAX: Self = Self { input: 2.00, cached_input: 0.20, output: 16.00 };
#[deprecated(note = "no published OpenAI rate as of 2026-08-23; lookup_pricing returns None")]
pub const GPT_51_CODEX_MINI: Self = Self { input: 0.30, cached_input: 0.03, output: 2.40 };
pub const GPT_5: Self = Self { input: 1.25, cached_input: 0.125, output: 10.00 };
pub const GPT_5_MINI: Self = Self { input: 0.25, cached_input: 0.025, output: 2.00 };
pub const GPT_5_NANO: Self = Self { input: 0.05, cached_input: 0.005, output: 0.40 };
pub const GPT_5_PRO: Self = Self { input: 15.00, cached_input: 15.00, output: 120.00 };
pub const GPT_5_SEARCH_API: Self = Self { input: 1.25, cached_input: 0.125, output: 10.00 };
pub const GPT_41: Self = Self { input: 2.00, cached_input: 0.50, output: 8.00 };
pub const GPT_41_MINI: Self = Self { input: 0.40, cached_input: 0.10, output: 1.60 };
pub const GPT_41_NANO: Self = Self { input: 0.10, cached_input: 0.025, output: 0.40 };
pub const O3: Self = Self { input: 2.00, cached_input: 0.50, output: 8.00 };
pub const O4_MINI: Self = Self { input: 1.10, cached_input: 0.275, output: 4.40 };
pub const O3_MINI: Self = Self { input: 1.10, cached_input: 0.55, output: 4.40 };
pub const O1: Self = Self { input: 15.00, cached_input: 7.50, output: 60.00 };
pub const GPT_4O: Self = Self { input: 2.50, cached_input: 1.25, output: 10.00 };
pub const GPT_4O_MINI: Self = Self { input: 0.15, cached_input: 0.075, output: 0.60 };
pub const GPT_REALTIME_15_TEXT: Self = Self { input: 4.00, cached_input: 0.40, output: 16.00 };
pub const GPT_REALTIME_15_AUDIO: Self =
Self { input: 32.00, cached_input: 0.40, output: 64.00 };
pub const GPT_REALTIME_21_TEXT: Self = Self { input: 4.00, cached_input: 0.40, output: 24.00 };
pub const GPT_REALTIME_21_AUDIO: Self =
Self { input: 32.00, cached_input: 0.40, output: 64.00 };
pub const GPT_IMAGE_15_TEXT: Self = Self { input: 5.00, cached_input: 1.25, output: 10.00 };
pub const GPT_IMAGE_15_IMAGE: Self = Self { input: 8.00, cached_input: 2.00, output: 32.00 };
pub const GPT_IMAGE_2_TEXT: Self = Self { input: 5.00, cached_input: 1.25, output: 0.0 };
pub const GPT_IMAGE_2_IMAGE: Self = Self { input: 8.00, cached_input: 2.00, output: 30.00 };
#[deprecated(note = "no published OpenAI rate as of 2026-08-23; lookup_pricing returns None")]
pub const O3_DEEP_RESEARCH: Self = Self { input: 2.00, cached_input: 0.50, output: 8.00 };
#[deprecated(note = "no published OpenAI rate as of 2026-08-23; lookup_pricing returns None")]
pub const O4_MINI_DEEP_RESEARCH: Self = Self { input: 1.10, cached_input: 0.275, output: 4.40 };
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CostBreakdown {
pub input_cost: f64,
pub cache_cost: f64,
pub output_cost: f64,
}
impl CostBreakdown {
pub fn total(&self) -> f64 {
self.input_cost + self.cache_cost + self.output_cost
}
}
impl std::fmt::Display for CostBreakdown {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"${:.6} (in=${:.6} cache=${:.6} out=${:.6})",
self.total(),
self.input_cost,
self.cache_cost,
self.output_cost
)
}
}
pub fn estimate_cost(
pricing: &OpenAIPricing,
input_tokens: u64,
output_tokens: u64,
cached_tokens: u64,
) -> CostBreakdown {
let mtok = 1_000_000.0;
CostBreakdown {
input_cost: input_tokens as f64 / mtok * pricing.input,
cache_cost: cached_tokens as f64 / mtok * pricing.cached_input,
output_cost: output_tokens as f64 / mtok * pricing.output,
}
}
pub fn estimate_batch_cost(
pricing: &OpenAIPricing,
input_tokens: u64,
output_tokens: u64,
cached_tokens: u64,
) -> CostBreakdown {
let mtok = 1_000_000.0;
CostBreakdown {
input_cost: input_tokens as f64 / mtok * pricing.input * 0.5,
cache_cost: cached_tokens as f64 / mtok * pricing.cached_input * 0.5,
output_cost: output_tokens as f64 / mtok * pricing.output * 0.5,
}
}
pub fn lookup_pricing(model_name: &str) -> Option<&'static OpenAIPricing> {
match model_name {
"gpt-5.6" | "gpt-5.6-sol" | "daybreak-blue-latest" => Some(&OpenAIPricing::GPT_56_SOL),
"gpt-5.6-terra" => Some(&OpenAIPricing::GPT_56_TERRA),
"gpt-5.6-luna" => Some(&OpenAIPricing::GPT_56_LUNA),
"gpt-5.6-cyber" | "daybreak-red-latest" => Some(&OpenAIPricing::GPT_56_CYBER),
"gpt-5.5" => Some(&OpenAIPricing::GPT_55),
"gpt-5.5-pro" => Some(&OpenAIPricing::GPT_55_PRO),
"gpt-5.5-cyber" => Some(&OpenAIPricing::GPT_55_CYBER),
"gpt-5.4" => Some(&OpenAIPricing::GPT_54),
"gpt-5.4-mini" => Some(&OpenAIPricing::GPT_54_MINI),
"gpt-5.4-nano" => Some(&OpenAIPricing::GPT_54_NANO),
"gpt-5.4-pro" => Some(&OpenAIPricing::GPT_54_PRO),
"gpt-5.3-codex" => Some(&OpenAIPricing::GPT_53_CODEX),
"gpt-5.2" => Some(&OpenAIPricing::GPT_52),
"gpt-5.2-pro" => Some(&OpenAIPricing::GPT_52_PRO),
"gpt-5.1" => Some(&OpenAIPricing::GPT_51),
"gpt-5" => Some(&OpenAIPricing::GPT_5),
"gpt-5-mini" => Some(&OpenAIPricing::GPT_5_MINI),
"gpt-5-nano" => Some(&OpenAIPricing::GPT_5_NANO),
"gpt-5-pro" => Some(&OpenAIPricing::GPT_5_PRO),
"gpt-5-search-api" => Some(&OpenAIPricing::GPT_5_SEARCH_API),
"chat-latest" => Some(&OpenAIPricing::CHAT_LATEST),
"gpt-4.1" => Some(&OpenAIPricing::GPT_41),
"gpt-4.1-mini" => Some(&OpenAIPricing::GPT_41_MINI),
"gpt-4.1-nano" => Some(&OpenAIPricing::GPT_41_NANO),
"o3" => Some(&OpenAIPricing::O3),
"o4-mini" => Some(&OpenAIPricing::O4_MINI),
"o3-mini" => Some(&OpenAIPricing::O3_MINI),
"o1" => Some(&OpenAIPricing::O1),
"gpt-4o" => Some(&OpenAIPricing::GPT_4O),
"gpt-4o-mini" => Some(&OpenAIPricing::GPT_4O_MINI),
"gpt-realtime-1.5" => Some(&OpenAIPricing::GPT_REALTIME_15_TEXT),
"gpt-realtime-1.5-audio" => Some(&OpenAIPricing::GPT_REALTIME_15_AUDIO),
"gpt-realtime-2.1" => Some(&OpenAIPricing::GPT_REALTIME_21_TEXT),
"gpt-realtime-2.1-audio" => Some(&OpenAIPricing::GPT_REALTIME_21_AUDIO),
"gpt-image-1.5" => Some(&OpenAIPricing::GPT_IMAGE_15_TEXT),
"gpt-image-1.5-image" => Some(&OpenAIPricing::GPT_IMAGE_15_IMAGE),
"gpt-image-2" => Some(&OpenAIPricing::GPT_IMAGE_2_TEXT),
"gpt-image-2-image" => Some(&OpenAIPricing::GPT_IMAGE_2_IMAGE),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gpt_41_basic_cost() {
let cost = estimate_cost(&OpenAIPricing::GPT_41, 1_000_000, 1_000_000, 0);
assert!((cost.input_cost - 2.00).abs() < 1e-9);
assert!((cost.output_cost - 8.00).abs() < 1e-9);
assert!((cost.total() - 10.00).abs() < 1e-9);
}
#[test]
fn gpt_41_with_cache() {
let cost = estimate_cost(&OpenAIPricing::GPT_41, 500_000, 100_000, 500_000);
assert!((cost.input_cost - 1.00).abs() < 1e-9);
assert!((cost.cache_cost - 0.25).abs() < 1e-9);
assert!((cost.output_cost - 0.80).abs() < 1e-9);
assert!((cost.total() - 2.05).abs() < 1e-9);
}
#[test]
fn gpt_5_cache_discount_90_percent() {
for p in [
OpenAIPricing::GPT_5,
OpenAIPricing::GPT_5_MINI,
OpenAIPricing::GPT_5_NANO,
OpenAIPricing::GPT_51,
OpenAIPricing::GPT_52,
OpenAIPricing::GPT_54,
OpenAIPricing::GPT_55,
OpenAIPricing::GPT_56_SOL,
OpenAIPricing::GPT_56_TERRA,
OpenAIPricing::GPT_56_LUNA,
] {
assert!(
(p.cached_input - p.input * 0.1).abs() < 1e-9,
"cached {} is not 10% of input {}",
p.cached_input,
p.input
);
}
}
#[test]
fn o4_mini_reasoning_cost() {
let cost = estimate_cost(&OpenAIPricing::O4_MINI, 1_000_000, 5_000_000, 0);
assert!((cost.input_cost - 1.10).abs() < 1e-9);
assert!((cost.output_cost - 22.00).abs() < 1e-9);
}
#[test]
fn batch_50_percent_discount() {
let standard = estimate_cost(&OpenAIPricing::GPT_41, 1_000_000, 1_000_000, 0);
let batch = estimate_batch_cost(&OpenAIPricing::GPT_41, 1_000_000, 1_000_000, 0);
assert!((batch.total() - standard.total() * 0.5).abs() < 1e-9);
}
#[test]
fn gpt_41_nano_cheapest() {
let cost = estimate_cost(&OpenAIPricing::GPT_41_NANO, 1_000_000, 1_000_000, 0);
assert!((cost.input_cost - 0.10).abs() < 1e-9);
assert!((cost.output_cost - 0.40).abs() < 1e-9);
}
#[test]
fn zero_tokens_zero_cost() {
let cost = estimate_cost(&OpenAIPricing::GPT_5, 0, 0, 0);
assert!((cost.total() - 0.0).abs() < 1e-9);
}
#[test]
fn display_format() {
let cost = CostBreakdown { input_cost: 0.003, cache_cost: 0.001, output_cost: 0.0075 };
let s = cost.to_string();
assert!(s.starts_with('$'));
assert!(s.contains("in="));
assert!(s.contains("cache="));
assert!(s.contains("out="));
}
#[test]
fn lookup_known_models() {
for id in [
"gpt-5.6",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.6-cyber",
"daybreak-blue-latest",
"daybreak-red-latest",
"gpt-realtime-2.1",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.4-pro",
"gpt-5.3-codex",
"chat-latest",
"gpt-5.2",
"gpt-5.2-pro",
"gpt-5.1",
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-5-pro",
"gpt-5-search-api",
"gpt-image-2",
"gpt-image-2-image",
] {
assert!(lookup_pricing(id).is_some(), "{id} should be priced");
}
}
#[test]
fn lookup_unknown_model_returns_none() {
assert!(lookup_pricing("unknown-model").is_none());
assert!(lookup_pricing("gpt-99").is_none());
assert!(lookup_pricing("").is_none());
}
#[test]
fn unpublished_models_are_unpriced() {
for id in [
"gpt-5.5-instant",
"gpt-5.2-codex",
"gpt-5.1-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5.3-chat-latest",
"o3-deep-research",
"o4-mini-deep-research",
] {
assert!(lookup_pricing(id).is_none(), "{id} has no published rate");
}
}
#[test]
fn lookup_pricing_matches_published_rates() {
for (id, input, cached, output) in [
("gpt-5.6-sol", 4.00, 0.40, 20.00),
("gpt-5.6-terra", 2.00, 0.20, 12.00),
("gpt-5.6-luna", 0.20, 0.02, 1.20),
("gpt-5.6-cyber", 12.50, 1.25, 75.00),
("gpt-5.5", 5.00, 0.50, 30.00),
("gpt-5.5-pro", 30.00, 30.00, 180.00),
("gpt-5.4", 2.50, 0.25, 15.00),
("gpt-5.4-mini", 0.75, 0.075, 4.50),
("gpt-5.4-nano", 0.20, 0.02, 1.25),
("gpt-5.4-pro", 30.00, 30.00, 180.00),
("gpt-5.3-codex", 1.75, 0.175, 14.00),
("chat-latest", 5.00, 0.50, 30.00),
("gpt-5.2", 1.75, 0.175, 14.00),
("gpt-5.2-pro", 21.00, 21.00, 168.00),
("gpt-5.1", 1.25, 0.125, 10.00),
("gpt-5", 1.25, 0.125, 10.00),
("gpt-5-mini", 0.25, 0.025, 2.00),
("gpt-5-nano", 0.05, 0.005, 0.40),
("gpt-5-pro", 15.00, 15.00, 120.00),
("gpt-4.1", 2.00, 0.50, 8.00),
("gpt-4.1-mini", 0.40, 0.10, 1.60),
("gpt-4.1-nano", 0.10, 0.025, 0.40),
("gpt-4o", 2.50, 1.25, 10.00),
("gpt-4o-mini", 0.15, 0.075, 0.60),
("o3", 2.00, 0.50, 8.00),
("o4-mini", 1.10, 0.275, 4.40),
("o3-mini", 1.10, 0.55, 4.40),
("o1", 15.00, 7.50, 60.00),
] {
let p = lookup_pricing(id).unwrap_or_else(|| panic!("{id} missing"));
assert!((p.input - input).abs() < 1e-9, "{id} input {} != {input}", p.input);
assert!(
(p.cached_input - cached).abs() < 1e-9,
"{id} cached {} != {cached}",
p.cached_input
);
assert!((p.output - output).abs() < 1e-9, "{id} output {} != {output}", p.output);
}
}
#[test]
fn tier_and_alias_invariants() {
for (short, long) in [
(OpenAIPricing::GPT_56_SOL, OpenAIPricing::GPT_56_SOL_LONG),
(OpenAIPricing::GPT_56_TERRA, OpenAIPricing::GPT_56_TERRA_LONG),
(OpenAIPricing::GPT_56_LUNA, OpenAIPricing::GPT_56_LUNA_LONG),
(OpenAIPricing::GPT_55, OpenAIPricing::GPT_55_LONG),
(OpenAIPricing::GPT_54, OpenAIPricing::GPT_54_LONG),
(OpenAIPricing::GPT_55_PRO, OpenAIPricing::GPT_55_PRO_LONG),
(OpenAIPricing::GPT_54_PRO, OpenAIPricing::GPT_54_PRO_LONG),
] {
assert!(long.input > short.input);
assert!(long.output > short.output);
}
let sol = lookup_pricing("gpt-5.6-sol").unwrap();
assert!((lookup_pricing("daybreak-blue-latest").unwrap().input - sol.input).abs() < 1e-9);
let cyber = lookup_pricing("gpt-5.6-cyber").unwrap();
assert!((lookup_pricing("daybreak-red-latest").unwrap().input - cyber.input).abs() < 1e-9);
}
}