diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index f19687bb2c..56134b8fee 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 = "97cf82ab7b18b255" + static let value = "514c63aac596993e" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift index 983f82c1e2..27cb4ee55e 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+CacheHelpers.swift @@ -1693,8 +1693,15 @@ extension CostUsageScanner { } let cachedStandardTokens = standardTokensByDayModel[day]?[model] let cachedPriorityTokens = priorityTokensByDayModel[day]?[model] - let standardCost = cachedStandardCost - let priorityCost = cachedPriorityCost + let (splitTokenTotal, splitTokenOverflow) = (cachedStandardTokens ?? 0) + .addingReportingOverflow(cachedPriorityTokens ?? 0) + // Fork-copied rows can inflate the per-file split maps; never publish a split + // that exceeds the canonical day/model total. + let splitIsUntrusted = splitTokenOverflow || splitTokenTotal > totalTokens + let standardCost = splitIsUntrusted ? nil : cachedStandardCost + let priorityCost = splitIsUntrusted ? nil : cachedPriorityCost + let standardTokens = splitIsUntrusted ? nil : cachedStandardTokens + let priorityTokens = splitIsUntrusted ? nil : cachedPriorityTokens let splitTotalCost: Double? = if standardCost != nil || priorityCost != nil { (standardCost ?? 0) + (priorityCost ?? 0) } else { @@ -1715,7 +1722,7 @@ extension CostUsageScanner { { cost = (cost ?? 0) + (Double(surchargeNanos) / Self.costScale) } - let hasModeSplit = priorityCost != nil || cachedPriorityTokens != nil + let hasModeSplit = priorityCost != nil || priorityTokens != nil breakdown.append( CostUsageDailyReport.ModelBreakdown( modelName: model, @@ -1723,8 +1730,8 @@ extension CostUsageScanner { totalTokens: totalTokens, standardCostUSD: hasModeSplit ? standardCost : nil, priorityCostUSD: hasModeSplit ? priorityCost : nil, - standardTokens: hasModeSplit ? cachedStandardTokens : nil, - priorityTokens: hasModeSplit ? cachedPriorityTokens : nil)) + standardTokens: hasModeSplit ? standardTokens : nil, + priorityTokens: hasModeSplit ? priorityTokens : nil)) if let cost { dayCost += cost dayCostSeen = true diff --git a/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift b/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift index f95da684ec..3701fc4bbe 100644 --- a/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift +++ b/Tests/CodexBarTests/CostUsageScannerPriorityTests.swift @@ -769,6 +769,133 @@ struct CostUsageScannerPriorityTests { #expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 0.000_000_001) } + @Test + func `codex cached report drops inflated standard fast split when it exceeds canonical total`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + + let day = try env.makeLocalNoon(year: 2026, month: 8, day: 6) + let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day) + let cache = Self.makeSplitCodexCache( + dayKey: dayKey, + model: "gpt-5.5", + canonical: (input: 1000, cached: 0, output: 500), + split: ( + standardTokens: 10_000_000, + priorityTokens: 5_000_000, + standardCostNanos: 10_000_000_000, + priorityCostNanos: 5_000_000_000), + baseCostNanos: 20_000_000) + + let report = CostUsageScanner.buildCodexReportFromCache( + cache: cache, + range: CostUsageScanner.CostUsageDayRange(since: day, until: day)) + + // Canonical ownership is 1,500 tokens, but the copied fork split claims 15,000,000. + // The report must not publish that split or its inflated cost; it falls back to the + // canonical base cost of $0.02 (1,000 input + 500 output at gpt-5.5 rates). + let breakdown = try #require(report.data.first?.modelBreakdowns?.first) + #expect(breakdown.totalTokens == 1500) + #expect(abs((breakdown.costUSD ?? 0) - 0.02) < 1e-12) + #expect(breakdown.standardCostUSD == nil) + #expect(breakdown.priorityCostUSD == nil) + #expect(breakdown.standardTokens == nil) + #expect(breakdown.priorityTokens == nil) + #expect(abs((report.summary?.totalCostUSD ?? 0) - 0.02) < 1e-12) + } + + @Test + func `codex cached report keeps valid standard fast split when tokens reconcile with canonical total`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + + let day = try env.makeLocalNoon(year: 2026, month: 8, day: 6) + let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day) + let cache = Self.makeSplitCodexCache( + dayKey: dayKey, + model: "gpt-5.5", + canonical: (input: 1000, cached: 0, output: 500), + split: ( + standardTokens: 1000, + priorityTokens: 500, + standardCostNanos: 20_000_000, + priorityCostNanos: 10_000_000), + baseCostNanos: 20_000_000) + + let report = CostUsageScanner.buildCodexReportFromCache( + cache: cache, + range: CostUsageScanner.CostUsageDayRange(since: day, until: day)) + + // A split that reconciles with canonical ownership keeps its exact cost and token values. + let breakdown = try #require(report.data.first?.modelBreakdowns?.first) + #expect(breakdown.totalTokens == 1500) + #expect(abs((breakdown.costUSD ?? 0) - 0.03) < 1e-12) + #expect(abs((breakdown.standardCostUSD ?? 0) - 0.02) < 1e-12) + #expect(abs((breakdown.priorityCostUSD ?? 0) - 0.01) < 1e-12) + #expect(breakdown.standardTokens == 1000) + #expect(breakdown.priorityTokens == 500) + #expect(abs((report.summary?.totalCostUSD ?? 0) - 0.03) < 1e-12) + } + + @Test + func `codex cached report treats overflowing standard fast token sum as untrusted`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + + let day = try env.makeLocalNoon(year: 2026, month: 8, day: 6) + let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day) + let cache = Self.makeSplitCodexCache( + dayKey: dayKey, + model: "gpt-5.5", + canonical: (input: 1000, cached: 0, output: 500), + split: ( + standardTokens: Int.max, + priorityTokens: 1, + standardCostNanos: 10_000_000_000, + priorityCostNanos: 5_000_000_000), + baseCostNanos: 20_000_000) + + let report = CostUsageScanner.buildCodexReportFromCache( + cache: cache, + range: CostUsageScanner.CostUsageDayRange(since: day, until: day)) + + let breakdown = try #require(report.data.first?.modelBreakdowns?.first) + #expect(breakdown.totalTokens == 1500) + #expect(abs((breakdown.costUSD ?? 0) - 0.02) < 1e-12) + #expect(breakdown.standardCostUSD == nil) + #expect(breakdown.priorityCostUSD == nil) + #expect(breakdown.standardTokens == nil) + #expect(breakdown.priorityTokens == nil) + } + + private static func makeSplitCodexCache( + dayKey: String, + model: String, + canonical: (input: Int, cached: Int, output: Int), + split: ( + standardTokens: Int, + priorityTokens: Int, + standardCostNanos: Int64, + priorityCostNanos: Int64), + baseCostNanos: Int64) -> CostUsageCache + { + var file = CostUsageFileUsage( + mtimeUnixMs: 1_751_000_000_000, + size: 1024, + days: [dayKey: [model: [canonical.input, canonical.cached, canonical.output]]]) + file.codexCostCacheComplete = true + file.codexCostNanos = [dayKey: [model: baseCostNanos]] + file.codexStandardCostNanos = [dayKey: [model: split.standardCostNanos]] + file.codexPriorityCostNanos = [dayKey: [model: split.priorityCostNanos]] + file.codexStandardTokens = [dayKey: [model: split.standardTokens]] + file.codexPriorityTokens = [dayKey: [model: split.priorityTokens]] + + var cache = CostUsageCache() + cache.days = [dayKey: [model: [canonical.input, canonical.cached, canonical.output]]] + cache.files = ["/sessions/2026-08-06/fork-copied.jsonl": file] + return cache + } + private func tokenCount(timestamp: String, input: Int, cached: Int, output: Int) -> [String: Any] { [ "type": "event_msg",