Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "3aa49b47f4b78e13"
static let value = "b21ac0df2edc86f1"
}
1 change: 1 addition & 0 deletions Sources/CodexBarCore/PiSessionCostScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ enum PiSessionCostScanner {
cachedInputTokens: usage.cacheReadTokens,
outputTokens: usage.outputTokens,
cacheWriteInputTokens: usage.cacheWriteTokens,
pricingDate: pricingDate,
modelsDevCatalog: pricingContext?.catalog,
modelsDevCacheRoot: pricingContext?.cacheRoot)
case .claude:
Expand Down
167 changes: 119 additions & 48 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import Foundation

private struct PricingHistoryPeriod<Pricing: Sendable>: Sendable {
let before: Date
let models: [String: Pricing]
}

private func effectivePricing<Pricing: Sendable>(
for model: String,
on date: Date?,
history: [PricingHistoryPeriod<Pricing>],
current: Pricing? = nil) -> Pricing?
{
guard let date, history.contains(where: { $0.models[model] != nil }) else { return nil }
for period in history where date < period.before {
if let pricing = period.models[model] {
return pricing
}
}
return current
}

enum CostUsagePricing {
private static let codexPriorityInputTokenLimit = 272_000
static let codexUnattributedModel = "unknown"

struct CodexPricing {
struct CodexPricing: Sendable {
let inputCostPerToken: Double
let outputCostPerToken: Double
let cacheReadInputCostPerToken: Double?
Expand Down Expand Up @@ -56,7 +76,7 @@ enum CostUsagePricing {
}
}

struct ClaudePricing {
struct ClaudePricing: Sendable {
let inputCostPerToken: Double
let outputCostPerToken: Double
let cacheCreationInputCostPerToken: Double
Expand Down Expand Up @@ -215,31 +235,31 @@ enum CostUsagePricing {
priorityCacheReadInputCostPerToken: 1e-6,
priorityCacheWriteInputCostPerToken: 1.25e-5),
"gpt-5.6-terra": CodexPricing(
inputCostPerToken: 2.5e-6,
outputCostPerToken: 1.5e-5,
cacheReadInputCostPerToken: 2.5e-7,
inputCostPerToken: 2e-6,
outputCostPerToken: 1.2e-5,
cacheReadInputCostPerToken: 2e-7,
Comment on lines +238 to +240

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve old rates before the July 30 cutover

For bundled-fallback pricing of Terra or Luna usage dated before 2026-07-30, these replacements retroactively apply the new rates. codexCostUSD accepts no pricing date, and callers such as codexRowsCostUSD and codexRowCostBreakdown do not pass row.day; the changed pricing fingerprint also forces cached history to be rescanned. Consequently, pre-cutover Terra costs are reduced by 20% and Luna costs by 80%, corrupting historical totals. Keep the old and new tuples behind an effective-date cutoff and price each row using its recorded date.

Useful? React with 👍 / 👎.

displayLabel: nil,
cacheWriteInputCostPerToken: 3.125e-6,
cacheWriteInputCostPerToken: 2.5e-6,
thresholdTokens: 272_000,
inputCostPerTokenAboveThreshold: 5e-6,
outputCostPerTokenAboveThreshold: 2.25e-5,
cacheReadInputCostPerTokenAboveThreshold: 5e-7,
cacheWriteInputCostPerTokenAboveThreshold: 6.25e-6,
inputCostPerTokenAboveThreshold: 4e-6,
outputCostPerTokenAboveThreshold: 1.8e-5,
cacheReadInputCostPerTokenAboveThreshold: 4e-7,
cacheWriteInputCostPerTokenAboveThreshold: 5e-6,
priorityInputCostPerToken: 5e-6,
priorityOutputCostPerToken: 3e-5,
priorityCacheReadInputCostPerToken: 5e-7,
priorityCacheWriteInputCostPerToken: 6.25e-6),
"gpt-5.6-luna": CodexPricing(
inputCostPerToken: 1e-6,
outputCostPerToken: 6e-6,
cacheReadInputCostPerToken: 1e-7,
inputCostPerToken: 2e-7,
outputCostPerToken: 1.2e-6,
cacheReadInputCostPerToken: 2e-8,
displayLabel: nil,
cacheWriteInputCostPerToken: 1.25e-6,
cacheWriteInputCostPerToken: 2.5e-7,
thresholdTokens: 272_000,
inputCostPerTokenAboveThreshold: 2e-6,
outputCostPerTokenAboveThreshold: 9e-6,
cacheReadInputCostPerTokenAboveThreshold: 2e-7,
cacheWriteInputCostPerTokenAboveThreshold: 2.5e-6,
inputCostPerTokenAboveThreshold: 4e-7,
outputCostPerTokenAboveThreshold: 1.8e-6,
cacheReadInputCostPerTokenAboveThreshold: 4e-8,
cacheWriteInputCostPerTokenAboveThreshold: 5e-7,
priorityInputCostPerToken: 2e-6,
priorityOutputCostPerToken: 1.2e-5,
priorityCacheReadInputCostPerToken: 2e-7,
Expand Down Expand Up @@ -429,30 +449,6 @@ enum CostUsagePricing {
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
]

private static let claudeFullContextStandardPricingCutoff = Date(timeIntervalSince1970: 1_773_360_000)
private static let claudeHistoricalLongContext: [String: ClaudePricing] = [
"claude-opus-4-6": ClaudePricing(
inputCostPerToken: 5e-6,
outputCostPerToken: 2.5e-5,
cacheCreationInputCostPerToken: 6.25e-6,
cacheReadInputCostPerToken: 5e-7,
thresholdTokens: 200_000,
inputCostPerTokenAboveThreshold: 1e-5,
outputCostPerTokenAboveThreshold: 3.75e-5,
cacheCreationInputCostPerTokenAboveThreshold: 1.25e-5,
cacheReadInputCostPerTokenAboveThreshold: 1e-6),
"claude-sonnet-4-6": ClaudePricing(
inputCostPerToken: 3e-6,
outputCostPerToken: 1.5e-5,
cacheCreationInputCostPerToken: 3.75e-6,
cacheReadInputCostPerToken: 3e-7,
thresholdTokens: 200_000,
inputCostPerTokenAboveThreshold: 6e-6,
outputCostPerTokenAboveThreshold: 2.25e-5,
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
]

private static let codexModelsDevProviderID = "openai"
private static let claudeModelsDevProviderID = "anthropic"

Expand Down Expand Up @@ -524,11 +520,24 @@ enum CostUsagePricing {
cachedInputTokens: Int,
outputTokens: Int,
cacheWriteInputTokens: Int = 0,
pricingDate: Date? = nil,
modelsDevCatalog: ModelsDevCatalog? = nil,
modelsDevCacheRoot: URL? = nil) -> Double?
{
let key = self.normalizeCodexModel(model)
guard key != self.codexUnattributedModel else { return nil }
if let historicalPricing = effectivePricing(
for: key,
on: pricingDate,
history: self.codexPricingHistory)
{
return self.codexCostUSD(
pricing: historicalPricing,
inputTokens: inputTokens,
cachedInputTokens: cachedInputTokens,
cacheWriteInputTokens: cacheWriteInputTokens,
outputTokens: outputTokens)
}
let modelsDevLookup = self.modelsDevLookup(
providerID: self.codexModelsDevProviderID,
model: model,
Expand Down Expand Up @@ -712,14 +721,14 @@ enum CostUsagePricing {
cacheCreation1h: cacheCreationInputTokens1h,
output: outputTokens)
let key = self.normalizeClaudeModel(model)
if let pricingDate,
let historicalPricing = self.claudeHistoricalLongContext[key],
let currentPricing = self.claude[key]
if let datedPricing = effectivePricing(
for: key,
on: pricingDate,
history: self.claudePricingHistory,
current: self.claude[key])
{
return self.claudeCostUSD(
pricing: pricingDate < self.claudeFullContextStandardPricingCutoff
? historicalPricing
: currentPricing,
pricing: datedPricing,
tokens: tokens)
}
if let lookup = self.modelsDevLookup(
Expand Down Expand Up @@ -809,3 +818,65 @@ enum CostUsagePricing {
cacheRoot: cacheRoot)
}
}

extension CostUsagePricing {
private static let claudePricingHistory = [PricingHistoryPeriod(
before: Date(timeIntervalSince1970: 1_773_360_000),
models: [
"claude-opus-4-6": ClaudePricing(
inputCostPerToken: 5e-6,
outputCostPerToken: 2.5e-5,
cacheCreationInputCostPerToken: 6.25e-6,
cacheReadInputCostPerToken: 5e-7,
thresholdTokens: 200_000,
inputCostPerTokenAboveThreshold: 1e-5,
outputCostPerTokenAboveThreshold: 3.75e-5,
cacheCreationInputCostPerTokenAboveThreshold: 1.25e-5,
cacheReadInputCostPerTokenAboveThreshold: 1e-6),
"claude-sonnet-4-6": ClaudePricing(
inputCostPerToken: 3e-6,
outputCostPerToken: 1.5e-5,
cacheCreationInputCostPerToken: 3.75e-6,
cacheReadInputCostPerToken: 3e-7,
thresholdTokens: 200_000,
inputCostPerTokenAboveThreshold: 6e-6,
outputCostPerTokenAboveThreshold: 2.25e-5,
cacheCreationInputCostPerTokenAboveThreshold: 7.5e-6,
cacheReadInputCostPerTokenAboveThreshold: 6e-7),
])]

private static let codexPricingHistory = [PricingHistoryPeriod(
before: Date(timeIntervalSince1970: 1_785_369_600),
models: [
"gpt-5.6-terra": CodexPricing(
inputCostPerToken: 2.5e-6,
outputCostPerToken: 1.5e-5,
cacheReadInputCostPerToken: 2.5e-7,
displayLabel: nil,
cacheWriteInputCostPerToken: 3.125e-6,
thresholdTokens: 272_000,
inputCostPerTokenAboveThreshold: 5e-6,
outputCostPerTokenAboveThreshold: 2.25e-5,
cacheReadInputCostPerTokenAboveThreshold: 5e-7,
cacheWriteInputCostPerTokenAboveThreshold: 6.25e-6,
priorityInputCostPerToken: 5e-6,
priorityOutputCostPerToken: 3e-5,
priorityCacheReadInputCostPerToken: 5e-7,
priorityCacheWriteInputCostPerToken: 6.25e-6),
"gpt-5.6-luna": CodexPricing(
inputCostPerToken: 1e-6,
outputCostPerToken: 6e-6,
cacheReadInputCostPerToken: 1e-7,
displayLabel: nil,
cacheWriteInputCostPerToken: 1.25e-6,
thresholdTokens: 272_000,
inputCostPerTokenAboveThreshold: 2e-6,
outputCostPerTokenAboveThreshold: 9e-6,
cacheReadInputCostPerTokenAboveThreshold: 2e-7,
cacheWriteInputCostPerTokenAboveThreshold: 2.5e-6,
priorityInputCostPerToken: 2e-6,
priorityOutputCostPerToken: 1.2e-5,
priorityCacheReadInputCostPerToken: 2e-7,
priorityCacheWriteInputCostPerToken: 2.5e-6),
])]
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ extension CostUsageScanner {
inputTokens: row.input,
cachedInputTokens: row.cached,
outputTokens: row.output,
pricingDate: CostUsageDateParser.parse(row.day),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse pricing cutover days in a fixed time zone

When the process runs east of UTC (for example, Asia/Tokyo), CostUsageDateParser interprets 2026-07-30 as local midnight, which is still before the UTC cutoff at epoch 1_785_369_600. As a result, every Terra/Luna row bucketed on July 30 is charged the old rates (including Luna at 5× the new price), and the added row-pricing test changes behavior with the runner's time zone. Compare the day key directly or parse it in a fixed cutoff time zone instead of using TimeZone.current.

Useful? React with 👍 / 👎.

modelsDevCatalog: modelsDevCatalog,
modelsDevCacheRoot: modelsDevCacheRoot)
else { continue }
Expand All @@ -164,6 +165,7 @@ extension CostUsageScanner {
inputTokens: row.input,
cachedInputTokens: row.cached,
outputTokens: row.output,
pricingDate: CostUsageDateParser.parse(row.day),
modelsDevCatalog: modelsDevCatalog,
modelsDevCacheRoot: modelsDevCacheRoot),
let priorityCost = CostUsagePricing.codexPriorityCostUSD(
Expand Down Expand Up @@ -250,6 +252,7 @@ extension CostUsageScanner {
inputTokens: row.input,
cachedInputTokens: row.cached,
outputTokens: row.output,
pricingDate: CostUsageDateParser.parse(row.day),
modelsDevCatalog: modelsDevCatalog,
modelsDevCacheRoot: modelsDevCacheRoot)
if isPriority, let priorityCost = CostUsagePricing.codexPriorityCostUSD(
Expand Down Expand Up @@ -461,6 +464,7 @@ extension CostUsageScanner {
inputTokens: row.input,
cachedInputTokens: row.cached,
outputTokens: row.output,
pricingDate: CostUsageDateParser.parse(row.day),
modelsDevCatalog: modelsDevCatalog,
modelsDevCacheRoot: modelsDevCacheRoot)
let exactCost: Double? = if priorityMetadata != nil,
Expand Down Expand Up @@ -604,6 +608,7 @@ extension CostUsageScanner {
inputTokens: row.input,
cachedInputTokens: row.cached,
outputTokens: row.output,
pricingDate: CostUsageDateParser.parse(row.day),
modelsDevCatalog: modelsDevCatalog,
modelsDevCacheRoot: modelsDevCacheRoot)

Expand Down Expand Up @@ -1565,6 +1570,7 @@ extension CostUsageScanner {
inputTokens: input,
cachedInputTokens: cached,
outputTokens: output,
pricingDate: CostUsageDateParser.parse(day),
modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader),
modelsDevCacheRoot: modelsDevCacheRoot)
if splitTotalCost == nil,
Expand Down
83 changes: 78 additions & 5 deletions Tests/CodexBarTests/CostUsagePricingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,88 @@ struct CostUsagePricingTests {
outputTokens: 5,
modelsDevCacheRoot: root)

// Rates per token: Sol $5/$30 per 1M, Terra $2.50/$15, Luna $1/$6;
// Rates per token: Sol $5/$30 per 1M, Terra $2/$12, Luna $0.20/$1.20;
// cache read is 10% of input. Non-cached input is 90 tokens.
#expect(sol == (90.0 * 5e-6) + (10.0 * 5e-7) + (5.0 * 3e-5))
#expect(terra == (90.0 * 2.5e-6) + (10.0 * 2.5e-7) + (5.0 * 1.5e-5))
#expect(luna == (90.0 * 1e-6) + (10.0 * 1e-7) + (5.0 * 6e-6))
#expect(terra == (90.0 * 2e-6) + (10.0 * 2e-7) + (5.0 * 1.2e-5))
#expect(luna == (90.0 * 2e-7) + (10.0 * 2e-8) + (5.0 * 1.2e-6))
// Unsuffixed gpt-5.6 alias routes to Sol.
#expect(alias == sol)
}

@Test
func `codex cost preserves historical gpt56 terra and luna rates`() throws {
let root = try Self.cacheRoot()
let beforeCutover = Date(timeIntervalSince1970: 1_785_369_599)
let atCutover = Date(timeIntervalSince1970: 1_785_369_600)

let oldTerra = CostUsagePricing.codexCostUSD(
model: "gpt-5.6-terra",
inputTokens: 100,
cachedInputTokens: 10,
outputTokens: 5,
pricingDate: beforeCutover,
modelsDevCacheRoot: root)
let newTerra = CostUsagePricing.codexCostUSD(
model: "gpt-5.6-terra",
inputTokens: 100,
cachedInputTokens: 10,
outputTokens: 5,
pricingDate: atCutover,
modelsDevCacheRoot: root)
let oldLuna = CostUsagePricing.codexCostUSD(
model: "gpt-5.6-luna",
inputTokens: 100,
cachedInputTokens: 10,
outputTokens: 5,
pricingDate: beforeCutover,
modelsDevCacheRoot: root)
let newLuna = CostUsagePricing.codexCostUSD(
model: "gpt-5.6-luna",
inputTokens: 100,
cachedInputTokens: 10,
outputTokens: 5,
pricingDate: atCutover,
modelsDevCacheRoot: root)

#expect(oldTerra == (90.0 * 2.5e-6) + (10.0 * 2.5e-7) + (5.0 * 1.5e-5))
#expect(newTerra == (90.0 * 2e-6) + (10.0 * 2e-7) + (5.0 * 1.2e-5))
#expect(oldLuna == (90.0 * 1e-6) + (10.0 * 1e-7) + (5.0 * 6e-6))
#expect(newLuna == (90.0 * 2e-7) + (10.0 * 2e-8) + (5.0 * 1.2e-6))
}

@Test
func `codex row pricing uses each row day`() throws {
let root = try Self.cacheRoot()
let rows = [
CostUsageScanner.CodexUsageRow(
day: "2026-07-29",
model: "gpt-5.6-terra",
turnID: nil,
eventIndex: nil,
input: 100,
cached: 10,
output: 5),
CostUsageScanner.CodexUsageRow(
day: "2026-07-30",
model: "gpt-5.6-terra",
turnID: nil,
eventIndex: nil,
input: 100,
cached: 10,
output: 5),
]

let cost = CostUsageScanner.codexRowsCostUSD(
rows: rows,
modelsDevCatalog: nil,
modelsDevCacheRoot: root)

let expectedOld = (90.0 * 2.5e-6) + (10.0 * 2.5e-7) + (5.0 * 1.5e-5)
let expectedNew = (90.0 * 2e-6) + (10.0 * 2e-7) + (5.0 * 1.2e-5)
#expect(cost == expectedOld + expectedNew)
}

@Test
func `codex models dev falls back from gpt56 alias to canonical sol pricing`() throws {
let canonicalOnlyRoot = try Self.seedModelsDevCache("""
Expand Down Expand Up @@ -263,8 +336,8 @@ struct CostUsagePricingTests {
// Long-context (>272K) rates apply to the entire request. Total input contains 10 cached,
// 20 cache-write, and 271,971 ordinary input tokens.
#expect(sol == (271_971.0 * 1e-5) + (10.0 * 1e-6) + (20.0 * 1.25e-5) + (10.0 * 4.5e-5))
#expect(terra == (271_971.0 * 5e-6) + (10.0 * 5e-7) + (20.0 * 6.25e-6) + (10.0 * 2.25e-5))
#expect(luna == (271_971.0 * 2e-6) + (10.0 * 2e-7) + (20.0 * 2.5e-6) + (10.0 * 9e-6))
#expect(terra == (271_971.0 * 4e-6) + (10.0 * 4e-7) + (20.0 * 5e-6) + (10.0 * 1.8e-5))
#expect(luna == (271_971.0 * 4e-7) + (10.0 * 4e-8) + (20.0 * 5e-7) + (10.0 * 1.8e-6))
}

@Test
Expand Down