Skip to content
Open
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
42 changes: 9 additions & 33 deletions rust/src/codex_costs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub(crate) fn add_codex_records_to_summary(

/// Merge billable records into a day→model→`[input,cached,output]` map.
pub(crate) fn merge_codex_records_into_days(
days: &mut std::collections::HashMap<String, std::collections::HashMap<String, Vec<i32>>>,
days: &mut std::collections::HashMap<String, std::collections::HashMap<String, Vec<i64>>>,
records: &[CodexUsageRecord],
) {
for record in records {
Expand All @@ -77,7 +77,7 @@ pub(crate) fn merge_codex_records_into_days(
pub(crate) fn add_codex_packed_tokens_to_summary(
summary: &mut CostSummary,
model: &str,
packed: &[i32],
packed: &[i64],
pricing_day: Option<NaiveDate>,
) -> Option<f64> {
let input = packed.first().copied().unwrap_or(0);
Expand All @@ -99,7 +99,7 @@ pub(crate) fn add_codex_packed_tokens_to_summary(
/// Returns `(session_cost, has_tokens)` — caller adds cost to `total_cost_usd`.
pub(crate) fn add_codex_days_map_to_summary(
summary: &mut CostSummary,
days: &std::collections::HashMap<String, std::collections::HashMap<String, Vec<i32>>>,
days: &std::collections::HashMap<String, std::collections::HashMap<String, Vec<i64>>>,
range: &CostUsageDayRange,
) -> (f64, bool) {
let mut total_cost = 0.0;
Expand Down Expand Up @@ -153,12 +153,12 @@ struct CodexTokenCounts {
}

impl CodexTokenCounts {
fn from_values(input: i32, cached: i32, output: i32) -> Self {
let input = input.max(0) as u64;
fn from_values(input: i64, cached: i64, output: i64) -> Self {
let input = u64::try_from(input.max(0)).unwrap_or(0);
Self {
input,
cached: (cached.max(0) as u64).min(input),
output: output.max(0) as u64,
cached: u64::try_from(cached.max(0)).unwrap_or(0).min(input),
output: u64::try_from(output.max(0)).unwrap_or(0),
reasoning: None,
}
}
Expand Down Expand Up @@ -404,35 +404,11 @@ fn codex_cost_usd_for_day(

let normalized = CostUsagePricing::normalize_codex_model(model);
if normalized.contains("fast") || normalized.contains("priority") {
// Fast pricing takes i32 token counts; usage-record counts fit far
// below i32::MAX, and the callee re-checks the long-context threshold
// against the original u64 magnitude.
#[allow(
clippy::cast_possible_truncation,
reason = "token counts from usage records fit i32"
)]
#[allow(
clippy::cast_possible_wrap,
reason = "token counts are non-negative; wrapping is impossible"
)]
let fast = pricing_day
.and_then(|day| {
CostUsagePricing::codex_fast_cost_usd_at_date(
model,
input as i32,
cached as i32,
output as i32,
day,
)
CostUsagePricing::codex_fast_cost_usd_at_date(model, input, cached, output, day)
})
.or_else(|| {
CostUsagePricing::codex_fast_cost_usd(
model,
input as i32,
cached as i32,
output as i32,
)
});
.or_else(|| CostUsagePricing::codex_fast_cost_usd(model, input, cached, output));
if let Some(cost) = fast {
return cost;
}
Expand Down
16 changes: 8 additions & 8 deletions rust/src/core/cost_cache_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ fn estimated_entry_bytes(entry: &CostUsageFileUsage) -> usize {
/// Conservative estimate of the encoded artifact size.
pub fn estimated_cache_bytes(
files: &HashMap<String, CostUsageFileUsage>,
days: &HashMap<String, HashMap<String, Vec<i32>>>,
days: &HashMap<String, HashMap<String, Vec<i64>>>,
) -> usize {
let mut bytes = 4096;
bytes += files.len() * 160;
Expand Down Expand Up @@ -121,7 +121,7 @@ pub fn estimated_cache_bytes(
/// shape (no fork lineages, no discovery/lookback state).
pub fn prune_out_of_window_for_budget(
files: &mut HashMap<String, CostUsageFileUsage>,
days: &mut HashMap<String, HashMap<String, Vec<i32>>>,
days: &mut HashMap<String, HashMap<String, Vec<i64>>>,
scan_since_key: Option<&str>,
scan_until_key: Option<&str>,
force: bool,
Expand Down Expand Up @@ -171,7 +171,7 @@ pub fn prune_out_of_window_for_budget(
/// cache shape.
pub fn trim_in_window_for_budget(
files: &mut HashMap<String, CostUsageFileUsage>,
days: &mut HashMap<String, HashMap<String, Vec<i32>>>,
days: &mut HashMap<String, HashMap<String, Vec<i64>>>,
scan_since_key: Option<&str>,
scan_until_key: Option<&str>,
max_bytes: usize,
Expand Down Expand Up @@ -248,8 +248,8 @@ pub fn trim_in_window_for_budget(
/// of the scanner's `rebuild_cache_days` accumulation), so pruned entries do
/// not inflate totals.
fn subtract_entry_days(
days: &mut HashMap<String, HashMap<String, Vec<i32>>>,
entry_days: &HashMap<String, HashMap<String, Vec<i32>>>,
days: &mut HashMap<String, HashMap<String, Vec<i64>>>,
entry_days: &HashMap<String, HashMap<String, Vec<i64>>>,
) {
let mut empty_days = Vec::new();
for (day, models) in entry_days {
Expand Down Expand Up @@ -315,7 +315,7 @@ mod tests {
use super::*;

fn entry(days: &[&str], parsed: Option<i64>, size: i64) -> CostUsageFileUsage {
let mut day_map: HashMap<String, HashMap<String, Vec<i32>>> = HashMap::new();
let mut day_map: HashMap<String, HashMap<String, Vec<i64>>> = HashMap::new();
for day in days {
day_map.insert(
(*day).to_string(),
Expand All @@ -342,12 +342,12 @@ mod tests {

type TestCache = (
HashMap<String, CostUsageFileUsage>,
HashMap<String, HashMap<String, Vec<i32>>>,
HashMap<String, HashMap<String, Vec<i64>>>,
);

fn cache(files: &[(&str, CostUsageFileUsage)]) -> TestCache {
let mut file_map = HashMap::new();
let mut days: HashMap<String, HashMap<String, Vec<i32>>> = HashMap::new();
let mut days: HashMap<String, HashMap<String, Vec<i64>>> = HashMap::new();
for (key, entry) in files {
for (day, models) in &entry.days {
let day_entry = days.entry(day.clone()).or_default();
Expand Down
27 changes: 8 additions & 19 deletions rust/src/core/cost_pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,22 +703,17 @@ impl CostUsagePricing {
/// suffixes), then applies the Fast multiplier. Returns `None` when the
/// model has no Fast lane or when a model without Astra's published
/// long-context Fast rates exceeds the 272 000 threshold.
pub fn codex_fast_cost_usd(model: &str, input: i32, cached: i32, output: i32) -> Option<f64> {
pub fn codex_fast_cost_usd(model: &str, input: u64, cached: u64, output: u64) -> Option<f64> {
let multiplier = Self::codex_api_fast_multiplier(model)?;
// Older models do not offer Fast for long-context requests. Astra
// publishes a Fast rate for the same whole-request long-context tier.
if (input.max(0) as u64) > codex_pricing::CODEX_LONG_CONTEXT_THRESHOLD
if input > codex_pricing::CODEX_LONG_CONTEXT_THRESHOLD
&& !codex_pricing::codex_fast_allows_long_context(model)
{
return None;
}
let base = Self::codex_fast_base_model(model);
let base_cost = Self::codex_cost_usd(
&base,
input.max(0) as u64,
cached.max(0) as u64,
output.max(0) as u64,
)?;
let base_cost = Self::codex_cost_usd(&base, input, cached, output)?;
Some(base_cost * multiplier)
}

Expand Down Expand Up @@ -782,25 +777,19 @@ impl CostUsagePricing {

pub fn codex_fast_cost_usd_at_date(
model: &str,
input: i32,
cached: i32,
output: i32,
input: u64,
cached: u64,
output: u64,
pricing_date: NaiveDate,
) -> Option<f64> {
let multiplier = Self::codex_api_fast_multiplier(model)?;
if (input.max(0) as u64) > codex_pricing::CODEX_LONG_CONTEXT_THRESHOLD
if input > codex_pricing::CODEX_LONG_CONTEXT_THRESHOLD
&& !codex_pricing::codex_fast_allows_long_context(model)
{
return None;
}
let base = Self::codex_fast_base_model(model);
let base_cost = Self::codex_cost_usd_at_date(
&base,
input.max(0) as u64,
cached.max(0) as u64,
output.max(0) as u64,
pricing_date,
)?;
let base_cost = Self::codex_cost_usd_at_date(&base, input, cached, output, pricing_date)?;
Some(base_cost * multiplier)
}

Expand Down
65 changes: 45 additions & 20 deletions rust/src/core/jsonl_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use std::io::{BufReader, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};

const CODEX_CACHE_SCHEMA_VERSION: u32 = 1;

#[derive(Debug, Clone, Default)]
pub struct CachedCostReadStatus {
pub has_days: bool,
Expand All @@ -31,6 +33,8 @@ pub struct CachedCostReadStatus {

#[derive(Deserialize, Default)]
struct CachedCostReadStatusProjection {
#[serde(default)]
codex_cache_schema_version: u32,
#[serde(
default,
rename = "days",
Expand Down Expand Up @@ -182,12 +186,15 @@ pub enum CodexScanPauseReason {
/// Cache for scanned file data
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CostUsageCache {
/// Codex cache schema. Version 0 is any pre-64-bit cache and must be rebuilt.
#[serde(default)]
pub codex_cache_schema_version: u32,
/// Last scan timestamp in milliseconds
pub last_scan_unix_ms: i64,
/// Per-file usage data
pub files: HashMap<String, CostUsageFileUsage>,
/// Aggregated daily data: day_key -> model -> [input, cached, output, reasoning?]
pub days: HashMap<String, HashMap<String, Vec<i32>>>,
pub days: HashMap<String, HashMap<String, Vec<i64>>>,
/// Inclusive range covered by the last successful full inspection.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scan_since_key: Option<String>,
Expand Down Expand Up @@ -244,7 +251,7 @@ pub struct CostUsageFileUsage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub codex_file_identity: Option<String>,
/// Daily usage data extracted from this file
pub days: HashMap<String, HashMap<String, Vec<i32>>>,
pub days: HashMap<String, HashMap<String, Vec<i64>>>,
/// Bytes parsed so far (for incremental parsing)
pub parsed_bytes: Option<i64>,
/// Frozen logical end of the scan target. A growing rollout may have a
Expand Down Expand Up @@ -293,11 +300,11 @@ pub(crate) struct CodexSessionMetadata {
/// Running totals for Codex token counting
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodexTotals {
pub input: i32,
pub cached: i32,
pub output: i32,
pub input: i64,
pub cached: i64,
pub output: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<i32>,
pub reasoning: Option<i64>,
}

/// Snapshot of the last validated cost report, persisted so spend surfaces keep
Expand All @@ -309,14 +316,14 @@ pub struct CachedCostReport {
/// Total cost in USD for the reported window.
pub total_cost_usd: f64,
/// Total input tokens.
pub input_tokens: i32,
pub input_tokens: i64,
/// Total cached tokens.
pub cached_tokens: i32,
pub cached_tokens: i64,
/// Total output tokens.
pub output_tokens: i32,
pub output_tokens: i64,
/// Total reasoning output tokens when every contributing packed row knows it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<i32>,
pub reasoning_tokens: Option<i64>,
/// Number of sessions contributing.
pub sessions_count: i32,
/// ISO 8601 timestamp when this report was generated.
Expand Down Expand Up @@ -360,10 +367,10 @@ pub struct CodexParseResult {
pub struct CodexUsageRecord {
pub day_key: String,
pub model: String,
pub input: i32,
pub cached: i32,
pub output: i32,
pub reasoning: Option<i32>,
pub input: i64,
pub cached: i64,
pub output: i64,
pub reasoning: Option<i64>,
}

/// Day range for scanning
Expand Down Expand Up @@ -440,7 +447,17 @@ impl JsonlScanner {
if let Ok(contents) = fs::read_to_string(&cache_path)
&& let Ok(mut cache) = serde_json::from_str::<CostUsageCache>(&contents)
{
cache.loaded_stamp = Some(Some(CacheStamp::from_bytes(contents.as_bytes())));
let stamp = CacheStamp::from_bytes(contents.as_bytes());
if provider == ProviderId::Codex
&& cache.codex_cache_schema_version != CODEX_CACHE_SCHEMA_VERSION
{
return CostUsageCache {
codex_cache_schema_version: CODEX_CACHE_SCHEMA_VERSION,
loaded_stamp: Some(Some(stamp)),
..CostUsageCache::default()
};
}
cache.loaded_stamp = Some(Some(stamp));
return cache;
}

Expand Down Expand Up @@ -480,6 +497,11 @@ impl JsonlScanner {
else {
return CachedCostReadStatus::default();
};
if provider == ProviderId::Codex
&& projection.codex_cache_schema_version != CODEX_CACHE_SCHEMA_VERSION
{
return CachedCostReadStatus::default();
}
CachedCostReadStatus {
has_days: projection.has_days,
previous_report: projection.previous_report,
Expand All @@ -488,10 +510,10 @@ impl JsonlScanner {
}
pub(crate) fn cached_cost_report_from_days(cache: &CostUsageCache) -> CachedCostReport {
let mut total_cost_usd = 0.0;
let mut input_tokens = 0_i32;
let mut cached_tokens = 0_i32;
let mut output_tokens = 0_i32;
let mut reasoning_tokens = 0_i32;
let mut input_tokens = 0_i64;
let mut cached_tokens = 0_i64;
let mut output_tokens = 0_i64;
let mut reasoning_tokens = 0_i64;
let mut reasoning_known = true;
let mut partial = false;

Expand Down Expand Up @@ -569,7 +591,7 @@ impl JsonlScanner {
/// Merge one Codex record into a packed day/model row. A three-slot row is
/// deliberately treated as reasoning-unknown, including when a known row
/// is merged into an existing legacy row.
pub(crate) fn merge_codex_record_into_packed(packed: &mut Vec<i32>, record: &CodexUsageRecord) {
pub(crate) fn merge_codex_record_into_packed(packed: &mut Vec<i64>, record: &CodexUsageRecord) {
let was_empty = packed.is_empty();
if packed.len() < 3 {
packed.resize(3, 0);
Expand Down Expand Up @@ -627,6 +649,9 @@ impl JsonlScanner {
{
return;
}
if provider == ProviderId::Codex {
cache.codex_cache_schema_version = CODEX_CACHE_SCHEMA_VERSION;
}

let Some(parent) = cache_path.parent() else {
return;
Expand Down
Loading