diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 085fda188b..0d65567d63 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 = "cdef6eb9658a43e2" + static let value = "c1352739994bce10" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift index 3727bf2ce2..5ac243282e 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift @@ -18,10 +18,6 @@ enum CostUsagePricing { let outputCostPerTokenAboveThreshold: Double? let cacheReadInputCostPerTokenAboveThreshold: Double? let cacheWriteInputCostPerTokenAboveThreshold: Double? - let priorityInputCostPerToken: Double? - let priorityOutputCostPerToken: Double? - let priorityCacheReadInputCostPerToken: Double? - let priorityCacheWriteInputCostPerToken: Double? init( inputCostPerToken: Double, @@ -33,11 +29,7 @@ enum CostUsagePricing { inputCostPerTokenAboveThreshold: Double? = nil, outputCostPerTokenAboveThreshold: Double? = nil, cacheReadInputCostPerTokenAboveThreshold: Double? = nil, - cacheWriteInputCostPerTokenAboveThreshold: Double? = nil, - priorityInputCostPerToken: Double? = nil, - priorityOutputCostPerToken: Double? = nil, - priorityCacheReadInputCostPerToken: Double? = nil, - priorityCacheWriteInputCostPerToken: Double? = nil) + cacheWriteInputCostPerTokenAboveThreshold: Double? = nil) { self.inputCostPerToken = inputCostPerToken self.outputCostPerToken = outputCostPerToken @@ -49,10 +41,6 @@ enum CostUsagePricing { self.outputCostPerTokenAboveThreshold = outputCostPerTokenAboveThreshold self.cacheReadInputCostPerTokenAboveThreshold = cacheReadInputCostPerTokenAboveThreshold self.cacheWriteInputCostPerTokenAboveThreshold = cacheWriteInputCostPerTokenAboveThreshold - self.priorityInputCostPerToken = priorityInputCostPerToken - self.priorityOutputCostPerToken = priorityOutputCostPerToken - self.priorityCacheReadInputCostPerToken = priorityCacheReadInputCostPerToken - self.priorityCacheWriteInputCostPerToken = priorityCacheWriteInputCostPerToken } } @@ -156,18 +144,12 @@ enum CostUsagePricing { thresholdTokens: 272_000, inputCostPerTokenAboveThreshold: 5e-6, outputCostPerTokenAboveThreshold: 2.25e-5, - cacheReadInputCostPerTokenAboveThreshold: 5e-7, - priorityInputCostPerToken: 5e-6, - priorityOutputCostPerToken: 3e-5, - priorityCacheReadInputCostPerToken: 5e-7), + cacheReadInputCostPerTokenAboveThreshold: 5e-7), "gpt-5.4-mini": CodexPricing( inputCostPerToken: 7.5e-7, outputCostPerToken: 4.5e-6, cacheReadInputCostPerToken: 7.5e-8, - displayLabel: nil, - priorityInputCostPerToken: 1.5e-6, - priorityOutputCostPerToken: 9e-6, - priorityCacheReadInputCostPerToken: 1.5e-7), + displayLabel: nil), "gpt-5.4-nano": CodexPricing( inputCostPerToken: 2e-7, outputCostPerToken: 1.25e-6, @@ -186,10 +168,7 @@ enum CostUsagePricing { thresholdTokens: 272_000, inputCostPerTokenAboveThreshold: 1e-5, outputCostPerTokenAboveThreshold: 4.5e-5, - cacheReadInputCostPerTokenAboveThreshold: 1e-6, - priorityInputCostPerToken: 1.25e-5, - priorityOutputCostPerToken: 7.5e-5, - priorityCacheReadInputCostPerToken: 1.25e-6), + cacheReadInputCostPerTokenAboveThreshold: 1e-6), "gpt-5.5-pro": CodexPricing( inputCostPerToken: 3e-5, outputCostPerToken: 1.8e-4, @@ -197,8 +176,8 @@ enum CostUsagePricing { displayLabel: nil), // GPT-5.6 Sol/Terra/Luna (OpenAI pricing page + model cards). // Long context: prompts with >272K input tokens are 2x input / 1.5x output for the full - // request. Cache writes: 1.25x uncached input. Priority rates are explicit because support - // and multipliers are provider contracts, not properties that can be inferred from Standard. + // request. Cache writes: 1.25x uncached input. Priority support and multipliers are applied + // separately after Standard pricing resolves from models.dev or this bundled fallback. "gpt-5.6-sol": CodexPricing( inputCostPerToken: 5e-6, outputCostPerToken: 3e-5, @@ -209,11 +188,7 @@ enum CostUsagePricing { inputCostPerTokenAboveThreshold: 1e-5, outputCostPerTokenAboveThreshold: 4.5e-5, cacheReadInputCostPerTokenAboveThreshold: 1e-6, - cacheWriteInputCostPerTokenAboveThreshold: 1.25e-5, - priorityInputCostPerToken: 1e-5, - priorityOutputCostPerToken: 6e-5, - priorityCacheReadInputCostPerToken: 1e-6, - priorityCacheWriteInputCostPerToken: 1.25e-5), + cacheWriteInputCostPerTokenAboveThreshold: 1.25e-5), "gpt-5.6-terra": CodexPricing( inputCostPerToken: 2.5e-6, outputCostPerToken: 1.5e-5, @@ -224,11 +199,7 @@ enum CostUsagePricing { 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), + cacheWriteInputCostPerTokenAboveThreshold: 6.25e-6), "gpt-5.6-luna": CodexPricing( inputCostPerToken: 1e-6, outputCostPerToken: 6e-6, @@ -239,11 +210,7 @@ enum CostUsagePricing { 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), + cacheWriteInputCostPerTokenAboveThreshold: 2.5e-6), ] static func codexBuiltInPricingFingerprint() -> String { @@ -262,10 +229,7 @@ enum CostUsagePricing { self.optionalPricingFingerprint(pricing.outputCostPerTokenAboveThreshold), self.optionalPricingFingerprint(pricing.cacheReadInputCostPerTokenAboveThreshold), self.optionalPricingFingerprint(pricing.cacheWriteInputCostPerTokenAboveThreshold), - self.optionalPricingFingerprint(pricing.priorityInputCostPerToken), - self.optionalPricingFingerprint(pricing.priorityOutputCostPerToken), - self.optionalPricingFingerprint(pricing.priorityCacheReadInputCostPerToken), - self.optionalPricingFingerprint(pricing.priorityCacheWriteInputCostPerToken), + self.optionalPricingFingerprint(self.codexPriorityMultiplier(model: model)), ].joined(separator: "|")) } return parts.joined(separator: "\n") @@ -590,31 +554,34 @@ enum CostUsagePricing { inputTokens: Int, cachedInputTokens: Int = 0, cacheWriteInputTokens: Int = 0, - outputTokens: Int) -> Double? + outputTokens: Int, + modelsDevCatalog: ModelsDevCatalog? = nil, + modelsDevCacheRoot: URL? = nil) -> Double? { - let key = self.normalizeCodexModel(model) - guard let pricing = self.codex[key], - let priorityInputCostPerToken = pricing.priorityInputCostPerToken, - let priorityOutputCostPerToken = pricing.priorityOutputCostPerToken - else { return nil } + guard let multiplier = self.codexPriorityMultiplier(model: model) else { return nil } // OpenAI does not support Priority processing for long-context requests. Do not combine // the independent Standard long-context and Priority short-context rate tables. if max(0, inputTokens) > self.codexPriorityInputTokenLimit { return nil } - let priorityPricing = CodexPricing( - inputCostPerToken: priorityInputCostPerToken, - outputCostPerToken: priorityOutputCostPerToken, - cacheReadInputCostPerToken: pricing.priorityCacheReadInputCostPerToken, - displayLabel: nil, - cacheWriteInputCostPerToken: pricing.priorityCacheWriteInputCostPerToken) return self.codexCostUSD( - pricing: priorityPricing, + model: model, inputTokens: inputTokens, cachedInputTokens: cachedInputTokens, + outputTokens: outputTokens, cacheWriteInputTokens: cacheWriteInputTokens, - outputTokens: outputTokens) + modelsDevCatalog: modelsDevCatalog, + modelsDevCacheRoot: modelsDevCacheRoot) + .map { $0 * multiplier } + } + + static func codexPriorityMultiplier(model: String) -> Double? { + switch self.normalizeCodexModel(model) { + case "gpt-5.4", "gpt-5.4-mini": 2 + case "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna": 2.5 + default: nil + } } private static func codexCostUSD( diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index bcc11d0b6f..1fa1f9ad95 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -166,7 +166,9 @@ extension CostUsageScanner { model: pricedModel, inputTokens: row.input, cachedInputTokens: row.cached, - outputTokens: row.output) + outputTokens: row.output, + modelsDevCatalog: modelsDevCatalog, + modelsDevCacheRoot: modelsDevCacheRoot) else { continue } total += max(priorityCost - baseCost, 0) seen = true @@ -252,7 +254,9 @@ extension CostUsageScanner { model: pricedModel, inputTokens: row.input, cachedInputTokens: row.cached, - outputTokens: row.output) + outputTokens: row.output, + modelsDevCatalog: modelsDevCatalog, + modelsDevCacheRoot: modelsDevCacheRoot) { breakdown.priorityCostUSD += max(priorityCost, baseCost ?? priorityCost) breakdown.sawPriorityCost = true @@ -542,7 +546,9 @@ extension CostUsageScanner { model: pricedModel, inputTokens: row.input, cachedInputTokens: row.cached, - outputTokens: row.output) + outputTokens: row.output, + modelsDevCatalog: modelsDevCatalog, + modelsDevCacheRoot: modelsDevCacheRoot) { priorityCostNanos[row.day, default: [:]][row.model, default: 0] += Int64( (max(priorityCost, baseCost ?? priorityCost) * Self.costScale).rounded()) diff --git a/Tests/CodexBarTests/CostUsagePricingTests.swift b/Tests/CodexBarTests/CostUsagePricingTests.swift index 7c5340418c..9adbb68211 100644 --- a/Tests/CodexBarTests/CostUsagePricingTests.swift +++ b/Tests/CodexBarTests/CostUsagePricingTests.swift @@ -301,14 +301,17 @@ struct CostUsagePricingTests { cachedInputTokens: 20, outputTokens: 10) - // Priority is 2x short-context rates (Sol input $10/1M, etc.). - #expect(sol == (80.0 * 1e-5) + (20.0 * 1e-6) + (10.0 * 6e-5)) - #expect(terra == (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)) - #expect(luna == (80.0 * 2e-6) + (20.0 * 2e-7) + (10.0 * 1.2e-5)) + // GPT-5.6 Priority is 2.5x short-context Standard pricing. + let expectedSol = (80.0 * 1.25e-5) + (20.0 * 1.25e-6) + (10.0 * 7.5e-5) + let expectedTerra = (80.0 * 6.25e-6) + (20.0 * 6.25e-7) + (10.0 * 3.75e-5) + let expectedLuna = (80.0 * 2.5e-6) + (20.0 * 2.5e-7) + (10.0 * 1.5e-5) + #expect(abs((sol ?? 0) - expectedSol) < 1e-12) + #expect(abs((terra ?? 0) - expectedTerra) < 1e-12) + #expect(abs((luna ?? 0) - expectedLuna) < 1e-12) } @Test - func `codex priority cost uses explicit cache write rates`() { + func `codex priority cost multiplies standard cache write rates`() { let sol = CostUsagePricing.codexPriorityCostUSD( model: "gpt-5.6-sol", inputTokens: 100, @@ -334,13 +337,43 @@ struct CostUsagePricingTests { cacheWriteInputTokens: 20, outputTokens: 5) - #expect(sol == (70.0 * 1e-5) + (10.0 * 1e-6) + (20.0 * 1.25e-5) + (5.0 * 6e-5)) - #expect(terra == (70.0 * 5e-6) + (10.0 * 5e-7) + (20.0 * 6.25e-6) + (5.0 * 3e-5)) - #expect(luna == (70.0 * 2e-6) + (10.0 * 2e-7) + (20.0 * 2.5e-6) + (5.0 * 1.2e-5)) - // A model without an explicit Priority cache-write price keeps the legacy input-rate fold. - #expect( - modelWithoutCacheWriteSupport == - (90.0 * 1.25e-5) + (10.0 * 1.25e-6) + (5.0 * 7.5e-5)) + let expectedSol = ((70.0 * 5e-6) + (10.0 * 5e-7) + (20.0 * 6.25e-6) + (5.0 * 3e-5)) * 2.5 + let expectedTerra = ((70.0 * 2.5e-6) + (10.0 * 2.5e-7) + (20.0 * 3.125e-6) + (5.0 * 1.5e-5)) * 2.5 + let expectedLuna = ((70.0 * 1e-6) + (10.0 * 1e-7) + (20.0 * 1.25e-6) + (5.0 * 6e-6)) * 2.5 + #expect(abs((sol ?? 0) - expectedSol) < 1e-12) + #expect(abs((terra ?? 0) - expectedTerra) < 1e-12) + #expect(abs((luna ?? 0) - expectedLuna) < 1e-12) + // A legacy model without a Standard cache-write price folds writes into uncached input. + let expectedLegacy = (90.0 * 1.25e-5) + (10.0 * 1.25e-6) + (5.0 * 7.5e-5) + #expect(abs((modelWithoutCacheWriteSupport ?? 0) - expectedLegacy) < 1e-12) + } + + @Test + func `codex priority cost multiplies models dev standard pricing`() throws { + let root = try Self.seedModelsDevCache(""" + { + "openai": { + "id": "openai", + "models": { + "gpt-5.6-sol": { + "id": "gpt-5.6-sol", + "cost": { "input": 7, "output": 31, "cache_read": 1, "cache_write": 8 } + } + } + } + } + """) + + let cost = CostUsagePricing.codexPriorityCostUSD( + model: "gpt-5.6-sol", + inputTokens: 100, + cachedInputTokens: 10, + cacheWriteInputTokens: 20, + outputTokens: 5, + modelsDevCacheRoot: root) + + // Standard is $0.000815 for this worked token mix; GPT-5.6 Priority is 2.5x. + #expect(cost == 0.0020375) } @Test diff --git a/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift b/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift index 2db80641cd..9eecdea746 100644 --- a/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift +++ b/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift @@ -52,6 +52,57 @@ struct CostUsageScannerPriorityTests { #expect(breakdown.priorityTokens == 110) } + @Test + func `codex daily report applies priority multiplier to models dev pricing`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + + let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10) + let catalog = try JSONDecoder().decode(ModelsDevCatalog.self, from: Data(""" + { + "openai": { + "id": "openai", + "models": { + "gpt-5.5": { + "id": "gpt-5.5", + "cost": { "input": 7, "output": 31, "cache_read": 1 } + } + } + } + } + """.utf8)) + #expect(ModelsDevCache.save(catalog: catalog, fetchedAt: day, cacheRoot: env.cacheRoot)) + + let iso0 = env.isoString(for: day) + let iso1 = env.isoString(for: day.addingTimeInterval(1)) + let entries: [[String: Any]] = [ + ["type": "turn_context", "timestamp": iso0, "payload": ["model": "gpt-5.5"]], + ["type": "event_msg", "timestamp": iso1, "payload": ["type": "task_started", "turn_id": "priority-turn"]], + self.tokenCount(timestamp: iso1, input: 100, cached: 20, output: 10), + ] + _ = try env.writeCodexSessionFile(day: day, filename: "session.jsonl", contents: env.jsonl(entries)) + + let dbURL = env.root.appendingPathComponent("logs_2.sqlite") + try CostUsageScannerCodexPriorityTests.createTestLogsDatabase(at: dbURL) + try self.insertPriorityTrace(dbURL: dbURL, timestamp: iso1) + + var options = CostUsageScanner.Options( + codexSessionsRoot: env.codexSessionsRoot, + cacheRoot: env.cacheRoot, + codexTraceDatabaseURL: dbURL) + options.refreshMinIntervalSeconds = 0 + let report = CostUsageScanner.loadDailyReport( + provider: .codex, + since: day, + until: day, + now: day, + options: options) + + // models.dev Standard is $0.00089 for this token mix; GPT-5.5 Priority is 2.5x. + let breakdown = try #require(report.data.first?.modelBreakdowns?.first) + #expect(abs((breakdown.priorityCostUSD ?? 0) - 0.002225) < 1e-12) + } + @Test func `codex daily report keeps cached priority surcharge without live sqlite metadata`() throws { let env = try CostUsageTestEnvironment()