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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.26 — Unreleased

### Added
- Codex: add tiered long-context and Fast/Priority pricing to local cost history using local app-server priority traces (#917). Thanks @iam-brain!
- Localization: add Simplified Chinese translations for Claude peak-hour labels (#921). Thanks @whtis!
- Display: add a setting to hide quota-warning tick marks on usage bars while keeping quota warning notifications active (#918, fixes #916). Thanks @ThiagoCAltoe!
- Menu: add an opt-in setting for provider changelog links, starting with Codex, Claude Code, and Gemini CLI (#929, fixes #660). Thanks @ThiagoCAltoe!
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ enum CostUsageCacheIO {
private static func artifactVersion(for provider: UsageProvider) -> Int {
switch provider {
case .codex:
4
6
case .claude, .vertexai:
2
default:
Expand Down Expand Up @@ -76,8 +76,10 @@ struct CostUsageFileUsage: Codable {
var parsedBytes: Int64?
var lastModel: String?
var lastTotals: CostUsageCodexTotals?
var lastCodexTurnID: String?
var sessionId: String?
var forkedFromId: String?
var codexRows: [CostUsageScanner.CodexUsageRow]?
var claudeRows: [CostUsageScanner.ClaudeUsageRow]?
}

Expand Down
112 changes: 105 additions & 7 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
import Foundation

enum CostUsagePricing {
private static let codexPriorityInputTokenLimit = 272_000

struct CodexPricing {
let inputCostPerToken: Double
let outputCostPerToken: Double
let cacheReadInputCostPerToken: Double?
let displayLabel: String?

let thresholdTokens: Int?
let inputCostPerTokenAboveThreshold: Double?
let outputCostPerTokenAboveThreshold: Double?
let cacheReadInputCostPerTokenAboveThreshold: Double?
let priorityInputCostPerToken: Double?
let priorityOutputCostPerToken: Double?
let priorityCacheReadInputCostPerToken: Double?

init(
inputCostPerToken: Double,
outputCostPerToken: Double,
cacheReadInputCostPerToken: Double?,
displayLabel: String?,
thresholdTokens: Int? = nil,
inputCostPerTokenAboveThreshold: Double? = nil,
outputCostPerTokenAboveThreshold: Double? = nil,
cacheReadInputCostPerTokenAboveThreshold: Double? = nil,
priorityInputCostPerToken: Double? = nil,
priorityOutputCostPerToken: Double? = nil,
priorityCacheReadInputCostPerToken: Double? = nil)
{
self.inputCostPerToken = inputCostPerToken
self.outputCostPerToken = outputCostPerToken
self.cacheReadInputCostPerToken = cacheReadInputCostPerToken
self.displayLabel = displayLabel
self.thresholdTokens = thresholdTokens
self.inputCostPerTokenAboveThreshold = inputCostPerTokenAboveThreshold
self.outputCostPerTokenAboveThreshold = outputCostPerTokenAboveThreshold
self.cacheReadInputCostPerTokenAboveThreshold = cacheReadInputCostPerTokenAboveThreshold
self.priorityInputCostPerToken = priorityInputCostPerToken
self.priorityOutputCostPerToken = priorityOutputCostPerToken
self.priorityCacheReadInputCostPerToken = priorityCacheReadInputCostPerToken
}
}

struct ClaudePricing {
Expand Down Expand Up @@ -96,12 +132,22 @@ enum CostUsagePricing {
inputCostPerToken: 2.5e-6,
outputCostPerToken: 1.5e-5,
cacheReadInputCostPerToken: 2.5e-7,
displayLabel: nil),
displayLabel: nil,
thresholdTokens: 272_000,
inputCostPerTokenAboveThreshold: 5e-6,
outputCostPerTokenAboveThreshold: 2.25e-5,
cacheReadInputCostPerTokenAboveThreshold: 5e-7,
priorityInputCostPerToken: 5e-6,
priorityOutputCostPerToken: 3e-5,
priorityCacheReadInputCostPerToken: 5e-7),
"gpt-5.4-mini": CodexPricing(
inputCostPerToken: 7.5e-7,
outputCostPerToken: 4.5e-6,
cacheReadInputCostPerToken: 7.5e-8,
displayLabel: nil),
displayLabel: nil,
priorityInputCostPerToken: 1.5e-6,
priorityOutputCostPerToken: 9e-6,
priorityCacheReadInputCostPerToken: 1.5e-7),
"gpt-5.4-nano": CodexPricing(
inputCostPerToken: 2e-7,
outputCostPerToken: 1.25e-6,
Expand All @@ -116,7 +162,14 @@ enum CostUsagePricing {
inputCostPerToken: 5e-6,
outputCostPerToken: 3e-5,
cacheReadInputCostPerToken: 5e-7,
displayLabel: nil),
displayLabel: nil,
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),
"gpt-5.5-pro": CodexPricing(
inputCostPerToken: 3e-5,
outputCostPerToken: 1.8e-4,
Expand Down Expand Up @@ -330,6 +383,7 @@ enum CostUsagePricing {
{
return self.codexCostUSD(
pricing: lookup.pricing,
thresholdTokens: self.codex[key]?.thresholdTokens,
inputTokens: inputTokens,
cachedInputTokens: cachedInputTokens,
outputTokens: outputTokens)
Expand All @@ -343,6 +397,33 @@ enum CostUsagePricing {
outputTokens: outputTokens)
}

static func codexPriorityCostUSD(
model: String,
inputTokens: Int,
cachedInputTokens: Int = 0,
outputTokens: Int) -> Double?
{
let key = self.normalizeCodexModel(model)
guard let pricing = self.codex[key],
let priorityInputCostPerToken = pricing.priorityInputCostPerToken,
let priorityOutputCostPerToken = pricing.priorityOutputCostPerToken
else { return nil }
if max(0, inputTokens) > self.codexPriorityInputTokenLimit {
return nil
}

let priorityPricing = CodexPricing(
inputCostPerToken: priorityInputCostPerToken,
outputCostPerToken: priorityOutputCostPerToken,
cacheReadInputCostPerToken: pricing.priorityCacheReadInputCostPerToken,
displayLabel: nil)
return self.codexCostUSD(
pricing: priorityPricing,
inputTokens: inputTokens,
cachedInputTokens: cachedInputTokens,
outputTokens: outputTokens)
}

private static func codexCostUSD(
pricing: CodexPricing,
inputTokens: Int,
Expand All @@ -352,13 +433,26 @@ enum CostUsagePricing {
let cached = min(max(0, cachedInputTokens), max(0, inputTokens))
let nonCached = max(0, inputTokens - cached)
let cachedRate = pricing.cacheReadInputCostPerToken ?? pricing.inputCostPerToken
return Double(nonCached) * pricing.inputCostPerToken
+ Double(cached) * cachedRate
+ Double(max(0, outputTokens)) * pricing.outputCostPerToken

let usesLongContextRates = pricing.thresholdTokens.map { max(0, inputTokens) > $0 } ?? false
let inputRate = usesLongContextRates
? pricing.inputCostPerTokenAboveThreshold ?? pricing.inputCostPerToken
: pricing.inputCostPerToken
let cachedInputRate = usesLongContextRates
? pricing.cacheReadInputCostPerTokenAboveThreshold ?? cachedRate
: cachedRate
let outputRate = usesLongContextRates
? pricing.outputCostPerTokenAboveThreshold ?? pricing.outputCostPerToken
: pricing.outputCostPerToken

return (Double(nonCached) * inputRate)
+ (Double(cached) * cachedInputRate)
+ (Double(max(0, outputTokens)) * outputRate)
}

private static func codexCostUSD(
pricing: ModelsDevPricingInfo,
thresholdTokens: Int? = nil,
inputTokens: Int,
cachedInputTokens: Int,
outputTokens: Int) -> Double
Expand All @@ -368,7 +462,11 @@ enum CostUsagePricing {
inputCostPerToken: pricing.inputCostPerToken,
outputCostPerToken: pricing.outputCostPerToken,
cacheReadInputCostPerToken: pricing.cacheReadInputCostPerToken,
displayLabel: nil),
displayLabel: nil,
thresholdTokens: thresholdTokens ?? pricing.thresholdTokens,
inputCostPerTokenAboveThreshold: pricing.inputCostPerTokenAboveThreshold,
outputCostPerTokenAboveThreshold: pricing.outputCostPerTokenAboveThreshold,
cacheReadInputCostPerTokenAboveThreshold: pricing.cacheReadInputCostPerTokenAboveThreshold),
inputTokens: inputTokens,
cachedInputTokens: cachedInputTokens,
outputTokens: outputTokens)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import Foundation
#if canImport(SQLite3)
import SQLite3
#endif

extension CostUsageScanner {
struct CodexPriorityTurnMetadata: Codable, Equatable, Sendable {
var threadID: String?
var turnID: String
var model: String?
var timestamp: String?
}

private static let requestMarker = "websocket request:"

static func defaultCodexPriorityDatabaseURL() -> URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".codex", isDirectory: true)
.appendingPathComponent("logs_2.sqlite", isDirectory: false)
}

static func codexPriorityTurns(
databaseURL: URL? = nil,
sinceDayKey: String? = nil,
untilDayKey: String? = nil) -> [String: CodexPriorityTurnMetadata]
{
let url = databaseURL ?? self.defaultCodexPriorityDatabaseURL()
guard FileManager.default.fileExists(atPath: url.path) else { return [:] }

#if canImport(SQLite3)
var db: OpaquePointer?
guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else {
sqlite3_close(db)
return [:]
}
defer { sqlite3_close(db) }
sqlite3_busy_timeout(db, 250)

let query = if sinceDayKey != nil || untilDayKey != nil {
"""
select ts, feedback_log_body
from logs
where ts >= ? and ts < ? and feedback_log_body like '%websocket request:%'
"""
} else {
"""
select ts, feedback_log_body
from logs
where feedback_log_body like '%websocket request:%'
"""
}
var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, query, -1, &stmt, nil) == SQLITE_OK else { return [:] }
defer { sqlite3_finalize(stmt) }

if sinceDayKey != nil || untilDayKey != nil {
let start = self.epochSeconds(forDayKey: sinceDayKey ?? "0000-01-01") ?? 0
let end = self.epochSeconds(forDayKey: self.nextDayKey(after: untilDayKey ?? "9999-12-30"))
?? Int64.max
sqlite3_bind_int64(stmt, 1, start)
sqlite3_bind_int64(stmt, 2, end)
}

var turns: [String: CodexPriorityTurnMetadata] = [:]
while sqlite3_step(stmt) == SQLITE_ROW {
let timestamp = self.timestamp(stmt: stmt, index: 0)
guard self.timestamp(timestamp, isInRangeSince: sinceDayKey, until: untilDayKey),
let body = self.text(stmt: stmt, index: 1),
let parsed = self.parseCodexPriorityTraceRow(timestamp: timestamp, body: body)
else { continue }
turns[parsed.turnID] = parsed
}
return turns
#else
return [:]
#endif
}

static func parseCodexPriorityTraceRow(timestamp: String?, body: String) -> CodexPriorityTurnMetadata? {
guard let markerRange = body.range(of: self.requestMarker) else { return nil }
let prefix = String(body[..<markerRange.lowerBound])
let jsonText = body[markerRange.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines)
guard let data = jsonText.data(using: .utf8),
let request = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
request["type"] as? String == "response.create",
request["service_tier"] as? String == "priority"
else { return nil }

let turnID = self.value(named: "turn.id", in: prefix)
?? self.value(named: "turn_id", in: prefix)
?? request["turn_id"] as? String
guard let turnID, !turnID.isEmpty else { return nil }

return CodexPriorityTurnMetadata(
threadID: self.value(named: "thread_id", in: prefix),
turnID: turnID,
model: request["model"] as? String,
timestamp: timestamp)
}

private static func value(named name: String, in text: String) -> String? {
guard let range = text.range(of: "\(name)=") else { return nil }
let tail = text[range.upperBound...]
let value = tail.prefix { char in
!char.isWhitespace && char != "," && char != "]" && char != ")"
}
return value.isEmpty ? nil : String(value)
}

#if canImport(SQLite3)
private static func text(stmt: OpaquePointer?, index: Int32) -> String? {
guard sqlite3_column_type(stmt, index) != SQLITE_NULL,
let cString = sqlite3_column_text(stmt, index)
else { return nil }
return String(cString: cString)
}

private static func timestamp(stmt: OpaquePointer?, index: Int32) -> String? {
guard sqlite3_column_type(stmt, index) != SQLITE_NULL else { return nil }
if sqlite3_column_type(stmt, index) == SQLITE_INTEGER {
return String(sqlite3_column_int64(stmt, index))
}
return self.text(stmt: stmt, index: index)
}
#endif

private static func timestamp(_ timestamp: String?, isInRangeSince since: String?, until: String?) -> Bool {
guard since != nil || until != nil else { return true }
guard let dayKey = self.dayKey(fromTimestamp: timestamp) else { return false }
if let since, dayKey < since { return false }
if let until, dayKey > until { return false }
return true
}

private static func dayKey(fromTimestamp timestamp: String?) -> String? {
guard let timestamp else { return nil }
if let seconds = Int64(timestamp) {
return CostUsageScanner.CostUsageDayRange.dayKey(
from: Date(timeIntervalSince1970: TimeInterval(seconds)))
}
let dayKey = timestamp.prefix(10)
return dayKey.count == 10 ? String(dayKey) : nil
}

private static func nextDayKey(after dayKey: String) -> String {
guard let date = self.localDate(forDayKey: dayKey),
let next = Calendar.current.date(byAdding: .day, value: 1, to: date)
else { return dayKey }
return CostUsageScanner.CostUsageDayRange.dayKey(from: next)
}

private static func epochSeconds(forDayKey dayKey: String) -> Int64? {
guard let date = self.localDate(forDayKey: dayKey) else { return nil }
return Int64(date.timeIntervalSince1970)
}

private static func localDate(forDayKey dayKey: String) -> Date? {
let parts = dayKey.split(separator: "-")
guard parts.count == 3,
let year = Int(parts[0]),
let month = Int(parts[1]),
let day = Int(parts[2])
else { return nil }
var components = DateComponents()
components.calendar = Calendar.current
components.year = year
components.month = month
components.day = day
return components.date
}
}
Loading