Skip to content
Open
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
12 changes: 7 additions & 5 deletions Sources/CodexBar/CostHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ struct CostHistoryChartMenuView: View {
var peak: (key: String, costUSD: Double)?
var maxCostUSD: Double = 0
for entry in sorted {
guard let costUSD = entry.costUSD, costUSD > 0 else { continue }
guard let costUSD = entry.costUSD, costUSD >= 0 else { continue }
guard let date = self.dateFromDayKey(entry.date) else { continue }
let point = Point(date: date, costUSD: costUSD, totalTokens: entry.totalTokens)
points.append(point)
Expand Down Expand Up @@ -310,16 +310,18 @@ struct CostHistoryChartMenuView: View {
guard let entry = model.entriesByDateKey[key] else { return nil }
guard let breakdown = entry.modelBreakdowns, !breakdown.isEmpty else { return nil }
let parts = breakdown
.compactMap { item -> (name: String, costUSD: Double)? in
guard let costUSD = item.costUSD, costUSD > 0 else { return nil }
return (UsageFormatter.modelDisplayName(item.modelName), costUSD)
.compactMap { item -> (name: String, detail: String, costUSD: Double)? in
guard let costUSD = item.costUSD else { return nil }
let name = UsageFormatter.modelDisplayName(item.modelName)
guard let detail = UsageFormatter.modelCostDetail(item.modelName, costUSD: costUSD) else { return nil }
return (name, detail, costUSD)
}
.sorted { lhs, rhs in
if lhs.costUSD == rhs.costUSD { return lhs.name < rhs.name }
return lhs.costUSD > rhs.costUSD
}
.prefix(3)
.map { "\($0.name) \(UsageFormatter.usdString($0.costUSD))" }
.map { "\($0.name) \($0.detail)" }
guard !parts.isEmpty else { return nil }
return "Top: \(parts.joined(separator: " · "))"
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBarCore/UsageFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ public enum UsageFormatter {
return cleaned.isEmpty ? raw : cleaned
}

public static func modelCostDetail(_ model: String, costUSD: Double?) -> String? {
if let label = CostUsagePricing.codexDisplayLabel(model: model) {
return label
}

guard let costUSD else { return nil }
return self.usdString(costUSD)
}

/// Cleans a provider plan string: strip ANSI/bracket noise, drop boilerplate words, collapse whitespace, and
/// ensure a leading capital if the result starts lowercase.
public static func cleanPlanName(_ text: String) -> String {
Expand Down
202 changes: 186 additions & 16 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,32 @@ enum CostUsagePricing {
struct CodexPricing: Sendable {
let inputCostPerToken: Double
let outputCostPerToken: Double
let cacheReadInputCostPerToken: Double
let cacheReadInputCostPerToken: Double?
let thresholdTokens: Int?
let inputCostPerTokenAboveThreshold: Double?
let outputCostPerTokenAboveThreshold: Double?
let cacheReadInputCostPerTokenAboveThreshold: Double?
let displayLabel: String?

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

struct ClaudePricing: Sendable {
Expand All @@ -24,31 +49,123 @@ enum CostUsagePricing {
"gpt-5": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7),
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5-chat": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5-chat-latest": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5-codex": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7),
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5-codex-mini": CodexPricing(
inputCostPerToken: 2.5e-7,
outputCostPerToken: 2e-6,
cacheReadInputCostPerToken: 2.5e-8,
displayLabel: nil),
"gpt-5-mini": CodexPricing(
inputCostPerToken: 2.5e-7,
outputCostPerToken: 2e-6,
cacheReadInputCostPerToken: 2.5e-8,
displayLabel: nil),
"gpt-5-nano": CodexPricing(
inputCostPerToken: 5e-8,
outputCostPerToken: 4e-7,
cacheReadInputCostPerToken: 5e-9,
displayLabel: nil),
"gpt-5-pro": CodexPricing(
inputCostPerToken: 1.5e-5,
outputCostPerToken: 1.2e-4,
cacheReadInputCostPerToken: nil,
displayLabel: nil),
"gpt-5.1": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7),
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5.1-chat-latest": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5.1-codex": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5.1-codex-max": CodexPricing(
inputCostPerToken: 1.25e-6,
outputCostPerToken: 1e-5,
cacheReadInputCostPerToken: 1.25e-7,
displayLabel: nil),
"gpt-5.1-codex-mini": CodexPricing(
inputCostPerToken: 2.5e-7,
outputCostPerToken: 2e-6,
cacheReadInputCostPerToken: 2.5e-8,
displayLabel: nil),
"gpt-5.2": CodexPricing(
inputCostPerToken: 1.75e-6,
outputCostPerToken: 1.4e-5,
cacheReadInputCostPerToken: 1.75e-7),
cacheReadInputCostPerToken: 1.75e-7,
displayLabel: nil),
"gpt-5.2-chat": CodexPricing(
inputCostPerToken: 1.75e-6,
outputCostPerToken: 1.4e-5,
cacheReadInputCostPerToken: 1.75e-7,
displayLabel: nil),
"gpt-5.2-chat-latest": CodexPricing(
inputCostPerToken: 1.75e-6,
outputCostPerToken: 1.4e-5,
cacheReadInputCostPerToken: 1.75e-7,
displayLabel: nil),
"gpt-5.2-codex": CodexPricing(
inputCostPerToken: 1.75e-6,
outputCostPerToken: 1.4e-5,
cacheReadInputCostPerToken: 1.75e-7),
cacheReadInputCostPerToken: 1.75e-7,
displayLabel: nil),
"gpt-5.2-pro": CodexPricing(
inputCostPerToken: 2.1e-5,
outputCostPerToken: 1.68e-4,
cacheReadInputCostPerToken: nil,
displayLabel: nil),
"gpt-5.3-codex": CodexPricing(
inputCostPerToken: 1.75e-6,
outputCostPerToken: 1.4e-5,
cacheReadInputCostPerToken: 1.75e-7,
displayLabel: nil),
"gpt-5.3": CodexPricing(
inputCostPerToken: 1.75e-6,
outputCostPerToken: 1.4e-5,
cacheReadInputCostPerToken: 1.75e-7),
"gpt-5.3-codex": CodexPricing(
cacheReadInputCostPerToken: 1.75e-7,
displayLabel: nil),
"gpt-5.3-chat-latest": CodexPricing(
inputCostPerToken: 1.75e-6,
outputCostPerToken: 1.4e-5,
cacheReadInputCostPerToken: 1.75e-7),
cacheReadInputCostPerToken: 1.75e-7,
displayLabel: nil),
"gpt-5.3-codex-spark": CodexPricing(
inputCostPerToken: 0,
outputCostPerToken: 0,
cacheReadInputCostPerToken: 0,
displayLabel: "Research Preview"),
"gpt-5.4": CodexPricing(
inputCostPerToken: 2.5e-6,
outputCostPerToken: 1.5e-5,
cacheReadInputCostPerToken: 2.5e-7,
displayLabel: nil),
"gpt-5.4-pro": CodexPricing(
inputCostPerToken: 3e-5,
outputCostPerToken: 1.8e-4,
cacheReadInputCostPerToken: nil,
displayLabel: nil),
]

private static let claude: [String: ClaudePricing] = [
Expand Down Expand Up @@ -165,17 +282,51 @@ enum CostUsagePricing {
]

static func normalizeCodexModel(_ raw: String) -> String {
var trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("openai/") {
trimmed = String(trimmed.dropFirst("openai/".count))
var trimmed = self.displayCodexModel(raw)
if let snapshotBase = self.codexSnapshotBaseModel(trimmed) {
trimmed = snapshotBase
}
if self.codex[trimmed] != nil {
return trimmed
}
if let codexRange = trimmed.range(of: "-codex") {
if let codexRange = trimmed.range(of: "-codex"), !trimmed.contains("-codex-mini") {
let base = String(trimmed[..<codexRange.lowerBound])
if self.codex[base] != nil { return base }
}
return trimmed
}

static func displayCodexModel(_ raw: String) -> String {
var trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("openai/") {
trimmed = String(trimmed.dropFirst("openai/".count))
}
return trimmed
}

private static func codexSnapshotBaseModel(_ raw: String) -> String? {
let patterns = [
#"-\d{4}-\d{2}-\d{2}$"#,
#"-\d{8}$"#,
]

for pattern in patterns {
if let range = raw.range(of: pattern, options: .regularExpression) {
let base = String(raw[..<range.lowerBound])
if self.codex[base] != nil {
return base
}
}
}

return nil
}

static func codexDisplayLabel(model: String) -> String? {
let key = self.normalizeCodexModel(model)
return self.codex[key]?.displayLabel
}

static func normalizeClaudeModel(_ raw: String) -> String {
var trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("anthropic.") {
Expand Down Expand Up @@ -210,9 +361,28 @@ enum CostUsagePricing {
guard let pricing = self.codex[key] else { return nil }
let cached = min(max(0, cachedInputTokens), max(0, inputTokens))
let nonCached = max(0, inputTokens - cached)
return Double(nonCached) * pricing.inputCostPerToken
+ Double(cached) * pricing.cacheReadInputCostPerToken
+ Double(max(0, outputTokens)) * pricing.outputCostPerToken
let effectiveInputTokens = max(0, inputTokens)
let useAboveThresholdPricing = if let threshold = pricing.thresholdTokens {
effectiveInputTokens > threshold
} else {
false
}
let inputRate = useAboveThresholdPricing
? (pricing.inputCostPerTokenAboveThreshold ?? pricing.inputCostPerToken)
: pricing.inputCostPerToken
let outputRate = useAboveThresholdPricing
? (pricing.outputCostPerTokenAboveThreshold ?? pricing.outputCostPerToken)
: pricing.outputCostPerToken
let cacheRate = useAboveThresholdPricing
? (pricing.cacheReadInputCostPerTokenAboveThreshold ?? pricing.cacheReadInputCostPerToken)
: pricing.cacheReadInputCostPerToken

if cached > 0, cacheRate == nil {
return nil
}
return Double(nonCached) * inputRate
+ Double(cached) * (cacheRate ?? 0)
+ Double(max(0, outputTokens)) * outputRate
}

static func claudeCostUSD(
Expand Down
48 changes: 48 additions & 0 deletions Tests/CodexBarTests/CLICostTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,52 @@ struct CLICostTests {
#expect(json.contains("\"totalCost\""))
#expect(json.contains("1700000000"))
}

@Test
func encodesExactCodexModelIDsAndZeroCostBreakdowns() throws {
let payload = CostPayload(
provider: "codex",
source: "local",
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
sessionTokens: 155,
sessionCostUSD: 0,
last30DaysTokens: 155,
last30DaysCostUSD: 0,
daily: [
CostDailyEntryPayload(
date: "2025-12-21",
inputTokens: 120,
outputTokens: 15,
cacheReadTokens: 20,
cacheCreationTokens: nil,
totalTokens: 155,
costUSD: 0,
modelsUsed: ["gpt-5.3-codex-spark", "gpt-5.2-codex"],
modelBreakdowns: [
CostModelBreakdownPayload(modelName: "gpt-5.3-codex-spark", costUSD: 0),
CostModelBreakdownPayload(modelName: "gpt-5.2-codex", costUSD: 1.23),
]),
],
totals: CostTotalsPayload(
totalInputTokens: 120,
totalOutputTokens: 15,
cacheReadTokens: 20,
cacheCreationTokens: nil,
totalTokens: 155,
totalCostUSD: 0),
error: nil)

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970
let data = try encoder.encode(payload)
guard let json = String(data: data, encoding: .utf8) else {
Issue.record("Failed to decode cost payload JSON")
return
}

#expect(json.contains("\"gpt-5.3-codex-spark\""))
#expect(json.contains("\"gpt-5.2-codex\""))
#expect(!json.contains("\"gpt-5.2\""))
#expect(json.contains("\"cost\":0"))
}
}
Loading