764 lines
35 KiB
Swift
764 lines
35 KiB
Swift
import Foundation
|
|
#if canImport(SQLite3)
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
struct CostUsageScannerPriorityTests {
|
|
@Test
|
|
func `codex daily report applies gpt55 priority rates`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
let iso0 = env.isoString(for: day)
|
|
let iso1 = env.isoString(for: day.addingTimeInterval(1))
|
|
let iso2 = env.isoString(for: day.addingTimeInterval(2))
|
|
let iso3 = env.isoString(for: day.addingTimeInterval(3))
|
|
|
|
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": "standard-turn"]],
|
|
self.tokenCount(timestamp: iso2, input: 100, cached: 20, output: 10),
|
|
["type": "event_msg", "timestamp": iso3, "payload": ["type": "task_started", "turn_id": "priority-turn"]],
|
|
self.tokenCount(timestamp: iso3, 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: iso3)
|
|
|
|
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)
|
|
let standardCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
let priorityCost = (80.0 * 1.25e-5) + (20.0 * 1.25e-6) + (10.0 * 7.5e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - (standardCost + priorityCost)) < 0.000_000_001)
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.costUSD ?? 0) - (standardCost + priorityCost)) < 0.000_000_001)
|
|
#expect(abs((breakdown.standardCostUSD ?? 0) - standardCost) < 0.000_000_001)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
#expect(breakdown.standardTokens == 110)
|
|
#expect(breakdown.priorityTokens == 110)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report keeps cached priority surcharge without live sqlite metadata`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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 refreshOptions = CostUsageScanner.Options(
|
|
codexSessionsRoot: env.codexSessionsRoot,
|
|
cacheRoot: env.cacheRoot,
|
|
codexTraceDatabaseURL: dbURL)
|
|
refreshOptions.refreshMinIntervalSeconds = 0
|
|
|
|
_ = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day,
|
|
options: refreshOptions)
|
|
|
|
var cachedOptions = CostUsageScanner.Options(
|
|
codexSessionsRoot: env.codexSessionsRoot,
|
|
cacheRoot: env.cacheRoot,
|
|
codexTraceDatabaseURL: env.root.appendingPathComponent("missing.sqlite"))
|
|
cachedOptions.refreshMinIntervalSeconds = 60
|
|
|
|
let cached = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day.addingTimeInterval(1),
|
|
options: cachedOptions)
|
|
let priorityCost = (80.0 * 1.25e-5) + (20.0 * 1.25e-6) + (10.0 * 7.5e-5)
|
|
|
|
#expect(abs((cached.summary?.totalCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
let breakdown = try #require(cached.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
#expect(breakdown.priorityTokens == 110)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report rescans when priority metadata appears`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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")
|
|
var missingOptions = CostUsageScanner.Options(
|
|
codexSessionsRoot: env.codexSessionsRoot,
|
|
cacheRoot: env.cacheRoot,
|
|
codexTraceDatabaseURL: dbURL)
|
|
missingOptions.refreshMinIntervalSeconds = 0
|
|
|
|
let first = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day,
|
|
options: missingOptions)
|
|
let baseCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
#expect(abs((first.summary?.totalCostUSD ?? 0) - baseCost) < 0.000_000_001)
|
|
|
|
try CostUsageScannerCodexPriorityTests.createTestLogsDatabase(at: dbURL)
|
|
try self.insertPriorityTrace(dbURL: dbURL, timestamp: iso1)
|
|
|
|
var liveOptions = missingOptions
|
|
liveOptions.refreshMinIntervalSeconds = 60
|
|
let rescanned = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day.addingTimeInterval(1),
|
|
options: liveOptions)
|
|
let priorityCost = (80.0 * 1.25e-5) + (20.0 * 1.25e-6) + (10.0 * 7.5e-5)
|
|
|
|
#expect(abs((rescanned.summary?.totalCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report ignores unrelated priority wal changes`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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)
|
|
|
|
var options = CostUsageScanner.Options(
|
|
codexSessionsRoot: env.codexSessionsRoot,
|
|
cacheRoot: env.cacheRoot,
|
|
codexTraceDatabaseURL: dbURL)
|
|
options.refreshMinIntervalSeconds = 0
|
|
|
|
let first = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day,
|
|
options: options)
|
|
let baseCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
#expect(abs((first.summary?.totalCostUSD ?? 0) - baseCost) < 0.000_000_001)
|
|
|
|
let walURL = URL(fileURLWithPath: dbURL.path + "-wal")
|
|
try Data("wal-changed".utf8).write(to: walURL)
|
|
|
|
options.refreshMinIntervalSeconds = 60
|
|
let cached = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day.addingTimeInterval(1),
|
|
options: options)
|
|
|
|
#expect(abs((cached.summary?.totalCostUSD ?? 0) - baseCost) < 0.000_000_001)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report reprices cached file when priority turn appears`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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)
|
|
|
|
var options = CostUsageScanner.Options(
|
|
codexSessionsRoot: env.codexSessionsRoot,
|
|
cacheRoot: env.cacheRoot,
|
|
codexTraceDatabaseURL: dbURL)
|
|
options.refreshMinIntervalSeconds = 0
|
|
|
|
let first = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day,
|
|
options: options)
|
|
let baseCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
#expect(abs((first.summary?.totalCostUSD ?? 0) - baseCost) < 0.000_000_001)
|
|
|
|
try self.insertPriorityTrace(dbURL: dbURL, timestamp: iso1)
|
|
|
|
options.refreshMinIntervalSeconds = 60
|
|
let repriced = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day.addingTimeInterval(61),
|
|
options: options)
|
|
let priorityCost = (80.0 * 1.25e-5) + (20.0 * 1.25e-6) + (10.0 * 7.5e-5)
|
|
|
|
#expect(abs((repriced.summary?.totalCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report applies gpt54 priority rates`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
let iso0 = env.isoString(for: day)
|
|
let iso1 = env.isoString(for: day.addingTimeInterval(1))
|
|
let iso2 = env.isoString(for: day.addingTimeInterval(2))
|
|
let iso3 = env.isoString(for: day.addingTimeInterval(3))
|
|
|
|
let entries: [[String: Any]] = [
|
|
["type": "turn_context", "timestamp": iso0, "payload": ["model": "gpt-5.4"]],
|
|
["type": "event_msg", "timestamp": iso1, "payload": ["type": "task_started", "turn_id": "standard-turn"]],
|
|
self.tokenCount(timestamp: iso2, input: 100, cached: 20, output: 10),
|
|
["type": "event_msg", "timestamp": iso3, "payload": ["type": "task_started", "turn_id": "priority-turn"]],
|
|
self.tokenCount(timestamp: iso3, 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: iso3, model: "gpt-5.4")
|
|
|
|
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)
|
|
let standardCost = (80.0 * 2.5e-6) + (20.0 * 2.5e-7) + (10.0 * 1.5e-5)
|
|
let priorityCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - (standardCost + priorityCost)) < 0.000_000_001)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report prices priority alias with completed response model`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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": "codex-auto-review"]],
|
|
["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, model: "codex-auto-review")
|
|
try CostUsageScannerCodexPriorityTests.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: iso1,
|
|
body: "thread_id=thread turn.id=priority-turn websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"gpt-5.4"}}"#)
|
|
|
|
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)
|
|
let priorityCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
#expect(breakdown.priorityTokens == 110)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report totals use completed model priority cost`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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.4"]],
|
|
["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, model: "gpt-5.4")
|
|
try CostUsageScannerCodexPriorityTests.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: iso1,
|
|
body: "thread_id=thread turn.id=priority-turn websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"gpt-5.5"}}"#)
|
|
|
|
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)
|
|
let priorityCost = (80.0 * 1.25e-5) + (20.0 * 1.25e-6) + (10.0 * 7.5e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.costUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report reprices cached priority alias when completed model arrives`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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": "codex-auto-review"]],
|
|
["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, model: "codex-auto-review")
|
|
|
|
var options = CostUsageScanner.Options(
|
|
codexSessionsRoot: env.codexSessionsRoot,
|
|
cacheRoot: env.cacheRoot,
|
|
codexTraceDatabaseURL: dbURL)
|
|
options.refreshMinIntervalSeconds = 0
|
|
|
|
let first = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day,
|
|
options: options)
|
|
#expect(first.summary?.totalCostUSD == nil)
|
|
|
|
try CostUsageScannerCodexPriorityTests.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: iso1,
|
|
body: "thread_id=thread turn.id=priority-turn websocket event: "
|
|
+ #"{"type":"response.completed","response":{"model":"gpt-5.4"}}"#)
|
|
|
|
options.refreshMinIntervalSeconds = 60
|
|
let repriced = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day.addingTimeInterval(61),
|
|
options: options)
|
|
let priorityCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
|
|
#expect(abs((repriced.summary?.totalCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
let breakdown = try #require(repriced.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
#expect(breakdown.priorityTokens == 110)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report falls back to session model for unpriced priority alias`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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.4"]],
|
|
["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, model: "codex-auto-review")
|
|
|
|
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)
|
|
let priorityCost = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - priorityCost) < 0.000_000_001)
|
|
#expect(breakdown.priorityTokens == 110)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report keeps base cost when sqlite metadata is missing`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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))
|
|
|
|
var options = CostUsageScanner.Options(
|
|
codexSessionsRoot: env.codexSessionsRoot,
|
|
cacheRoot: env.cacheRoot,
|
|
codexTraceDatabaseURL: env.root.appendingPathComponent("missing.sqlite"))
|
|
options.refreshMinIntervalSeconds = 0
|
|
|
|
let report = CostUsageScanner.loadDailyReport(
|
|
provider: .codex,
|
|
since: day,
|
|
until: day,
|
|
now: day,
|
|
options: options)
|
|
let expected = (80.0 * 5e-6) + (20.0 * 5e-7) + (10.0 * 3e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.costUSD ?? 0) - expected) < 0.000_000_001)
|
|
#expect(breakdown.standardCostUSD == nil)
|
|
#expect(breakdown.priorityCostUSD == nil)
|
|
#expect(breakdown.standardTokens == nil)
|
|
#expect(breakdown.priorityTokens == nil)
|
|
}
|
|
|
|
@Test
|
|
func `codex daily report attributes base priced priority rows to fast bucket`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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.4-nano"]],
|
|
["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, model: "gpt-5.4-nano")
|
|
|
|
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)
|
|
let expected = (80.0 * 2e-7) + (20.0 * 2e-8) + (10.0 * 1.25e-6)
|
|
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
#expect(abs((breakdown.costUSD ?? 0) - expected) < 0.000_000_001)
|
|
#expect(breakdown.standardCostUSD == nil)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
#expect(breakdown.standardTokens == nil)
|
|
#expect(breakdown.priorityTokens == 110)
|
|
}
|
|
|
|
@Test
|
|
func `codex pricing skips priority surcharge for long context rows`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
let iso0 = env.isoString(for: day)
|
|
let iso1 = env.isoString(for: day.addingTimeInterval(1))
|
|
let iso2 = env.isoString(for: day.addingTimeInterval(2))
|
|
let iso3 = env.isoString(for: day.addingTimeInterval(3))
|
|
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": "standard-turn"]],
|
|
self.tokenCount(timestamp: iso1, input: 272_001, cached: 0, output: 10),
|
|
["type": "event_msg", "timestamp": iso2, "payload": ["type": "task_started", "turn_id": "priority-turn"]],
|
|
self.tokenCount(timestamp: iso2, input: 300_000, cached: 0, output: 5),
|
|
self.tokenCount(timestamp: iso3, input: 100_001, cached: 0, output: 5),
|
|
]
|
|
_ = 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: iso2)
|
|
|
|
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)
|
|
let standardTurnBase = (272_001.0 * 1e-5) + (10.0 * 4.5e-5)
|
|
let standardFirstRow = (300_000.0 * 1e-5) + (5.0 * 4.5e-5)
|
|
let prioritySecondRow = (100_001.0 * 1.25e-5) + (5.0 * 7.5e-5)
|
|
|
|
let expected = standardTurnBase + standardFirstRow + prioritySecondRow
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
}
|
|
|
|
@Test
|
|
func `codex gpt56 long context rows keep base cost in priority bucket`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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.6-sol"]],
|
|
["type": "event_msg", "timestamp": iso1, "payload": ["type": "task_started", "turn_id": "priority-turn"]],
|
|
self.tokenCount(timestamp: iso1, input: 272_001, cached: 100_000, output: 5),
|
|
]
|
|
_ = 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, model: "gpt-5.6-sol")
|
|
|
|
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)
|
|
let expected = (172_001.0 * 1e-5) + (100_000.0 * 1e-6) + (5.0 * 4.5e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
#expect(breakdown.standardCostUSD == nil)
|
|
#expect(breakdown.priorityTokens == 272_006)
|
|
}
|
|
|
|
@Test
|
|
func `codex pricing applies priority surcharge when cached reads exceed limit but input stays under it`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
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: 200_000, cached: 100_000, output: 5),
|
|
]
|
|
_ = 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)
|
|
// cached input is a subset of input, so the 272K priority limit applies to the 200K
|
|
// input alone (not input+cached). Input stays under the limit, so the priority surcharge
|
|
// applies at priority rates, and only the 100K non-cached input is billed at the input rate.
|
|
let expected = (100_000.0 * 1.25e-5) + (100_000.0 * 1.25e-6) + (5.0 * 7.5e-5)
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
let breakdown = try #require(report.data.first?.modelBreakdowns?.first)
|
|
#expect(abs((breakdown.priorityCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
#expect(breakdown.priorityTokens == 200_005)
|
|
}
|
|
|
|
@Test
|
|
func `codex cumulative totals do not trigger long context pricing`() throws {
|
|
let env = try CostUsageTestEnvironment()
|
|
defer { env.cleanup() }
|
|
|
|
let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)
|
|
let iso0 = env.isoString(for: day)
|
|
let iso1 = env.isoString(for: day.addingTimeInterval(1))
|
|
let iso2 = env.isoString(for: day.addingTimeInterval(2))
|
|
let iso3 = env.isoString(for: day.addingTimeInterval(3))
|
|
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": "standard-turn"]],
|
|
self.totalTokenCount(timestamp: iso1, input: 120_000, cached: 60000, output: 100),
|
|
self.totalTokenCount(timestamp: iso2, input: 240_000, cached: 120_000, output: 200),
|
|
["type": "event_msg", "timestamp": iso3, "payload": ["type": "task_started", "turn_id": "priority-turn"]],
|
|
self.totalTokenCount(timestamp: iso3, input: 360_000, cached: 180_000, output: 300),
|
|
]
|
|
_ = 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: iso3)
|
|
|
|
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)
|
|
let standardRow = (Double(60000) * 5e-6) + (Double(60000) * 5e-7) + (Double(100) * 3e-5)
|
|
let priorityRow = (Double(60000) * 1.25e-5) + (Double(60000) * 1.25e-6)
|
|
+ (Double(100) * 7.5e-5)
|
|
let expected = standardRow + standardRow + priorityRow
|
|
|
|
#expect(abs((report.summary?.totalCostUSD ?? 0) - expected) < 0.000_000_001)
|
|
}
|
|
|
|
private func tokenCount(timestamp: String, input: Int, cached: Int, output: Int) -> [String: Any] {
|
|
[
|
|
"type": "event_msg",
|
|
"timestamp": timestamp,
|
|
"payload": [
|
|
"type": "token_count",
|
|
"info": [
|
|
"last_token_usage": [
|
|
"input_tokens": input,
|
|
"cached_input_tokens": cached,
|
|
"output_tokens": output,
|
|
],
|
|
],
|
|
],
|
|
]
|
|
}
|
|
|
|
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,
|
|
],
|
|
],
|
|
],
|
|
]
|
|
}
|
|
|
|
private func insertPriorityTrace(dbURL: URL, timestamp: String, model: String = "gpt-5.5") throws {
|
|
try CostUsageScannerCodexPriorityTests.insertTestLog(
|
|
dbURL: dbURL,
|
|
timestamp: timestamp,
|
|
body: "thread_id=thread turn.id=priority-turn websocket request: "
|
|
+ #"{"type":"response.create","model":""# + model + #"","service_tier":"priority"}"#)
|
|
}
|
|
}
|
|
#endif
|