Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions rust/src/core/cost_pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,24 @@ impl CostUsagePricing {
cached_input_tokens: u64,
output_tokens: u64,
pricing_date: NaiveDate,
) -> Option<f64> {
Self::codex_cost_usd_at_date_with_pricing_snapshot(
model,
input_tokens,
cached_input_tokens,
output_tokens,
pricing_date,
None,
)
}

pub fn codex_cost_usd_at_date_with_pricing_snapshot(
model: &str,
input_tokens: u64,
cached_input_tokens: u64,
output_tokens: u64,
pricing_date: NaiveDate,
pricing_snapshot: Option<&models_dev_pricing::ModelsDevPricingSnapshot>,
) -> Option<f64> {
let key = Self::normalize_codex_model(model);
let cutoff = NaiveDate::from_ymd_opt(2026, 7, 30).expect("valid pricing cutoff");
Expand All @@ -779,7 +797,13 @@ impl CostUsagePricing {
));
}
}
Self::codex_cost_usd(model, input_tokens, cached_input_tokens, output_tokens)
Self::codex_cost_usd_with_pricing_snapshot(
model,
input_tokens,
cached_input_tokens,
output_tokens,
pricing_snapshot,
)
}

pub fn codex_fast_cost_usd_at_date(
Expand Down Expand Up @@ -810,6 +834,22 @@ impl CostUsagePricing {
input_tokens: u64,
cached_input_tokens: u64,
output_tokens: u64,
) -> Option<f64> {
Self::codex_cost_usd_with_pricing_snapshot(
model,
input_tokens,
cached_input_tokens,
output_tokens,
None,
)
}

pub fn codex_cost_usd_with_pricing_snapshot(
model: &str,
input_tokens: u64,
cached_input_tokens: u64,
output_tokens: u64,
pricing_snapshot: Option<&models_dev_pricing::ModelsDevPricingSnapshot>,
) -> Option<f64> {
let key = Self::normalize_codex_model(model);
// Model-less / deliberately unattributed usage stays unpriced even if a
Expand Down Expand Up @@ -861,7 +901,10 @@ impl CostUsagePricing {
}
None => ("openai", model),
};
let pricing = models_dev_pricing::lookup(provider_id, lookup_model)?;
let pricing = match pricing_snapshot {
Some(snapshot) => snapshot.lookup(provider_id, lookup_model),
None => models_dev_pricing::lookup(provider_id, lookup_model),
}?;
let use_tier = pricing
.threshold_tokens
.is_some_and(|threshold| input_tokens > threshold);
Expand Down
22 changes: 22 additions & 0 deletions rust/src/core/models_dev_pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,21 @@ struct ModelsDevCatalog {
providers: HashMap<String, ModelsDevProvider>,
}

/// Immutable models.dev view for callers that price many rows in one pass.
/// Loading this once avoids repeating cache metadata checks for every row.
#[derive(Debug, Clone)]
pub struct ModelsDevPricingSnapshot {
artifact: Option<Arc<ModelsDevCacheArtifact>>,
}

impl ModelsDevPricingSnapshot {
pub fn lookup(&self, provider_id: &str, model_id: &str) -> Option<DynamicModelPricing> {
self.artifact
.as_ref()
.and_then(|artifact| artifact.catalog.lookup(provider_id, model_id))
}
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ModelsDevCatalogWire {
Expand Down Expand Up @@ -714,6 +729,13 @@ async fn wait_for_refresh(mut receiver: watch::Receiver<Option<bool>>) -> bool {
static REFRESH_COORDINATOR: LazyLock<ModelsDevRefreshCoordinator> =
LazyLock::new(ModelsDevRefreshCoordinator::default);

/// Loads the cached models.dev catalog once for bulk-pricing callers.
pub fn pricing_snapshot() -> ModelsDevPricingSnapshot {
let load = ModelsDevCache::load(SystemTime::now(), None);
let artifact = (!load.is_stale).then_some(load.artifact).flatten();
ModelsDevPricingSnapshot { artifact }
}

/// Looks up a cached models.dev price for a provider/model pair.
pub fn lookup(provider_id: &str, model_id: &str) -> Option<DynamicModelPricing> {
let load = ModelsDevCache::load(SystemTime::now(), None);
Expand Down
17 changes: 13 additions & 4 deletions rust/src/spend_contract/opencodex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ fn aggregate(
let mut daily: BTreeMap<String, DailyAccumulator> = BTreeMap::new();
let mut known_cost = 0.0;
let mut saw_known_cost = false;
// Upstream 0.55.0 #3136: resolve the dynamic pricing catalog once per
// aggregate instead of re-checking its cache metadata for every usage row.
let pricing_snapshot = crate::core::pricing_snapshot();

for entry in &entries {
if let Some(conversation) = entry.conversation_id.as_ref() {
Expand All @@ -163,7 +166,7 @@ fn aggregate(
token_mix.reasoning_tokens =
add_optional(token_mix.reasoning_tokens, entry.reasoning_tokens);

let cost = entry_cost(entry, custom);
let cost = entry_cost(entry, custom, &pricing_snapshot);
match entry.usage_status.as_str() {
"reported" if cost.is_some() => coverage.priced = coverage.priced.saturating_add(1),
"estimated" if cost.is_some() => {
Expand Down Expand Up @@ -273,7 +276,11 @@ fn aggregate(
})
}

fn entry_cost(entry: &OpenCodexEntry, custom: &CustomPricing) -> Option<f64> {
fn entry_cost(
entry: &OpenCodexEntry,
custom: &CustomPricing,
pricing_snapshot: &crate::core::ModelsDevPricingSnapshot,
) -> Option<f64> {
if !matches!(entry.usage_status.as_str(), "reported" | "estimated") {
return None;
}
Expand All @@ -293,12 +300,13 @@ fn entry_cost(entry: &OpenCodexEntry, custom: &CustomPricing) -> Option<f64> {
return rates.cost_parts(input, output, cache_read, cache_write);
}
let pricing_model = pricing_model(entry)?;
CostUsagePricing::codex_cost_usd_at_date(
CostUsagePricing::codex_cost_usd_at_date_with_pricing_snapshot(
&pricing_model,
input,
cache_read,
output,
entry.timestamp.date_naive(),
Some(pricing_snapshot),
)
}

Expand Down Expand Up @@ -625,7 +633,8 @@ mod tests {
#[test]
fn opencodex_uses_request_day_for_historical_gpt56_pricing() {
let entry = entry("openai", "gpt-5.6-terra");
let cost = entry_cost(&entry, &CustomPricing::default()).unwrap();
let pricing_snapshot = crate::core::pricing_snapshot();
let cost = entry_cost(&entry, &CustomPricing::default(), &pricing_snapshot).unwrap();
let expected = 90.0 * 2.5e-6 + 10.0 * 2.5e-7 + 5.0 * 1.5e-5;
assert!((cost - expected).abs() < 1e-12);
}
Expand Down
Loading