Skip to content

Commit 954267f

Browse files
committed
fix(pricing): resolve Codex model aliases
Codex logs can contain internal or shortened model identifiers that are not published as pricing keys. Treat these as model-name aliases instead of adding duplicated pricing rows. The fallback now preserves the raw model label in reports while resolving codex-auto-review to gpt-5.5 and gpt-5.3-spark to the existing gpt-5.3-codex-spark pricing path.
1 parent f89e52f commit 954267f

1 file changed

Lines changed: 56 additions & 28 deletions

File tree

‎rust/crates/ccusage/src/pricing.rs‎

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -361,22 +361,29 @@ impl PricingMap {
361361
}
362362

363363
pub(crate) fn find(&self, model: &str) -> Option<Pricing> {
364-
self.find_entry(model)
364+
self.find_entry_or_alias(model)
365365
.or_else(|| {
366366
self.enable_models_dev_fallback
367-
.then(|| models_dev_pricing().and_then(|pricing| pricing.find_entry(model)))
367+
.then(|| {
368+
models_dev_pricing().and_then(|pricing| pricing.find_entry_or_alias(model))
369+
})
368370
.flatten()
369371
})
370372
// The embedded models.dev snapshot is a separate map, so it only
371373
// resolves models the primary table misses and never perturbs its
372374
// fuzzy alias matching. It works offline, unlike the network source.
373375
.or_else(|| {
374376
self.enable_embedded_models_dev_fallback
375-
.then(|| embedded_models_dev_pricing().find_entry(model))
377+
.then(|| embedded_models_dev_pricing().find_entry_or_alias(model))
376378
.flatten()
377379
})
378380
}
379381

382+
fn find_entry_or_alias(&self, model: &str) -> Option<Pricing> {
383+
self.find_entry(model)
384+
.or_else(|| pricing_alias(model).and_then(|alias| self.find_entry(alias)))
385+
}
386+
380387
fn find_entry(&self, model: &str) -> Option<Pricing> {
381388
self.entries.get(model).copied().or_else(|| {
382389
let normalized_model = normalized_pricing_key(model);
@@ -733,23 +740,23 @@ impl PricingMap {
733740
fast_multiplier: 1.0,
734741
},
735742
);
736-
let gpt_5_5_pricing = Pricing {
737-
input: 5e-6,
738-
output: 30e-6,
739-
cache_create: 5e-6,
740-
cache_read: 0.5e-6,
741-
cache_read_explicit: true,
742-
input_above_200k: None,
743-
output_above_200k: None,
744-
cache_create_above_200k: None,
745-
cache_read_above_200k: None,
746-
fast_multiplier: fast_multiplier_overrides
747-
.multiplier_for("gpt-5.5")
748-
.unwrap_or(1.0),
749-
};
750-
self.entries.insert("gpt-5.5".to_string(), gpt_5_5_pricing);
751-
self.entries
752-
.insert("codex-auto-review".to_string(), gpt_5_5_pricing);
743+
self.entries.insert(
744+
"gpt-5.5".to_string(),
745+
Pricing {
746+
input: 5e-6,
747+
output: 30e-6,
748+
cache_create: 5e-6,
749+
cache_read: 0.5e-6,
750+
cache_read_explicit: true,
751+
input_above_200k: None,
752+
output_above_200k: None,
753+
cache_create_above_200k: None,
754+
cache_read_above_200k: None,
755+
fast_multiplier: fast_multiplier_overrides
756+
.multiplier_for("gpt-5.5")
757+
.unwrap_or(1.0),
758+
},
759+
);
753760
self.entries.insert(
754761
"grok-4.3".to_string(),
755762
Pricing {
@@ -826,14 +833,15 @@ impl PricingMap {
826833
};
827834
self.entries
828835
.insert("gpt-5.2-codex".to_string(), gpt_5_codex_pricing);
829-
let gpt_5_3_codex_pricing = Pricing {
830-
fast_multiplier: fast_multiplier_overrides
831-
.multiplier_for("gpt-5.3-codex")
832-
.unwrap_or(1.0),
833-
..gpt_5_codex_pricing
834-
};
835-
self.entries
836-
.insert("gpt-5.3-codex".to_string(), gpt_5_3_codex_pricing);
836+
self.entries.insert(
837+
"gpt-5.3-codex".to_string(),
838+
Pricing {
839+
fast_multiplier: fast_multiplier_overrides
840+
.multiplier_for("gpt-5.3-codex")
841+
.unwrap_or(1.0),
842+
..gpt_5_codex_pricing
843+
},
844+
);
837845
self.entries
838846
.insert("gpt-5.2".to_string(), gpt_5_codex_pricing);
839847
self.entries.insert(
@@ -1117,6 +1125,14 @@ fn normalized_pricing_key(value: &str) -> Cow<'_, str> {
11171125
}
11181126
}
11191127

1128+
fn pricing_alias(model: &str) -> Option<&'static str> {
1129+
match model {
1130+
"codex-auto-review" => Some("gpt-5.5"),
1131+
"gpt-5.3-spark" => Some("gpt-5.3-codex-spark"),
1132+
_ => None,
1133+
}
1134+
}
1135+
11201136
fn matches_model_suffix(part: &str, base: &str) -> bool {
11211137
let Some(index) = part.rfind(base) else {
11221138
return false;
@@ -1783,6 +1799,18 @@ mod tests {
17831799
assert_eq!(auto_review.fast_multiplier, latest_codex.fast_multiplier);
17841800
}
17851801

1802+
#[test]
1803+
fn embedded_pricing_resolves_codex_spark_short_model() {
1804+
let pricing = PricingMap::load_embedded();
1805+
let short_spark = pricing.find("gpt-5.3-spark").unwrap();
1806+
let codex_spark = pricing.find("gpt-5.3-codex-spark").unwrap();
1807+
1808+
assert_eq!(short_spark.input, codex_spark.input);
1809+
assert_eq!(short_spark.output, codex_spark.output);
1810+
assert_eq!(short_spark.cache_read, codex_spark.cache_read);
1811+
assert_eq!(short_spark.fast_multiplier, codex_spark.fast_multiplier);
1812+
}
1813+
17861814
#[test]
17871815
fn embedded_pricing_includes_claude_fast_multiplier_for_provider_models() {
17881816
let pricing = PricingMap::load_embedded();

0 commit comments

Comments
 (0)