From 29a46c48a3e9960b5d8fed0a6db92f46eb838b6c Mon Sep 17 00:00:00 2001 From: Bryan Font Date: Thu, 30 Jul 2026 18:32:40 -0400 Subject: [PATCH 1/3] Update GPT-5.6 pricing --- .../Generated/CodexParserHash.generated.swift | 2 +- .../Vendored/CostUsage/CostUsagePricing.swift | 32 +++++++++---------- .../CodexBarTests/CostUsagePricingTests.swift | 10 +++--- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index ca353e24bb..23588f9088 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -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 = "bcdbd18124a00943" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift index 3727bf2ce2..c27e667315 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift @@ -215,31 +215,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, 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, diff --git a/Tests/CodexBarTests/CostUsagePricingTests.swift b/Tests/CodexBarTests/CostUsagePricingTests.swift index 7c5340418c..3aa1441532 100644 --- a/Tests/CodexBarTests/CostUsagePricingTests.swift +++ b/Tests/CodexBarTests/CostUsagePricingTests.swift @@ -133,11 +133,11 @@ 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) } @@ -263,8 +263,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 From ae55da1b645810bc3e9225b00cb43506f0207de1 Mon Sep 17 00:00:00 2001 From: Bryan Font Date: Thu, 30 Jul 2026 22:39:40 -0400 Subject: [PATCH 2/3] Preserve historical model pricing --- .../Generated/CodexParserHash.generated.swift | 2 +- .../CodexBarCore/PiSessionCostScanner.swift | 1 + .../Vendored/CostUsage/CostUsagePricing.swift | 135 +++++++++++++----- .../CostUsageScanner+CacheHelpers.swift | 6 + .../CodexBarTests/CostUsagePricingTests.swift | 73 ++++++++++ 5 files changed, 184 insertions(+), 33 deletions(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 23588f9088..40975c439b 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "bcdbd18124a00943" + static let value = "b21ac0df2edc86f1" } diff --git a/Sources/CodexBarCore/PiSessionCostScanner.swift b/Sources/CodexBarCore/PiSessionCostScanner.swift index 4ecdbd3d63..8b3426e426 100644 --- a/Sources/CodexBarCore/PiSessionCostScanner.swift +++ b/Sources/CodexBarCore/PiSessionCostScanner.swift @@ -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: diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift index c27e667315..f2b959d104 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift @@ -1,10 +1,30 @@ import Foundation +private struct PricingHistoryPeriod: Sendable { + let before: Date + let models: [String: Pricing] +} + +private func effectivePricing( + for model: String, + on date: Date?, + history: [PricingHistoryPeriod], + 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? @@ -56,7 +76,7 @@ enum CostUsagePricing { } } - struct ClaudePricing { + struct ClaudePricing: Sendable { let inputCostPerToken: Double let outputCostPerToken: Double let cacheCreationInputCostPerToken: Double @@ -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" @@ -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, @@ -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( @@ -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), + ])] +} diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index d6c5882db9..e899acab32 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -139,6 +139,7 @@ extension CostUsageScanner { inputTokens: row.input, cachedInputTokens: row.cached, outputTokens: row.output, + pricingDate: CostUsageDateParser.parse(row.day), modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot) else { continue } @@ -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( @@ -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( @@ -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, @@ -604,6 +608,7 @@ extension CostUsageScanner { inputTokens: row.input, cachedInputTokens: row.cached, outputTokens: row.output, + pricingDate: CostUsageDateParser.parse(row.day), modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot) @@ -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, diff --git a/Tests/CodexBarTests/CostUsagePricingTests.swift b/Tests/CodexBarTests/CostUsagePricingTests.swift index 3aa1441532..0af30377b3 100644 --- a/Tests/CodexBarTests/CostUsagePricingTests.swift +++ b/Tests/CodexBarTests/CostUsagePricingTests.swift @@ -142,6 +142,79 @@ struct CostUsagePricingTests { #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(""" From 5e3576315ac77b34d648f5be9e93acff51bac251 Mon Sep 17 00:00:00 2001 From: Bryan Font Date: Fri, 31 Jul 2026 00:38:33 -0400 Subject: [PATCH 3/3] Fix pricing cutoff time zone --- Sources/CodexBarCore/CostUsageModels.swift | 18 +++++++++++++++--- .../Generated/CodexParserHash.generated.swift | 2 +- .../CostUsageScanner+CacheHelpers.swift | 12 ++++++------ .../CodexBarTests/CostUsagePricingTests.swift | 12 ++++++++++++ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/Sources/CodexBarCore/CostUsageModels.swift b/Sources/CodexBarCore/CostUsageModels.swift index 88f2760bbb..d9d0bf2dc1 100644 --- a/Sources/CodexBarCore/CostUsageModels.swift +++ b/Sources/CodexBarCore/CostUsageModels.swift @@ -992,7 +992,7 @@ enum CostUsageDateParser { { return d } - if let d = self.dateFormatter(key: self.dayFormatterKey, format: "yyyy-MM-dd").date(from: trimmed) { + if let d = self.parseDay(trimmed, timeZone: .current) { return d } if let d = self.dateFormatter(key: self.monthDayYearFormatterKey, format: "MMM d, yyyy") @@ -1004,6 +1004,15 @@ enum CostUsageDateParser { return nil } + static func parseDay(_ text: String?, timeZone: TimeZone) -> Date? { + guard let text, !text.isEmpty else { return nil } + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + return self.dateFormatter( + key: self.dayFormatterKey, + format: "yyyy-MM-dd", + timeZone: timeZone).date(from: trimmed) + } + static func parseMonth(_ text: String?) -> Date? { guard let text, !text.isEmpty else { return nil } let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) @@ -1035,9 +1044,12 @@ enum CostUsageDateParser { return formatter } - private static func dateFormatter(key: String, format: String) -> DateFormatter { + private static func dateFormatter( + key: String, + format: String, + timeZone: TimeZone = .current) -> DateFormatter + { let threadDict = Thread.current.threadDictionary - let timeZone = TimeZone.current let cacheKey = "\(key).\(timeZone.identifier)" if let cached = threadDict[cacheKey] as? DateFormatter { return cached diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 40975c439b..f4b80b2d32 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "b21ac0df2edc86f1" + static let value = "5f28546cd6929ff0" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index e899acab32..87820a4356 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -139,7 +139,7 @@ extension CostUsageScanner { inputTokens: row.input, cachedInputTokens: row.cached, outputTokens: row.output, - pricingDate: CostUsageDateParser.parse(row.day), + pricingDate: CostUsageDateParser.parseDay(row.day, timeZone: .gmt), modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot) else { continue } @@ -165,7 +165,7 @@ extension CostUsageScanner { inputTokens: row.input, cachedInputTokens: row.cached, outputTokens: row.output, - pricingDate: CostUsageDateParser.parse(row.day), + pricingDate: CostUsageDateParser.parseDay(row.day, timeZone: .gmt), modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot), let priorityCost = CostUsagePricing.codexPriorityCostUSD( @@ -252,7 +252,7 @@ extension CostUsageScanner { inputTokens: row.input, cachedInputTokens: row.cached, outputTokens: row.output, - pricingDate: CostUsageDateParser.parse(row.day), + pricingDate: CostUsageDateParser.parseDay(row.day, timeZone: .gmt), modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot) if isPriority, let priorityCost = CostUsagePricing.codexPriorityCostUSD( @@ -464,7 +464,7 @@ extension CostUsageScanner { inputTokens: row.input, cachedInputTokens: row.cached, outputTokens: row.output, - pricingDate: CostUsageDateParser.parse(row.day), + pricingDate: CostUsageDateParser.parseDay(row.day, timeZone: .gmt), modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot) let exactCost: Double? = if priorityMetadata != nil, @@ -608,7 +608,7 @@ extension CostUsageScanner { inputTokens: row.input, cachedInputTokens: row.cached, outputTokens: row.output, - pricingDate: CostUsageDateParser.parse(row.day), + pricingDate: CostUsageDateParser.parseDay(row.day, timeZone: .gmt), modelsDevCatalog: modelsDevCatalog, modelsDevCacheRoot: modelsDevCacheRoot) @@ -1570,7 +1570,7 @@ extension CostUsageScanner { inputTokens: input, cachedInputTokens: cached, outputTokens: output, - pricingDate: CostUsageDateParser.parse(day), + pricingDate: CostUsageDateParser.parseDay(day, timeZone: .gmt), modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader), modelsDevCacheRoot: modelsDevCacheRoot) if splitTotalCost == nil, diff --git a/Tests/CodexBarTests/CostUsagePricingTests.swift b/Tests/CodexBarTests/CostUsagePricingTests.swift index 0af30377b3..6974ea2696 100644 --- a/Tests/CodexBarTests/CostUsagePricingTests.swift +++ b/Tests/CodexBarTests/CostUsagePricingTests.swift @@ -215,6 +215,18 @@ struct CostUsagePricingTests { #expect(cost == expectedOld + expectedNew) } + @Test + func `pricing day parsing uses UTC regardless of local time zone`() throws { + let utc = try #require(TimeZone(secondsFromGMT: 0)) + let tokyo = try #require(TimeZone(identifier: "Asia/Tokyo")) + + let utcDate = try #require(CostUsageDateParser.parseDay("2026-07-30", timeZone: utc)) + let tokyoDate = try #require(CostUsageDateParser.parseDay("2026-07-30", timeZone: tokyo)) + + #expect(utcDate == Date(timeIntervalSince1970: 1_785_369_600)) + #expect(tokyoDate < utcDate) + } + @Test func `codex models dev falls back from gpt56 alias to canonical sol pricing`() throws { let canonicalOnlyRoot = try Self.seedModelsDevCache("""