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 @@ -12,6 +12,7 @@

### Fixed
- CLI server: retain timed-out route and provider work until it actually exits, preventing repeated requests or config changes from stacking background fetches. Thanks @Yuxin-Qiao!
- Token costs: coalesce bounded pricing-catalog refreshes when a newly observed model is still unpriced, preserving its exact usage until pricing arrives. Thanks @iam-brain!
- Ollama: validate API keys against an authenticated endpoint instead of the public model catalog while preserving refresh cancellation. Thanks @joeVenner!
- Claude CLI: resolve yearless and time-only reset timestamps against their quota window and exact calendar occurrence, keeping recently stale resets current without moving future, leap-day, or repeated-hour resets into the past. Thanks @fanwenlin!
- Catalan: complete current strings, align instructional voice, and enforce catalog parity. Thanks @pmontp19!
Expand Down
150 changes: 132 additions & 18 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ public struct CostUsageFetcher: Sendable {
refreshPricingInBackground: Bool = true,
scannerOptions overrideScannerOptions: CostUsageScanner.Options? = nil,
piScannerOptions overridePiScannerOptions: PiSessionCostScanner
.Options? = nil) async throws -> CostUsageTokenSnapshot
.Options? = nil,
modelsDevClient: ModelsDevClient = ModelsDevClient(),
retryUnknownPricing: Bool = true) async throws -> CostUsageTokenSnapshot
{
guard provider == .codex || provider == .claude || provider == .vertexai || provider == .bedrock else {
throw CostUsageError.unsupportedProvider(provider)
Expand Down Expand Up @@ -148,14 +150,20 @@ public struct CostUsageFetcher: Sendable {
options.codexSessionsRoot = URL(fileURLWithPath: codexHomePath, isDirectory: true)
.appendingPathComponent("sessions", isDirectory: true)
}
if provider == .codex || provider == .claude {
if retryUnknownPricing, provider == .codex || provider == .claude {
let pricingCacheRoot = options.cacheRoot
if refreshPricingInBackground {
Task.detached(priority: .utility) {
await ModelsDevPricingPipeline.refreshIfNeeded(now: now, cacheRoot: pricingCacheRoot)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: now,
cacheRoot: pricingCacheRoot,
client: modelsDevClient)
}
} else {
await ModelsDevPricingPipeline.refreshIfNeeded(now: now, cacheRoot: pricingCacheRoot)
await ModelsDevPricingPipeline.refreshIfNeeded(
now: now,
cacheRoot: pricingCacheRoot,
client: modelsDevClient)
}
}

Expand Down Expand Up @@ -239,13 +247,91 @@ public struct CostUsageFetcher: Sendable {
return (daily: daily, projects: projects)
}

if retryUnknownPricing,
let request = Self.unknownPricingRefreshRequest(
provider: provider,
daily: scanResult.daily,
now: now,
cacheRoot: options.cacheRoot,
client: modelsDevClient),
await Self.refreshUnknownPricingIfNeeded(request, inBackground: refreshPricingInBackground)
{
return try await self.loadTokenSnapshot(
provider: provider,
environment: environment,
now: now,
forceRefresh: forceRefresh,
allowVertexClaudeFallback: allowVertexClaudeFallback,
codexHomePath: codexHomePath,
historyDays: historyDays,
refreshPricingInBackground: false,
scannerOptions: options,
piScannerOptions: piOptions,
modelsDevClient: modelsDevClient,
retryUnknownPricing: false)
}

return Self.tokenSnapshot(
from: scanResult.daily,
now: now,
historyDays: clampedHistoryDays,
projects: scanResult.projects)
}

private struct UnknownPricingRefreshRequest: Sendable {
let providerID: String
let modelIDs: Set<String>
let now: Date
let cacheRoot: URL?
let client: ModelsDevClient
}

private static func unknownPricingRefreshRequest(
provider: UsageProvider,
daily: CostUsageDailyReport,
now: Date,
cacheRoot: URL?,
client: ModelsDevClient) -> UnknownPricingRefreshRequest?
{
guard provider == .codex || provider == .claude else { return nil }
let unknownModelIDs = Set(daily.data.flatMap { entry in
entry.modelBreakdowns?.compactMap { breakdown in
breakdown.costUSD == nil ? breakdown.modelName : nil
} ?? []
})
guard !unknownModelIDs.isEmpty else { return nil }

return UnknownPricingRefreshRequest(
providerID: provider == .codex ? "openai" : "anthropic",
modelIDs: unknownModelIDs,
now: now,
cacheRoot: cacheRoot,
client: client)
}

private static func refreshUnknownPricingIfNeeded(
_ request: UnknownPricingRefreshRequest,
inBackground: Bool) async -> Bool
{
if inBackground {
Task.detached(priority: .utility) {
_ = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: request.providerID,
modelIDs: request.modelIDs,
now: request.now,
cacheRoot: request.cacheRoot,
client: request.client)
}
return false
}
return await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: request.providerID,
modelIDs: request.modelIDs,
now: request.now,
cacheRoot: request.cacheRoot,
client: request.client) == .pricingAvailable
}

static func loadCachedCodexTokenSnapshot(
now: Date = Date(),
codexHomePath: String? = nil,
Expand Down Expand Up @@ -470,10 +556,14 @@ public struct CostUsageFetcher: Sendable {
.sorted { lhs, rhs in
let lhsCost = lhs.totalCostUSD ?? -1
let rhsCost = rhs.totalCostUSD ?? -1
if lhsCost != rhsCost { return lhsCost > rhsCost }
if lhsCost != rhsCost {
return lhsCost > rhsCost
}
let lhsTokens = lhs.totalTokens ?? -1
let rhsTokens = rhs.totalTokens ?? -1
if lhsTokens != rhsTokens { return lhsTokens > rhsTokens }
if lhsTokens != rhsTokens {
return lhsTokens > rhsTokens
}
return lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}
}
Expand All @@ -495,10 +585,14 @@ public struct CostUsageFetcher: Sendable {
.sorted { lhs, rhs in
let lhsCost = lhs.totalCostUSD ?? -1
let rhsCost = rhs.totalCostUSD ?? -1
if lhsCost != rhsCost { return lhsCost > rhsCost }
if lhsCost != rhsCost {
return lhsCost > rhsCost
}
let lhsTokens = lhs.totalTokens ?? -1
let rhsTokens = rhs.totalTokens ?? -1
if lhsTokens != rhsTokens { return lhsTokens > rhsTokens }
if lhsTokens != rhsTokens {
return lhsTokens > rhsTokens
}
return lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}
}
Expand Down Expand Up @@ -546,46 +640,66 @@ public struct CostUsageFetcher: Sendable {
.sorted { lhs, rhs in
let lhsCost = lhs.costUSD ?? -1
let rhsCost = rhs.costUSD ?? -1
if lhsCost != rhsCost { return lhsCost > rhsCost }
if lhsCost != rhsCost {
return lhsCost > rhsCost
}
let lhsTokens = lhs.totalTokens ?? -1
let rhsTokens = rhs.totalTokens ?? -1
if lhsTokens != rhsTokens { return lhsTokens > rhsTokens }
if lhsTokens != rhsTokens {
return lhsTokens > rhsTokens
}
return lhs.modelName > rhs.modelName
}
}

static func selectCurrentSession(from sessions: [CostUsageSessionReport.Entry])
-> CostUsageSessionReport.Entry?
{
if sessions.isEmpty { return nil }
if sessions.isEmpty {
return nil
}
return sessions.max { lhs, rhs in
let lDate = CostUsageDateParser.parse(lhs.lastActivity) ?? .distantPast
let rDate = CostUsageDateParser.parse(rhs.lastActivity) ?? .distantPast
if lDate != rDate { return lDate < rDate }
if lDate != rDate {
return lDate < rDate
}
let lCost = lhs.costUSD ?? -1
let rCost = rhs.costUSD ?? -1
if lCost != rCost { return lCost < rCost }
if lCost != rCost {
return lCost < rCost
}
let lTokens = lhs.totalTokens ?? -1
let rTokens = rhs.totalTokens ?? -1
if lTokens != rTokens { return lTokens < rTokens }
if lTokens != rTokens {
return lTokens < rTokens
}
return lhs.session < rhs.session
}
}

static func selectMostRecentMonth(from months: [CostUsageMonthlyReport.Entry])
-> CostUsageMonthlyReport.Entry?
{
if months.isEmpty { return nil }
if months.isEmpty {
return nil
}
return months.max { lhs, rhs in
let lDate = CostUsageDateParser.parseMonth(lhs.month) ?? .distantPast
let rDate = CostUsageDateParser.parseMonth(rhs.month) ?? .distantPast
if lDate != rDate { return lDate < rDate }
if lDate != rDate {
return lDate < rDate
}
let lCost = lhs.costUSD ?? -1
let rCost = rhs.costUSD ?? -1
if lCost != rCost { return lCost < rCost }
if lCost != rCost {
return lCost < rCost
}
let lTokens = lhs.totalTokens ?? -1
let rTokens = rhs.totalTokens ?? -1
if lTokens != rTokens { return lTokens < rTokens }
if lTokens != rTokens {
return lTokens < rTokens
}
return lhs.month < rhs.month
}
}
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 = "1745966d77d9dae8"
static let value = "3edc97b8139ae59e"
}
Loading