Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Fireworks: track 30-day rated billing spend with an API key and account slug (#2687). Thanks @x0mh0x!

### Fixed
- Codex: reject Standard/Fast pricing rows that exceed canonical fork-deduplicated usage, preventing copied fork rows and their Fast surcharge from inflating cost estimates (#2754). Thanks @1328189205 for the report and @Yuxin-Qiao for the initial fix and regression-test approach!
- Claude: stop rotating Claude Code's own refresh-token chain on keychain-only installs — ownership evidence is now tri-state with indeterminate treated as CLI-owned, so delegated refreshes can never invalidate credentials CodexBar cannot read (#2745, refs #2634). Thanks @avenoxai!
- Plugins: run the QuickJS worker and TypeScript transpiler on Thread subclasses instead of Thread(block:) closures — binaries built with the Xcode 26.3 SDK inferred @MainActor on those blocks, and macOS runtimes that enforce dynamic isolation crashed (SIGTRAP) on the first plugin fetch; this was the deterministic macOS CI shard crash since #2775 and could crash shipped builds at runtime.
- Plugins: the QuickJS HTTP/cookie bridge now starts a request's per-call timeout when the transport actually begins executing instead of when it is scheduled, so short deadlines (like OpenRouter's one-second key fast join) no longer fire spuriously under CPU load (refs #2778).
Expand All @@ -14,7 +15,6 @@
- OpenRouter: keep optional key-quota enrichment on its one-second production fast join while making degraded results explicit and preventing loaded CI parity runs from mistaking the fallback snapshot for a golden mismatch (fixes #2778).
- Codex: the SQLite cost store now writes each save cycle inside one transaction, so a crash or kill mid-save can never leave session rows updated against stale day aggregates — the previous state survives intact, matching the old JSON path's atomic file replace (refs #2760).
- Provider plugins: run each QuickJS context on a dedicated 4 MiB-stack thread, refresh stack bounds at every JavaScript entry, and leave 3 MiB of native headroom so deep recursion raises a clean stack-overflow error instead of crashing.
>>>>>>> bae013ccb (feat: make QuickJS the default plugin engine everywhere)
- Codex: SQLite cost saves no longer rescan every stored row and snapshot per file — baseline counts and file lookups are precomputed once, cutting a large-corpus (1,700+ sessions) save pass from minutes of CPU to seconds (refs #2760).
- Codex: restore JSON-cache retention semantics lost in the SQLite cutover — discovery pruning now reaches the scanner's round-tripped payload so deleted files stop resurfacing, the row budget never sacrifices in-window or recently active sessions, and fork-parent protection again drops stale lineage-only parents (refs #2760).
- Codex: the SQLite cost store no longer deletes the whole database on transient failures — lock contention from a concurrent CLI/app writer, disk-full, or a constraint violation now preserve history and only genuine corruption or schema drift triggers a rebuild, which is now logged (refs #2760).
Expand Down
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 = "b9175d171384dd58"
static let value = "b975eb705f905b9a"
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ extension CostUsageScanner {
var hasModeSplit: Bool {
self.sawPriorityCost || self.priorityTokens > 0
}

func isTrusted(canonicalTotalTokens: Int) -> Bool {
let (rowTokenTotal, overflow) = self.standardTokens.addingReportingOverflow(self.priorityTokens)
return !overflow && rowTokenTotal <= canonicalTotalTokens
}
}

static func codexRowCostBreakdown(
Expand Down Expand Up @@ -1412,19 +1417,24 @@ extension CostUsageScanner {
priorityTurns: priorityTurns,
modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader),
modelsDevCacheRoot: modelsDevCacheRoot)
let rowCostIsTrusted = rowCost?.isTrusted(canonicalTotalTokens: totalTokens) ?? true
let authoritativeCost = authoritativeCostNanosByDayModel[day]?[model].map {
Double($0) / Self.costScale
}
let cost = rowCost?.totalCostUSD
?? authoritativeCost
?? CostUsagePricing.codexCostUSD(
model: model,
inputTokens: input,
cachedInputTokens: cached,
outputTokens: output,
modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader),
modelsDevCacheRoot: modelsDevCacheRoot)
let hasModeSplit = rowCost?.hasModeSplit == true
let canonicalCost = CostUsagePricing.codexCostUSD(
model: model,
inputTokens: input,
cachedInputTokens: cached,
outputTokens: output,
modelsDevCatalog: catalogResolver.load(modelsDevCatalogLoader),
modelsDevCacheRoot: modelsDevCacheRoot)
// Physical pricing rows can retain fork-copied usage after canonical ownership
// has deduplicated the day/model totals. Reject the whole row-derived price so
// Fast uplift from the same unowned rows cannot leak into the fallback cost.
let cost = rowCostIsTrusted
? rowCost?.totalCostUSD ?? authoritativeCost ?? canonicalCost
: canonicalCost
let hasModeSplit = rowCostIsTrusted && rowCost?.hasModeSplit == true
breakdown.append(
CostUsageDailyReport.ModelBreakdown(
modelName: model,
Expand Down
177 changes: 177 additions & 0 deletions Tests/CodexBarTests/CostUsageScannerForkSplitTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import Foundation
#if canImport(SQLite3)
import Testing
@testable import CodexBarCore

struct CostUsageScannerForkSplitTests {
@Test
func `codex report rejects fork inflated row split and its fast uplift`() throws {
let fixture = try self.makeFixture()
defer { fixture.environment.cleanup() }

var cache = fixture.cache
let parent = try #require(cache.files.first { $0.value.sessionId == "parent-session" })
let child = try #require(cache.files.first { $0.value.sessionId == "child-session" })
let copiedParentRows = try #require(parent.value.codexRows)
var inflatedChild = child.value
inflatedChild.codexRows = (inflatedChild.codexRows ?? []) + copiedParentRows
cache.files[child.key] = inflatedChild

let canonical = try #require(cache.days[fixture.dayKey]?[fixture.model])
#expect(canonical == [150, 60, 15])
let canonicalTokens = canonical[0] + canonical[2]
let rowTokens = cache.files.values
.flatMap { $0.codexRows ?? [] }
.reduce(0) { $0 + $1.input + $1.output }
#expect(rowTokens > canonicalTokens)

let report = CostUsageScanner.buildCodexReportFromCache(cache: cache, range: fixture.range)
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
let canonicalCost = try #require(CostUsagePricing.codexCostUSD(
model: fixture.model,
inputTokens: canonical[0],
cachedInputTokens: canonical[1],
outputTokens: canonical[2]))

#expect(abs((breakdown.costUSD ?? 0) - canonicalCost) < 1e-12)
#expect(breakdown.standardCostUSD == nil)
#expect(breakdown.priorityCostUSD == nil)
#expect(breakdown.standardTokens == nil)
#expect(breakdown.priorityTokens == nil)
#expect(abs((report.summary?.totalCostUSD ?? 0) - canonicalCost) < 1e-12)
}

@Test
func `codex report keeps trusted fork deduplicated row split`() throws {
let fixture = try self.makeFixture()
defer { fixture.environment.cleanup() }

let canonical = try #require(fixture.cache.days[fixture.dayKey]?[fixture.model])
#expect(canonical == [150, 60, 15])
let child = try #require(fixture.cache.files.first { $0.value.sessionId == "child-session" }?.value)
#expect(child.days[fixture.dayKey]?[fixture.model] == [50, 20, 5])

let report = CostUsageScanner.buildCodexReportFromCache(cache: fixture.cache, range: fixture.range)
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
let standardCost = try #require(CostUsagePricing.codexCostUSD(
model: fixture.model,
inputTokens: 50,
cachedInputTokens: 20,
outputTokens: 5))
let priorityCost = try #require(CostUsagePricing.codexPriorityCostUSD(
model: fixture.model,
inputTokens: 100,
cachedInputTokens: 40,
outputTokens: 10))

#expect(abs((breakdown.costUSD ?? 0) - (standardCost + priorityCost)) < 1e-12)
#expect(abs((breakdown.standardCostUSD ?? 0) - standardCost) < 1e-12)
#expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 1e-12)
#expect(breakdown.standardTokens == 55)
#expect(breakdown.priorityTokens == 110)
}

private struct Fixture {
let environment: CostUsageTestEnvironment
let range: CostUsageScanner.CostUsageDayRange
let dayKey: String
let model: String
let cache: CostUsageCache
}

private func makeFixture() throws -> Fixture {
let env = try CostUsageTestEnvironment()
let day = try env.makeLocalNoon(year: 2026, month: 8, day: 6)
let parentTimestamp = env.isoString(for: day)
let parentUsageTimestamp = env.isoString(for: day.addingTimeInterval(1))
let forkTimestamp = env.isoString(for: day.addingTimeInterval(2))
let childUsageTimestamp = env.isoString(for: day.addingTimeInterval(3))
let model = "gpt-5.5"

_ = try env.writeCodexSessionFile(
day: day,
filename: "a-parent.jsonl",
contents: env.jsonl([
[
"type": "session_meta",
"timestamp": parentTimestamp,
"payload": ["id": "parent-session", "timestamp": parentTimestamp],
],
["type": "turn_context", "timestamp": parentTimestamp, "payload": ["model": model]],
[
"type": "event_msg",
"timestamp": parentUsageTimestamp,
"payload": ["type": "task_started", "turn_id": "priority-turn"],
],
self.totalTokenCount(timestamp: parentUsageTimestamp, input: 100, cached: 40, output: 10),
]))
_ = try env.writeCodexSessionFile(
day: day,
filename: "z-child.jsonl",
contents: env.jsonl([
[
"type": "session_meta",
"timestamp": forkTimestamp,
"payload": [
"id": "child-session",
"forked_from_id": "parent-session",
"timestamp": forkTimestamp,
],
],
["type": "turn_context", "timestamp": forkTimestamp, "payload": ["model": model]],
[
"type": "event_msg",
"timestamp": childUsageTimestamp,
"payload": ["type": "task_started", "turn_id": "standard-turn"],
],
self.totalTokenCount(timestamp: childUsageTimestamp, input: 150, cached: 60, output: 15),
]))

let dbURL = env.root.appendingPathComponent("logs_2.sqlite")
try CostUsageScannerCodexPriorityTests.createTestLogsDatabase(at: dbURL)
try CostUsageScannerCodexPriorityTests.insertTestLog(
dbURL: dbURL,
timestamp: parentUsageTimestamp,
body: "thread_id=thread turn.id=priority-turn websocket request: "
+ #"{"type":"response.create","model":"gpt-5.5","service_tier":"priority"}"#)
var options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot,
codexTraceDatabaseURL: dbURL,
forceRescan: true,
preferNewestCodexSessionsFirst: false)
options.refreshMinIntervalSeconds = 0
_ = CostUsageScanner.loadDailyReport(
provider: .codex,
since: day,
until: day,
now: day,
options: options)

let range = CostUsageScanner.CostUsageDayRange(since: day, until: day)
return Fixture(
environment: env,
range: range,
dayKey: range.sinceKey,
model: model,
cache: CostUsageStoreAccess.read(cacheRoot: env.cacheRoot, calendar: range.calendar))
}

private func totalTokenCount(timestamp: String, input: Int, cached: Int, output: Int) -> [String: Any] {
[
"type": "event_msg",
"timestamp": timestamp,
"payload": [
"type": "token_count",
"info": [
"total_token_usage": [
"input_tokens": input,
"cached_input_tokens": cached,
"output_tokens": output,
],
],
],
]
}
}
#endif