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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.77.1 — Unreleased

### Added

- Pi: retain valid token-only history from unsupported Pi/OMP backends without borrowing native-provider prices (#4605). Thanks @h3x89!

- Overview: optionally show every Claude adapter, managed Codex, and saved token account using cached usage, while retaining the six-provider selection limit (#4594). Thanks @pedrozimmermannsoares!
- Settings: edit saved token-account labels and credentials in place and reorder accounts without changing the active account (#4612). Thanks @lghsigma597!
### Fixed
Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBarCore/PiSessionCostScanner+Aggregation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ extension PiSessionCostScanner {
range: CostUsageScanner.CostUsageDayRange)
-> (codex: CostUsageDailyReport, claude: CostUsageDailyReport, pi: CostUsageDailyReport)?
{
// Provider-specific by design: Pi has native mirrors and a standalone Bedrock partition.
// Provider-specific by design: Pi has native mirrors and a standalone backend partition.
guard let codex = self.buildReport(provider: .codex, cache: cache, range: range),
let claude = self.buildReport(provider: .claude, cache: cache, range: range),
let bedrock = self.buildReport(provider: .pi, cache: cache, range: range)
let standalone = self.buildReport(provider: .pi, cache: cache, range: range)
else { return nil }
return (codex, claude, CostUsageDailyReport.merged([codex, claude, bedrock], calendar: range.calendar))
return (codex, claude, CostUsageDailyReport.merged([codex, claude, standalone], calendar: range.calendar))
}

static func buildReport(
Expand Down
120 changes: 61 additions & 59 deletions Sources/CodexBarCore/PiSessionCostScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ enum PiSessionCostScanner {
private struct AssistantIdentity {
let provider: UsageProvider
let modelName: String
let pricingProvider: UsageProvider?
}

private struct ModelsDevPricingContext {
Expand All @@ -102,7 +103,7 @@ enum PiSessionCostScanner {

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 = 4
private static let costFormulaVersion = 5
private static let maxLineBytes = 16 * 1024 * 1024
private static let sessionStartFilenameRegex = try? NSRegularExpression(
pattern: "^(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})-(\\d{3})Z_")
Expand Down Expand Up @@ -297,7 +298,7 @@ enum PiSessionCostScanner {
try checkCancellation?()
PiSessionCostCacheIO.save(cache: cache, cacheRoot: options.cacheRoot, calendar: range.calendar)
}
// Provider-specific by design: Pi aggregates native vendor mirrors and standalone Bedrock sessions.
// Provider-specific by design: Pi aggregates native vendor mirrors and standalone backend sessions.
let lastScanAt = cache.lastScanUnixMs > 0
? Date(timeIntervalSince1970: TimeInterval(cache.lastScanUnixMs) / 1000)
: nil
Expand Down Expand Up @@ -921,62 +922,54 @@ enum PiSessionCostScanner {
entry: object,
message: message,
fallback: currentModelContext)
guard let identity else {
let unsupported = if let explicit = self.extractProviderText(
entry: object,
message: message)
let date = self.timestampDate(entry: object, message: message)
if let identity, let date {
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(
from: date,
calendar: range.calendar)
guard CostUsageScanner.CostUsageDayRange.isInRange(
dayKey: dayKey, since: range.scanSinceKey, until: range.scanUntilKey)
else { return }
if let usage = self.extractUsage(
provider: identity.pricingProvider,
modelName: identity.modelName,
message: message,
pricingDate: date,
pricingContext: pricingContext)
{
self.mappedProvider(fromPiProvider: explicit) == nil
} else {
currentModelContext?.isUnsupportedBackend == true
}
guard unsupported else {
isComplete = false
add(
provider: identity.provider,
dayKey: dayKey,
modelName: identity.modelName,
usage: usage,
entryID: self.entryIdentifier(from: object))
return
}
if let date = self.timestampDate(entry: object, message: message) {
let day = CostUsageScanner.CostUsageDayRange.dayKey(
from: date,
calendar: range.calendar)
if CostUsageScanner.CostUsageDayRange.isInRange(
dayKey: day, since: range.scanSinceKey, until: range.scanUntilKey)
{
unsupportedAssistantDayKeys.insert(day)
}
} else {
hasUndatedUnsupportedAssistant = true
}
return
}
guard let date = self.timestampDate(entry: object, message: message) else {
// A recognized assistant row without a usable timestamp cannot be
// assigned to a day. Keep the scan incomplete so cache advancement
// never permanently hides its usage.
isComplete = false
return

// Incomplete unsupported rows affect only standalone history; native failures
// must prevent cache advancement even when their date or model is unknown.
let unsupported = if let identity {
identity.pricingProvider == nil
} else if let explicit = self.extractProviderText(entry: object, message: message) {
self.mappedProvider(fromPiProvider: explicit) == nil
} else {
currentModelContext?.isUnsupportedBackend == true
}
let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(
from: date,
calendar: range.calendar)
guard CostUsageScanner.CostUsageDayRange.isInRange(
dayKey: dayKey, since: range.scanSinceKey, until: range.scanUntilKey)
else { return }
guard let usage = self.extractUsage(
provider: identity.provider,
modelName: identity.modelName,
message: message,
pricingDate: date,
pricingContext: pricingContext)
else {
guard unsupported else {
isComplete = false
return
}
add(
provider: identity.provider,
dayKey: dayKey,
modelName: identity.modelName,
usage: usage,
entryID: self.entryIdentifier(from: object))
if let date {
let day = CostUsageScanner.CostUsageDayRange.dayKey(from: date, calendar: range.calendar)
if CostUsageScanner.CostUsageDayRange.isInRange(
dayKey: day, since: range.scanSinceKey, until: range.scanUntilKey)
{
unsupportedAssistantDayKeys.insert(day)
}
} else {
hasUndatedUnsupportedAssistant = true
}
}
})
// A scan can stop at the last complete newline while an active writer leaves a
Expand Down Expand Up @@ -1058,11 +1051,13 @@ enum PiSessionCostScanner {
}

private static func modelContext(from object: [String: Any]) -> PiModelContext? {
guard let providerText = object["provider"] as? String else { return nil }
guard let providerText = (object["provider"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
else { return nil }
let rawModelName = (object["modelId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard let provider = self.mappedProvider(fromPiProvider: providerText) else {
return PiModelContext(providerRawValue: "", modelName: "", isUnsupportedBackend: true)
return PiModelContext(
providerRawValue: providerText, modelName: rawModelName, isUnsupportedBackend: true)
}
let rawModelName = (object["modelId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard let modelName = self.normalizeModelName(rawModelName, provider: provider) else { return nil }
return PiModelContext(providerRawValue: provider.rawValue, modelName: modelName)
}
Expand All @@ -1076,17 +1071,22 @@ enum PiSessionCostScanner {
let explicitProvider = explicitProviderText.flatMap(self.mappedProvider(fromPiProvider:))
let explicitModelText = self.extractModelText(entry: entry, message: message)

if explicitProviderText != nil, explicitProvider == nil {
return nil
if let backend = explicitProviderText ??
(fallback?.isUnsupportedBackend == true ? fallback?.providerRawValue : nil),
self.mappedProvider(fromPiProvider: backend) == nil
{
let model = explicitModelText ?? (fallback?.providerRawValue == backend ? fallback?.modelName : nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize unsupported backend IDs before matching context

When an unsupported backend is repeated with different casing between a model_change record and an assistant message (for example, OpenRouter followed by openrouter) and the message omits its model, this exact comparison refuses to reuse the otherwise matching model context. The row is then treated as incomplete and its valid tokens are discarded, even though provider classification elsewhere is case-insensitive; normalize both backend IDs before comparing them.

Useful? React with 👍 / 👎.

guard !backend.isEmpty, let model, !model.isEmpty else { return nil }
return AssistantIdentity(provider: .pi, modelName: "\(backend)/\(model)", pricingProvider: nil)
}

guard let provider = explicitProvider ?? fallback.flatMap({ UsageProvider(rawValue: $0.providerRawValue) }),
let rawModel = explicitModelText ?? fallback.flatMap({
$0.providerRawValue == provider.rawValue ? $0.modelName : nil
!$0.isUnsupportedBackend && $0.providerRawValue == provider.rawValue ? $0.modelName : nil
}),
let model = self.normalizeModelName(rawModel, provider: provider)
else { return nil }
return AssistantIdentity(provider: provider, modelName: model)
return AssistantIdentity(provider: provider, modelName: model, pricingProvider: provider)
}

private static func extractProviderText(entry: [String: Any], message: [String: Any]) -> String? {
Expand Down Expand Up @@ -1149,7 +1149,7 @@ enum PiSessionCostScanner {
}

private static func extractUsage(
provider: UsageProvider,
provider: UsageProvider?,
modelName: String,
message: [String: Any],
pricingDate: Date? = nil,
Expand Down Expand Up @@ -1234,13 +1234,15 @@ enum PiSessionCostScanner {
}

private static func computedCostUSD(
provider: UsageProvider,
provider: UsageProvider?,
modelName: String,
usage: PiPackedUsage,
cacheWrite1h: Int,
pricingDate: Date? = nil,
pricingContext: ModelsDevPricingContext? = nil) -> Double?
{
// Unknown backends contribute standalone tokens without borrowing another provider's prices.
guard let provider else { return nil }
// Provider-specific by design: Pi delegates disjoint token costs to the shared tariff calculators.
switch provider {
case .codex:
Expand Down
65 changes: 57 additions & 8 deletions Tests/CodexBarTests/PiSessionCostReliabilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ struct PiSessionCostReliabilityTests {
}

@Test(arguments: [false, true])
func `unsupported backends stay incomplete across cached reads and recover after a growing rewrite`(
func `unsupported backends retain unpriced tokens across cached reads and growing rewrites`(
mixed: Bool) throws
{
let env = try CostUsageTestEnvironment()
Expand All @@ -142,8 +142,14 @@ struct PiSessionCostReliabilityTests {
if mixed { rows.append(self.row(env, day, id: "supported", input: 10)) }
let path = try env.writePiSessionFile(relativePath: "coverage.jsonl", contents: env.jsonl(rows))
let first = try self.scan(day, options: options)
#expect(!first.isComplete)
#expect(first.report.summary?.totalTokens == (mixed ? 10 : nil))
#expect(first.isComplete)
#expect(first.report.summary?.totalTokens == (mixed ? 40 : 30))
#expect(first.report.data.first?.coverageCounts.unpriced == 1)
let unpriced = try #require(first.report.data.first?.modelBreakdowns?.first {
$0.modelName == "openrouter/gpt-5.4"
})
#expect(unpriced.totalTokens == 30)
#expect(unpriced.costUSD == nil)
#expect(first.lastScanAt == day)
#expect(PiSessionCostScanner.loadCachedDailyReportResult(
provider: .pi,
Expand All @@ -152,7 +158,7 @@ struct PiSessionCostReliabilityTests {
now: day,
cacheRoot: env.cacheRoot,
options: options,
allowEstablishedEmpty: true) == nil)
allowEstablishedEmpty: true)?.report.summary?.totalTokens == (mixed ? 40 : 30))
let native = try self.scan(day, options: options, provider: .codex)
#expect(native.isComplete)
#expect(native.report.summary?.totalTokens == (mixed ? 10 : nil))
Expand All @@ -165,21 +171,64 @@ struct PiSessionCostReliabilityTests {
#expect(restored.report.summary?.totalTokens == (mixed ? 47 : 37))
}

@Test(arguments: [false, true])
func `malformed unsupported usage remains incomplete without poisoning native history`(undated: Bool) throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
let options = try self.options(env)
let input: Any = undated ? 10 : "invalid"
var invalid = self.row(env, day, id: "invalid", input: input, provider: "openrouter")
if undated { invalid.removeValue(forKey: "timestamp") }
_ = try env.writePiSessionFile(relativePath: "invalid-backend.jsonl", contents: env.jsonl([
self.row(env, day, id: "valid", input: 10), invalid,
]))
#expect(try !self.scan(day, options: options).isComplete)
let native = try self.scan(day, options: options, provider: .codex)
#expect(native.isComplete)
#expect(native.report.summary?.totalTokens == 10)
}

@Test
func `unsupported backend context cannot supply a native provider model`() throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
let options = try self.options(env)
_ = try env.writePiSessionFile(relativePath: "context-collision.jsonl", contents: env.jsonl([
["type": "model_change", "provider": "claude", "modelId": "fixture-model"],
[
"type": "message", "timestamp": env.isoString(for: day),
"message": ["role": "assistant", "provider": "anthropic", "usage": ["input": 10, "output": 1]],
],
]))
let native = try self.scan(day, options: options, provider: .claude)
#expect(!native.isComplete)
#expect(native.report.summary?.totalTokens == nil)
}

@Test
func `unsupported model change context survives an append without poisoning native partitions`() throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
let options = try self.options(env)
let change: [String: Any] = ["type": "model_change", "provider": "google", "modelId": "gemini-2.5-pro"]
let change: [String: Any] = [
"type": "model_change", "provider": " google ", "modelId": "gemini-2.5-pro",
]
let message: [String: Any] = [
"type": "message", "timestamp": env.isoString(for: day),
"message": ["role": "assistant", "usage": ["input": 10, "output": 1]],
]
let file = try env.writePiSessionFile(relativePath: "context.jsonl", contents: env.jsonl([change, message]))
#expect(try !self.scan(day, options: options).isComplete)
let file = try env.writePiSessionFile(relativePath: "context.jsonl", contents: env.jsonl([change]))
_ = try self.scan(day, options: options)
try self.append(env.jsonl([message]), to: file)
#expect(try self.scan(day, options: options).isComplete)
#expect(try self.scan(day, options: options).report.summary?.totalTokens == 11)
#expect(try self.scan(day, options: options).report.data.first?.modelsUsed == ["google/gemini-2.5-pro"])
try self.append(env.jsonl([message]), to: file)
#expect(try !self.scan(day.addingTimeInterval(1), options: options).isComplete)
#expect(try self.scan(day.addingTimeInterval(1), options: options).isComplete)
#expect(try self.scan(day, options: options).report.summary?.totalTokens == 22)
#expect(try self.scan(day, options: options, provider: .claude).isComplete)
}

Expand Down
7 changes: 7 additions & 0 deletions Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3456,6 +3456,13 @@ struct ProviderArchitectureGatekeeperTests {
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."),
AllowedProviderConstruct(
path: "Sources/CodexBarCore/PiSessionCostScanner.swift",
anchor: "guard !backend.isEmpty, let model, !model.isEmpty else { return nil }",
expectedProviderIDs: ["pi"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["pi@0"],
reason: "Unsupported backends retain tokens only in standalone Pi history without native-provider pricing."),
AllowedProviderConstruct(
path: "Sources/CodexBarCore/PiSessionCostScanner.swift",
anchor: "guard provider == .codex || provider == .claude || provider == .pi else {",
Expand Down
Loading
Loading