Skip to content

Commit dc2a90d

Browse files
committed
fix(pricing): add Claude Opus 4.8 rates
Embed Claude Opus 4.8 pricing for offline reports so usage does not fall back to the deprecated Claude Opus 4 rate when the runtime LiteLLM refresh is unavailable. Use the current Anthropic fast mode rate for Opus 4.8, which is input and output per MTok, so the fast multiplier is 2x instead of the 6x multiplier used by Opus 4.6 and 4.7. Tighten pricing-key fuzzy matching so date-suffixed model IDs can still resolve through their base key while unknown numeric model revisions such as claude-opus-4.8 or claude-opus-4.70 no longer match claude-opus-4.
1 parent 284899c commit dc2a90d

2 files changed

Lines changed: 113 additions & 5 deletions

File tree

‎rust/crates/ccusage/src/fast-multiplier-overrides.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
},
77
"normalized_prefix": {
88
"claude-opus-4-6": 6.0,
9-
"claude-opus-4-7": 6.0
9+
"claude-opus-4-7": 6.0,
10+
"claude-opus-4-8": 2.0
1011
}
1112
}

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

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,23 @@ impl PricingMap {
259259
.unwrap_or(1.0),
260260
},
261261
);
262+
self.entries.insert(
263+
"claude-opus-4-8".to_string(),
264+
Pricing {
265+
input: 5e-6,
266+
output: 25e-6,
267+
cache_create: 6.25e-6,
268+
cache_read: 0.5e-6,
269+
cache_read_explicit: true,
270+
input_above_200k: None,
271+
output_above_200k: None,
272+
cache_create_above_200k: None,
273+
cache_read_above_200k: None,
274+
fast_multiplier: fast_multiplier_overrides
275+
.multiplier_for("claude-opus-4-8")
276+
.unwrap_or(1.0),
277+
},
278+
);
262279
self.entries.insert(
263280
"claude-haiku-4-5".to_string(),
264281
Pricing {
@@ -550,7 +567,12 @@ impl PricingMap {
550567
self.context_limits
551568
.insert("grok-4.3".to_string(), 1_000_000);
552569
self.context_limits.insert("gpt-5.4".to_string(), 1_050_000);
553-
for model in ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6"] {
570+
for model in [
571+
"claude-opus-4-8",
572+
"claude-opus-4-7",
573+
"claude-opus-4-6",
574+
"claude-sonnet-4-6",
575+
] {
554576
self.context_limits.insert(model.to_string(), 1_000_000);
555577
}
556578
self.context_limits
@@ -591,8 +613,8 @@ fn contains_pricing_key(value: &str, key: &str) -> bool {
591613
.checked_sub(1)
592614
.and_then(|before| value.as_bytes().get(before))
593615
.copied();
594-
let after = value.as_bytes().get(index + key.len()).copied();
595-
before.is_none_or(is_pricing_key_boundary) && after.is_none_or(is_pricing_key_boundary)
616+
let suffix = &value[index + key.len()..];
617+
before.is_none_or(is_pricing_key_boundary) && suffix_allows_pricing_key_match(key, suffix)
596618
})
597619
}
598620

@@ -601,6 +623,37 @@ fn is_pricing_key_boundary(byte: u8) -> bool {
601623
!byte.is_ascii_alphanumeric()
602624
}
603625

626+
fn suffix_allows_pricing_key_match(key: &str, suffix: &str) -> bool {
627+
let Some(separator) = suffix.as_bytes().first().copied() else {
628+
return true;
629+
};
630+
if !is_pricing_key_boundary(separator) {
631+
return false;
632+
}
633+
!suffix_starts_with_numeric_model_version(key, suffix)
634+
}
635+
636+
fn suffix_starts_with_numeric_model_version(key: &str, suffix: &str) -> bool {
637+
if !key.as_bytes().last().is_some_and(u8::is_ascii_digit) {
638+
return false;
639+
}
640+
if !matches!(suffix.as_bytes().first(), Some(b'-' | b'.')) {
641+
return false;
642+
}
643+
644+
let rest = &suffix[1..];
645+
let digit_len = rest
646+
.as_bytes()
647+
.iter()
648+
.take_while(|byte| byte.is_ascii_digit())
649+
.count();
650+
if digit_len == 0 {
651+
return false;
652+
}
653+
let after_digits = rest.as_bytes().get(digit_len).copied();
654+
!(digit_len == 8 && after_digits.is_none_or(is_pricing_key_boundary))
655+
}
656+
604657
/// Normalizes known model separator variants without allocating for canonical keys.
605658
fn normalized_pricing_key(value: &str) -> Cow<'_, str> {
606659
if value.contains(['.', '@']) {
@@ -807,6 +860,13 @@ mod tests {
807860
.fast_multiplier,
808861
6.0
809862
);
863+
assert_eq!(
864+
pricing
865+
.find("anthropic.claude-opus-4-8")
866+
.unwrap()
867+
.fast_multiplier,
868+
2.0
869+
);
810870
}
811871

812872
#[test]
@@ -827,6 +887,18 @@ mod tests {
827887
);
828888
}
829889

890+
#[test]
891+
fn embedded_pricing_resolves_opus_48_dot_model_names() {
892+
let pricing = PricingMap::load_embedded();
893+
894+
let opus_48 = pricing.find("claude-opus-4.8-20260528").unwrap();
895+
assert_eq!(opus_48.input, 5e-6);
896+
assert_eq!(opus_48.output, 25e-6);
897+
assert_eq!(opus_48.cache_create, 6.25e-6);
898+
assert_eq!(opus_48.cache_read, 0.5e-6);
899+
assert_eq!(pricing.context_limit("claude-opus-4.8"), Some(1_000_000));
900+
}
901+
830902
#[test]
831903
fn embedded_pricing_resolves_separator_aliases_for_other_claude_models() {
832904
let pricing = PricingMap::load_embedded();
@@ -885,7 +957,31 @@ mod tests {
885957
},
886958
);
887959

888-
assert_eq!(pricing.find("claude-opus-4.70").unwrap().input, 15e-6);
960+
assert!(pricing.find("claude-opus-4.70").is_none());
961+
}
962+
963+
#[test]
964+
fn fuzzy_match_does_not_fall_back_across_numeric_model_versions() {
965+
let mut pricing = PricingMap::default();
966+
pricing.entries.insert(
967+
"claude-opus-4".to_string(),
968+
Pricing {
969+
input: 15e-6,
970+
output: 75e-6,
971+
cache_create: 18.75e-6,
972+
cache_read: 1.5e-6,
973+
cache_read_explicit: true,
974+
input_above_200k: None,
975+
output_above_200k: None,
976+
cache_create_above_200k: None,
977+
cache_read_above_200k: None,
978+
fast_multiplier: 1.0,
979+
},
980+
);
981+
982+
assert!(pricing.find("claude-opus-4.8-20260528").is_none());
983+
assert!(pricing.find("claude-opus-4.70").is_none());
984+
assert!(pricing.find("claude-opus-4-20250514").is_some());
889985
}
890986

891987
#[test]
@@ -939,6 +1035,10 @@ mod tests {
9391035
"input_cost_per_token": 0.000005,
9401036
"output_cost_per_token": 0.000025
9411037
},
1038+
"claude-opus-4.8-20260528": {
1039+
"input_cost_per_token": 0.000005,
1040+
"output_cost_per_token": 0.000025
1041+
},
9421042
"claude-opus-4-70": {
9431043
"input_cost_per_token": 0.000005,
9441044
"output_cost_per_token": 0.000025
@@ -967,6 +1067,13 @@ mod tests {
9671067
.fast_multiplier,
9681068
6.0
9691069
);
1070+
assert_eq!(
1071+
pricing
1072+
.find("claude-opus-4.8-20260528")
1073+
.unwrap()
1074+
.fast_multiplier,
1075+
2.0
1076+
);
9701077
assert_eq!(
9711078
pricing.find("claude-opus-4-70").unwrap().fast_multiplier,
9721079
1.0

0 commit comments

Comments
 (0)