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
65 changes: 39 additions & 26 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,13 @@ public struct CostUsageFetcher: Sendable {
}
}

private struct UnknownPricingRefreshRequest: Sendable {
private struct ModelsDevPricingTarget: Hashable, Sendable {
let providerID: String
let modelIDs: Set<String>
let modelID: String
}

private struct UnknownPricingRefreshRequest: Sendable {
let targets: Set<ModelsDevPricingTarget>
let now: Date
let cacheRoot: URL?
let client: ModelsDevClient
Expand All @@ -632,22 +636,24 @@ public struct CostUsageFetcher: Sendable {
client: ModelsDevClient) -> UnknownPricingRefreshRequest?
{
guard provider == .codex || provider == .claude else { return nil }
let unknownModelIDs = Set(daily.data.flatMap { entry in
entry.modelBreakdowns?.compactMap { breakdown -> String? in
guard breakdown.costUSD == nil else { return nil }
if provider == .codex,
CostUsagePricing.isCodexUnattributedModel(breakdown.modelName)
{
return nil
var targets = Set<ModelsDevPricingTarget>()
for entry in daily.data {
for breakdown in entry.modelBreakdowns ?? [] {
guard breakdown.costUSD == nil else { continue }
if provider == .codex {
guard !CostUsagePricing.isCodexUnattributedModel(breakdown.modelName) else { continue }
for target in CostUsagePricing.codexModelsDevPricingTargets(for: breakdown.modelName) {
targets.insert(ModelsDevPricingTarget(providerID: target.providerID, modelID: target.modelID))
}
} else {
targets.insert(ModelsDevPricingTarget(providerID: "anthropic", modelID: breakdown.modelName))
}
return breakdown.modelName
} ?? []
})
guard !unknownModelIDs.isEmpty else { return nil }
}
}
guard !targets.isEmpty else { return nil }

return UnknownPricingRefreshRequest(
providerID: provider == .codex ? "openai" : "anthropic",
modelIDs: unknownModelIDs,
targets: targets,
now: now,
cacheRoot: cacheRoot,
client: client)
Expand All @@ -657,23 +663,30 @@ public struct CostUsageFetcher: Sendable {
_ request: UnknownPricingRefreshRequest,
inBackground: Bool) async -> Bool
{
if inBackground {
Task.detached(priority: .utility) {
_ = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: request.providerID,
modelIDs: request.modelIDs,
func refreshTargets() async -> Bool {
let targetsByProvider = Dictionary(grouping: request.targets, by: \.providerID)
for providerID in targetsByProvider.keys.sorted() {
let modelIDs = Set(targetsByProvider[providerID, default: []].map(\.modelID))
let outcome = await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: providerID,
modelIDs: modelIDs,
now: request.now,
cacheRoot: request.cacheRoot,
client: request.client)
if outcome == .pricingAvailable {
return true
}
}
return false
}

if inBackground {
Task.detached(priority: .utility) {
_ = await refreshTargets()
}
return false
}
return await ModelsDevPricingPipeline.refreshForUnknownModelsIfNeeded(
providerID: request.providerID,
modelIDs: request.modelIDs,
now: request.now,
cacheRoot: request.cacheRoot,
client: request.client) == .pricingAvailable
return await refreshTargets()
}

static func loadCachedCodexTokenSnapshot(
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 = "e2899fcb0234e5c1"
static let value = "3c1ec2b780582978"
}
4 changes: 2 additions & 2 deletions Sources/CodexBarCore/PiSessionCostScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ enum PiSessionCostScanner {

private static let costScale = 1_000_000_000.0
/// Bump for Pi-only cost formula changes not represented by the parser or pricing fingerprints.
private static let costFormulaVersion = 1
private static let costFormulaVersion = 2
private static let maxLineBytes = 16 * 1024 * 1024
private static let maxSafeRoundedInt = Double(Int.max) - 1
private static let sessionStartFilenameRegex = try? NSRegularExpression(
Expand Down Expand Up @@ -256,7 +256,7 @@ enum PiSessionCostScanner {
modelsDevArtifact: modelsDevArtifact,
formulaVersion: Self.costFormulaVersion,
parserHash: CodexParserHash.value,
modelsDevProviderIDs: ["anthropic", "openai"]))
modelsDevProviderIDs: CostUsagePricing.codexModelsDevProviderIDs.union(["anthropic"])))
}

private static func requestedWindowExpandsCache(
Expand Down
85 changes: 77 additions & 8 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsagePricing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,62 @@ enum CostUsagePricing {
]

private static let codexModelsDevProviderID = "openai"
/// Provider IDs emitted by Codex-compatible clients that have matching entries in models.dev.
///
/// The route prefix is part of the model identity for local usage estimates. Keep both the
/// client-facing aliases and their models.dev provider IDs here so pricing-cache fingerprints
/// invalidate when any supported route's rates change.
static let codexModelsDevProviderIDs: Set<String> = [
"deepseek",
"kimi-coding",
"kimi-for-coding",
"openai",
"opencode",
"opencode-free",
"opencode-go",
]
private static let claudeModelsDevProviderID = "anthropic"

/// Returns the provider/model identities that may price a Codex model. Keep this mapping
/// shared by direct lookup and unknown-price refresh so a newly downloaded catalog is checked
/// under the same identity that was used to resolve the model.
static func codexModelsDevPricingTargets(for rawModel: String) -> [(providerID: String, modelID: String)] {
let trimmed = rawModel.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return [] }
if let slash = trimmed.firstIndex(of: "/") {
let routeID = String(trimmed[..<slash]).lowercased()
let modelID = String(trimmed[trimmed.index(after: slash)...])
guard !routeID.isEmpty, !modelID.isEmpty,
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) }
if routeID == self.codexModelsDevProviderID {
let normalized = self.normalizeCodexModel(modelID)
if normalized != modelID {
targets.append((self.codexModelsDevProviderID, normalized))
}
}
return targets
}

let normalized = self.normalizeCodexModel(trimmed)
var targets = [(self.codexModelsDevProviderID, trimmed)]
if normalized != trimmed {
targets.append((self.codexModelsDevProviderID, normalized))
}
return targets
}

static func normalizeCodexModel(_ raw: String) -> String {
var trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("openai/") {
Expand Down Expand Up @@ -541,18 +595,12 @@ enum CostUsagePricing {
{
let key = self.normalizeCodexModel(model)
guard key != self.codexUnattributedModel else { return nil }
let modelsDevLookup = self.modelsDevLookup(
providerID: self.codexModelsDevProviderID,
let modelsDevLookup = self.codexModelsDevLookup(
model: model,
catalog: modelsDevCatalog,
cacheRoot: modelsDevCacheRoot)
?? (model == key ? nil : self.modelsDevLookup(
providerID: self.codexModelsDevProviderID,
model: key,
catalog: modelsDevCatalog,
cacheRoot: modelsDevCacheRoot))
if let lookup = modelsDevLookup {
let bundled = self.codex[key]
let bundled = lookup.pricing.providerID == self.codexModelsDevProviderID ? self.codex[key] : nil
// A missing catalog context block means models.dev has no long-context opinion, so use
// the bundled tuple. Once the block exists, preserve its omissions and normal fallback
// semantics instead of filling individual fields from a different pricing source.
Expand Down Expand Up @@ -590,6 +638,27 @@ enum CostUsagePricing {
return pricing
}

/// Resolves the provider-qualified model IDs written by Codex-compatible clients without
/// falling back to OpenAI pricing for an unrelated route. Unqualified model IDs retain the
/// historical OpenAI behavior, including the gpt-5.6 alias lookup.
private static func codexModelsDevLookup(
model rawModel: String,
catalog: ModelsDevCatalog?,
cacheRoot: URL?) -> ModelsDevPricingLookup?
{
for target in self.codexModelsDevPricingTargets(for: rawModel) {
if let lookup = self.modelsDevLookup(
providerID: target.providerID,
model: target.modelID,
catalog: catalog,
cacheRoot: cacheRoot)
{
return lookup
}
}
return nil
}

static func codexPriorityCostUSD(
model: String,
inputTokens: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ enum CostUsagePricingKey {
modelsDevArtifact: ModelsDevCacheArtifact?,
formulaVersion: Int,
parserHash: String? = nil,
modelsDevProviderIDs: Set<String> = ["openai"]) -> String
modelsDevProviderIDs: Set<String> = CostUsagePricing.codexModelsDevProviderIDs) -> String
{
var parts = [
"costFormulaVersion=\(formulaVersion)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2081,7 +2081,7 @@ enum CostUsageScanner {

/// Bump when the report pricing formula changes. Rates are resolved when reports are read;
/// this fingerprint only invalidates downstream presentation caches such as Workspaces snapshots.
private static let codexCostFormulaVersion = 2
private static let codexCostFormulaVersion = 3

static func codexPricingKey(modelsDevArtifact: ModelsDevCacheArtifact?) -> String {
CostUsagePricingKey.codex(
Expand Down
51 changes: 51 additions & 0 deletions Tests/CodexBarTests/CostUsageFetcherUnknownModelPricingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,48 @@ struct CostUsageFetcherUnknownModelPricingTests {
#expect(abs((breakdown.costUSD ?? 0) - 0.00028) < 0.0000001)
}

@Test
func `fetcher reprices a provider qualified model after an on demand catalog refresh`() async throws {
let fixture = try UnknownModelPricingFixture()
defer { fixture.environment.cleanup() }
let qualifiedTurnContext: [String: Any] = [
"type": "turn_context",
"timestamp": fixture.environment.isoString(for: fixture.day),
"payload": ["model": "opencode-go/deepseek-v4-flash"],
]
let qualifiedTokenCount: [String: Any] = [
"type": "event_msg",
"timestamp": fixture.environment.isoString(for: fixture.day.addingTimeInterval(1)),
"payload": [
"type": "token_count",
"info": [
"total_token_usage": [
"input_tokens": 100,
"cached_input_tokens": 20,
"output_tokens": 10,
],
],
],
]
_ = try fixture.environment.writeCodexSessionFile(
day: fixture.day,
filename: "unknown-qualified-model.jsonl",
contents: fixture.environment.jsonl([qualifiedTurnContext, qualifiedTokenCount]))

let snapshot = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: fixture.day,
refreshPricingInBackground: false,
scannerOptions: fixture.options,
modelsDevClient: ModelsDevClient(transport: CostUsageFetcherModelsDevTransport(
data: fixture.refreshedCatalog)))

let breakdown = try #require(snapshot.daily
.flatMap { $0.modelBreakdowns ?? [] }
.first { $0.modelName == "opencode-go/deepseek-v4-flash" })
#expect(abs((breakdown.costUSD ?? 0) - 0.0000084) < 0.0000001)
}

@Test
func `pricing retry preserves disabled pi session merging`() async throws {
let fixture = try UnknownModelPricingFixture()
Expand Down Expand Up @@ -222,6 +264,15 @@ private struct UnknownModelPricingFixture {
"id": "openai",
"models": { "gpt-new": { "id": "gpt-new", "cost": { "input": 2, "output": 8 } } }
},
"opencode-go": {
"id": "opencode-go",
"models": {
"deepseek-v4-flash": {
"id": "deepseek-v4-flash",
"cost": { "input": 0.07, "output": 0.14 }
}
}
},
"anthropic": {
"id": "anthropic",
"models": { "claude-new": { "id": "claude-new", "cost": { "input": 3, "output": 15 } } }
Expand Down
Loading