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 @@ -18,6 +18,10 @@ on Columbus Labs QuotaKit releases and product-facing changes.
- Synced upstream localization and provider menu copy through `daba50847`
(`8fb8c742..daba50847`), completing Traditional Chinese coverage while
preserving QuotaKit public naming in localized values.
- Synced upstream CrossModel provider support through `0a18391e`
(`daba50847..0a18391e`), adding CrossModel API spend parsing, CLI
diagnostics, widget/menu display, provider docs, and tests while preserving
QuotaKit public docs and config ownership.

### Fixed

Expand Down
8 changes: 8 additions & 0 deletions CodexBarMobile/CodexBarMobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
A644FDE234CC676EC59CC891 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8B2EE4A4A283389BC42245 /* ContentView.swift */; };
A72D9E150D7E9982CBF02747 /* SyncFreshnessChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42B55286F096DB85587A72E /* SyncFreshnessChip.swift */; };
A7A9772DEE62BDA0E16FC91E /* AccountIdentityMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E315BCDE160D8764FE14F2 /* AccountIdentityMergeTests.swift */; };
A82C861A5C7DDDB86F0CBD21 /* CrossModelUsageCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026226E3F5DB8E1DD9E87257 /* CrossModelUsageCard.swift */; };
A8ECDA27E9F213AFA6A13FAB /* CodexBarSync.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D34FB83FA26B8508F5A3D9FB /* CodexBarSync.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A910B3424F6BF1DCE7543E2E /* CodexBarMobileApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3A52976034BA3F0CA44327 /* CodexBarMobileApp.swift */; };
A9305459F82B07AAE8ADC1A2 /* ProviderUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAFBA243E25814E22BAAA6B /* ProviderUsageView.swift */; };
Expand Down Expand Up @@ -190,6 +191,7 @@
D5E7E85EEE6528E04FDA9446 /* PreviewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C92153BD74B2B08AE61B76E2 /* PreviewData.swift */; };
DCE0242A2B08E8D23B2CCAC1 /* ClaudeAdminUsageCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5F72C23944E23883A57CA1 /* ClaudeAdminUsageCard.swift */; };
E031A1BFF5739D8E33C2FB1A /* CodexBarMobileWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DDDD1BE193D78E1F8BA7E3C7 /* CodexBarMobileWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E0606A65601F8C29F072AC5D /* V036Snapshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1895BA2767B5F6563BB4A0 /* V036Snapshots.swift */; };
E1662D1E480AF1620D0C745F /* QuotaKitWidgetPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C8CEC5B8B76D1E5AAE3229 /* QuotaKitWidgetPresentation.swift */; };
E408D96A71E1052A78D540CC /* SyncErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B11DCC99D3362E7225B2D1 /* SyncErrorTests.swift */; };
E79487590ED842AE33B360D7 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9578A435334E38DB06D035CB /* OnboardingView.swift */; };
Expand Down Expand Up @@ -310,6 +312,7 @@

/* Begin PBXFileReference section */
01AA3F7A048F7301BC92A1A2 /* QuotaZoneNotificationParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotaZoneNotificationParser.swift; sourceTree = "<group>"; };
026226E3F5DB8E1DD9E87257 /* CrossModelUsageCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossModelUsageCard.swift; sourceTree = "<group>"; };
0316E90523AC431875826A46 /* QKSurfaceCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QKSurfaceCard.swift; sourceTree = "<group>"; };
04473E13F793B1EDF3EF4280 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
051D9B8E971CCC037766870E /* CloudKitRuntimeGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRuntimeGate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -410,6 +413,7 @@
7A260F62AFF50B05ED7E6401 /* ShareCardRenderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareCardRenderTests.swift; sourceTree = "<group>"; };
7AAB8310784E24CC51831768 /* ProviderAccountLinkage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderAccountLinkage.swift; sourceTree = "<group>"; };
7AABCE82169AC4EF72708082 /* CWLAggregateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CWLAggregateTests.swift; sourceTree = "<group>"; };
7C1895BA2767B5F6563BB4A0 /* V036Snapshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V036Snapshots.swift; sourceTree = "<group>"; };
7F02C90907BEFBD7030162A3 /* QuotaKitWidgetSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotaKitWidgetSnapshot.swift; sourceTree = "<group>"; };
81B87DC9EB24AAEFE036F9F7 /* CodexBarMobilePushExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = CodexBarMobilePushExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
82B8A4D1FFD04AA72E89022F /* MobileDisplayPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileDisplayPreferences.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -887,6 +891,7 @@
A9F340DA45364A878F1BB616 /* V027Snapshots.swift */,
BCE6CB7DCBC0976E4414C63D /* V029Snapshots.swift */,
E29AF435AB2A4FE691117704 /* V030Snapshots.swift */,
7C1895BA2767B5F6563BB4A0 /* V036Snapshots.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -941,6 +946,7 @@
74B0A596604D1C8CA137C073 /* CostMetricCard.swift */,
AEB7FFB253C0CC0CA9876CCB /* CostShareCardView.swift */,
8515047932479670A9749119 /* CostShareSheet.swift */,
026226E3F5DB8E1DD9E87257 /* CrossModelUsageCard.swift */,
158C8DC0415A1D743CD3D63A /* CyberShareCardView.swift */,
AA293C8F7A388B3FE79C2856 /* DeepgramUsageCard.swift */,
477F069536CCC8340DF0B198 /* DeepSeekUsageCard.swift */,
Expand Down Expand Up @@ -1279,6 +1285,7 @@
44179B7669F19C60FFA54FCD /* CostShareService.swift in Sources */,
7921A012D44F975F8AFC63DE /* CostShareSheet.swift in Sources */,
B2CFA52F43CD4D449586C3B2 /* CostTab.swift in Sources */,
A82C861A5C7DDDB86F0CBD21 /* CrossModelUsageCard.swift in Sources */,
CF5946FFB17517210EEC1EFF /* CyberShareCardView.swift in Sources */,
9360E35B526CE57E9C5ED2D0 /* DeepSeekUsageCard.swift in Sources */,
78A8D4ECC85D519DB04C6B07 /* DeepgramUsageCard.swift in Sources */,
Expand Down Expand Up @@ -1399,6 +1406,7 @@
BB98D3ECAFF6D381B6D775FE /* V027Snapshots.swift in Sources */,
00BDE086E0C208CE385602B8 /* V029Snapshots.swift in Sources */,
724CDFD257C13ABA5020E7C9 /* V030Snapshots.swift in Sources */,
E0606A65601F8C29F072AC5D /* V036Snapshots.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ enum ProviderColorPalette {
(["venice"], RawColor(red: 0.2, green: 0.6, blue: 1)),
(["commandcode"], RawColor(red: 71 / 255, green: 85 / 255, blue: 105 / 255)),
(["stepfun"], RawColor(red: 0.13, green: 0.59, blue: 0.95)),
(["crossmodel"], RawColor(red: 88 / 255, green: 86 / 255, blue: 214 / 255)),
(["bedrock"], RawColor(red: 1, green: 0.6, blue: 0)),
(["grok"], RawColor(red: 26 / 255, green: 26 / 255, blue: 26 / 255)),
(["groq", "groqcloud", "groqapi"], RawColor(red: 245 / 255, green: 104 / 255, blue: 68 / 255)),
Expand Down
1 change: 1 addition & 0 deletions CodexBarMobile/CodexBarMobile/Models/SnapshotCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ struct SnapshotCache: Sendable {
&& provider.rateWindows.isEmpty
&& provider.costSummary == nil
&& provider.budget == nil
&& provider.crossModelUsage == nil
&& !provider.isError
&& provider.statusMessage == nil
}
Expand Down
9 changes: 8 additions & 1 deletion CodexBarMobile/CodexBarMobile/Storage/SwiftDataBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ enum SwiftDataBridge {
let costSummaryData = provider.costSummary.flatMap { try? encoder.encode($0) }
let budgetData = provider.budget.flatMap { try? encoder.encode($0) }
let perplexityCreditsData = provider.perplexityCredits.flatMap { try? encoder.encode($0) }
let crossModelUsageData = provider.crossModelUsage.flatMap { try? encoder.encode($0) }

let model: ProviderSnapshotModel
if let existing = try context.fetch(descriptor).first {
Expand All @@ -173,6 +174,7 @@ enum SwiftDataBridge {
existing.costSummaryData = costSummaryData
existing.budgetData = budgetData
existing.perplexityCreditsData = perplexityCreditsData
existing.crossModelUsageData = crossModelUsageData
existing.device = device
model = existing
} else {
Expand All @@ -189,6 +191,7 @@ enum SwiftDataBridge {
costSummaryData: costSummaryData,
budgetData: budgetData,
perplexityCreditsData: perplexityCreditsData,
crossModelUsageData: crossModelUsageData,
device: device)
context.insert(created)
model = created
Expand Down Expand Up @@ -306,6 +309,9 @@ enum SwiftDataBridge {
let perplexityCredits = row.perplexityCreditsData.flatMap {
try? decoder.decode(SyncPerplexityCreditSummary.self, from: $0)
}
let crossModelUsage = row.crossModelUsageData.flatMap {
try? decoder.decode(SyncCrossModelUsage.self, from: $0)
}

// Reconstruct utilization history by grouping the flat entry rows
// back into series. Sort by series name for stability, and by
Expand Down Expand Up @@ -344,7 +350,8 @@ enum SwiftDataBridge {
budget: budget,
rateWindows: rateWindows,
utilizationHistory: seriesList.isEmpty ? nil : seriesList,
perplexityCredits: perplexityCredits))
perplexityCredits: perplexityCredits,
crossModelUsage: crossModelUsage))
}

// Skip devices that have no provider rows — they're placeholders from
Expand Down
5 changes: 5 additions & 0 deletions CodexBarMobile/CodexBarMobile/Storage/SwiftDataSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ final class ProviderSnapshotModel {
/// pushing structured credit data (Mac 0.20.3+); nil for every other
/// provider and for legacy Mac payloads.
var perplexityCreditsData: Data?
/// JSON-encoded `SyncCrossModelUsage` — opaque blob, decoded on read.
/// Populated only for `providerID == "crossmodel"`.
var crossModelUsageData: Data?

@Relationship(deleteRule: .cascade, inverse: \UtilizationEntryModel.provider)
var utilizationEntries: [UtilizationEntryModel] = []
Expand All @@ -100,6 +103,7 @@ final class ProviderSnapshotModel {
costSummaryData: Data? = nil,
budgetData: Data? = nil,
perplexityCreditsData: Data? = nil,
crossModelUsageData: Data? = nil,
device: DeviceRecord? = nil)
{
self.compositeKey = Self.makeCompositeKey(
Expand All @@ -118,6 +122,7 @@ final class ProviderSnapshotModel {
self.costSummaryData = costSummaryData
self.budgetData = budgetData
self.perplexityCreditsData = perplexityCreditsData
self.crossModelUsageData = crossModelUsageData
self.device = device
}

Expand Down
115 changes: 115 additions & 0 deletions CodexBarMobile/CodexBarMobile/Views/CrossModelUsageCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import CodexBarSync
import SwiftUI

struct CrossModelUsageCard: View {
let usage: SyncCrossModelUsage
let tintColor: Color

var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(verbatim: "CrossModel balance")
.font(.headline)
Spacer(minLength: 8)
Text(Self.currencyString(self.usage.balance, currency: self.usage.currency))
.font(.headline.monospacedDigit())
.foregroundStyle(self.tintColor)
.lineLimit(1)
.minimumScaleFactor(0.8)
}

if self.usage.uncollected != 0 {
HStack {
Text(verbatim: "Uncollected")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Text(Self.currencyString(self.usage.uncollected, currency: self.usage.currency))
.font(.caption.bold().monospacedDigit())
}
}

VStack(spacing: 8) {
if let daily = self.usage.daily {
self.windowRow(label: "Today", window: daily)
}
if let weekly = self.usage.weekly {
self.windowRow(label: "Week", window: weekly)
}
if let monthly = self.usage.monthly {
self.windowRow(label: "Month", window: monthly)
}
}
}
.padding(16)
.qkCardBackground(cornerRadius: 14)
.accessibilityElement(children: .contain)
.accessibilityIdentifier("crossmodel-usage-card")
}

private func windowRow(label: String, window: SyncCrossModelUsageWindow) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(verbatim: label)
.font(.caption)
.foregroundStyle(.secondary)
.frame(width: 52, alignment: .leading)
VStack(alignment: .trailing, spacing: 2) {
Text(Self.currencyString(window.cost, currency: self.usage.currency))
.font(.subheadline.monospacedDigit())
Text(verbatim: "\(Self.formatInt(window.totalTokens)) tok / \(Self.formatInt(window.requestCount)) req")
.font(.caption2.monospacedDigit())
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
}

private static func currencyString(_ value: Double, currency: String) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currency
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
return formatter.string(from: NSNumber(value: value))
?? "\(currency) \(String(format: "%.2f", value))"
}

private static func formatInt(_ value: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.usesGroupingSeparator = true
return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
}
}

#Preview {
CrossModelUsageCard(
usage: SyncCrossModelUsage(
currency: "USD",
balance: 8.06,
uncollected: 1.24,
daily: SyncCrossModelUsageWindow(
cost: 0.42,
promptTokens: 8100,
completionTokens: 4367,
totalTokens: 12467,
requestCount: 42,
successCount: 40),
weekly: SyncCrossModelUsageWindow(
cost: 2.14,
promptTokens: 98000,
completionTokens: 21000,
totalTokens: 119_000,
requestCount: 280,
successCount: 276),
monthly: SyncCrossModelUsageWindow(
cost: 5.37,
promptTokens: 410_000,
completionTokens: 119_000,
totalTokens: 529_000,
requestCount: 3166,
successCount: 3112),
updatedAt: Date()),
tintColor: .blue)
.padding()
}
10 changes: 10 additions & 0 deletions CodexBarMobile/CodexBarMobile/Views/ProviderDetailSections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum ProviderDetailSection: Identifiable {
case azureOpenAI(SyncAzureOpenAIInfo)
case alibabaTokenPlan(SyncAlibabaTokenPlan)
case deepSeek(SyncDeepSeekUsage)
case crossModel(SyncCrossModelUsage)
case claudeAdmin(SyncClaudeAdminUsage)
case claudeExtra(SyncClaudeExtraUsage)
case openCodeGoZen(SyncOpenCodeGoZenBalance)
Expand Down Expand Up @@ -61,6 +62,8 @@ enum ProviderDetailSection: Identifiable {
"alibabatokenplan"
case .deepSeek:
"deepseek"
case .crossModel:
"crossmodel"
case .claudeAdmin:
"claude-admin"
case .claudeExtra:
Expand Down Expand Up @@ -142,6 +145,9 @@ enum ProviderDetailSectionDispatcher {
if provider.providerID == "deepseek", let value = provider.deepSeekUsage {
sections.append(.deepSeek(value))
}
if provider.providerID == "crossmodel", let value = provider.crossModelUsage {
sections.append(.crossModel(value))
}
if provider.providerID == "claude", let value = provider.claudeAdminUsage {
sections.append(.claudeAdmin(value))
}
Expand Down Expand Up @@ -173,6 +179,8 @@ enum ProviderDetailSectionDispatcher {
true
case "moonshot" where provider.moonshotBalance != nil:
true
case "crossmodel" where provider.crossModelUsage != nil:
true
default:
false
}
Expand Down Expand Up @@ -232,6 +240,8 @@ struct ProviderDetailSectionView: View {
AlibabaTokenPlanCard(plan: plan, tintColor: self.tintColor)
case let .deepSeek(usage):
DeepSeekUsageCard(usage: usage, tintColor: self.tintColor)
case let .crossModel(usage):
CrossModelUsageCard(usage: usage, tintColor: self.tintColor)
case let .claudeAdmin(usage):
ClaudeAdminUsageCard(usage: usage, tintColor: self.tintColor)
case let .claudeExtra(extraUsage):
Expand Down
3 changes: 2 additions & 1 deletion CodexBarMobile/CodexBarMobile/iCloud/CloudSyncReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,8 @@ final class CloudSyncReader: @unchecked Sendable {
budget: Self.latestNonNil(entries, \.budget),
rateWindows: base.rateWindows,
utilizationHistory: mergedUtilization,
perplexityCredits: Self.latestNonNil(entries, \.perplexityCredits))
perplexityCredits: Self.latestNonNil(entries, \.perplexityCredits),
crossModelUsage: Self.latestNonNil(entries, \.crossModelUsage))
}

/// Sums cost data from multiple devices.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ struct ProviderColorPaletteTests {
("commandcode", 71 / 255, 85 / 255, 105 / 255),
("qoder", 16 / 255, 185 / 255, 129 / 255),
("stepfun", 0.13, 0.59, 0.95),
("crossmodel", 88 / 255, 86 / 255, 214 / 255),
("bedrock", 1, 0.6, 0),
("grok", 26 / 255, 26 / 255, 26 / 255),
("groq", 245 / 255, 104 / 255, 68 / 255),
Expand Down Expand Up @@ -136,6 +137,7 @@ private let knownDistinctProviders = [
"warp", "openrouter", "elevenlabs", "windsurf", "perplexity", "mimo",
"doubao", "sakana", "abacus", "mistral", "deepseek", "codebuff", "crof", "venice",
"commandcode", "qoder", "stepfun", "bedrock", "grok", "groq", "llmproxy", "litellm", "deepgram",
"crossmodel",
]

private func expectDistinctColors(
Expand Down
Loading
Loading