Skip to content

Commit 20004f3

Browse files
authored
fix: correct Claude usage pricing (#1372)
1 parent 6f6cb09 commit 20004f3

13 files changed

Lines changed: 901 additions & 117 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixed
66
- Antigravity: exclude model quotas without a remaining fraction from family summaries so they no longer mask tracked usage in the automatic menu-bar metric (#1369). Thanks @Martin-Hausleitner!
7+
- Claude: add bundled Fable 5 pricing, account for native 1-hour cache-write usage, and refresh Sonnet 4.6 full-context rates (#1368). Thanks @MoollaMore!
78

89
## 0.32.5 — 2026-06-09
910

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.
22

33
enum CodexParserHash {
4-
static let value = "7049d0743a7cd98f"
4+
static let value = "6162cc6387d12e86"
55
}

Sources/CodexBarCore/PiSessionCostCache.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
enum PiSessionCostCacheIO {
4-
private static let artifactVersion = 2
4+
private static let artifactVersion = 3
55

66
private static func defaultCacheRoot() -> URL {
77
let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
@@ -35,7 +35,11 @@ enum PiSessionCostCacheIO {
3535
let data = (try? JSONEncoder().encode(cache)) ?? Data()
3636
do {
3737
try data.write(to: tmp, options: [.atomic])
38-
_ = try FileManager.default.replaceItemAt(url, withItemAt: tmp)
38+
if FileManager.default.fileExists(atPath: url.path) {
39+
_ = try FileManager.default.replaceItemAt(url, withItemAt: tmp)
40+
} else {
41+
try FileManager.default.moveItem(at: tmp, to: url)
42+
}
3943
} catch {
4044
try? FileManager.default.removeItem(at: tmp)
4145
}
@@ -50,7 +54,7 @@ struct PiSessionCostCache: Codable {
5054
var daysByProvider: [String: [String: [String: PiPackedUsage]]] = [:]
5155
var files: [String: PiSessionFileUsage] = [:]
5256

53-
init(version: Int = 2) {
57+
init(version: Int = 3) {
5458
self.version = version
5559
}
5660
}

Sources/CodexBarCore/PiSessionCostScanner.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ enum PiSessionCostScanner {
384384
provider: identity.provider,
385385
modelName: identity.modelName,
386386
message: message,
387+
pricingDate: date,
387388
pricingContext: pricingContext)
388389
add(provider: identity.provider, dayKey: dayKey, modelName: identity.modelName, usage: usage)
389390
}
@@ -524,6 +525,7 @@ enum PiSessionCostScanner {
524525
provider: UsageProvider,
525526
modelName: String,
526527
message: [String: Any],
528+
pricingDate: Date? = nil,
527529
pricingContext: ModelsDevPricingContext? = nil) -> PiPackedUsage
528530
{
529531
let usage = (message["usage"] as? [String: Any]) ?? [:]
@@ -571,10 +573,12 @@ enum PiSessionCostScanner {
571573
cacheWriteTokens: cacheWrite,
572574
outputTokens: output,
573575
totalTokens: totalTokens)
576+
// Pi JSONL does not record Anthropic cache retention, so use Pi's persisted default tariff.
574577
let costUSD = self.computedCostUSD(
575578
provider: provider,
576579
modelName: modelName,
577580
usage: rawUsage,
581+
pricingDate: pricingDate,
578582
pricingContext: pricingContext)
579583
let costNanos = costUSD.map { Int64(($0 * self.costScale).rounded()) } ?? 0
580584

@@ -593,6 +597,7 @@ enum PiSessionCostScanner {
593597
provider: UsageProvider,
594598
modelName: String,
595599
usage: PiPackedUsage,
600+
pricingDate: Date? = nil,
596601
pricingContext: ModelsDevPricingContext? = nil) -> Double?
597602
{
598603
switch provider {
@@ -611,6 +616,7 @@ enum PiSessionCostScanner {
611616
cacheReadInputTokens: usage.cacheReadTokens,
612617
cacheCreationInputTokens: usage.cacheWriteTokens,
613618
outputTokens: usage.outputTokens,
619+
pricingDate: pricingDate,
614620
modelsDevCatalog: pricingContext?.catalog,
615621
modelsDevCacheRoot: pricingContext?.cacheRoot)
616622
default:
@@ -634,7 +640,9 @@ enum PiSessionCostScanner {
634640
}
635641
return 0
636642
}
643+
}
637644

645+
extension PiSessionCostScanner {
638646
private static func mappedProvider(fromPiProvider provider: String) -> UsageProvider? {
639647
switch provider.lowercased() {
640648
case "openai-codex":

Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ enum CostUsageCacheIO {
66
case .codex:
77
8
88
case .claude, .vertexai:
9-
2
9+
4
1010
default:
1111
1
1212
}

Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift

Lines changed: 96 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ enum CostUsagePricing {
5757
let cacheReadInputCostPerTokenAboveThreshold: Double?
5858
}
5959

60+
private struct ClaudeCostTokens {
61+
let input: Int
62+
let cacheRead: Int
63+
let cacheCreation: Int
64+
let cacheCreation1h: Int
65+
let output: Int
66+
}
67+
6068
private static let codex: [String: CodexPricing] = [
6169
"gpt-5": CodexPricing(
6270
inputCostPerToken: 1.25e-6,
@@ -205,6 +213,16 @@ enum CostUsagePricing {
205213
}
206214

207215
private static let claude: [String: ClaudePricing] = [
216+
"claude-fable-5": ClaudePricing(
217+
inputCostPerToken: 1e-5,
218+
outputCostPerToken: 5e-5,
219+
cacheCreationInputCostPerToken: 1.25e-5,
220+
cacheReadInputCostPerToken: 1e-6,
221+
thresholdTokens: nil,
222+
inputCostPerTokenAboveThreshold: nil,
223+
outputCostPerTokenAboveThreshold: nil,
224+
cacheCreationInputCostPerTokenAboveThreshold: nil,
225+
cacheReadInputCostPerTokenAboveThreshold: nil),
208226
"claude-haiku-4-5-20251001": ClaudePricing(
209227
inputCostPerToken: 1e-6,
210228
outputCostPerToken: 5e-6,
@@ -300,11 +318,11 @@ enum CostUsagePricing {
300318
outputCostPerToken: 1.5e-5,
301319
cacheCreationInputCostPerToken: 3.75e-6,
302320
cacheReadInputCostPerToken: 3e-7,
303-
thresholdTokens: 200_000,
304-
inputCostPerTokenAboveThreshold: 6e-6,
305-
outputCostPerTokenAboveThreshold: 2.25e-5,
306-
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
307-
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
321+
thresholdTokens: nil,
322+
inputCostPerTokenAboveThreshold: nil,
323+
outputCostPerTokenAboveThreshold: nil,
324+
cacheCreationInputCostPerTokenAboveThreshold: nil,
325+
cacheReadInputCostPerTokenAboveThreshold: nil),
308326
"claude-sonnet-4-5-20250929": ClaudePricing(
309327
inputCostPerToken: 3e-6,
310328
outputCostPerToken: 1.5e-5,
@@ -347,6 +365,30 @@ enum CostUsagePricing {
347365
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
348366
]
349367

368+
private static let claudeFullContextStandardPricingCutoff = Date(timeIntervalSince1970: 1_773_360_000)
369+
private static let claudeHistoricalLongContext: [String: ClaudePricing] = [
370+
"claude-opus-4-6": ClaudePricing(
371+
inputCostPerToken: 5e-6,
372+
outputCostPerToken: 2.5e-5,
373+
cacheCreationInputCostPerToken: 6.25e-6,
374+
cacheReadInputCostPerToken: 5e-7,
375+
thresholdTokens: 200_000,
376+
inputCostPerTokenAboveThreshold: 1e-5,
377+
outputCostPerTokenAboveThreshold: 3.75e-5,
378+
cacheCreationInputCostPerTokenAboveThreshold: 1.25e-5,
379+
cacheReadInputCostPerTokenAboveThreshold: 1e-6),
380+
"claude-sonnet-4-6": ClaudePricing(
381+
inputCostPerToken: 3e-6,
382+
outputCostPerToken: 1.5e-5,
383+
cacheCreationInputCostPerToken: 3.75e-6,
384+
cacheReadInputCostPerToken: 3e-7,
385+
thresholdTokens: 200_000,
386+
inputCostPerTokenAboveThreshold: 6e-6,
387+
outputCostPerTokenAboveThreshold: 2.25e-5,
388+
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
389+
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
390+
]
391+
350392
private static let codexModelsDevProviderID = "openai"
351393
private static let claudeModelsDevProviderID = "anthropic"
352394

@@ -514,10 +556,29 @@ enum CostUsagePricing {
514556
inputTokens: Int,
515557
cacheReadInputTokens: Int,
516558
cacheCreationInputTokens: Int,
559+
cacheCreationInputTokens1h: Int = 0,
517560
outputTokens: Int,
561+
pricingDate: Date? = nil,
518562
modelsDevCatalog: ModelsDevCatalog? = nil,
519563
modelsDevCacheRoot: URL? = nil) -> Double?
520564
{
565+
let tokens = ClaudeCostTokens(
566+
input: inputTokens,
567+
cacheRead: cacheReadInputTokens,
568+
cacheCreation: cacheCreationInputTokens,
569+
cacheCreation1h: cacheCreationInputTokens1h,
570+
output: outputTokens)
571+
let key = self.normalizeClaudeModel(model)
572+
if let pricingDate,
573+
let historicalPricing = self.claudeHistoricalLongContext[key],
574+
let currentPricing = self.claude[key]
575+
{
576+
return self.claudeCostUSD(
577+
pricing: pricingDate < self.claudeFullContextStandardPricingCutoff
578+
? historicalPricing
579+
: currentPricing,
580+
tokens: tokens)
581+
}
521582
if let lookup = self.modelsDevLookup(
522583
providerID: self.claudeModelsDevProviderID,
523584
model: model,
@@ -526,64 +587,50 @@ enum CostUsagePricing {
526587
{
527588
return self.claudeCostUSD(
528589
pricing: lookup.pricing,
529-
inputTokens: inputTokens,
530-
cacheReadInputTokens: cacheReadInputTokens,
531-
cacheCreationInputTokens: cacheCreationInputTokens,
532-
outputTokens: outputTokens)
590+
tokens: tokens)
533591
}
534592

535-
let key = self.normalizeClaudeModel(model)
536593
guard let pricing = self.claude[key] else { return nil }
537594
return self.claudeCostUSD(
538595
pricing: pricing,
539-
inputTokens: inputTokens,
540-
cacheReadInputTokens: cacheReadInputTokens,
541-
cacheCreationInputTokens: cacheCreationInputTokens,
542-
outputTokens: outputTokens)
596+
tokens: tokens)
543597
}
544598

545599
private static func claudeCostUSD(
546600
pricing: ClaudePricing,
547-
inputTokens: Int,
548-
cacheReadInputTokens: Int,
549-
cacheCreationInputTokens: Int,
550-
outputTokens: Int) -> Double
601+
tokens: ClaudeCostTokens) -> Double
551602
{
552-
func tiered(_ tokens: Int, base: Double, above: Double?, threshold: Int?) -> Double {
553-
guard let threshold, let above else { return Double(tokens) * base }
554-
let below = min(tokens, threshold)
555-
let over = max(tokens - threshold, 0)
556-
return Double(below) * base + Double(over) * above
557-
}
603+
let input = max(0, tokens.input)
604+
let cacheRead = max(0, tokens.cacheRead)
605+
let cacheCreationTotal = max(0, tokens.cacheCreation)
606+
let cacheCreation1h = min(max(0, tokens.cacheCreation1h), cacheCreationTotal)
607+
let cacheCreation5m = cacheCreationTotal - cacheCreation1h
608+
let usesLongContextRates = pricing.thresholdTokens.map {
609+
input + cacheRead + cacheCreationTotal > $0
610+
} ?? false
611+
let inputRate = usesLongContextRates
612+
? pricing.inputCostPerTokenAboveThreshold ?? pricing.inputCostPerToken
613+
: pricing.inputCostPerToken
614+
let cacheReadRate = usesLongContextRates
615+
? pricing.cacheReadInputCostPerTokenAboveThreshold ?? pricing.cacheReadInputCostPerToken
616+
: pricing.cacheReadInputCostPerToken
617+
let cacheCreation5mRate = usesLongContextRates
618+
? pricing.cacheCreationInputCostPerTokenAboveThreshold ?? pricing.cacheCreationInputCostPerToken
619+
: pricing.cacheCreationInputCostPerToken
620+
let outputRate = usesLongContextRates
621+
? pricing.outputCostPerTokenAboveThreshold ?? pricing.outputCostPerToken
622+
: pricing.outputCostPerToken
558623

559-
return tiered(
560-
max(0, inputTokens),
561-
base: pricing.inputCostPerToken,
562-
above: pricing.inputCostPerTokenAboveThreshold,
563-
threshold: pricing.thresholdTokens)
564-
+ tiered(
565-
max(0, cacheReadInputTokens),
566-
base: pricing.cacheReadInputCostPerToken,
567-
above: pricing.cacheReadInputCostPerTokenAboveThreshold,
568-
threshold: pricing.thresholdTokens)
569-
+ tiered(
570-
max(0, cacheCreationInputTokens),
571-
base: pricing.cacheCreationInputCostPerToken,
572-
above: pricing.cacheCreationInputCostPerTokenAboveThreshold,
573-
threshold: pricing.thresholdTokens)
574-
+ tiered(
575-
max(0, outputTokens),
576-
base: pricing.outputCostPerToken,
577-
above: pricing.outputCostPerTokenAboveThreshold,
578-
threshold: pricing.thresholdTokens)
624+
return Double(input) * inputRate
625+
+ Double(cacheRead) * cacheReadRate
626+
+ Double(cacheCreation5m) * cacheCreation5mRate
627+
+ Double(cacheCreation1h) * inputRate * 2
628+
+ Double(max(0, tokens.output)) * outputRate
579629
}
580630

581631
private static func claudeCostUSD(
582632
pricing: ModelsDevPricingInfo,
583-
inputTokens: Int,
584-
cacheReadInputTokens: Int,
585-
cacheCreationInputTokens: Int,
586-
outputTokens: Int) -> Double
633+
tokens: ClaudeCostTokens) -> Double
587634
{
588635
self.claudeCostUSD(
589636
pricing: ClaudePricing(
@@ -596,10 +643,7 @@ enum CostUsagePricing {
596643
outputCostPerTokenAboveThreshold: pricing.outputCostPerTokenAboveThreshold,
597644
cacheCreationInputCostPerTokenAboveThreshold: pricing.cacheCreationInputCostPerTokenAboveThreshold,
598645
cacheReadInputCostPerTokenAboveThreshold: pricing.cacheReadInputCostPerTokenAboveThreshold),
599-
inputTokens: inputTokens,
600-
cacheReadInputTokens: cacheReadInputTokens,
601-
cacheCreationInputTokens: cacheCreationInputTokens,
602-
outputTokens: outputTokens)
646+
tokens: tokens)
603647
}
604648

605649
static func modelsDevCatalog(now: Date = Date(), cacheRoot: URL? = nil) -> ModelsDevCatalog? {

0 commit comments

Comments
 (0)