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 Sources/CodexBar/SpendDashboardController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ enum SpendDashboardSource {
failedSourceIDs.formUnion(lateInvalidatedSourceIDs)
invalidatedSourceIDs.formUnion(lateInvalidatedSourceIDs)
inputs.removeAll { lateInvalidatedSourceIDs.contains($0.id) }
let openCodex = self.mergingOpenCodexInputsWithObservation(inputs, request: request)
let openCodex = await self.mergingOpenCodexInputsAfterRefreshingPricing(inputs, request: request)
return SpendDashboardLoadResult(
inputs: openCodex.inputs,
failedSourceIDs: failedSourceIDs,
Expand Down
40 changes: 40 additions & 0 deletions Sources/CodexBar/SpendDashboardSource+OpenCodex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@ import CodexBarCore
import Foundation

extension SpendDashboardSource {
static func mergingOpenCodexInputsAfterRefreshingPricing(
_ inputs: [SpendDashboardModel.ProviderInput],
request: SpendDashboardLoadRequest,
environment: [String: String] = ProcessInfo.processInfo.environment,
entryLoader: (@Sendable (URL) throws -> [OpenCodexUsageEntry])? = nil,
pricingRefresher: @escaping @Sendable ([OpenCodexUsageEntry], Date) async -> Void = { entries, now in
await OpenCodexUsageStore.refreshPricingIfNeeded(entries: entries, now: now)
}) async -> (
inputs: [SpendDashboardModel.ProviderInput],
observation: SpendDashboardLoadResult.OpenCodexObservation)
{
guard request.configuration.openCodexUsageLogsEnabled,
!request.configuration.hiddenSourceIDs.contains(SpendDashboardModel.openCodexSourceID)
else {
return (
inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID },
.disabled)
}
guard let logURL = OpenCodexUsageLog.usageLogURL(environment: environment) else {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .unavailable)
}
let store = OpenCodexUsageStore(cacheRoot: OpenCodexUsageLog.cacheRoot())
let entries: [OpenCodexUsageEntry]
do {
entries = try entryLoader?(logURL) ?? store.loadEntries(logURL: logURL)
} catch {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .unavailable)
}
guard !entries.isEmpty else {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .confirmedEmpty)
}

await pricingRefresher(entries, request.now)
return self.mergingOpenCodexInputsWithObservation(
inputs,
request: request,
environment: environment,
entryLoader: { _ in entries })
}

static func mergingOpenCodexInputsWithObservation(
_ inputs: [SpendDashboardModel.ProviderInput],
request: SpendDashboardLoadRequest,
Expand Down
6 changes: 4 additions & 2 deletions Sources/CodexBarCLI/CLICostCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ extension CodexBarCLI {
}

if format == .json,
let openCodex = Self.loadOpenCodexCostPayload(
let openCodex = await Self.loadOpenCodexCostPayload(
historyDays: historyDays,
calendar: bucketCalendar)
{
Expand Down Expand Up @@ -653,12 +653,14 @@ extension CodexBarCLI {
private static func loadOpenCodexCostPayload(
historyDays: Int,
calendar: Calendar,
now: Date = Date()) -> CostPayload?
now: Date = Date()) async -> CostPayload?
{
guard boolFromAppDefaults("openCodexUsageLogsEnabled") == true else { return nil }
let environment = ProcessInfo.processInfo.environment
guard let logURL = OpenCodexUsageLog.usageLogURL(environment: environment) else { return nil }
let store = OpenCodexUsageStore(cacheRoot: OpenCodexUsageLog.cacheRoot())
guard let entries = try? store.loadEntries(logURL: logURL), !entries.isEmpty else { return nil }
await OpenCodexUsageStore.refreshPricingIfNeeded(entries: entries, now: now)
guard let snapshot = try? store.loadSnapshot(
logURL: logURL,
now: now,
Expand Down
5 changes: 0 additions & 5 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -707,11 +707,6 @@ public struct CostUsageFetcher: Sendable {
}
}

private struct ModelsDevPricingTarget: Hashable, Sendable {
let providerID: String
let modelID: String
}

private struct UnknownPricingRefreshRequest: Sendable {
let targets: Set<ModelsDevPricingTarget>
let now: Date
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 = "aa57b010b3c0bee4"
static let value = "710f475c3d1cfb61"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Foundation

extension CostUsagePricing {
// Price a recorded billing route without interpreting a model's vendor namespace as its host.
// Catalog prices use independent token classes; OpenAI and application overrides retain historical input semantics.
// swiftlint:disable:next function_parameter_count
static func providerCostUSD(
providerID: String,
model: String,
inputTokens: Int,
cachedInputTokens: Int,
cacheWriteInputTokens: Int,
outputTokens: Int,
pricingDate: Date?,
catalog: ModelsDevCatalog,
customPricing: CostUsageCustomPricing) -> Double?
{
let targets = ModelsDevPricingTargetResolver.targets(providerID: providerID, modelID: model)
guard let target = targets.first else { return nil }
let customRates = customPricing.rates(providerID: providerID, model: model)
?? targets.lazy.compactMap { customPricing.rates(providerID: $0.providerID, model: $0.modelID) }.first
if let customRates {
let uncachedInput = max(0, max(0, inputTokens) - max(0, cachedInputTokens))
return CostUsageCustomPricing.costUSD(
rates: customRates,
inputTokens: max(0, uncachedInput - max(0, cacheWriteInputTokens)),
outputTokens: outputTokens,
cacheReadTokens: max(0, cachedInputTokens),
cacheWriteTokens: max(0, cacheWriteInputTokens))
}
// Retain OpenAI's historical rates, model aliases and long-context overrides only on its own route.
if target.providerID == self.codexModelsDevProviderID,
!target.modelID.contains("/")
{
return self.codexCostUSD(
model: target.modelID,
inputTokens: inputTokens,
cachedInputTokens: cachedInputTokens,
outputTokens: outputTokens,
cacheWriteInputTokens: cacheWriteInputTokens,
pricingDate: pricingDate,
modelsDevCatalog: catalog,
customPricing: .empty)
}
for target in targets {
guard let lookup = catalog.pricing(
providerID: target.providerID,
modelID: target.modelID,
exactModelID: true)
else { continue }
let rate = lookup.pricing
// A new provider must not inherit OpenAI's legacy assumption that unspecified cache rates
// equal ordinary input. Keep rows with consumed but unpriced token classes unknown.
guard cachedInputTokens <= 0 || rate.cacheReadInputCostPerToken != nil,
cacheWriteInputTokens <= 0 || rate.cacheCreationInputCostPerToken != nil
else { return nil }
let (inputWithCache, cacheOverflow) = max(0, inputTokens)
.addingReportingOverflow(max(0, cachedInputTokens))
let (inclusiveInput, writeOverflow) = inputWithCache
.addingReportingOverflow(max(0, cacheWriteInputTokens))
guard !cacheOverflow, !writeOverflow else { return nil }
let pricing = CodexPricing(
inputCostPerToken: rate.inputCostPerToken,
outputCostPerToken: rate.outputCostPerToken,
cacheReadInputCostPerToken: rate.cacheReadInputCostPerToken,
displayLabel: nil,
cacheWriteInputCostPerToken: rate.cacheCreationInputCostPerToken,
thresholdTokens: rate.thresholdTokens,
inputCostPerTokenAboveThreshold: rate.inputCostPerTokenAboveThreshold,
outputCostPerTokenAboveThreshold: rate.outputCostPerTokenAboveThreshold,
cacheReadInputCostPerTokenAboveThreshold: rate.cacheReadInputCostPerTokenAboveThreshold,
cacheWriteInputCostPerTokenAboveThreshold: rate.cacheCreationInputCostPerTokenAboveThreshold)
return self.codexCostUSD(
pricing: pricing,
inputTokens: inclusiveInput,
cachedInputTokens: cachedInputTokens,
cacheWriteInputTokens: cacheWriteInputTokens,
outputTokens: outputTokens)
}
return nil
}
}
12 changes: 2 additions & 10 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,16 +493,8 @@ enum CostUsagePricing {
self.codexModelsDevProviderIDs.contains(routeID)
else { return [] }

var providerIDs = [routeID]
switch routeID {
case "kimi-coding":
providerIDs.append("kimi-for-coding")
case "opencode-free":
providerIDs.append("opencode")
default:
break
}
var targets = providerIDs.map { ($0, modelID) }
var targets = ModelsDevPricingTargetResolver.targets(providerID: routeID, modelID: trimmed)
.map { ($0.providerID, $0.modelID) }
if routeID == self.codexModelsDevProviderID {
let normalized = self.normalizeCodexModel(modelID)
if normalized != modelID {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ actor CostUsageStore {
parserHash: CodexParserHash.value)
static let cacheGeneration = "sqlite:\(CostUsageStore.schemaVersion)"
static let compatiblePredecessorParserHashes: Set<String> = [
"aa57b010b3c0bee4", // Provider-aware pricing preserves native rows and scan checkpoints.
"aef0df6c73f8052c", // 0.60.1 rows and checkpoints survive routine rescan repairs.
"4969a789db679c93", // 0.58.0 native rows, checkpoints, and reports survive queue reordering.
"c4fa7db2cf54bc41", // Parser revisions reparse older native files while preserving stored rows and checkpoints.
Expand Down
19 changes: 13 additions & 6 deletions Sources/CodexBarCore/Vendored/CostUsage/ModelsDevPricing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@ struct ModelsDevCatalog: Codable, Equatable {
try container.encode(self.providers, forKey: ModelsDevAnyCodingKey(stringValue: "providers")!)
}

func pricing(providerID rawProviderID: String, modelID rawModelID: String) -> ModelsDevPricingLookup? {
func pricing(
providerID rawProviderID: String,
modelID rawModelID: String,
exactModelID: Bool = false) -> ModelsDevPricingLookup?
{
let providerID = ModelsDevProvider.normalizeProviderID(rawProviderID)
return self.providers[providerID]?.pricing(modelID: rawModelID)
return self.providers[providerID]?.pricing(modelID: rawModelID, exactModelID: exactModelID)
}

func isPlausibleRefresh() -> Bool {
Expand Down Expand Up @@ -165,8 +169,10 @@ struct ModelsDevProvider: Codable, Equatable {
raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

func pricing(modelID rawModelID: String) -> ModelsDevPricingLookup? {
let candidates = ModelsDevModelIDNormalizer.candidates(rawModelID)
func pricing(modelID rawModelID: String, exactModelID: Bool = false) -> ModelsDevPricingLookup? {
let candidates = exactModelID
? [ModelsDevModelIDNormalizer.normalize(rawModelID)]
: ModelsDevModelIDNormalizer.candidates(rawModelID)
for candidate in candidates {
if let model = self.models[candidate],
let pricing = model.pricing(providerID: self.id ?? self.mapKey ?? "", providerName: self.name)
Expand Down Expand Up @@ -678,14 +684,15 @@ enum ModelsDevPricingPipeline {
static func refreshForUnknownModelsIfNeeded(
providerID: String,
modelIDs: Set<String>,
exactModelIDs: Bool = false,
now: Date = Date(),
cacheRoot: URL? = nil,
client: ModelsDevClient = ModelsDevClient()) async -> ModelsDevUnknownModelRefreshOutcome
{
guard !modelIDs.isEmpty else { return .unavailable }
let load = ModelsDevCache.load(now: now, cacheRoot: cacheRoot)
let unknownModelIDs = modelIDs.filter {
load.artifact?.catalog.pricing(providerID: providerID, modelID: $0) == nil
load.artifact?.catalog.pricing(providerID: providerID, modelID: $0, exactModelID: exactModelIDs) == nil
}
guard !unknownModelIDs.isEmpty else { return .pricingAvailable }
if let fetchedAt = load.artifact?.fetchedAt,
Expand All @@ -704,7 +711,7 @@ enum ModelsDevPricingPipeline {

let refreshedCatalog = ModelsDevCache.load(now: now, cacheRoot: cacheRoot).artifact?.catalog
let pricingBecameAvailable = unknownModelIDs.contains {
refreshedCatalog?.pricing(providerID: providerID, modelID: $0) != nil
refreshedCatalog?.pricing(providerID: providerID, modelID: $0, exactModelID: exactModelIDs) != nil
}
return pricingBecameAvailable ? .pricingAvailable : .unavailable
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

struct ModelsDevPricingTarget: Hashable, Sendable {
let providerID: String
let modelID: String
}

enum ModelsDevPricingTargetResolver {
static func targets(providerID rawProviderID: String, modelID rawModelID: String) -> [ModelsDevPricingTarget] {
let providerID = self.normalizedProviderID(rawProviderID)
let modelID = rawModelID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !providerID.isEmpty, self.isValidModelID(modelID) else { return [] }

let resolvedModelID = self.modelID(modelID, for: providerID)
guard self.isValidModelID(resolvedModelID) else { return [] }

var providerIDs = [providerID]
switch providerID {
case "kimi-coding":
providerIDs.append("kimi-for-coding")
case "opencode-free":
providerIDs.append("opencode")
default:
break
}
return providerIDs.map { ModelsDevPricingTarget(providerID: $0, modelID: resolvedModelID) }
}

private static func normalizedProviderID(_ rawProviderID: String) -> String {
switch ModelsDevProvider.normalizeProviderID(rawProviderID) {
case "x-ai":
"xai"
default:
ModelsDevProvider.normalizeProviderID(rawProviderID)
}
}

private static func modelID(_ modelID: String, for providerID: String) -> String {
guard let slash = modelID.firstIndex(of: "/") else { return modelID }
let prefix = String(modelID[..<slash])
let remainder = String(modelID[modelID.index(after: slash)...])
guard !remainder.isEmpty else { return modelID }

if providerID == "openrouter" {
return self.normalizedProviderID(prefix) == "openrouter" ? remainder : modelID
}

return self.normalizedProviderID(prefix) == providerID ? remainder : modelID
}

private static func isValidModelID(_ modelID: String) -> Bool {
!modelID.isEmpty && !modelID.hasPrefix("/") && !modelID.hasSuffix("/")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ public enum OpenCodexRouteDispatcher {
}

public static func route(provider: String, modelName: String) -> OpenCodexRouteTarget {
let provider = provider.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let trimmedModel = modelName.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedModel.contains("/") {
// Only legacy openai transport labels delegate attribution to an explicit route prefix.
// A model such as openai/gpt-5.4 served by OpenRouter does not consume a Codex subscription.
if provider == "openai", trimmedModel.contains("/") {
let modelRoute = self.route(modelName: trimmedModel)
if modelRoute != .unknown {
return modelRoute
Expand Down
Loading
Loading