From 7b4a7783c97b400f6b41d593ca583f621e932e76 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Mon, 27 Jul 2026 21:42:56 -0700 Subject: [PATCH 01/51] Add cumulative annual spend --- README.md | 2 +- .../PreferencesSpendDashboardPane.swift | 120 +++++++++++++++- Sources/CodexBar/ProviderRegistry.swift | 2 +- .../Resources/ar.lproj/Localizable.strings | 8 ++ .../Resources/ca.lproj/Localizable.strings | 8 ++ .../Resources/de.lproj/Localizable.strings | 8 ++ .../Resources/en.lproj/Localizable.strings | 8 ++ .../Resources/es.lproj/Localizable.strings | 8 ++ .../Resources/fa.lproj/Localizable.strings | 8 ++ .../Resources/fr.lproj/Localizable.strings | 8 ++ .../Resources/gl.lproj/Localizable.strings | 8 ++ .../Resources/id.lproj/Localizable.strings | 8 ++ .../Resources/it.lproj/Localizable.strings | 8 ++ .../Resources/ja.lproj/Localizable.strings | 8 ++ .../Resources/ko.lproj/Localizable.strings | 8 ++ .../Resources/nl.lproj/Localizable.strings | 8 ++ .../Resources/pl.lproj/Localizable.strings | 8 ++ .../Resources/pt-BR.lproj/Localizable.strings | 8 ++ .../Resources/ru.lproj/Localizable.strings | 8 ++ .../Resources/sv.lproj/Localizable.strings | 8 ++ .../Resources/th.lproj/Localizable.strings | 8 ++ .../Resources/tr.lproj/Localizable.strings | 8 ++ .../Resources/uk.lproj/Localizable.strings | 8 ++ .../Resources/vi.lproj/Localizable.strings | 8 ++ .../zh-Hans.lproj/Localizable.strings | 8 ++ .../zh-Hant.lproj/Localizable.strings | 8 ++ Sources/CodexBar/SettingsStore+Defaults.swift | 8 ++ Sources/CodexBar/SettingsStore.swift | 1 + .../CodexBar/SpendDashboardController.swift | 131 ++++++++++++++++-- Sources/CodexBar/SpendDashboardModel.swift | 2 +- .../CodexBar/UsageStore+TokenAccounts.swift | 2 +- Sources/CodexBar/UsageStore+TokenCost.swift | 35 +++-- Sources/CodexBar/UsageStore.swift | 2 +- .../LocalizationLanguageCatalogTests.swift | 1 + .../SpendDashboardControllerTests.swift | 73 +++++++++- .../SpendDashboardModelTests.swift | 44 +++--- .../SpendDashboardTrackedSourceTests.swift | 51 +++++++ .../UsageStoreCoverageTests.swift | 26 ++++ docs/codex.md | 7 +- docs/providers.md | 8 +- 40 files changed, 642 insertions(+), 57 deletions(-) create mode 100644 Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift diff --git a/README.md b/README.md index e007123aec..72133b5e22 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ show an incident indicator. - Optional Codex web dashboard enrichments (code review remaining, usage breakdown, credits history). - Inline spend and usage charts for API-backed providers such as OpenAI, Claude Admin API, OpenRouter, LiteLLM, z.ai, MiniMax, Mistral, and AWS Bedrock. - Configurable cost-usage scans for Codex + Claude, plus reused chart UI for supported provider histories. -- A persistent Settings → Usage & Spend view for local 7/30-day estimates, grouped by native currency and limited to providers that expose cost history. +- A persistent Settings → Usage & Spend view for local 7/30/365-day estimates, grouped by native currency, with every tracked subscription/key visible and unsupported cost sources excluded from totals. - Provider status polling with incident badges in the menu and icon overlay. - Merge Icons mode to combine providers into one status item + switcher. - Display controls for provider icons, labels, bars, reset-time style, and highest-usage auto-selection. diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 8cd6d85ba8..74ba41f286 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -8,6 +8,7 @@ func spendDashboardDayRangeText(_ days: Int) -> String { switch days { case 7: template = L("7d") case 30: template = L("30d") + case 365: template = L("365d") default: return codexBarLocalizedInteger(days) } return template.replacingOccurrences( @@ -15,6 +16,10 @@ func spendDashboardDayRangeText(_ days: Int) -> String { with: codexBarLocalizedInteger(days)) } +func spendDashboardRequiredHistoryDays(selectedDays: Int, configuredDays: Int) -> Int { + max(1, min(365, max(selectedDays, configuredDays))) +} + func spendDashboardRankText(_ rank: Int) -> String { "#\(codexBarLocalizedInteger(rank))" } @@ -44,6 +49,17 @@ func codexCostCatchUpProgressText(_ activity: CodexCostCatchUpActivity) -> Strin return L("Loading…") } +func spendDashboardTrackedSourceStatusText(_ source: SpendDashboardTrackedSource) -> String { + if source.contributesCostHistory { + return source.state == .connected + ? L("Cost history connected") + : L("Cost history pending") + } + return source.state == .connected + ? L("Usage connected · not in cost total") + : L("Configured · not in cost total") +} + enum SpendDashboardModelHistoryPresentation: Equatable { case unavailable case empty @@ -83,6 +99,7 @@ struct SpendDashboardPane: View { self.header self.codexCostCatchUpPanel self.content + self.trackedAccess self.provenance self.shareAction } @@ -91,6 +108,7 @@ struct SpendDashboardPane: View { .background(FocusResigningBackground()) .onAppear { self.isVisible = true + self.applySelectedHistoryCoverage() self.controller.refreshDateWindow() self.controller.update(configuration: self.configuration) if !self.controller.isRefreshing { @@ -110,6 +128,7 @@ struct SpendDashboardPane: View { } .onDisappear { self.isVisible = false + self.settings.setSpendDashboardHistoryDaysOverride(nil) self.controller.stop() } .onReceive(NotificationCenter.default.publisher(for: .NSCalendarDayChanged)) { _ in @@ -140,10 +159,11 @@ struct SpendDashboardPane: View { Picker(L("Time range"), selection: self.daysBinding) { Text(spendDashboardDayRangeText(7)).tag(7) Text(spendDashboardDayRangeText(30)).tag(30) + Text(spendDashboardDayRangeText(365)).tag(365) } .labelsHidden() .pickerStyle(.segmented) - .frame(width: 116) + .frame(width: 174) Button { self.controller.refresh() @@ -339,6 +359,49 @@ struct SpendDashboardPane: View { } } + @ViewBuilder + private var trackedAccess: some View { + let sources = self.configuration.trackedSources + if !sources.isEmpty { + SpendDashboardPanel { + VStack(alignment: .leading, spacing: 14) { + HStack(alignment: .firstTextBaseline) { + VStack(alignment: .leading, spacing: 3) { + Text(L("Tracked access")) + .font(.headline) + Text(self.trackedAccessDescription) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Text(codexBarLocalizedInteger(sources.count)) + .font(.title3.monospacedDigit().weight(.semibold)) + .foregroundStyle(.secondary) + .accessibilityLabel( + "\(codexBarLocalizedInteger(sources.count)) \(L("tracked sources"))") + } + + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10), + ], + alignment: .leading, + spacing: 10) + { + ForEach(sources) { source in + SpendTrackedSourceRow(source: source) + } + } + } + } + } + } + + private var trackedAccessDescription: String { + L("Every configured subscription or key stays visible. Only compatible sources enter cost totals.") + } + private var shareAction: some View { HStack { Spacer() @@ -388,7 +451,60 @@ struct SpendDashboardPane: View { private var daysBinding: Binding { Binding( get: { self.controller.selectedDays }, - set: { self.controller.selectDays($0) }) + set: { + self.controller.selectDays($0) + self.applySelectedHistoryCoverage() + self.controller.refreshDateWindow() + }) + } + + private func applySelectedHistoryCoverage() { + let requiredDays = spendDashboardRequiredHistoryDays( + selectedDays: self.controller.selectedDays, + configuredDays: self.settings.costUsageHistoryDays) + self.settings.setSpendDashboardHistoryDaysOverride( + requiredDays == self.settings.costUsageHistoryDays ? nil : requiredDays) + } +} + +private struct SpendTrackedSourceRow: View { + let source: SpendDashboardTrackedSource + + var body: some View { + HStack(spacing: 10) { + SpendProviderIcon(provider: self.source.provider) + + VStack(alignment: .leading, spacing: 2) { + Text(self.source.providerName) + .font(.subheadline.weight(.medium)) + .lineLimit(1) + if let accountName = self.source.accountName { + Text(accountName) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + Text(spendDashboardTrackedSourceStatusText(self.source)) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + } + + Spacer(minLength: 4) + + Circle() + .fill(self.source.state == .connected ? Color.green : Color.secondary.opacity(0.45)) + .frame(width: 7, height: 7) + .accessibilityHidden(true) + } + .padding(10) + .frame(maxWidth: .infinity, minHeight: 58, alignment: .leading) + .background(.background.opacity(0.72), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 10, style: .continuous) + .strokeBorder(Color(nsColor: .separatorColor).opacity(0.25)) + } + .accessibilityElement(children: .combine) } } diff --git a/Sources/CodexBar/ProviderRegistry.swift b/Sources/CodexBar/ProviderRegistry.swift index 769cb752ea..ca9add3ad3 100644 --- a/Sources/CodexBar/ProviderRegistry.swift +++ b/Sources/CodexBar/ProviderRegistry.swift @@ -85,7 +85,7 @@ struct ProviderRegistry { } } }, - costUsageHistoryDays: settings.costUsageHistoryDays, + costUsageHistoryDays: settings.effectiveCostUsageHistoryDays, persistsCLISessions: true, persistentCLISessionIdleWindow: Self.persistentCLISessionIdleWindow( refreshInterval: Self.nominalRefreshInterval(for: settings.refreshFrequency))) diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 0ec7c51d61..1bba0bbc34 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " providers"; "(System)" = "(النظام)"; "30d" = "30 يومًا"; +"365d" = "365 يومًا"; "7d" = "7 أيام"; "A managed Codex login is already running. Wait for it to finish before adding " = "تسجيل دخول Codex المدار يعمل بالفعل. انتظر حتى ينتهي قبل إضافة "; "API key" = "مفتاح API"; @@ -1294,6 +1295,13 @@ "Model breakdown unavailable" = "تفصيل الإنفاق حسب النموذج غير متاح"; "Local estimated history" = "السجل التقديري المحلي"; "Coverage" = "التغطية"; +"Tracked access" = "الوصول المُتتبَّع"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "يبقى كل اشتراك أو مفتاح مُهيأ مرئيًا. المصادر المتوافقة فقط تدخل في إجماليات التكلفة."; +"tracked sources" = "المصادر المُتتبَّعة"; +"Cost history connected" = "سجل التكلفة متصل"; +"Cost history pending" = "سجل التكلفة قيد الانتظار"; +"Usage connected · not in cost total" = "الاستخدام متصل · غير مشمول في إجمالي التكلفة"; +"Configured · not in cost total" = "مُهيأ · غير مشمول في إجمالي التكلفة"; "Estimated spend" = "الإنفاق التقديري"; "Tracked tokens" = "الرموز المتتبعة"; "Subscriptions" = "الاشتراكات"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 946ac6c48b..d985e73749 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " proveïdors"; "(System)" = "(Sistema)"; "30d" = "30 d"; +"365d" = "365 d"; "7d" = "7 d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Ja hi ha un inici de sessió gestionat de Codex en curs. Espereu que acabi abans d'afegir "; "API key" = "Clau d'API"; @@ -1293,6 +1294,13 @@ "Model breakdown unavailable" = "Desglossament per model no disponible"; "Local estimated history" = "Historial local estimat"; "Coverage" = "Cobertura"; +"Tracked access" = "Accés fet un seguiment"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Totes les subscripcions o claus configurades resten visibles. Només les fonts compatibles entren als totals de cost."; +"tracked sources" = "fonts amb seguiment"; +"Cost history connected" = "Historial de costos connectat"; +"Cost history pending" = "Historial de costos pendent"; +"Usage connected · not in cost total" = "Ús connectat · no inclòs al total de cost"; +"Configured · not in cost total" = "Configurat · no inclòs al total de cost"; "Estimated spend" = "Despesa estimada"; "Tracked tokens" = "Tokens registrats"; "Subscriptions" = "Subscripcions"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 3596829dbe..0ac365c0e2 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = "Anbieter"; "(System)" = "(System)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Eine verwaltete Codex-Anmeldung läuft bereits. Warten Sie, bis der Vorgang abgeschlossen ist, bevor Sie ihn hinzufügen"; "API key" = "API-Schlüssel"; @@ -1291,6 +1292,13 @@ "Model breakdown unavailable" = "Modellaufschlüsselung nicht verfügbar"; "Local estimated history" = "Lokaler Schätzverlauf"; "Coverage" = "Abdeckung"; +"Tracked access" = "Verfolgter Zugriff"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Jedes konfigurierte Abonnement oder jeder Schlüssel bleibt sichtbar. Nur kompatible Quellen fließen in die Kostensummen ein."; +"tracked sources" = "verfolgte Quellen"; +"Cost history connected" = "Kostenverlauf verbunden"; +"Cost history pending" = "Kostenverlauf ausstehend"; +"Usage connected · not in cost total" = "Nutzung verbunden · nicht in der Kostensumme"; +"Configured · not in cost total" = "Konfiguriert · nicht in der Kostensumme"; "Estimated spend" = "Geschätzte Ausgaben"; "Tracked tokens" = "Erfasste Token"; "Subscriptions" = "Abonnements"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index bfeeef5f74..98af518556 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -7,6 +7,7 @@ " providers" = " providers"; "(System)" = "(System)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "A managed Codex login is already running. Wait for it to finish before adding "; "API key" = "API key"; @@ -1272,6 +1273,13 @@ "Model breakdown unavailable" = "Model breakdown unavailable"; "Local estimated history" = "Local estimated history"; "Coverage" = "Coverage"; +"Tracked access" = "Tracked access"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Every configured subscription or key stays visible. Only compatible sources enter cost totals."; +"tracked sources" = "tracked sources"; +"Cost history connected" = "Cost history connected"; +"Cost history pending" = "Cost history pending"; +"Usage connected · not in cost total" = "Usage connected · not in cost total"; +"Configured · not in cost total" = "Configured · not in cost total"; "Estimated spend" = "Estimated spend"; "Tracked tokens" = "Tracked tokens"; "Subscriptions" = "Subscriptions"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index b0b4f5cfc8..fb4e26c8af 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " proveedores"; "(System)" = "(Sistema)"; "30d" = "30 d"; +"365d" = "365 d"; "7d" = "7 d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Ya hay un inicio de sesión gestionado de Codex en curso. Espera a que termine antes de añadir "; "API key" = "Clave de API"; @@ -1289,6 +1290,13 @@ "Model breakdown unavailable" = "Desglose por modelo no disponible"; "Local estimated history" = "Historial local estimado"; "Coverage" = "Cobertura"; +"Tracked access" = "Acceso rastreado"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Cada suscripción o clave configurada permanece visible. Solo las fuentes compatibles entran en los totales de costo."; +"tracked sources" = "fuentes rastreadas"; +"Cost history connected" = "Historial de costos conectado"; +"Cost history pending" = "Historial de costos pendiente"; +"Usage connected · not in cost total" = "Uso conectado · no incluido en el total de costo"; +"Configured · not in cost total" = "Configurado · no incluido en el total de costo"; "Estimated spend" = "Gasto estimado"; "Tracked tokens" = "Tokens registrados"; "Subscriptions" = "Suscripciones"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index 1f42a99280..f45f054697 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " providers"; "(System)" = "(سیستم)"; "30d" = "30 روز"; +"365d" = "365 روز"; "7d" = "7 روز"; "A managed Codex login is already running. Wait for it to finish before adding " = "یک ورود Codex مدیریت شده هم اکنون در حال اجرا است. صبر کنید تا تمام شود و بعد را اضافه کنید"; "API key" = "کلید API"; @@ -1294,6 +1295,13 @@ "Model breakdown unavailable" = "تفکیک مدل در دسترس نیست"; "Local estimated history" = "تاریخچه برآورد محلی"; "Coverage" = "پوشش"; +"Tracked access" = "دسترسی ردیابی‌شده"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "هر اشتراک یا کلید پیکربندی‌شده قابل مشاهده می‌ماند. فقط منابع سازگار در مجموع هزینه‌ها لحاظ می‌شوند."; +"tracked sources" = "منابع ردیابی‌شده"; +"Cost history connected" = "تاریخچه هزینه متصل است"; +"Cost history pending" = "تاریخچه هزینه در انتظار"; +"Usage connected · not in cost total" = "مصرف متصل · در مجموع هزینه لحاظ نشده"; +"Configured · not in cost total" = "پیکربندی‌شده · در مجموع هزینه لحاظ نشده"; "Estimated spend" = "برآورد هزینه"; "Tracked tokens" = "توکن‌های پیگیری‌شده"; "Subscriptions" = "اشتراک‌ها"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index 77a6858d55..cfd723a388 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " fournisseurs"; "(System)" = "(System)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Une connexion Codex gérée est déjà en cours d'exécution. Attendez qu'il soit terminé avant d'ajouter"; "API key" = "Clé API"; @@ -1290,6 +1291,13 @@ "Model breakdown unavailable" = "Répartition par modèle indisponible"; "Local estimated history" = "Historique local estimé"; "Coverage" = "Couverture"; +"Tracked access" = "Accès suivi"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Chaque abonnement ou clé configuré reste visible. Seules les sources compatibles entrent dans les totaux de coût."; +"tracked sources" = "sources suivies"; +"Cost history connected" = "Historique des coûts connecté"; +"Cost history pending" = "Historique des coûts en attente"; +"Usage connected · not in cost total" = "Utilisation connectée · non incluse dans le total des coûts"; +"Configured · not in cost total" = "Configuré · non inclus dans le total des coûts"; "Estimated spend" = "Dépenses estimées"; "Tracked tokens" = "Jetons suivis"; "Subscriptions" = "Abonnements"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 2415388220..a9d1e21482 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " provedores"; "(System)" = "(Sistema)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Xa hai un inicio de sesión xestionado de Codex en curso. Agarda a que remate antes de engadir "; "API key" = "Chave de API"; @@ -1290,6 +1291,13 @@ "Model breakdown unavailable" = "Desglose por modelo non dispoñible"; "Local estimated history" = "Historial local estimado"; "Coverage" = "Cobertura"; +"Tracked access" = "Acceso rastrexado"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Cada subscrición ou clave configurada permanece visible. Só as fontes compatibles entran nos totais de custo."; +"tracked sources" = "fontes rastrexadas"; +"Cost history connected" = "Historial de custos conectado"; +"Cost history pending" = "Historial de custos pendente"; +"Usage connected · not in cost total" = "Uso conectado · non incluído no total de custo"; +"Configured · not in cost total" = "Configurado · non incluído no total de custo"; "Estimated spend" = "Gasto estimado"; "Tracked tokens" = "Tokens rexistrados"; "Subscriptions" = "Subscricións"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index e28bf9a183..3333d6945e 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " penyedia"; "(System)" = "(Sistem)"; "30d" = "30 hari"; +"365d" = "365 hari"; "7d" = "7 hari"; "A managed Codex login is already running. Wait for it to finish before adding " = "Login Codex terkelola sudah berjalan. Tunggu hingga selesai sebelum menambahkan "; "API key" = "Kunci API"; @@ -1294,6 +1295,13 @@ "Model breakdown unavailable" = "Rincian per model tidak tersedia"; "Local estimated history" = "Riwayat perkiraan lokal"; "Coverage" = "Cakupan"; +"Tracked access" = "Akses terlacak"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Setiap langganan atau kunci yang dikonfigurasi tetap terlihat. Hanya sumber yang kompatibel yang masuk ke total biaya."; +"tracked sources" = "sumber terlacak"; +"Cost history connected" = "Riwayat biaya terhubung"; +"Cost history pending" = "Riwayat biaya tertunda"; +"Usage connected · not in cost total" = "Penggunaan terhubung · tidak termasuk dalam total biaya"; +"Configured · not in cost total" = "Dikonfigurasi · tidak termasuk dalam total biaya"; "Estimated spend" = "Perkiraan pengeluaran"; "Tracked tokens" = "Token yang dilacak"; "Subscriptions" = "Langganan"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index db6f30be30..d8912a3346 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " provider"; "(System)" = "(Sistema)"; "30d" = "30 g"; +"365d" = "365 g"; "7d" = "7 g"; "A managed Codex login is already running. Wait for it to finish before adding " = "È già in corso un accesso gestito a Codex. Attendi che termini prima di aggiungere "; "API key" = "Chiave API"; @@ -1294,6 +1295,13 @@ "Model breakdown unavailable" = "Ripartizione per modello non disponibile"; "Local estimated history" = "Cronologia locale stimata"; "Coverage" = "Copertura"; +"Tracked access" = "Accesso tracciato"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Ogni abbonamento o chiave configurata rimane visibile. Solo le fonti compatibili entrano nei totali dei costi."; +"tracked sources" = "fonti tracciate"; +"Cost history connected" = "Cronologia costi connessa"; +"Cost history pending" = "Cronologia costi in sospeso"; +"Usage connected · not in cost total" = "Utilizzo connesso · non incluso nel totale dei costi"; +"Configured · not in cost total" = "Configurato · non incluso nel totale dei costi"; "Estimated spend" = "Spesa stimata"; "Tracked tokens" = "Token tracciati"; "Subscriptions" = "Abbonamenti"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index d1fa466c22..5af851a9c2 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " 件のプロバイダ"; "(System)" = "(システム)"; "30d" = "30日"; +"365d" = "365日"; "7d" = "7日"; "A managed Codex login is already running. Wait for it to finish before adding " = "管理対象の Codex ログインがすでに実行中です。完了を待ってから追加してください "; "API key" = "API キー"; @@ -1291,6 +1292,13 @@ "Model breakdown unavailable" = "モデル別の内訳を取得できません"; "Local estimated history" = "ローカル推定履歴"; "Coverage" = "対象範囲"; +"Tracked access" = "追跡中のアクセス"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "設定済みのサブスクリプションやキーはすべて表示されたままになります。互換性のあるソースのみがコスト合計に含まれます。"; +"tracked sources" = "追跡中のソース"; +"Cost history connected" = "コスト履歴が接続済み"; +"Cost history pending" = "コスト履歴が保留中"; +"Usage connected · not in cost total" = "使用状況接続済み · コスト合計に含まれません"; +"Configured · not in cost total" = "設定済み · コスト合計に含まれません"; "Estimated spend" = "推定支出"; "Tracked tokens" = "追跡対象トークン"; "Subscriptions" = "サブスクリプション"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index e671b4264f..909287efc9 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " 공급자"; "(System)" = "(시스템)"; "30d" = "30일"; +"365d" = "365일"; "7d" = "7일"; "A managed Codex login is already running. Wait for it to finish before adding " = "관리되는 Codex 로그인이 이미 실행 중입니다. 추가하기 전에 완료될 때까지 기다리세요. "; "API key" = "API 키"; @@ -1258,6 +1259,13 @@ "Model breakdown unavailable" = "모델별 내역을 사용할 수 없습니다"; "Local estimated history" = "로컬 예상 내역"; "Coverage" = "포함 범위"; +"Tracked access" = "추적된 접근"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "구성된 모든 구독 또는 키가 계속 표시됩니다. 호환되는 소스만 비용 합계에 포함됩니다."; +"tracked sources" = "추적된 소스"; +"Cost history connected" = "비용 기록 연결됨"; +"Cost history pending" = "비용 기록 대기 중"; +"Usage connected · not in cost total" = "사용량 연결됨 · 비용 합계에 포함되지 않음"; +"Configured · not in cost total" = "구성됨 · 비용 합계에 포함되지 않음"; "Estimated spend" = "예상 지출"; "Tracked tokens" = "추적된 토큰"; "Subscriptions" = "구독"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index 15eedb420b..452e654364 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " providers"; "(System)" = "(Systeem)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Er is al een beheerde Codex-aanmelding actief. Wacht tot het klaar is voordat je het toevoegt"; "API key" = "API-sleutel"; @@ -1290,6 +1291,13 @@ "Model breakdown unavailable" = "Uitsplitsing per model niet beschikbaar"; "Local estimated history" = "Lokaal geschatte geschiedenis"; "Coverage" = "Dekking"; +"Tracked access" = "Bijgehouden toegang"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Elk geconfigureerd abonnement of elke sleutel blijft zichtbaar. Alleen compatibele bronnen tellen mee in de kostentotalen."; +"tracked sources" = "bijgehouden bronnen"; +"Cost history connected" = "Kostengeschiedenis verbonden"; +"Cost history pending" = "Kostengeschiedenis in behandeling"; +"Usage connected · not in cost total" = "Gebruik verbonden · niet in het kostentotaal"; +"Configured · not in cost total" = "Geconfigureerd · niet in het kostentotaal"; "Estimated spend" = "Geschatte uitgaven"; "Tracked tokens" = "Bijgehouden tokens"; "Subscriptions" = "Abonnementen"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 2349aa33c1..82448a6ea3 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " providers"; "(System)" = "(System)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Trwa już zarządzane logowanie Codex. Poczekaj na jego zakończenie, zanim dodasz "; "API key" = "Klucz API"; @@ -1294,6 +1295,13 @@ "Model breakdown unavailable" = "Podział według modeli jest niedostępny"; "Local estimated history" = "Lokalna historia szacunkowa"; "Coverage" = "Pokrycie"; +"Tracked access" = "Śledzony dostęp"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Każda skonfigurowana subskrypcja lub klucz pozostaje widoczna. Tylko kompatybilne źródła wchodzą do sum kosztów."; +"tracked sources" = "śledzone źródła"; +"Cost history connected" = "Historia kosztów połączona"; +"Cost history pending" = "Historia kosztów oczekująca"; +"Usage connected · not in cost total" = "Użycie połączone · nie wliczane do sumy kosztów"; +"Configured · not in cost total" = "Skonfigurowane · nie wliczane do sumy kosztów"; "Estimated spend" = "Szacowane wydatki"; "Tracked tokens" = "Śledzone tokeny"; "Subscriptions" = "Subskrypcje"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 337ab1b28d..ab251a2701 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " provedores"; "(System)" = "(Sistema)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Um login gerenciado do Codex já está em andamento. Aguarde terminar antes de adicionar "; "API key" = "Chave de API"; @@ -1291,6 +1292,13 @@ "Model breakdown unavailable" = "Detalhamento por modelo indisponível"; "Local estimated history" = "Histórico local estimado"; "Coverage" = "Cobertura"; +"Tracked access" = "Acesso rastreado"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Cada assinatura ou chave configurada permanece visível. Apenas fontes compatíveis entram nos totais de custo."; +"tracked sources" = "fontes rastreadas"; +"Cost history connected" = "Histórico de custos conectado"; +"Cost history pending" = "Histórico de custos pendente"; +"Usage connected · not in cost total" = "Uso conectado · não incluído no total de custo"; +"Configured · not in cost total" = "Configurado · não incluído no total de custo"; "Estimated spend" = "Gastos estimados"; "Tracked tokens" = "Tokens acompanhados"; "Subscriptions" = "Assinaturas"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index 6a84cd0304..e67dfe61ea 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " провайдеров"; "(System)" = "(Система)"; "30d" = "30 дн."; +"365d" = "365 дн."; "7d" = "7 дн."; "A managed Codex login is already running. Wait for it to finish before adding " = "Управляемый вход Codex уже активен. Подождите, пока он завершится, прежде чем добавлять "; "API key" = "API-ключ"; @@ -1292,6 +1293,13 @@ "Model breakdown unavailable" = "Разбивка по моделям недоступна"; "Local estimated history" = "Локальная история оценок"; "Coverage" = "Охват"; +"Tracked access" = "Отслеживаемый доступ"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Каждая настроенная подписка или ключ остаются видимыми. Только совместимые источники входят в итоговые суммы затрат."; +"tracked sources" = "отслеживаемые источники"; +"Cost history connected" = "История затрат подключена"; +"Cost history pending" = "История затрат ожидается"; +"Usage connected · not in cost total" = "Использование подключено · не входит в сумму затрат"; +"Configured · not in cost total" = "Настроено · не входит в сумму затрат"; "Estimated spend" = "Предполагаемые расходы"; "Tracked tokens" = "Отслеживаемые токены"; "Subscriptions" = "Подписки"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 7f53f36e29..fa1008e3f7 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " leverantörer"; "(System)" = "(System)"; "30d" = "30 d"; +"365d" = "365 d"; "7d" = "7 d"; "A managed Codex login is already running. Wait for it to finish before adding " = "En hanterad Codex-inloggning körs redan. Vänta tills den är klar innan du lägger till "; "API key" = "API-nyckel"; @@ -1289,6 +1290,13 @@ "Model breakdown unavailable" = "Modellfördelning ej tillgänglig"; "Local estimated history" = "Lokal uppskattad historik"; "Coverage" = "Täckning"; +"Tracked access" = "Spårad åtkomst"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Varje konfigurerad prenumeration eller nyckel förblir synlig. Endast kompatibla källor ingår i kostnadssummorna."; +"tracked sources" = "spårade källor"; +"Cost history connected" = "Kostnadshistorik ansluten"; +"Cost history pending" = "Kostnadshistorik väntar"; +"Usage connected · not in cost total" = "Användning ansluten · ingår inte i kostnadssumman"; +"Configured · not in cost total" = "Konfigurerad · ingår inte i kostnadssumman"; "Estimated spend" = "Uppskattade utgifter"; "Tracked tokens" = "Spårade token"; "Subscriptions" = "Abonnemang"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index dc1e488923..e08c60a8b7 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = "ผู้ให้บริการ "; "(System)" = "(ระบบ)"; "30d" = "30 วัน"; +"365d" = "365 วัน"; "7d" = "7 วัน"; "A managed Codex login is already running. Wait for it to finish before adding " = "การเข้าสู่ระบบ Codex ที่มีการจัดการกําลังทํางานอยู่แล้ว รอให้เสร็จก่อนที่จะเพิ่ม "; "API key" = "ปุ่ม API"; @@ -1294,6 +1295,13 @@ "Model breakdown unavailable" = "ไม่มีรายละเอียดแยกตามโมเดล"; "Local estimated history" = "ประวัติโดยประมาณในเครื่อง"; "Coverage" = "ความครอบคลุม"; +"Tracked access" = "การเข้าถึงที่ติดตาม"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "การสมัครสมาชิกหรือคีย์ที่กำหนดค่าไว้ทุกรายการยังคงมองเห็นได้ เฉพาะแหล่งที่เข้ากันได้เท่านั้นที่นับรวมในยอดรวมต้นทุน"; +"tracked sources" = "แหล่งที่ติดตาม"; +"Cost history connected" = "เชื่อมต่อประวัติต้นทุนแล้ว"; +"Cost history pending" = "ประวัติต้นทุนรอดำเนินการ"; +"Usage connected · not in cost total" = "เชื่อมต่อการใช้งานแล้ว · ไม่รวมในยอดรวมต้นทุน"; +"Configured · not in cost total" = "กำหนดค่าแล้ว · ไม่รวมในยอดรวมต้นทุน"; "Estimated spend" = "ค่าใช้จ่ายโดยประมาณ"; "Tracked tokens" = "โทเค็นที่ติดตาม"; "Subscriptions" = "การสมัครสมาชิก"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 67b77d3674..8b8687b2a1 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -30,6 +30,7 @@ " providers" = " sağlayıcı"; "(System)" = "(Sistem)"; "30d" = "30 gün"; +"365d" = "365 gün"; "7d" = "7 gün"; "A managed Codex login is already running. Wait for it to finish before adding " = "Yönetilen bir Codex girişi zaten çalışıyor. Eklemeden önce bitmesini bekleyin "; "API key" = "API anahtarı"; @@ -1292,6 +1293,13 @@ "Model breakdown unavailable" = "Model dökümü kullanılamıyor"; "Local estimated history" = "Yerel tahmini geçmiş"; "Coverage" = "Kapsam"; +"Tracked access" = "İzlenen erişim"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Yapılandırılan her abonelik veya anahtar görünür kalır. Yalnızca uyumlu kaynaklar maliyet toplamlarına girer."; +"tracked sources" = "izlenen kaynaklar"; +"Cost history connected" = "Maliyet geçmişi bağlandı"; +"Cost history pending" = "Maliyet geçmişi beklemede"; +"Usage connected · not in cost total" = "Kullanım bağlandı · maliyet toplamına dahil değil"; +"Configured · not in cost total" = "Yapılandırıldı · maliyet toplamına dahil değil"; "Estimated spend" = "Tahmini harcama"; "Tracked tokens" = "İzlenen tokenlar"; "Subscriptions" = "Abonelikler"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 8a786fbe76..c019fb2ae0 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = "провайдерів"; "(System)" = "(Система)"; "30d" = "30д"; +"365d" = "365д"; "7d" = "7д"; "A managed Codex login is already running. Wait for it to finish before adding " = "Керований вхід до Codex вже запущено. Перш ніж додавати, зачекайте, поки він закінчиться"; "API key" = "Ключ API"; @@ -1290,6 +1291,13 @@ "Model breakdown unavailable" = "Розподіл за моделями недоступний"; "Local estimated history" = "Локальна історія оцінок"; "Coverage" = "Охоплення"; +"Tracked access" = "Відстежуваний доступ"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Кожна налаштована підписка або ключ залишаються видимими. Лише сумісні джерела входять до підсумкових сум витрат."; +"tracked sources" = "відстежувані джерела"; +"Cost history connected" = "Історію витрат підключено"; +"Cost history pending" = "Історія витрат очікується"; +"Usage connected · not in cost total" = "Використання підключено · не входить до суми витрат"; +"Configured · not in cost total" = "Налаштовано · не входить до суми витрат"; "Estimated spend" = "Орієнтовні витрати"; "Tracked tokens" = "Відстежувані токени"; "Subscriptions" = "Підписки"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index acb06e5148..cb4cfdc0e6 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = "nhà cung cấp"; "(System)" = "(Hệ thống)"; "30d" = "30d"; +"365d" = "365d"; "7d" = "7d"; "A managed Codex login is already running. Wait for it to finish before adding " = "Đăng nhập Codex được quản lý đã chạy. Đợi quá trình này hoàn tất trước khi thêm"; "API key" = "API khóa"; @@ -1291,6 +1292,13 @@ "Model breakdown unavailable" = "Phân tích theo mô hình không khả dụng"; "Local estimated history" = "Lịch sử ước tính cục bộ"; "Coverage" = "Phạm vi"; +"Tracked access" = "Truy cập được theo dõi"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "Mọi gói đăng ký hoặc khóa đã cấu hình đều vẫn hiển thị. Chỉ các nguồn tương thích mới được tính vào tổng chi phí."; +"tracked sources" = "nguồn được theo dõi"; +"Cost history connected" = "Đã kết nối lịch sử chi phí"; +"Cost history pending" = "Lịch sử chi phí đang chờ"; +"Usage connected · not in cost total" = "Đã kết nối mức sử dụng · không tính vào tổng chi phí"; +"Configured · not in cost total" = "Đã cấu hình · không tính vào tổng chi phí"; "Estimated spend" = "Chi tiêu ước tính"; "Tracked tokens" = "Token được theo dõi"; "Subscriptions" = "Gói đăng ký"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 9d89887d94..6b7a91ca0e 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " 提供商"; "(System)" = "(System)"; "30d" = "30 天"; +"365d" = "365 天"; "7d" = "7 天"; "A managed Codex login is already running. Wait for it to finish before adding " = "托管 Codex 登录已在运行。请等待其完成后再添加 "; "API key" = "API 密钥"; @@ -1269,6 +1270,13 @@ "Model breakdown unavailable" = "模型明细不可用"; "Local estimated history" = "本地估算历史"; "Coverage" = "覆盖范围"; +"Tracked access" = "已跟踪的访问"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "每个已配置的订阅或密钥都保持可见。只有兼容的来源才会计入成本总额。"; +"tracked sources" = "已跟踪的来源"; +"Cost history connected" = "成本历史已连接"; +"Cost history pending" = "成本历史待处理"; +"Usage connected · not in cost total" = "用量已连接 · 不计入成本总额"; +"Configured · not in cost total" = "已配置 · 不计入成本总额"; "Estimated spend" = "估算支出"; "Tracked tokens" = "已跟踪 token"; "Subscriptions" = "订阅"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 2b2e21119c..d54ffae0e0 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -50,6 +50,7 @@ " providers" = " 提供者"; "(System)" = "(系統)"; "30d" = "30 天"; +"365d" = "365 天"; "7d" = "7 天"; "A managed Codex login is already running. Wait for it to finish before adding " = "託管 Codex 登入已在執行。請等待其完成後再新增 "; "API key" = "API 金鑰"; @@ -1321,6 +1322,13 @@ "Model breakdown unavailable" = "無法取得模型明細"; "Local estimated history" = "本機預估歷史"; "Coverage" = "涵蓋範圍"; +"Tracked access" = "已追蹤的存取"; +"Every configured subscription or key stays visible. Only compatible sources enter cost totals." = "每個已設定的訂閱或密鑰都保持可見。只有相容的來源才會計入成本總額。"; +"tracked sources" = "已追蹤的來源"; +"Cost history connected" = "成本歷史已連接"; +"Cost history pending" = "成本歷史待處理"; +"Usage connected · not in cost total" = "用量已連接 · 不計入成本總額"; +"Configured · not in cost total" = "已設定 · 不計入成本總額"; "Estimated spend" = "預估支出"; "Tracked tokens" = "已追蹤 token"; "Subscriptions" = "訂閱"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 687fdc65ae..01ca0d6dbf 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -525,6 +525,14 @@ extension SettingsStore { } } + var effectiveCostUsageHistoryDays: Int { + max(self.costUsageHistoryDays, self.spendDashboardHistoryDaysOverride ?? 0) + } + + func setSpendDashboardHistoryDaysOverride(_ days: Int?) { + self.spendDashboardHistoryDaysOverride = days.map { max(1, min(365, $0)) } + } + var costComparisonPeriodsEnabled: Bool { get { self.defaultsState.costComparisonPeriodsEnabled } set { diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index b0277ef06f..afbe76308b 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -231,6 +231,7 @@ final class SettingsStore { var providerDetailSettingsRevision: Int = 0 var backgroundWorkSettingsRevision: Int = 0 var costUsageSettingsRevision: UInt64 = 0 + @ObservationIgnored var spendDashboardHistoryDaysOverride: Int? var providerOrder: [ProviderInstanceID] = [] var providerEnablement: [ProviderInstanceID: Bool] = [:] @ObservationIgnored var providerEnablementRevisions: [ProviderInstanceID: UInt64] = [:] diff --git a/Sources/CodexBar/SpendDashboardController.swift b/Sources/CodexBar/SpendDashboardController.swift index 606b989435..4c9102e01e 100644 --- a/Sources/CodexBar/SpendDashboardController.swift +++ b/Sources/CodexBar/SpendDashboardController.swift @@ -3,12 +3,28 @@ import CryptoKit import Foundation import Observation +struct SpendDashboardTrackedSource: Identifiable, Equatable, Sendable { + enum State: Equatable, Sendable { + case connected + case configured + } + + let id: String + let provider: UsageProvider + let providerName: String + let accountName: String? + let state: State + let supportsCostHistory: Bool + let contributesCostHistory: Bool +} + struct SpendDashboardConfiguration: Equatable, Sendable { let costUsageEnabled: Bool let preferredCurrencyCode: String let providerIDs: [String] let codexAccountIdentities: [String] let codexAccountDisplayNames: [String: String] + let trackedSources: [SpendDashboardTrackedSource] let sourceOwnershipFingerprints: [String] let sourceRevisions: [String] @@ -18,6 +34,7 @@ struct SpendDashboardConfiguration: Equatable, Sendable { providerIDs: [String], codexAccountIdentities: [String], codexAccountDisplayNames: [String: String] = [:], + trackedSources: [SpendDashboardTrackedSource] = [], sourceOwnershipFingerprints: [String] = [], sourceRevisions: [String] = []) { @@ -26,6 +43,7 @@ struct SpendDashboardConfiguration: Equatable, Sendable { self.providerIDs = providerIDs self.codexAccountIdentities = codexAccountIdentities self.codexAccountDisplayNames = codexAccountDisplayNames + self.trackedSources = trackedSources self.sourceOwnershipFingerprints = sourceOwnershipFingerprints self.sourceRevisions = sourceRevisions } @@ -67,6 +85,7 @@ struct SpendDashboardLoadRequest: Sendable { let codexRequests: [CodexSpendScanRequest] let now: Date let force: Bool + let historyDays: Int init( configuration: SpendDashboardConfiguration, @@ -75,7 +94,8 @@ struct SpendDashboardLoadRequest: Sendable { confirmedEmptySourceIDs: Set = [], codexRequests: [CodexSpendScanRequest], now: Date, - force: Bool) + force: Bool, + historyDays: Int = 30) { self.configuration = configuration self.capturedInputs = capturedInputs @@ -84,6 +104,7 @@ struct SpendDashboardLoadRequest: Sendable { self.codexRequests = codexRequests self.now = now self.force = force + self.historyDays = max(1, min(365, historyDays)) } } @@ -126,9 +147,7 @@ enum SpendDashboardSource { -> CostUsageTokenSnapshot? typealias CodexCacheRootResolver = @Sendable (CodexSpendScanRequest) -> URL - static let scanDays = 30 static let activityDays = 365 - @MainActor static func configuration(settings: SettingsStore, store: UsageStore) -> SpendDashboardConfiguration { let providers = self.costCapableProviders(store: store) @@ -155,6 +174,7 @@ enum SpendDashboardSource { providerIDs: providers.map(\.rawValue), codexAccountIdentities: codexRequests.map { "\($0.id)|\($0.cacheIdentity)" }, codexAccountDisplayNames: self.codexDisplayNamesByID(codexRequests), + trackedSources: self.trackedSources(settings: settings, store: store), sourceOwnershipFingerprints: self.sourceOwnershipFingerprints( providers: providers, settings: settings, @@ -170,6 +190,7 @@ enum SpendDashboardSource { now: Date? = nil, nowProvider: @escaping @Sendable () -> Date = { Date() }) async -> SpendDashboardLoadRequest { + let historyDays = settings.effectiveCostUsageHistoryDays guard settings.costUsageEnabled else { return SpendDashboardLoadRequest( configuration: self.configuration(settings: settings, store: store), @@ -177,7 +198,8 @@ enum SpendDashboardSource { unavailableSourceIDs: [], codexRequests: [], now: now ?? nowProvider(), - force: mode.forcesLoader) + force: mode.forcesLoader, + historyDays: historyDays) } let initialProviders = self.costCapableProviders(store: store) @@ -215,7 +237,8 @@ enum SpendDashboardSource { unavailableSourceIDs: [], codexRequests: [], now: captureNow, - force: mode.forcesLoader) + force: mode.forcesLoader, + historyDays: historyDays) } var inputs: [SpendDashboardModel.ProviderInput] = [] @@ -252,7 +275,8 @@ enum SpendDashboardSource { confirmedEmptySourceIDs: confirmedEmptySourceIDs, codexRequests: codexRequests, now: captureNow, - force: mode.forcesLoader) + force: mode.forcesLoader, + historyDays: historyDays) } static func load(_ request: SpendDashboardLoadRequest) async -> SpendDashboardLoadResult { @@ -309,7 +333,7 @@ enum SpendDashboardSource { cacheRoot: cacheRootResolver(account), now: request.now, force: false, - historyDays: Self.scanDays, + historyDays: request.historyDays, refreshPricingInBackground: false, includePiSessions: false)) guard !Task.isCancelled, @@ -355,7 +379,7 @@ enum SpendDashboardSource { cacheRoot: cacheRoot, now: request.now, force: request.force, - historyDays: Self.scanDays, + historyDays: request.historyDays, refreshPricingInBackground: false, includePiSessions: false)) try Task.checkCancellation() @@ -434,6 +458,92 @@ enum SpendDashboardSource { } } + @MainActor + static func trackedSources( + settings: SettingsStore, + store: UsageStore) -> [SpendDashboardTrackedSource] + { + let enabled = Set(store.enabledProvidersForDisplay()) + var sources: [SpendDashboardTrackedSource] = [] + + for provider in UsageProvider.allCases { + let providerName = store.metadata(for: provider).displayName + let supportsCostHistory = ProviderDescriptorRegistry.descriptor(for: provider) + .tokenCost.supportsTokenCost + if provider == .codex { + let accounts = settings.codexVisibleAccountProjection.visibleAccounts + let snapshotsByID = Dictionary(uniqueKeysWithValues: store.codexAccountSnapshots.map { + ($0.id, $0.snapshot != nil) + }) + if !accounts.isEmpty { + sources.append(contentsOf: accounts.map { account in + let connected = snapshotsByID[account.id] == true + || (account.isActive && store.snapshot(for: .codex) != nil) + return SpendDashboardTrackedSource( + id: "codex:\(account.id)", + provider: provider, + providerName: providerName, + accountName: self.trackedAccountName(account.email, providerName: providerName), + state: connected ? .connected : .configured, + supportsCostHistory: supportsCostHistory, + contributesCostHistory: supportsCostHistory && enabled.contains(provider)) + }) + continue + } + } + + let accounts = settings.tokenAccounts(for: provider) + if !accounts.isEmpty { + let activeAccountID = settings.effectiveSelectedTokenAccount(for: provider)?.id + let cachedByID = Dictionary(uniqueKeysWithValues: (store.accountSnapshots[provider] ?? []).map { + ($0.id, $0.snapshot != nil) + }) + sources.append(contentsOf: accounts.map { account in + let isActive = account.id == activeAccountID + let connected = cachedByID[account.id] == true + || (isActive && store.snapshot(for: provider) != nil) + return SpendDashboardTrackedSource( + id: "\(provider.rawValue):account:\(account.id.uuidString.lowercased())", + provider: provider, + providerName: providerName, + accountName: self.trackedAccountName(account.displayName, providerName: providerName), + state: connected ? .connected : .configured, + supportsCostHistory: supportsCostHistory, + contributesCostHistory: supportsCostHistory && isActive && enabled.contains(provider)) + }) + continue + } + + let config = settings.providerConfig(for: provider) + let hasConfiguredCredential = config?.sanitizedAPIKey != nil + || config?.sanitizedSecretKey != nil + || config?.sanitizedCookieHeader != nil + let hasLiveSnapshot = store.snapshot(for: provider) != nil + || store.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) != nil + guard hasConfiguredCredential || hasLiveSnapshot else { continue } + sources.append(SpendDashboardTrackedSource( + id: "\(provider.rawValue):current", + provider: provider, + providerName: providerName, + accountName: nil, + state: hasLiveSnapshot ? .connected : .configured, + supportsCostHistory: supportsCostHistory, + contributesCostHistory: supportsCostHistory && enabled.contains(provider))) + } + + return sources + } + + private static func trackedAccountName(_ rawValue: String?, providerName: String) -> String? { + guard let value = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), + !value.isEmpty, + value.localizedCaseInsensitiveCompare(providerName) != .orderedSame + else { + return nil + } + return value + } + @MainActor static func codexRequests(settings: SettingsStore, store: UsageStore) -> [CodexSpendScanRequest] { let accounts = settings.codexVisibleAccountProjection.visibleAccounts @@ -1199,6 +1309,9 @@ final class SpendDashboardController { } private static func normalizedDays(_ value: Int) -> Int { - value == 7 ? 7 : 30 + switch value { + case 7, 30, 365: value + default: 30 + } } } diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index 1ec6fde68e..10a41f7748 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -119,7 +119,7 @@ struct SpendDashboardModel: Equatable, Sendable { calendar: Calendar = .current, preferredCurrencyCode: String = "auto") -> Self { - let days = max(1, min(30, requestedDays)) + let days = max(1, min(365, requestedDays)) let calculationCalendar = Self.gregorianCalendar(timeZone: calendar.timeZone) let classifiedInputs = inputs.compactMap { input -> ClassifiedInput? in guard let sourceCurrencyCode = Self.currencyCode(input.snapshot.currencyCode) else { return nil } diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift index 8e6e2a4820..fdd16e3f66 100644 --- a/Sources/CodexBar/UsageStore+TokenAccounts.swift +++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift @@ -980,7 +980,7 @@ extension UsageStore { generation: publicationGeneration) } }, - costUsageHistoryDays: self.settings.costUsageHistoryDays, + costUsageHistoryDays: self.settings.effectiveCostUsageHistoryDays, claudeOwnerCLIRecoveryOnly: claudeOwnerCLIRecoveryOnly, persistsCLISessions: true, persistentCLISessionIdleWindow: ProviderRegistry.persistentCLISessionIdleWindow( diff --git a/Sources/CodexBar/UsageStore+TokenCost.swift b/Sources/CodexBar/UsageStore+TokenCost.swift index 24cf19d654..646b2fdbc0 100644 --- a/Sources/CodexBar/UsageStore+TokenCost.swift +++ b/Sources/CodexBar/UsageStore+TokenCost.swift @@ -15,6 +15,7 @@ struct TokenSnapshotPublication: Sendable, Equatable { let snapshot: CostUsageTokenSnapshot? let publicationRevision: UInt64 let providerConfigRevision: UInt64 + let historyDays: Int let scopeSignature: String } @@ -106,8 +107,13 @@ extension UsageStore { for provider: UsageProvider) -> CurrentProviderConfigTokenPublication? { guard let publication = self.tokenSnapshotPublications[provider.instanceID], - publication.providerConfigRevision == self.settings.providerConfigRevision(for: provider), - publication.scopeSignature == self.tokenSnapshotScopeSignature(for: provider) + publication.providerConfigRevision == self.settings.providerConfigRevision(for: provider) + else { return nil } + let requiredHistoryDays = self.settings.effectiveCostUsageHistoryDays + guard publication.historyDays >= requiredHistoryDays, + publication.scopeSignature == self.tokenSnapshotScopeSignature( + for: provider, + historyDays: publication.historyDays) else { return nil } return CurrentProviderConfigTokenPublication( snapshot: publication.snapshot, @@ -129,21 +135,25 @@ extension UsageStore { } private func publishTokenSnapshotState(_ snapshot: CostUsageTokenSnapshot?, for provider: UsageProvider) { + let historyDays = self.settings.effectiveCostUsageHistoryDays self.tokenSnapshotPublicationRevisions[provider.instanceID, default: 0] &+= 1 self.tokenSnapshotPublications[provider.instanceID] = TokenSnapshotPublication( snapshot: snapshot, publicationRevision: self.tokenSnapshotPublicationRevision(for: provider), providerConfigRevision: self.settings.providerConfigRevision(for: provider), - scopeSignature: self.tokenSnapshotScopeSignature(for: provider)) + historyDays: historyDays, + scopeSignature: self.tokenSnapshotScopeSignature(for: provider, historyDays: historyDays)) } func installCachedTokenSnapshot(_ snapshot: CostUsageTokenSnapshot, for provider: UsageProvider) { + let historyDays = self.settings.effectiveCostUsageHistoryDays self.tokenSnapshots[provider.instanceID] = snapshot self.tokenSnapshotPublications[provider.instanceID] = TokenSnapshotPublication( snapshot: snapshot, publicationRevision: self.tokenSnapshotPublicationRevision(for: provider), providerConfigRevision: self.settings.providerConfigRevision(for: provider), - scopeSignature: self.tokenSnapshotScopeSignature(for: provider)) + historyDays: historyDays, + scopeSignature: self.tokenSnapshotScopeSignature(for: provider, historyDays: historyDays)) } func clearTokenSnapshot(for provider: UsageProvider) { @@ -207,7 +217,7 @@ extension UsageStore { } let scope = self.tokenCostScope(for: .codex) - let historyDays = self.settings.costUsageHistoryDays + let historyDays = self.settings.effectiveCostUsageHistoryDays let publicationRevision = self.providerPublicationRevision(for: .codex) let providerConfigRevision = self.settings.providerConfigRevision(for: .codex) let costUsageSettingsRevision = self.settings.costUsageSettingsRevision @@ -244,7 +254,7 @@ extension UsageStore { self.settings.isCostUsageEffectivelyEnabled(for: .codex), self.isEnabled(.codex), self.tokenCostScope(for: .codex).signature == scope.signature, - self.settings.costUsageHistoryDays == historyDays, + self.settings.effectiveCostUsageHistoryDays == historyDays, self.tokenSnapshotScopeSignature(for: .codex) == tokenSnapshotScopeSignature, self.tokenSnapshotPublicationRevision(for: .codex) == tokenSnapshotPublicationRevision, self.tokenSnapshotPublicationForCurrentProviderConfig(for: .codex) == nil @@ -316,9 +326,12 @@ extension UsageStore { } } - func tokenSnapshotScopeSignature(for provider: UsageProvider) -> String { + func tokenSnapshotScopeSignature( + for provider: UsageProvider, + historyDays requestedHistoryDays: Int? = nil) -> String + { let scope = self.tokenCostScope(for: provider) - let historyDays = self.settings.costUsageHistoryDays + let historyDays = max(1, min(365, requestedHistoryDays ?? self.settings.effectiveCostUsageHistoryDays)) let base = "\(scope.signature)|historyDays=\(historyDays)" + "|settingsRevision=\(self.settings.costUsageSettingsRevision)" guard provider == .cursor else { @@ -378,7 +391,7 @@ extension UsageStore { self.settings.providerConfigRevision(for: provider) == providerConfigRevision, self.settings.costUsageEnabled, self.isEnabled(provider), - self.settings.costUsageHistoryDays == historyDays + self.settings.effectiveCostUsageHistoryDays == historyDays else { return false } @@ -422,14 +435,14 @@ extension UsageStore { case .openai: snapshot?.openAIAPIUsage?.toCostUsageTokenSnapshot() case .mistral: - snapshot?.mistralUsage?.toCostUsageTokenSnapshot(historyDays: self.settings.costUsageHistoryDays) + snapshot?.mistralUsage?.toCostUsageTokenSnapshot(historyDays: self.settings.effectiveCostUsageHistoryDays) case .opencodego: // Web-only source mode and machines with no readable local database leave // `opencodegoUsage.daily` empty; a non-nil-but-dataless projection would still // surface a Cost row whose history submenu has nothing to render. snapshot?.opencodegoUsage.flatMap { usage in usage.daily.isEmpty ? nil : usage - .toCostUsageTokenSnapshot(historyDays: self.settings.costUsageHistoryDays) + .toCostUsageTokenSnapshot(historyDays: self.settings.effectiveCostUsageHistoryDays) } default: nil diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index 945afdbeff..7d4fa1b84d 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -1442,7 +1442,7 @@ extension UsageStore { guard !self.tokenRefreshInFlight.contains(provider.instanceID) else { return } let now = Date() - let historyDays = self.settings.costUsageHistoryDays + let historyDays = self.settings.effectiveCostUsageHistoryDays // Cursor cost reuses the status cookie policy: a Manual source forwards the manual header so // cost and status share the same session; other sources fall back to auto resolution. guard case let .proceed(cursorCookieHeaderOverride) = self.prepareCursorCostCookie(for: provider) else { diff --git a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift index 9114bf1e91..5f96d6336d 100644 --- a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift +++ b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift @@ -578,6 +578,7 @@ struct LocalizationLanguageCatalogTests { #expect(indonesian["tab_general"] == "Umum") #expect(indonesian["quit_app"] == "Keluar CodexBar") #expect(indonesian["30d"] == "30 hari") + #expect(indonesian["365d"] == "365 hari") #expect(indonesian["On"] == "Aktif") #expect(indonesian["Off"] == "Nonaktif") diff --git a/Tests/CodexBarTests/SpendDashboardControllerTests.swift b/Tests/CodexBarTests/SpendDashboardControllerTests.swift index 98018e1821..a25e649563 100644 --- a/Tests/CodexBarTests/SpendDashboardControllerTests.swift +++ b/Tests/CodexBarTests/SpendDashboardControllerTests.swift @@ -6,6 +6,29 @@ import Testing @MainActor @Suite(.serialized) struct SpendDashboardControllerTests { + @Test + func `dashboard selection derives an ephemeral provider history window`() throws { + #expect(spendDashboardRequiredHistoryDays(selectedDays: 365, configuredDays: 30) == 365) + #expect(spendDashboardRequiredHistoryDays(selectedDays: 7, configuredDays: 30) == 30) + #expect(spendDashboardRequiredHistoryDays(selectedDays: 30, configuredDays: 365) == 365) + + let suite = "SpendDashboardControllerTests-history-override" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + defer { defaults.removePersistentDomain(forName: suite) } + defaults.set(30, forKey: "tokenCostUsageHistoryDays") + let settings = testSettingsStore(suiteName: suite) + + settings.setSpendDashboardHistoryDaysOverride(365) + #expect(settings.costUsageHistoryDays == 30) + #expect(settings.effectiveCostUsageHistoryDays == 365) + #expect(defaults.integer(forKey: "tokenCostUsageHistoryDays") == 30) + + settings.setSpendDashboardHistoryDaysOverride(nil) + #expect(settings.costUsageHistoryDays == 30) + #expect(settings.effectiveCostUsageHistoryDays == 30) + } + @Test func `empty codex history loads as successful inactive source`() async { let now = Date(timeIntervalSince1970: 1_784_179_200) @@ -24,7 +47,8 @@ struct SpendDashboardControllerTests { unavailableSourceIDs: [], codexRequests: [account], now: now, - force: false) + force: false, + historyDays: 7) let result = await SpendDashboardSource.load(request, codexSnapshotLoader: { context in await recorder.record(context) @@ -48,7 +72,7 @@ struct SpendDashboardControllerTests { #expect(contexts.first?.cacheRoot.lastPathComponent == "inactive-cache") #expect(contexts.first?.now == now) #expect(contexts.first?.force == false) - #expect(contexts.first?.historyDays == 30) + #expect(contexts.first?.historyDays == 7) #expect(contexts.first?.refreshPricingInBackground == false) #expect(contexts.first?.includePiSessions == false) } @@ -749,6 +773,10 @@ struct SpendDashboardControllerTests { controller.selectDays(7) #expect(controller.selectedDays == 7) #expect(defaults.integer(forKey: "settingsSpendDashboardDays") == 7) + controller.selectDays(30) + #expect(controller.selectedDays == 30) + controller.selectDays(365) + #expect(controller.selectedDays == 365) controller.selectDays(9) #expect(controller.selectedDays == 30) } @@ -880,6 +908,34 @@ struct SpendDashboardControllerTests { } } +extension SpendDashboardControllerTests { + @Test + func `range selection persists only supported windows`() throws { + let suite = "SpendDashboardControllerTests-days" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + defer { defaults.removePersistentDomain(forName: suite) } + let controller = SpendDashboardController( + userDefaults: defaults, + requestBuilder: { mode in + Self.request( + configuration: Self.configuration(account: "unused"), + force: mode.forcesLoader) + }) + + #expect(controller.selectedDays == 30) + controller.selectDays(7) + #expect(controller.selectedDays == 7) + #expect(defaults.integer(forKey: "settingsSpendDashboardDays") == 7) + controller.selectDays(30) + #expect(controller.selectedDays == 30) + controller.selectDays(365) + #expect(controller.selectedDays == 365) + controller.selectDays(9) + #expect(controller.selectedDays == 30) + } +} + @MainActor struct SpendDashboardRequestTimeTests { @Test @@ -903,6 +959,19 @@ struct SpendDashboardRequestTimeTests { #expect(request.now == afterMidnight) } + @Test + func `request carries configured history into provider loads`() async { + let (settings, store) = Self.store(suiteName: "SpendDashboardRequestTimeTests-history") + settings.costUsageHistoryDays = 7 + + let request = await SpendDashboardSource.makeRequest( + settings: settings, + store: store, + mode: .captureOnly) + + #expect(request.historyDays == 7) + } + @Test func `explicit request time remains authoritative after refresh`() async throws { let (settings, store) = Self.store(suiteName: "SpendDashboardRequestTimeTests-explicit") diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index f932c2103f..a780a3a1dc 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -4,27 +4,6 @@ import Testing @testable import CodexBar struct SpendDashboardModelTests { - @Test - func `count labels avoid plural agreement and localize numbers`() { - CodexBarLocalizationOverride.$appLanguage.withValue("en") { - #expect(spendDashboardRefreshFailureText(1) == "Refresh failures: 1") - #expect(spendDashboardRefreshFailureText(2) == "Refresh failures: 2") - #expect(spendDashboardCoverageText(covered: 3, requested: 7) == "Coverage: 3 / 7") - } - CodexBarLocalizationOverride.$appLanguage.withValue("de") { - #expect(spendDashboardRefreshFailureText(1234) == "Fehlgeschlagene Aktualisierungen: 1.234") - #expect(spendDashboardCoverageText(covered: 3, requested: 30) == "Abdeckung: 3 / 30") - } - CodexBarLocalizationOverride.$appLanguage.withValue("fa") { - #expect(codexBarLocalizedInteger(12) == "۱۲") - #expect(spendDashboardDayRangeText(7) == "۷ روز") - #expect(spendDashboardDayRangeText(30) == "۳۰ روز") - #expect(spendDashboardRankText(1234) == "#۱٬۲۳۴") - #expect(spendDashboardRefreshFailureText(2) == "\(L("Refresh failures")): ۲") - #expect(spendDashboardCoverageText(covered: 3, requested: 30) == "پوشش: ۳ / ۳۰") - } - } - @Test func `Codex account indices use app locale numerals`() throws { let home = FileManager.default.temporaryDirectory @@ -753,7 +732,6 @@ struct SpendDashboardModelTests { #expect(!request.authFileWasReadable) #expect(request.displayName == "Codex · #2") #expect(request.cacheIdentity.count == 64) - #expect(SpendDashboardSource.scanDays == 30) #expect(SpendDashboardSource.codexRequest( account: account, homePath: "relative/path", @@ -980,4 +958,26 @@ extension SpendDashboardModelTests { #expect(group.modelHistoryCompleteness == .incomplete) #expect(group.models.isEmpty) } + + @Test + func `count labels avoid plural agreement and localize numbers`() { + CodexBarLocalizationOverride.$appLanguage.withValue("en") { + #expect(spendDashboardRefreshFailureText(1) == "Refresh failures: 1") + #expect(spendDashboardRefreshFailureText(2) == "Refresh failures: 2") + #expect(spendDashboardCoverageText(covered: 3, requested: 7) == "Coverage: 3 / 7") + } + CodexBarLocalizationOverride.$appLanguage.withValue("de") { + #expect(spendDashboardRefreshFailureText(1234) == "Fehlgeschlagene Aktualisierungen: 1.234") + #expect(spendDashboardCoverageText(covered: 3, requested: 30) == "Abdeckung: 3 / 30") + } + CodexBarLocalizationOverride.$appLanguage.withValue("fa") { + #expect(codexBarLocalizedInteger(12) == "۱۲") + #expect(spendDashboardDayRangeText(7) == "۷ روز") + #expect(spendDashboardDayRangeText(30) == "۳۰ روز") + #expect(spendDashboardDayRangeText(365) == "۳۶۵ روز") + #expect(spendDashboardRankText(1234) == "#۱٬۲۳۴") + #expect(spendDashboardRefreshFailureText(2) == "\(L("Refresh failures")): ۲") + #expect(spendDashboardCoverageText(covered: 3, requested: 30) == "پوشش: ۳ / ۳۰") + } + } } diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift new file mode 100644 index 0000000000..b3b8be129c --- /dev/null +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -0,0 +1,51 @@ +import CodexBarCore +import Testing +@testable import CodexBar + +struct SpendDashboardTrackedSourceTests { + @Test + @MainActor + func `tracked access includes every saved provider credential without inventing cost coverage`() { + let settings = testSettingsStore(suiteName: "SpendDashboardTrackedSourceTests-credentials") + let supportedProviders = UsageProvider.allCases.filter { + TokenAccountSupportCatalog.support(for: $0) != nil + } + for provider in supportedProviders { + settings.addTokenAccount(provider: provider, label: "\(provider.rawValue) account", token: "fixture") + } + settings.addTokenAccount(provider: .openrouter, label: "second account", token: "fixture-2") + + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + let sources = SpendDashboardSource.trackedSources(settings: settings, store: store) + let credentialSources = sources.filter { $0.id.contains(":account:") } + + #expect(Set(credentialSources.map(\.provider)) == Set(supportedProviders)) + #expect(credentialSources.count == supportedProviders.count + 1) + #expect(Set(credentialSources.map(\.id)).count == credentialSources.count) + + let openRouterSources = credentialSources.filter { $0.provider == .openrouter } + #expect(openRouterSources.count == 2) + #expect(openRouterSources.allSatisfy { $0.state == .configured }) + #expect(openRouterSources.allSatisfy { !$0.supportsCostHistory }) + #expect(openRouterSources.allSatisfy { !$0.contributesCostHistory }) + } + + @Test + func `tracked access copy distinguishes missing cost history from zero spend`() { + let source = SpendDashboardTrackedSource( + id: "openrouter:account:test", + provider: .openrouter, + providerName: "OpenRouter", + accountName: "Work", + state: .connected, + supportsCostHistory: false, + contributesCostHistory: false) + + #expect(spendDashboardTrackedSourceStatusText(source) == "Usage connected · not in cost total") + } +} diff --git a/Tests/CodexBarTests/UsageStoreCoverageTests.swift b/Tests/CodexBarTests/UsageStoreCoverageTests.swift index d70d01bc40..13a09fdd06 100644 --- a/Tests/CodexBarTests/UsageStoreCoverageTests.swift +++ b/Tests/CodexBarTests/UsageStoreCoverageTests.swift @@ -176,6 +176,32 @@ struct UsageStoreCoverageTests { fetchedCredentialScopeFingerprint: fingerprint)) } + @Test + func `dashboard history override keeps sparse provider coverage current when it closes`() throws { + let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-dashboard-history") + settings.costUsageEnabled = true + settings.costUsageHistoryDays = 30 + settings.setSpendDashboardHistoryDaysOverride(365) + let metadata = try #require(ProviderRegistry.shared.metadata[.claude]) + settings.setProviderEnabled(provider: .claude, metadata: metadata, enabled: true) + let store = Self.makeUsageStore(settings: settings) + let snapshot = CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 10, + last30DaysCostUSD: 1, + historyDays: 1, + daily: [], + updatedAt: Date()) + + store.publishTokenSnapshot(snapshot, for: .claude) + settings.setSpendDashboardHistoryDaysOverride(nil) + + #expect(settings.costUsageHistoryDays == 30) + #expect(store.tokenSnapshotForCurrentProviderConfig(for: .claude)?.snapshot == snapshot) + #expect(store.tokenSnapshotForCurrentProviderConfig(for: .claude)?.snapshot.historyDays == 1) + } + @Test func `source label adds open AI web`() { let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-source") diff --git a/docs/codex.md b/docs/codex.md index bf75aea5d1..dd337a744e 100644 --- a/docs/codex.md +++ b/docs/codex.md @@ -173,9 +173,10 @@ Example: ### Usage & Spend account rows -Settings → Usage & Spend performs a separate fixed 30-day scan for every visible Codex account. Each request freezes -the account source, exact Codex home, authentication fingerprint, and cache identity before scanning. A missing or -invalid home is omitted; it never falls back to ambient `~/.codex` or to the global Codex token snapshot. +Settings → Usage & Spend performs a separate scan at the selected 7-, 30-, or 365-day window for every visible Codex +account. Each request freezes the account source, exact Codex home, authentication fingerprint, and cache identity +before scanning. A missing or invalid home is omitted; it never falls back to ambient `~/.codex` or to the global +Codex token snapshot. These account rows intentionally exclude pi and OMP sessions because their history is machine-local rather than owned by one Codex account. The normal Codex cost menu and CLI scan continue to include supported pi-compatible history. The diff --git a/docs/providers.md b/docs/providers.md index c1fab9957f..e9fab8f2cc 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -21,9 +21,11 @@ headers, source selection, provider ordering, and token accounts are stored in ` ## Usage & Spend settings -Settings → Usage & Spend combines local 7- or 30-day estimated history only for enabled descriptors that advertise -token-cost support: Codex, Claude, Vertex AI, OpenAI, Mistral, and AWS Bedrock. Providers without a cost-history -contract are omitted instead of appearing as empty subscriptions. +Settings → Usage & Spend combines local 7-, 30-, or 365-day estimated history for enabled descriptors that advertise +token-cost support: Codex, Claude, Vertex AI, OpenAI, Mistral, AWS Bedrock, Cursor, and OpenCode Go. Its tracked-access +section separately lists every saved subscription/key plus live authenticated provider sources. Sources without a +cost-history contract stay visible there and are explicitly excluded from cost totals instead of appearing as zero +spend. Each native currency has its own total, subscription/model ranking, and daily chart. CodexBar never adds or ranks amounts across currencies. Coverage text reports how many days of the selected local calendar window are covered by From 6510faae9cf05f3ab7ab7b9001c1b1470e5d5ce2 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Tue, 28 Jul 2026 19:53:09 -0700 Subject: [PATCH 02/51] Polish tracked access layout --- .../PreferencesSpendDashboardPane.swift | 233 +++++++++++------- .../SpendDashboardTrackedSourceTests.swift | 119 +++++++++ .../spend-dashboard-proof/README.md | 17 ++ .../spend-dashboard-tracked-access-narrow.png | Bin 0 -> 48477 bytes .../spend-dashboard-tracked-access-wide.png | Bin 0 -> 44428 bytes 5 files changed, 285 insertions(+), 84 deletions(-) create mode 100644 docs/screenshots/spend-dashboard-proof/README.md create mode 100644 docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-narrow.png create mode 100644 docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-wide.png diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 74ba41f286..84ce16b113 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -147,35 +147,12 @@ struct SpendDashboardPane: View { } private var header: some View { - HStack(alignment: .top, spacing: 16) { - VStack(alignment: .leading, spacing: 4) { - Text(L("Usage & Spend")) - .font(.title2.weight(.semibold)) - Text(L("Local estimated cost history across supported providers.")) - .font(.subheadline) - .foregroundStyle(.secondary) - } - Spacer() - Picker(L("Time range"), selection: self.daysBinding) { - Text(spendDashboardDayRangeText(7)).tag(7) - Text(spendDashboardDayRangeText(30)).tag(30) - Text(spendDashboardDayRangeText(365)).tag(365) - } - .labelsHidden() - .pickerStyle(.segmented) - .frame(width: 174) - - Button { - self.controller.refresh() - } label: { - if self.controller.isRefreshing { - ProgressView().controlSize(.small) - } else { - Label(L("Refresh"), systemImage: "arrow.clockwise") - } - } - .disabled(self.controller.isRefreshing || !self.settings.costUsageEnabled) - } + SpendDashboardHeader( + selectedDays: self.controller.selectedDays, + isRefreshing: self.controller.isRefreshing, + isCostTrackingEnabled: self.settings.costUsageEnabled, + selectDays: { self.daysBinding.wrappedValue = $0 }, + refresh: { self.controller.refresh() }) } @ViewBuilder @@ -363,38 +340,9 @@ struct SpendDashboardPane: View { private var trackedAccess: some View { let sources = self.configuration.trackedSources if !sources.isEmpty { - SpendDashboardPanel { - VStack(alignment: .leading, spacing: 14) { - HStack(alignment: .firstTextBaseline) { - VStack(alignment: .leading, spacing: 3) { - Text(L("Tracked access")) - .font(.headline) - Text(self.trackedAccessDescription) - .font(.caption) - .foregroundStyle(.secondary) - } - Spacer() - Text(codexBarLocalizedInteger(sources.count)) - .font(.title3.monospacedDigit().weight(.semibold)) - .foregroundStyle(.secondary) - .accessibilityLabel( - "\(codexBarLocalizedInteger(sources.count)) \(L("tracked sources"))") - } - - LazyVGrid( - columns: [ - GridItem(.flexible(), spacing: 10), - GridItem(.flexible(), spacing: 10), - ], - alignment: .leading, - spacing: 10) - { - ForEach(sources) { source in - SpendTrackedSourceRow(source: source) - } - } - } - } + SpendTrackedAccessPanel( + sources: sources, + description: self.trackedAccessDescription) } } @@ -467,38 +415,141 @@ struct SpendDashboardPane: View { } } +struct SpendDashboardHeader: View { + let selectedDays: Int + let isRefreshing: Bool + let isCostTrackingEnabled: Bool + let selectDays: (Int) -> Void + let refresh: () -> Void + + var body: some View { + ViewThatFits(in: .horizontal) { + HStack(alignment: .top, spacing: 16) { + self.title + Spacer(minLength: 16) + self.controls + } + VStack(alignment: .leading, spacing: 12) { + self.title + self.controls + } + } + } + + private var title: some View { + VStack(alignment: .leading, spacing: 4) { + Text(L("Usage & Spend")) + .font(.title2.weight(.semibold)) + Text(L("Local estimated cost history across supported providers.")) + .font(.subheadline) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + + private var controls: some View { + HStack(spacing: 12) { + Picker(L("Time range"), selection: self.daysBinding) { + Text(spendDashboardDayRangeText(7)).tag(7) + Text(spendDashboardDayRangeText(30)).tag(30) + Text(spendDashboardDayRangeText(365)).tag(365) + } + .labelsHidden() + .pickerStyle(.segmented) + .frame(width: 174) + + Button(action: self.refresh) { + if self.isRefreshing { + ProgressView().controlSize(.small) + } else { + Label(L("Refresh"), systemImage: "arrow.clockwise") + } + } + .disabled(self.isRefreshing || !self.isCostTrackingEnabled) + } + } + + private var daysBinding: Binding { + Binding(get: { self.selectedDays }, set: { self.selectDays($0) }) + } +} + +struct SpendTrackedAccessPanel: View { + let sources: [SpendDashboardTrackedSource] + let description: String + + private let columns = [ + GridItem(.adaptive(minimum: 245, maximum: 420), spacing: 12), + ] + + var body: some View { + SpendDashboardPanel { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .firstTextBaseline, spacing: 16) { + VStack(alignment: .leading, spacing: 4) { + Text(L("Tracked access")) + .font(.headline) + Text(self.description) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + Spacer(minLength: 12) + HStack(spacing: 6) { + Image(systemName: "key.fill") + .font(.caption) + Text(codexBarLocalizedInteger(self.sources.count)) + .font(.subheadline.monospacedDigit().weight(.semibold)) + } + .foregroundStyle(.secondary) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(.quaternary.opacity(0.7), in: Capsule()) + .accessibilityLabel( + "\(codexBarLocalizedInteger(self.sources.count)) \(L("tracked sources"))") + } + + LazyVGrid(columns: self.columns, alignment: .leading, spacing: 12) { + ForEach(self.sources) { source in + SpendTrackedSourceRow(source: source) + } + } + } + } + } +} + private struct SpendTrackedSourceRow: View { let source: SpendDashboardTrackedSource var body: some View { - HStack(spacing: 10) { - SpendProviderIcon(provider: self.source.provider) - - VStack(alignment: .leading, spacing: 2) { - Text(self.source.providerName) - .font(.subheadline.weight(.medium)) - .lineLimit(1) - if let accountName = self.source.accountName { - Text(accountName) - .font(.caption) - .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 9) { + HStack(spacing: 10) { + SpendProviderIcon(provider: self.source.provider) + + VStack(alignment: .leading, spacing: 2) { + Text(self.source.providerName) + .font(.subheadline.weight(.medium)) .lineLimit(1) + if let accountName = self.source.accountName { + Text(accountName) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } } - Text(spendDashboardTrackedSourceStatusText(self.source)) - .font(.caption2) - .foregroundStyle(.secondary) - .lineLimit(1) + Spacer(minLength: 4) } - Spacer(minLength: 4) - - Circle() - .fill(self.source.state == .connected ? Color.green : Color.secondary.opacity(0.45)) - .frame(width: 7, height: 7) - .accessibilityHidden(true) + Label( + spendDashboardTrackedSourceStatusText(self.source), + systemImage: self.statusSymbol) + .font(.caption.weight(.medium)) + .foregroundStyle(self.statusColor) + .fixedSize(horizontal: false, vertical: true) } - .padding(10) - .frame(maxWidth: .infinity, minHeight: 58, alignment: .leading) + .padding(12) + .frame(maxWidth: .infinity, minHeight: 72, alignment: .leading) .background(.background.opacity(0.72), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 10, style: .continuous) @@ -506,6 +557,20 @@ private struct SpendTrackedSourceRow: View { } .accessibilityElement(children: .combine) } + + private var statusSymbol: String { + if self.source.contributesCostHistory { + return self.source.state == .connected ? "checkmark.circle.fill" : "clock.fill" + } + return self.source.state == .connected ? "minus.circle.fill" : "circle.dashed" + } + + private var statusColor: Color { + if self.source.contributesCostHistory { + return self.source.state == .connected ? .green : .orange + } + return .secondary + } } struct SpendDashboardEmptyState: Equatable { diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index b3b8be129c..dfe982ef87 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -1,4 +1,6 @@ +import AppKit import CodexBarCore +import SwiftUI import Testing @testable import CodexBar @@ -48,4 +50,121 @@ struct SpendDashboardTrackedSourceTests { #expect(spendDashboardTrackedSourceStatusText(source) == "Usage connected · not in cost total") } + + @Test + @MainActor + func `tracked access renders without clipping at wide and narrow settings widths`() throws { + let proofDirectory = ProcessInfo.processInfo.environment["CODEXBAR_SPEND_DASHBOARD_PROOF_DIR"].map { + URL(fileURLWithPath: $0, isDirectory: true) + } + if let proofDirectory { + try FileManager.default.createDirectory( + at: proofDirectory, + withIntermediateDirectories: true) + } + + for (size, filename) in [ + (CGSize(width: 760, height: 440), "spend-dashboard-tracked-access-wide.png"), + (CGSize(width: 430, height: 720), "spend-dashboard-tracked-access-narrow.png"), + ] { + let view = VStack(alignment: .leading, spacing: 18) { + SpendDashboardHeader( + selectedDays: 365, + isRefreshing: false, + isCostTrackingEnabled: true, + selectDays: { _ in }, + refresh: {}) + SpendTrackedAccessPanel( + sources: Self.proofSources, + description: "Every configured subscription or key stays visible. " + + "Only compatible sources enter cost totals.") + } + .padding(24) + .frame(width: size.width, height: size.height, alignment: .topLeading) + .background(Color(nsColor: .windowBackgroundColor)) + + let data = try #require(Self.pngData(for: view, size: size)) + let bitmap = try #require(NSBitmapImageRep(data: data)) + #expect(bitmap.pixelsWide == Int(size.width)) + #expect(bitmap.pixelsHigh == Int(size.height)) + if let proofDirectory { + try data.write(to: proofDirectory.appendingPathComponent(filename), options: .atomic) + } + } + } + + private static let proofSources = [ + SpendDashboardTrackedSource( + id: "codex:account:personal", + provider: .codex, + providerName: "Codex", + accountName: "Personal", + state: .connected, + supportsCostHistory: true, + contributesCostHistory: true), + SpendDashboardTrackedSource( + id: "claude:account:team", + provider: .claude, + providerName: "Claude", + accountName: "Team", + state: .connected, + supportsCostHistory: true, + contributesCostHistory: true), + SpendDashboardTrackedSource( + id: "openrouter:account:research", + provider: .openrouter, + providerName: "OpenRouter", + accountName: "Research", + state: .connected, + supportsCostHistory: false, + contributesCostHistory: false), + SpendDashboardTrackedSource( + id: "cursor:account:work", + provider: .cursor, + providerName: "Cursor", + accountName: "Work", + state: .configured, + supportsCostHistory: true, + contributesCostHistory: false), + SpendDashboardTrackedSource( + id: "gemini:account:studio", + provider: .gemini, + providerName: "Gemini", + accountName: "Studio", + state: .connected, + supportsCostHistory: false, + contributesCostHistory: false), + SpendDashboardTrackedSource( + id: "mistral:account:api", + provider: .mistral, + providerName: "Mistral", + accountName: "API", + state: .configured, + supportsCostHistory: true, + contributesCostHistory: false), + ] + + @MainActor + private static func pngData(for rootView: some View, size: CGSize) -> Data? { + let view = NSHostingView(rootView: rootView) + view.frame = CGRect(origin: .zero, size: size) + view.layoutSubtreeIfNeeded() + + guard let representation = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(size.width), + pixelsHigh: Int(size.height), + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0) + else { return nil } + representation.size = size + guard let context = NSGraphicsContext(bitmapImageRep: representation) else { return nil } + view.displayIgnoringOpacity(view.bounds, in: context) + return representation.representation(using: .png, properties: [:]) + } } diff --git a/docs/screenshots/spend-dashboard-proof/README.md b/docs/screenshots/spend-dashboard-proof/README.md new file mode 100644 index 0000000000..73d5bd0223 --- /dev/null +++ b/docs/screenshots/spend-dashboard-proof/README.md @@ -0,0 +1,17 @@ +# Spend dashboard proof + +These PNGs are rendered by the production `SpendDashboardHeader` and +`SpendTrackedAccessPanel` SwiftUI components at wide and narrow settings widths. +The provider names and account labels are synthetic fixtures; no account data, +keys, or usage values are included. + +Regenerate them with a full Xcode toolchain: + +```sh +CODEXBAR_SPEND_DASHBOARD_PROOF_DIR="$PWD/docs/screenshots/spend-dashboard-proof" \ + swift test --filter SpendDashboardTrackedSourceTests +``` + +The narrow render verifies that the range controls wrap below the title and the +tracked-source grid collapses to one column. The wide render uses two columns. +Both states keep cost-history inclusion and exclusion explicit. diff --git a/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-narrow.png b/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-narrow.png new file mode 100644 index 0000000000000000000000000000000000000000..caad81e2c284b73da15a397223a0d47a3b73ec60 GIT binary patch literal 48477 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-wide.png b/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-wide.png new file mode 100644 index 0000000000000000000000000000000000000000..243b926d9c215fa7497cba7c6098ac4cf8425559 GIT binary patch literal 44428 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 AtpET3 literal 0 HcmV?d00001 From 3c18d8d9f5135ace339da51529b5834434c73f22 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Tue, 28 Jul 2026 20:25:05 -0700 Subject: [PATCH 03/51] Clarify tracked access proof --- .../PreferencesSpendDashboardPane.swift | 36 +++++++++--------- .../spend-dashboard-tracked-access-narrow.png | Bin 48477 -> 49750 bytes .../spend-dashboard-tracked-access-wide.png | Bin 44428 -> 45679 bytes 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 84ce16b113..70ebc8fd65 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -485,28 +485,26 @@ struct SpendTrackedAccessPanel: View { var body: some View { SpendDashboardPanel { VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .firstTextBaseline, spacing: 16) { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .firstTextBaseline, spacing: 12) { Text(L("Tracked access")) .font(.headline) - Text(self.description) - .font(.caption) + Spacer(minLength: 12) + Text( + "\(codexBarLocalizedInteger(self.sources.count)) " + + L("tracked sources")) + .font(.caption.monospacedDigit().weight(.semibold)) .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - Spacer(minLength: 12) - HStack(spacing: 6) { - Image(systemName: "key.fill") - .font(.caption) - Text(codexBarLocalizedInteger(self.sources.count)) - .font(.subheadline.monospacedDigit().weight(.semibold)) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(.quaternary.opacity(0.7), in: Capsule()) + .accessibilityLabel( + "\(codexBarLocalizedInteger(self.sources.count)) \(L("tracked sources"))") } - .foregroundStyle(.secondary) - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background(.quaternary.opacity(0.7), in: Capsule()) - .accessibilityLabel( - "\(codexBarLocalizedInteger(self.sources.count)) \(L("tracked sources"))") + Text(self.description) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) } LazyVGrid(columns: self.columns, alignment: .leading, spacing: 12) { @@ -562,7 +560,7 @@ private struct SpendTrackedSourceRow: View { if self.source.contributesCostHistory { return self.source.state == .connected ? "checkmark.circle.fill" : "clock.fill" } - return self.source.state == .connected ? "minus.circle.fill" : "circle.dashed" + return self.source.state == .connected ? "minus.circle.fill" : "minus.circle" } private var statusColor: Color { diff --git a/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-narrow.png b/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-narrow.png index caad81e2c284b73da15a397223a0d47a3b73ec60..faf07c98a4ad7f81444a2bf3a49e42e75385d67f 100644 GIT binary patch literal 49750 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%0DteD;?^iykp_reDZ6k9Z+m-}KQ#2Ez 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 literal 48477 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 diff --git a/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-wide.png b/docs/screenshots/spend-dashboard-proof/spend-dashboard-tracked-access-wide.png index 243b926d9c215fa7497cba7c6098ac4cf8425559..6d2cd907d7975b4c8dbb57cf02c087fe370448bc 100644 GIT binary patch literal 45679 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 Qhro}HhJku9%I3lU0PX%dGynhq literal 44428 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 AtpET3 From 9c12cc6843fa53e753b50379f545c18dc0c93011 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Wed, 5 Aug 2026 09:18:55 -0700 Subject: [PATCH 04/51] Preserve annual model coverage --- .../SpendDashboardModel+ModelBreakdown.swift | 21 +++ Sources/CodexBar/SpendDashboardModel.swift | 3 +- ...Store+SpendDashboardCodexCostCatchUp.swift | 8 +- ...pendDashboardCachedPresentationTests.swift | 18 ++- ...ashboardIntegratedModelCoverageTests.swift | 139 ++++++++++++++++++ ...eSpendDashboardCodexCostCatchUpTests.swift | 20 ++- 6 files changed, 198 insertions(+), 11 deletions(-) create mode 100644 Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift diff --git a/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift b/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift index 106c001eca..86e808922d 100644 --- a/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift +++ b/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift @@ -118,6 +118,27 @@ extension SpendDashboardModel { } } + static func canRetainTokenOnlyModelHistory(_ summary: InputSummary) -> Bool { + var sawTokenOnlyModel = false + for windowEntry in summary.entries { + let entry = windowEntry.entry + guard self.validCost(entry.costUSD) != nil else { return false } + guard entry.modelBreakdowns?.isEmpty == false else { + continue + } + guard self.hasCompleteModelTokenCoverage(entry), + entry.modelBreakdowns?.allSatisfy({ breakdown in + !breakdown.modelName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && breakdown.costUSD == nil + }) == true + else { + return false + } + sawTokenOnlyModel = true + } + return sawTokenOnlyModel + } + private static func hasRetainablePartialCodexModelCostCoverage( _ entry: CostUsageDailyReport.Entry) -> Bool { diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index 10a41f7748..aad74fe13c 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -228,7 +228,8 @@ struct SpendDashboardModel: Equatable, Sendable { guard summary.totalCost != nil else { return false } let summaryModelHistory = Self.modelSummary(summaries: [summary]) return summaryModelHistory.completeness == .complete || - Self.canRetainPartialCodexModelHistory(summary) + Self.canRetainPartialCodexModelHistory(summary) || + Self.canRetainTokenOnlyModelHistory(summary) } // A Codex session can have valid priced rows alongside model-less or unpriced rows. // Keep only the directly priced portion, but mark the aggregate partial and remove ranking. diff --git a/Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift b/Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift index 0ee8c911ec..29002813ab 100644 --- a/Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift +++ b/Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift @@ -31,8 +31,9 @@ extension UsageStore { return } - let scopeSignature = accounts - .map { "\($0.id)|\($0.cacheIdentity)" } + let historyDays = self.settings.effectiveCostUsageHistoryDays + let scopeSignature = (["historyDays=\(historyDays)"] + accounts + .map { "\($0.id)|\($0.cacheIdentity)" }) .joined(separator: "\u{0}") if self.spendDashboardCodexCostCatchUpTask != nil, self.spendDashboardCodexCostCatchUpScopeSignature == scopeSignature @@ -51,7 +52,7 @@ extension UsageStore { let context = SpendDashboardCodexCostCatchUpContext( token: token, accounts: accounts, - historyDays: SpendDashboardSource.scanDays, + historyDays: historyDays, scopeSignature: scopeSignature, providerConfigRevision: self.settings.providerConfigRevision(for: .codex), costUsageSettingsRevision: self.settings.costUsageSettingsRevision) @@ -243,6 +244,7 @@ extension UsageStore { !Task.isCancelled && self.spendDashboardCodexCostCatchUpToken == context.token && self.spendDashboardCodexCostCatchUpScopeSignature == context.scopeSignature + && self.settings.effectiveCostUsageHistoryDays == context.historyDays && self.settings.providerConfigRevision(for: .codex) == context.providerConfigRevision && self.settings.costUsageSettingsRevision == context.costUsageSettingsRevision && self.settings.isCostUsageEffectivelyEnabled(for: .codex) diff --git a/Tests/CodexBarTests/SpendDashboardCachedPresentationTests.swift b/Tests/CodexBarTests/SpendDashboardCachedPresentationTests.swift index e801e93ceb..9d4c63d4d5 100644 --- a/Tests/CodexBarTests/SpendDashboardCachedPresentationTests.swift +++ b/Tests/CodexBarTests/SpendDashboardCachedPresentationTests.swift @@ -15,6 +15,7 @@ struct SpendDashboardCachedPresentationTests { func `production loader reads a validated scoped account report`() async throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } + let historyDays = 30 let day = try env.makeLocalNoon(year: 2026, month: 7, day: 15) _ = try env.writeCodexSessionFile( day: day, @@ -45,7 +46,7 @@ struct SpendDashboardCachedPresentationTests { provider: .codex, now: day, codexHomePath: env.codexHomeRoot.path, - historyDays: SpendDashboardSource.scanDays, + historyDays: historyDays, includePiSessions: false) let account = CodexSpendScanRequest( id: "profile", @@ -64,7 +65,8 @@ struct SpendDashboardCachedPresentationTests { unavailableSourceIDs: [], codexRequests: [account], now: day, - force: false) + force: false, + historyDays: historyDays) let cacheRoot = env.cacheRoot let result = await SpendDashboardSource.loadCached(request, cacheRootResolver: { _ in cacheRoot }) @@ -117,12 +119,13 @@ struct SpendDashboardCachedPresentationTests { unavailableSourceIDs: [], codexRequests: [first, second], now: Self.fixtureNow, - force: false) + force: false, + historyDays: 365) let result = await SpendDashboardSource.loadCached(request, cachedCodexSnapshotLoader: { context in switch context.account.id { - case "first": Self.input(cost: 2).snapshot - case "second": Self.input(cost: 5).snapshot + case "first": Self.input(cost: 2, historyDays: context.historyDays).snapshot + case "second": Self.input(cost: 5, historyDays: context.historyDays).snapshot default: nil } }) @@ -131,6 +134,7 @@ struct SpendDashboardCachedPresentationTests { "codex:first": 2, "codex:second": 5, ]) + #expect(result.inputs.allSatisfy { $0.snapshot.historyDays == 365 }) #expect(SpendDashboardSource.codexCacheRoot(for: first).lastPathComponent == "first-cache") #expect(SpendDashboardSource.codexCacheRoot(for: second).lastPathComponent == "second-cache") } @@ -207,7 +211,8 @@ struct SpendDashboardCachedPresentationTests { private nonisolated static func input( id: String? = nil, - cost: Double) -> SpendDashboardModel.ProviderInput + cost: Double, + historyDays: Int = 30) -> SpendDashboardModel.ProviderInput { let entry = CostUsageDailyReport.Entry( date: "2026-07-15", @@ -222,6 +227,7 @@ struct SpendDashboardCachedPresentationTests { sessionCostUSD: nil, last30DaysTokens: 10, last30DaysCostUSD: cost, + historyDays: historyDays, daily: [entry], updatedAt: Self.fixtureNow) return SpendDashboardModel.ProviderInput( diff --git a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift new file mode 100644 index 0000000000..f85256a492 --- /dev/null +++ b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift @@ -0,0 +1,139 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct SpendDashboardIntegratedModelCoverageTests { + @Test + func `dashboard aggregation keeps every model from every cost capable integrated provider`() throws { + let providers = ProviderDescriptorRegistry.all + .filter(\.tokenCost.supportsTokenCost) + .map(\.id) + let inputs = providers.enumerated().map { index, provider in + Self.input(provider: provider, costOffset: Double(index)) + } + + let group = try #require(SpendDashboardModel.build( + inputs: inputs, + requestedDays: 365, + now: Self.now, + calendar: Self.calendar).groups.first) + let expectedIDs = Set(providers.flatMap { provider in + [ + "\(provider.rawValue):fixture-shared-model", + "\(provider.rawValue):fixture-\(provider.rawValue)-primary", + ] + }) + + #expect(Set(providers) == [ + .bedrock, + .claude, + .codex, + .cursor, + .mistral, + .openai, + .opencodego, + .vertexai, + ]) + #expect(Set(group.models.map(\.id)) == expectedIDs) + #expect(group.models.count == providers.count * 2) + #expect(group.models.allSatisfy { $0.totalTokens != nil && $0.totalCost != nil }) + #expect(group.modelHistoryCompleteness == .complete) + } + + @Test + func `OpenAI adapter keeps token only model rows without inventing model cost`() throws { + let firstModel = OpenAIAPIUsageSnapshot.ModelBreakdown( + name: "fixture-openai-primary", + requests: 1, + inputTokens: 6, + cachedInputTokens: 0, + outputTokens: 4, + totalTokens: 10) + let secondModel = OpenAIAPIUsageSnapshot.ModelBreakdown( + name: "fixture-openai-secondary", + requests: 1, + inputTokens: 12, + cachedInputTokens: 0, + outputTokens: 8, + totalTokens: 20) + let usage = OpenAIAPIUsageSnapshot( + daily: [ + OpenAIAPIUsageSnapshot.DailyBucket( + day: "2026-08-05", + startTime: Self.now, + endTime: Self.now.addingTimeInterval(86400), + costUSD: 3, + requests: 2, + inputTokens: 18, + cachedInputTokens: 0, + outputTokens: 12, + totalTokens: 30, + lineItems: [], + models: [firstModel, secondModel]), + ], + updatedAt: Self.now, + historyDays: 365) + let input = SpendDashboardModel.ProviderInput( + provider: .openai, + displayName: "OpenAI", + snapshot: usage.toCostUsageTokenSnapshot()) + + let group = try #require(SpendDashboardModel.build( + inputs: [input], + requestedDays: 365, + now: Self.now, + calendar: Self.calendar).groups.first) + + #expect(Set(group.models.map(\.modelName)) == [firstModel.name, secondModel.name]) + #expect(Set(group.models.compactMap(\.totalTokens)) == [10, 20]) + #expect(group.models.allSatisfy { $0.totalCost == nil }) + #expect(group.modelHistoryCompleteness == .incomplete) + } + + private static func input( + provider: UsageProvider, + costOffset: Double) -> SpendDashboardModel.ProviderInput + { + let primaryCost = costOffset + 1 + let sharedCost = costOffset + 2 + let breakdowns = [ + CostUsageDailyReport.ModelBreakdown( + modelName: "fixture-\(provider.rawValue)-primary", + costUSD: primaryCost, + totalTokens: 10), + CostUsageDailyReport.ModelBreakdown( + modelName: "fixture-shared-model", + costUSD: sharedCost, + totalTokens: 20), + ] + let entry = CostUsageDailyReport.Entry( + date: "2026-08-05", + inputTokens: nil, + outputTokens: nil, + totalTokens: 30, + costUSD: primaryCost + sharedCost, + modelsUsed: breakdowns.map(\.modelName), + modelBreakdowns: breakdowns) + let snapshot = CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 30, + last30DaysCostUSD: primaryCost + sharedCost, + currencyCode: "USD", + historyDays: 365, + daily: [entry], + updatedAt: Self.now) + return SpendDashboardModel.ProviderInput( + provider: provider, + displayName: "Fixture \(provider.rawValue)", + snapshot: snapshot) + } + + private static let now = Date(timeIntervalSince1970: 1_785_888_000) // 2026-08-05 00:00:00 UTC + private static var calendar: Calendar { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + return calendar + } +} diff --git a/Tests/CodexBarTests/UsageStoreSpendDashboardCodexCostCatchUpTests.swift b/Tests/CodexBarTests/UsageStoreSpendDashboardCodexCostCatchUpTests.swift index 93a5419a75..d3cf7c051f 100644 --- a/Tests/CodexBarTests/UsageStoreSpendDashboardCodexCostCatchUpTests.swift +++ b/Tests/CodexBarTests/UsageStoreSpendDashboardCodexCostCatchUpTests.swift @@ -13,6 +13,8 @@ struct UsageStoreSpendDashboardCodexCostCatchUpTests { Self.account(id: "first", cacheIdentity: "cache-first"), Self.account(id: "second", cacheIdentity: "cache-second"), ] + store.settings.setSpendDashboardHistoryDaysOverride(365) + let expectedHistoryDays = store.settings.effectiveCostUsageHistoryDays let baselineConfiguration = SpendDashboardSource.configuration(settings: store.settings, store: store) var completedCacheIdentities: Set = [] var statusAccounts: [String] = [] @@ -50,7 +52,7 @@ struct UsageStoreSpendDashboardCodexCostCatchUpTests { let replacementConfiguration = SpendDashboardSource.configuration(settings: store.settings, store: store) #expect(statusAccounts == ["first", "second"]) #expect(advancedAccounts == ["first", "second"]) - #expect(receivedHistoryDays == [SpendDashboardSource.scanDays, SpendDashboardSource.scanDays]) + #expect(receivedHistoryDays == [expectedHistoryDays, expectedHistoryDays]) #expect(store.spendDashboardCodexCostCatchUpRevision == 1) #expect(baselineConfiguration.sourceRevisions != replacementConfiguration.sourceRevisions) #expect(store.spendDashboardCodexCostCatchUpActivity?.phase == .complete) @@ -145,6 +147,22 @@ struct UsageStoreSpendDashboardCodexCostCatchUpTests { store.cancelSpendDashboardCodexCostCatchUp() } + @Test + func `changing the dashboard history window restarts catch-up with a new identity`() throws { + let store = try Self.makeStore(suite: "history-window-identity") + let accounts = [Self.account(id: "account", cacheIdentity: "cache-account")] + store.settings.setSpendDashboardHistoryDaysOverride(30) + + store.startSpendDashboardCodexCostCatchUpIfNeeded(accounts: accounts) + let thirtyDayToken = try #require(store.spendDashboardCodexCostCatchUpToken) + store.settings.setSpendDashboardHistoryDaysOverride(365) + store.startSpendDashboardCodexCostCatchUpIfNeeded(accounts: accounts) + + #expect(store.spendDashboardCodexCostCatchUpToken != nil) + #expect(store.spendDashboardCodexCostCatchUpToken != thirtyDayToken) + store.cancelSpendDashboardCodexCostCatchUp() + } + private static func makeStore(suite: String) throws -> UsageStore { let settings = testSettingsStore( suiteName: "UsageStoreSpendDashboardCodexCostCatchUpTests-\(suite)") From c45cd3c83aa664d554706e593205fbaf4b4d300a Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Wed, 5 Aug 2026 09:28:22 -0700 Subject: [PATCH 05/51] Remove duplicate dashboard range test --- .../SpendDashboardControllerTests.swift | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardControllerTests.swift b/Tests/CodexBarTests/SpendDashboardControllerTests.swift index a25e649563..909a4b5b63 100644 --- a/Tests/CodexBarTests/SpendDashboardControllerTests.swift +++ b/Tests/CodexBarTests/SpendDashboardControllerTests.swift @@ -755,32 +755,6 @@ struct SpendDashboardControllerTests { #expect(controller.model.groups.isEmpty) } - @Test - func `range selection persists only supported windows`() throws { - let suite = "SpendDashboardControllerTests-days" - let defaults = try #require(UserDefaults(suiteName: suite)) - defaults.removePersistentDomain(forName: suite) - defer { defaults.removePersistentDomain(forName: suite) } - let controller = SpendDashboardController( - userDefaults: defaults, - requestBuilder: { mode in - Self.request( - configuration: Self.configuration(account: "unused"), - force: mode.forcesLoader) - }) - - #expect(controller.selectedDays == 30) - controller.selectDays(7) - #expect(controller.selectedDays == 7) - #expect(defaults.integer(forKey: "settingsSpendDashboardDays") == 7) - controller.selectDays(30) - #expect(controller.selectedDays == 30) - controller.selectDays(365) - #expect(controller.selectedDays == 365) - controller.selectDays(9) - #expect(controller.selectedDays == 30) - } - private nonisolated static let fixtureNow = Date(timeIntervalSince1970: 1_784_179_200) private static func dashboardController( From 955e9f98905d31e5c8b01ebb3d298e6e1b17d72d Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Wed, 5 Aug 2026 09:34:58 -0700 Subject: [PATCH 06/51] Use provider instance IDs in spend tracking --- Sources/CodexBar/SpendDashboardController.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/CodexBar/SpendDashboardController.swift b/Sources/CodexBar/SpendDashboardController.swift index 4c9102e01e..c352b1c5c3 100644 --- a/Sources/CodexBar/SpendDashboardController.swift +++ b/Sources/CodexBar/SpendDashboardController.swift @@ -463,7 +463,7 @@ enum SpendDashboardSource { settings: SettingsStore, store: UsageStore) -> [SpendDashboardTrackedSource] { - let enabled = Set(store.enabledProvidersForDisplay()) + let enabled = Set(store.enabledFirstPartyProvidersForDisplay()) var sources: [SpendDashboardTrackedSource] = [] for provider in UsageProvider.allCases { @@ -495,13 +495,14 @@ enum SpendDashboardSource { let accounts = settings.tokenAccounts(for: provider) if !accounts.isEmpty { let activeAccountID = settings.effectiveSelectedTokenAccount(for: provider)?.id - let cachedByID = Dictionary(uniqueKeysWithValues: (store.accountSnapshots[provider] ?? []).map { - ($0.id, $0.snapshot != nil) - }) + let cachedByID = Dictionary( + uniqueKeysWithValues: (store.accountSnapshots[provider.instanceID] ?? []).map { + ($0.id, $0.snapshot != nil) + }) sources.append(contentsOf: accounts.map { account in let isActive = account.id == activeAccountID let connected = cachedByID[account.id] == true - || (isActive && store.snapshot(for: provider) != nil) + || (isActive && store.snapshot(for: provider.instanceID) != nil) return SpendDashboardTrackedSource( id: "\(provider.rawValue):account:\(account.id.uuidString.lowercased())", provider: provider, @@ -518,7 +519,7 @@ enum SpendDashboardSource { let hasConfiguredCredential = config?.sanitizedAPIKey != nil || config?.sanitizedSecretKey != nil || config?.sanitizedCookieHeader != nil - let hasLiveSnapshot = store.snapshot(for: provider) != nil + let hasLiveSnapshot = store.snapshot(for: provider.instanceID) != nil || store.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) != nil guard hasConfiguredCredential || hasLiveSnapshot else { continue } sources.append(SpendDashboardTrackedSource( From b4c2ff2fcc12eafd03f408934cf553de2d1cd4c1 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Wed, 5 Aug 2026 09:52:22 -0700 Subject: [PATCH 07/51] Keep Codex unpriced routing unavailable --- Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift b/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift index 86e808922d..dc4e3e6bd4 100644 --- a/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift +++ b/Sources/CodexBar/SpendDashboardModel+ModelBreakdown.swift @@ -119,6 +119,7 @@ extension SpendDashboardModel { } static func canRetainTokenOnlyModelHistory(_ summary: InputSummary) -> Bool { + guard summary.input.provider != .codex else { return false } var sawTokenOnlyModel = false for windowEntry in summary.entries { let entry = windowEntry.entry From e764356224746b85ec43a80334510004a06eb613 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 20:00:56 -0700 Subject: [PATCH 08/51] Keep enabled providers visible in spend dashboard --- .../PreferencesSpendDashboardPane.swift | 19 +++++ .../CodexBar/SpendDashboardController.swift | 53 ++++++++++--- .../SpendDashboardTrackedSourceTests.swift | 75 ++++++++++++++++++- 3 files changed, 135 insertions(+), 12 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 70ebc8fd65..f0f86b1429 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -50,6 +50,14 @@ func codexCostCatchUpProgressText(_ activity: CodexCostCatchUpActivity) -> Strin } func spendDashboardTrackedSourceStatusText(_ source: SpendDashboardTrackedSource) -> String { + switch source.state { + case .needsAttention: + return L("Unavailable") + case .awaitingUsage: + return L("No usage yet") + case .connected, .configured: + break + } if source.contributesCostHistory { return source.state == .connected ? L("Cost history connected") @@ -557,6 +565,14 @@ private struct SpendTrackedSourceRow: View { } private var statusSymbol: String { + switch self.source.state { + case .needsAttention: + return "exclamationmark.triangle.fill" + case .awaitingUsage: + return "clock" + case .connected, .configured: + break + } if self.source.contributesCostHistory { return self.source.state == .connected ? "checkmark.circle.fill" : "clock.fill" } @@ -564,6 +580,9 @@ private struct SpendTrackedSourceRow: View { } private var statusColor: Color { + if self.source.state == .needsAttention { + return .orange + } if self.source.contributesCostHistory { return self.source.state == .connected ? .green : .orange } diff --git a/Sources/CodexBar/SpendDashboardController.swift b/Sources/CodexBar/SpendDashboardController.swift index c352b1c5c3..c7e234cab9 100644 --- a/Sources/CodexBar/SpendDashboardController.swift +++ b/Sources/CodexBar/SpendDashboardController.swift @@ -7,6 +7,8 @@ struct SpendDashboardTrackedSource: Identifiable, Equatable, Sendable { enum State: Equatable, Sendable { case connected case configured + case needsAttention + case awaitingUsage } let id: String @@ -463,7 +465,11 @@ enum SpendDashboardSource { settings: SettingsStore, store: UsageStore) -> [SpendDashboardTrackedSource] { - let enabled = Set(store.enabledFirstPartyProvidersForDisplay()) + // Tracked access reflects user enablement, not refresh-time availability. Providers with no + // current snapshot still need a row so the dashboard can explain their connection state. + let enabled = Set(UsageProvider.allCases.filter { provider in + settings.isProviderEnabled(provider: provider, metadata: store.metadata(for: provider)) + }) var sources: [SpendDashboardTrackedSource] = [] for provider in UsageProvider.allCases { @@ -473,18 +479,24 @@ enum SpendDashboardSource { if provider == .codex { let accounts = settings.codexVisibleAccountProjection.visibleAccounts let snapshotsByID = Dictionary(uniqueKeysWithValues: store.codexAccountSnapshots.map { - ($0.id, $0.snapshot != nil) + ($0.id, $0) }) if !accounts.isEmpty { sources.append(contentsOf: accounts.map { account in - let connected = snapshotsByID[account.id] == true + let accountSnapshot = snapshotsByID[account.id] + let connected = accountSnapshot?.snapshot != nil || (account.isActive && store.snapshot(for: .codex) != nil) + let hasError = accountSnapshot?.error != nil + || (account.isActive && store.error(for: .codex) != nil) return SpendDashboardTrackedSource( id: "codex:\(account.id)", provider: provider, providerName: providerName, accountName: self.trackedAccountName(account.email, providerName: providerName), - state: connected ? .connected : .configured, + state: self.trackedSourceState( + hasLiveSnapshot: connected, + hasConfiguredCredential: true, + hasError: hasError), supportsCostHistory: supportsCostHistory, contributesCostHistory: supportsCostHistory && enabled.contains(provider)) }) @@ -497,18 +509,24 @@ enum SpendDashboardSource { let activeAccountID = settings.effectiveSelectedTokenAccount(for: provider)?.id let cachedByID = Dictionary( uniqueKeysWithValues: (store.accountSnapshots[provider.instanceID] ?? []).map { - ($0.id, $0.snapshot != nil) + ($0.id, $0) }) sources.append(contentsOf: accounts.map { account in let isActive = account.id == activeAccountID - let connected = cachedByID[account.id] == true + let accountSnapshot = cachedByID[account.id] + let connected = accountSnapshot?.snapshot != nil || (isActive && store.snapshot(for: provider.instanceID) != nil) + let hasError = accountSnapshot?.error != nil + || (isActive && (store.error(for: provider) != nil || store.tokenError(for: provider) != nil)) return SpendDashboardTrackedSource( id: "\(provider.rawValue):account:\(account.id.uuidString.lowercased())", provider: provider, providerName: providerName, accountName: self.trackedAccountName(account.displayName, providerName: providerName), - state: connected ? .connected : .configured, + state: self.trackedSourceState( + hasLiveSnapshot: connected, + hasConfiguredCredential: true, + hasError: hasError), supportsCostHistory: supportsCostHistory, contributesCostHistory: supportsCostHistory && isActive && enabled.contains(provider)) }) @@ -520,14 +538,18 @@ enum SpendDashboardSource { || config?.sanitizedSecretKey != nil || config?.sanitizedCookieHeader != nil let hasLiveSnapshot = store.snapshot(for: provider.instanceID) != nil - || store.tokenSnapshotPublicationForCurrentProviderConfig(for: provider) != nil - guard hasConfiguredCredential || hasLiveSnapshot else { continue } + || store.tokenSnapshotPublicationForCurrentProviderConfig(for: provider)?.snapshot != nil + guard enabled.contains(provider) || hasConfiguredCredential || hasLiveSnapshot else { continue } + let hasError = store.error(for: provider) != nil || store.tokenError(for: provider) != nil sources.append(SpendDashboardTrackedSource( id: "\(provider.rawValue):current", provider: provider, providerName: providerName, accountName: nil, - state: hasLiveSnapshot ? .connected : .configured, + state: self.trackedSourceState( + hasLiveSnapshot: hasLiveSnapshot, + hasConfiguredCredential: hasConfiguredCredential, + hasError: hasError), supportsCostHistory: supportsCostHistory, contributesCostHistory: supportsCostHistory && enabled.contains(provider))) } @@ -535,6 +557,17 @@ enum SpendDashboardSource { return sources } + private static func trackedSourceState( + hasLiveSnapshot: Bool, + hasConfiguredCredential: Bool, + hasError: Bool) -> SpendDashboardTrackedSource.State + { + if hasError { return .needsAttention } + if hasLiveSnapshot { return .connected } + if hasConfiguredCredential { return .configured } + return .awaitingUsage + } + private static func trackedAccountName(_ rawValue: String?, providerName: String) -> String? { guard let value = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty, diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index dfe982ef87..cae5ce4965 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -5,6 +5,57 @@ import Testing @testable import CodexBar struct SpendDashboardTrackedSourceTests { + @Test + @MainActor + func `tracked access keeps enabled providers visible across connection states`() throws { + let settings = testSettingsStore(suiteName: "SpendDashboardTrackedSourceTests-enabled") + let metadata = ProviderRegistry.shared.metadata + for provider in UsageProvider.allCases { + try settings.setProviderEnabled( + provider: provider, + metadata: #require(metadata[provider]), + enabled: [.cursor, .gemini, .grok, .openrouter].contains(provider)) + } + settings.addTokenAccount(provider: .cursor, label: "Work", token: "fixture") + + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + store._setSnapshotForTesting( + UsageSnapshot( + primary: RateWindow( + usedPercent: 10, + windowMinutes: nil, + resetsAt: nil, + resetDescription: nil), + secondary: nil, + updatedAt: Date()), + provider: .grok) + store._setSnapshotForTesting( + UsageSnapshot( + primary: RateWindow( + usedPercent: 20, + windowMinutes: nil, + resetsAt: nil, + resetDescription: nil), + secondary: nil, + updatedAt: Date()), + provider: .gemini) + store._setErrorForTesting("Not logged in", provider: .gemini) + + let sources = SpendDashboardSource.trackedSources(settings: settings, store: store) + let rows = Dictionary(uniqueKeysWithValues: sources.map { ($0.provider, $0) }) + + #expect(Set(rows.keys) == [.cursor, .gemini, .grok, .openrouter]) + #expect(rows[.grok]?.state == .connected) + #expect(rows[.gemini]?.state == .needsAttention) + #expect(rows[.openrouter]?.state == .awaitingUsage) + #expect(rows.values.filter(\.contributesCostHistory).map(\.provider) == [.cursor]) + } + @Test @MainActor func `tracked access includes every saved provider credential without inventing cost coverage`() { @@ -49,6 +100,26 @@ struct SpendDashboardTrackedSourceTests { contributesCostHistory: false) #expect(spendDashboardTrackedSourceStatusText(source) == "Usage connected · not in cost total") + + let attention = SpendDashboardTrackedSource( + id: "gemini:current", + provider: .gemini, + providerName: "Gemini", + accountName: nil, + state: .needsAttention, + supportsCostHistory: false, + contributesCostHistory: false) + #expect(spendDashboardTrackedSourceStatusText(attention) == "Unavailable") + + let setup = SpendDashboardTrackedSource( + id: "openrouter:current", + provider: .openrouter, + providerName: "OpenRouter", + accountName: nil, + state: .awaitingUsage, + supportsCostHistory: false, + contributesCostHistory: false) + #expect(spendDashboardTrackedSourceStatusText(setup) == "No usage yet") } @Test @@ -115,7 +186,7 @@ struct SpendDashboardTrackedSourceTests { provider: .openrouter, providerName: "OpenRouter", accountName: "Research", - state: .connected, + state: .awaitingUsage, supportsCostHistory: false, contributesCostHistory: false), SpendDashboardTrackedSource( @@ -131,7 +202,7 @@ struct SpendDashboardTrackedSourceTests { provider: .gemini, providerName: "Gemini", accountName: "Studio", - state: .connected, + state: .needsAttention, supportsCostHistory: false, contributesCostHistory: false), SpendDashboardTrackedSource( From 7cb08b30da4c8921e4b5637faec158a689305786 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 20:24:59 -0700 Subject: [PATCH 09/51] Show partial spend and compact overview cards --- .../PreferencesSpendDashboardPane.swift | 24 +++- Sources/CodexBar/SpendDashboardModel.swift | 15 ++ .../StatusItemController+MenuTypes.swift | 134 ++++++++++++++---- .../SpendDashboardModelTests.swift | 7 + .../UsageMenuCardLayoutTests.swift | 82 +++++++++-- 5 files changed, 216 insertions(+), 46 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index f0f86b1429..9bc0ff951d 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -68,6 +68,17 @@ func spendDashboardTrackedSourceStatusText(_ source: SpendDashboardTrackedSource : L("Configured · not in cost total") } +func spendDashboardAggregateCostText(_ group: SpendDashboardModel.CurrencyGroup) -> String { + guard let cost = group.totalCost ?? group.knownCost else { return L("Spend unavailable") } + let formatted = UsageFormatter.currencyString(cost, currencyCode: group.currencyCode) + return group.totalCost == nil ? "≥ \(formatted)" : formatted +} + +func spendDashboardCostCoverageText(_ group: SpendDashboardModel.CurrencyGroup) -> String { + "\(codexBarLocalizedInteger(group.knownCostProviderCount)) / " + + "\(codexBarLocalizedInteger(group.providers.count)) \(L("Accounts"))" +} + enum SpendDashboardModelHistoryPresentation: Equatable { case unavailable case empty @@ -616,9 +627,7 @@ private struct SpendCurrencySection: View { Text(self.group.currencyCode) .font(.headline) Spacer() - Text(self.group.totalCost.map { - UsageFormatter.currencyString($0, currencyCode: self.group.currencyCode) - } ?? L("Spend unavailable")) + Text(spendDashboardAggregateCostText(self.group)) .font(.title3.weight(.semibold)) .monospacedDigit() } @@ -635,16 +644,16 @@ private struct SpendCurrencySection: View { HStack(spacing: 24) { SpendSummaryValue( title: L("Estimated spend"), - value: self.group.totalCost.map { - UsageFormatter.currencyString($0, currencyCode: self.group.currencyCode) - } ?? "—") + value: spendDashboardAggregateCostText(self.group)) SpendSummaryValue( title: L("Tracked tokens"), value: self.group.totalTokens.map(UsageFormatter.tokenCountString) ?? "—") + SpendSummaryValue( + title: L("Coverage"), + value: spendDashboardCostCoverageText(self.group)) SpendSummaryValue( title: L("Subscriptions"), value: codexBarLocalizedInteger(self.group.providers.count)) - Spacer() } } @@ -668,6 +677,7 @@ private struct SpendSummaryValue: View { .font(.system(.title2, design: .rounded, weight: .semibold)) .monospacedDigit() } + .frame(maxWidth: .infinity, alignment: .leading) } } diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index aad74fe13c..7b0d1f5aeb 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -94,6 +94,21 @@ struct SpendDashboardModel: Equatable, Sendable { var id: String { self.currencyCode } + + var knownCostProviderCount: Int { + self.providers.count { $0.totalCost != nil } + } + + var knownCost: Double? { + let costs = self.providers.compactMap(\.totalCost) + guard !costs.isEmpty else { return nil } + var total = 0.0 + for cost in costs { + total += cost + guard total.isFinite else { return nil } + } + return total + } } let requestedDays: Int diff --git a/Sources/CodexBar/StatusItemController+MenuTypes.swift b/Sources/CodexBar/StatusItemController+MenuTypes.swift index 47ed1a890b..f5094e714e 100644 --- a/Sources/CodexBar/StatusItemController+MenuTypes.swift +++ b/Sources/CodexBar/StatusItemController+MenuTypes.swift @@ -35,48 +35,128 @@ extension ProviderSwitcherSelection { struct OverviewMenuCardRowView: View { static let showsSectionDividers = false + static let rowHeight: CGFloat = 88 + static let accessibilityRowHeight: CGFloat = 112 let model: UsageMenuCardView.Model let storageText: String? let width: CGFloat @Environment(\.menuItemHighlighted) private var isHighlighted + @Environment(\.menuCardRefreshMonitor) private var refreshMonitor + @Environment(\.dynamicTypeSize) private var dynamicTypeSize var body: some View { - VStack(alignment: .leading, spacing: 0) { - UsageMenuCardHeaderSectionView( - model: self.model, - showDivider: Self.showsSectionDividers && self.hasUsageBlock, - width: self.width) - if self.hasUsageBlock { - UsageMenuCardUsageSectionView( - model: self.model, - showBottomDivider: false, - bottomPadding: 6, - width: self.width, - showsSectionDividers: Self.showsSectionDividers) + let liveModel = self.liveModel + let liveSubtitle = Self.liveSubtitle(for: liveModel, refreshMonitor: self.refreshMonitor) + let metric = Self.primaryMetric(for: liveModel) + let prioritizesStatus = Self.prioritizesStatus(for: liveSubtitle.style) + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline, spacing: 10) { + Text(liveModel.providerName) + .font(.headline.weight(.semibold)) + .lineLimit(1) + Spacer(minLength: 8) + Text(liveModel.email) + .font(.caption) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + .lineLimit(1) + .truncationMode(.middle) } - if let storageText { - HStack(alignment: .firstTextBaseline, spacing: 4) { - Text("\(L("Storage")):") - .font(.footnote) - .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) - Text(storageText) - .font(.footnote) - .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + + if prioritizesStatus { + Text(liveSubtitle.text) + .font(.subheadline.weight(.medium)) + .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) + .lineLimit(self.dynamicTypeSize.isAccessibilitySize ? 2 : 1) + .fixedSize(horizontal: false, vertical: true) + } else if let metric { + let presentation = metric.linePresentation( + title: UsageMenuCardView.popupMetricTitle(provider: liveModel.provider, metric: metric)) + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(metric.statusText ?? presentation.titleText) + .font(.subheadline.weight(.medium)) .lineLimit(1) - Spacer() + Spacer(minLength: 8) + if let resetText = presentation.resetText, metric.statusText == nil { + Text(resetText) + .font(.caption2) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + .lineLimit(1) + } + } + if metric.statusText == nil { + UsageProgressBar( + percent: metric.percent, + tint: liveModel.progressColor, + accessibilityLabel: metric.percentStyle.accessibilityLabel) } - .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) - .padding(.top, self.hasUsageBlock ? 0 : 8) - .padding(.bottom, 6) - .frame(width: self.width, alignment: .leading) + } else { + Text(Self.spendReference(for: liveModel) ?? liveModel.subtitleText) + .font(.subheadline.weight(.medium)) + .lineLimit(1) } + + HStack(alignment: .firstTextBaseline, spacing: 6) { + if metric != nil || prioritizesStatus { + Text(Self.spendReference(for: liveModel) ?? liveModel.subtitleText) + .lineLimit(1) + .truncationMode(.tail) + } + Spacer(minLength: 4) + if let storageText { + Text("\(L("Storage")): \(storageText)") + .lineLimit(1) + } + } + .font(.caption) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) } + .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) + .padding(.vertical, 7) .frame(width: self.width, alignment: .leading) + .frame(minHeight: Self.rowHeight(for: self.dynamicTypeSize), alignment: .leading) } - private var hasUsageBlock: Bool { - self.model.hasUsageContent + static func primaryMetric(for model: UsageMenuCardView.Model) -> UsageMenuCardView.Model.Metric? { + model.metrics.first + } + + static func prioritizesStatus(for style: UsageMenuCardView.Model.SubtitleStyle) -> Bool { + style != .info + } + + static func rowHeight(for dynamicTypeSize: DynamicTypeSize) -> CGFloat { + dynamicTypeSize.isAccessibilitySize ? self.accessibilityRowHeight : self.rowHeight + } + + static func spendReference(for model: UsageMenuCardView.Model) -> String? { + model.providerCost?.spendLine + ?? model.tokenUsage?.monthLine + ?? model.creditsText + ?? model.placeholder + } + + static func liveSubtitle( + for model: UsageMenuCardView.Model, + refreshMonitor: MenuCardRefreshMonitor?) -> MenuCardLiveSubtitle + { + let fallback = MenuCardLiveSubtitle(text: model.subtitleText, style: model.subtitleStyle) + guard model.usesLiveSubtitle else { return fallback } + return refreshMonitor?.subtitle(for: model.provider, fallback: fallback) ?? fallback + } + + private var liveModel: UsageMenuCardView.Model { + guard self.model.usesLiveSubtitle else { return self.model } + return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model + } + + private func subtitleColor(for style: UsageMenuCardView.Model.SubtitleStyle) -> Color { + switch style { + case .error: + MenuHighlightStyle.error(self.isHighlighted) + case .info, .loading: + MenuHighlightStyle.secondary(self.isHighlighted) + } } } diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index a780a3a1dc..1e6ba01553 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -240,12 +240,16 @@ struct SpendDashboardModelTests { calendar: Self.calendar).groups.first) #expect(group.totalCost == nil) + #expect(group.knownCost == 4) + #expect(group.knownCostProviderCount == 1) #expect(group.totalTokens == nil) #expect(group.modelHistoryCompleteness == .incomplete) #expect(group.models.map(\.provider) == [.claude]) #expect(group.models.map(\.modelName) == ["test-model"]) #expect(group.models.map(\.totalCost) == [4]) #expect(spendDashboardModelHistoryPresentation(group) == .partial) + #expect(spendDashboardAggregateCostText(group) == "≥ $4.00") + #expect(spendDashboardCostCoverageText(group) == "1 / 2 Accounts") } @Test @@ -374,6 +378,9 @@ struct SpendDashboardModelTests { #expect(group.providers.first(where: { $0.id == "invalid" })?.totalCost == nil) #expect(group.totalCost == nil) + #expect(group.knownCost == nil) + #expect(group.knownCostProviderCount == 2) + #expect(spendDashboardAggregateCostText(group) == "Spend unavailable") #expect(group.totalTokens == nil) #expect(group.dailyPoints.isEmpty) } diff --git a/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift b/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift index 8a6e711d40..0288c31c5c 100644 --- a/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift +++ b/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift @@ -31,7 +31,7 @@ struct UsageMenuCardLayoutTests { } @Test - func `full provider card matches overview height`() { + func `overview uses a fixed compact card instead of the full provider detail height`() { let model = Self.model(metrics: [ UsageMenuCardView.Model.Metric( id: "session", @@ -44,24 +44,80 @@ struct UsageMenuCardLayoutTests { detailRightText: "Lasts until reset", pacePercent: nil, paceOnTop: true), + UsageMenuCardView.Model.Metric( + id: "weekly", + title: "Weekly", + percent: 52, + percentStyle: .left, + resetText: "Resets Friday", + detailText: nil, + detailLeftText: nil, + detailRightText: nil, + pacePercent: nil, + paceOnTop: true), ]) let width: CGFloat = 296 let fullCardSize = NSHostingController(rootView: UsageMenuCardView(model: model, width: width)) .sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) - let overviewStyleSize = NSHostingController(rootView: UsageMenuCardHeaderAndUsageSectionView( + let overviewSize = NSHostingController(rootView: OverviewMenuCardRowView( model: model, - layoutModel: model, - bottomPadding: UsageMenuCardLayout.sectionBottomPadding, + storageText: nil, width: width)) .sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) + let emptyOverviewSize = NSHostingController(rootView: OverviewMenuCardRowView( + model: Self.model(), + storageText: nil, + width: width)) + .sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) + + #expect(overviewSize.height == OverviewMenuCardRowView.rowHeight) + #expect(emptyOverviewSize.height == OverviewMenuCardRowView.rowHeight) + #expect(overviewSize.height < fullCardSize.height) + #expect(OverviewMenuCardRowView.primaryMetric(for: model)?.id == "session") + } + + @Test + func `overview prioritizes refresh status and expands for accessibility text`() { + let metric = UsageMenuCardView.Model.Metric( + id: "weekly", + title: "Weekly", + percent: 52, + percentStyle: .left, + resetText: "Resets Friday", + detailText: nil, + detailLeftText: nil, + detailRightText: nil, + pacePercent: nil, + paceOnTop: true) + var configuredErrorModel = Self.model( + metrics: [metric], + subtitleText: "Could not refresh usage", + subtitleStyle: .error) + configuredErrorModel.usesLiveSubtitle = true + let errorModel = configuredErrorModel + let width: CGFloat = 296 + let monitor = MenuCardRefreshMonitor( + resolveModel: { _ in errorModel }, + isProviderRefreshActive: { _ in true }) + monitor.beginManualRefresh(frozenModels: [.codex: errorModel]) + let refreshSubtitle = OverviewMenuCardRowView.liveSubtitle( + for: errorModel, + refreshMonitor: monitor) - #expect(UsageMenuCardLayout.postHeaderDividerContentSpacing == 16) - #expect(UsageMenuCardLayout.headerOnlyVerticalPadding == 6) - #expect(UsageMenuCardLayout.sectionTopPadding == 6) - #expect(UsageMenuCardLayout.sectionBottomPadding == 6) + let accessibilitySize = NSHostingController(rootView: OverviewMenuCardRowView( + model: errorModel, + storageText: nil, + width: width) + .environment(\.dynamicTypeSize, .accessibility5)) + .sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) - #expect(abs(fullCardSize.height - overviewStyleSize.height) < Self.heightTolerance) + #expect(refreshSubtitle.text == "Refreshing…") + #expect(refreshSubtitle.style == .loading) + #expect(OverviewMenuCardRowView.prioritizesStatus(for: refreshSubtitle.style)) + #expect(OverviewMenuCardRowView.primaryMetric(for: errorModel)?.id == "weekly") + #expect(accessibilitySize.height >= OverviewMenuCardRowView.accessibilityRowHeight) + #expect(accessibilitySize.height > OverviewMenuCardRowView.rowHeight) } @Test @@ -185,14 +241,16 @@ struct UsageMenuCardLayoutTests { metrics: [UsageMenuCardView.Model.Metric] = [], usageNotes: [String] = [], creditsText: String? = nil, - placeholder: String? = nil) -> UsageMenuCardView.Model + placeholder: String? = nil, + subtitleText: String = "Not fetched yet", + subtitleStyle: UsageMenuCardView.Model.SubtitleStyle = .info) -> UsageMenuCardView.Model { UsageMenuCardView.Model( provider: .codex, providerName: "Codex", email: "steipete@gmail.com", - subtitleText: "Not fetched yet", - subtitleStyle: .info, + subtitleText: subtitleText, + subtitleStyle: subtitleStyle, planText: "Pro 20x", metrics: metrics, usageNotes: usageNotes, From 1483eda0676a9b67b8e2e0d23e4c21b0868b0784 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 21:47:21 -0700 Subject: [PATCH 10/51] Show all-provider spend in Overview --- .../PreferencesSpendDashboardPane.swift | 21 +- Sources/CodexBar/SettingsStore.swift | 3 +- Sources/CodexBar/ShareStatsCardView.swift | 15 +- Sources/CodexBar/ShareStatsPayload.swift | 71 ++++++- .../CodexBar/StatusItemController+Menu.swift | 78 ++++++- .../StatusItemController+MenuTypes.swift | 192 +++++++++++++++++- .../StatusItemController+OverviewScroll.swift | 12 +- Tests/CodexBarTests/SettingsStoreTests.swift | 11 +- Tests/CodexBarTests/ShareStatsTests.swift | 48 ++++- .../SpendDashboardModelTests.swift | 2 +- .../StatusMenuOverviewScrollTests.swift | 23 ++- Tests/CodexBarTests/StatusMenuTests.swift | 3 +- .../UsageMenuCardLayoutTests.swift | 116 +++++++++++ .../spend-dashboard-proof/README.md | 21 +- .../overview-all-providers.png | Bin 0 -> 38696 bytes .../share-all-providers.png | Bin 0 -> 64974 bytes 16 files changed, 572 insertions(+), 44 deletions(-) create mode 100644 docs/screenshots/spend-dashboard-proof/overview-all-providers.png create mode 100644 docs/screenshots/spend-dashboard-proof/share-all-providers.png diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 9bc0ff951d..1fd0b74fee 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -71,7 +71,7 @@ func spendDashboardTrackedSourceStatusText(_ source: SpendDashboardTrackedSource func spendDashboardAggregateCostText(_ group: SpendDashboardModel.CurrencyGroup) -> String { guard let cost = group.totalCost ?? group.knownCost else { return L("Spend unavailable") } let formatted = UsageFormatter.currencyString(cost, currencyCode: group.currencyCode) - return group.totalCost == nil ? "≥ \(formatted)" : formatted + return group.totalCost == nil ? "~\(formatted)" : formatted } func spendDashboardCostCoverageText(_ group: SpendDashboardModel.CurrencyGroup) -> String { @@ -621,6 +621,21 @@ private struct SpendCurrencySection: View { let group: SpendDashboardModel.CurrencyGroup let requestedDays: Int + var body: some View { + VStack(alignment: .leading, spacing: 12) { + SpendCurrencySummaryView(group: self.group, requestedDays: self.requestedDays) + + SpendProviderPanel(group: self.group) + SpendModelPanel(group: self.group) + SpendDailyChart(group: self.group) + } + } +} + +struct SpendCurrencySummaryView: View { + let group: SpendDashboardModel.CurrencyGroup + let requestedDays: Int + var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(alignment: .firstTextBaseline) { @@ -656,10 +671,6 @@ private struct SpendCurrencySection: View { value: codexBarLocalizedInteger(self.group.providers.count)) } } - - SpendProviderPanel(group: self.group) - SpendModelPanel(group: self.group) - SpendDailyChart(group: self.group) } } } diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index afbe76308b..65e8597a70 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -188,7 +188,8 @@ enum CodexAccountMenuProjectionRevalidationResult: Equatable { @Observable final class SettingsStore { static let sharedDefaults = AppGroupSupport.sharedDefaults() - static let mergedOverviewProviderLimit = 6 + static let mergedOverviewProviderLimit = UsageProvider.allCases.count + static let mergedOverviewLegacyWheelNavigationLimit = 6 static let productionCodexAccountReconciliationSnapshotCacheInterval: TimeInterval = 2 static let isRunningTests: Bool = { let env = ProcessInfo.processInfo.environment diff --git a/Sources/CodexBar/ShareStatsCardView.swift b/Sources/CodexBar/ShareStatsCardView.swift index 1cd70ccfe2..5fc5666ce2 100644 --- a/Sources/CodexBar/ShareStatsCardView.swift +++ b/Sources/CodexBar/ShareStatsCardView.swift @@ -72,7 +72,9 @@ struct ShareStatsCardView: View { .font(.system(size: 20, weight: .semibold, design: .rounded)) .tracking(1.8) .foregroundStyle(self.secondary) - Text(self.payload.totalTokens.map(ShareStatsFormatting.compactCount) ?? "—") + Text(self.payload.totalTokens.map { + "\(self.payload.totalTokensIsPartial ? "~" : "")\(ShareStatsFormatting.compactCount($0))" + } ?? "—") .font(.system(size: 104, weight: .semibold, design: .rounded)) .monospacedDigit() .lineLimit(1) @@ -92,7 +94,8 @@ struct ShareStatsCardView: View { .foregroundStyle(self.secondary) Spacer() Text(currency.estimatedCost.map { - ShareStatsFormatting.currency($0, code: currency.currencyCode) + let value = ShareStatsFormatting.currency($0, code: currency.currencyCode) + return "\(currency.isPartial ? "~" : "")\(value)" } ?? "Unavailable") .font(.system(size: 32, weight: .semibold, design: .rounded)) .monospacedDigit() @@ -112,8 +115,12 @@ struct ShareStatsCardView: View { private var currencySummary: String { let hiddenCount = self.payload.currencies.count - min(self.payload.currencies.count, 2) return hiddenCount > 0 - ? "+\(hiddenCount) more currencies · see subscription rows" - : "\(self.payload.providers.count) subscriptions · native currencies kept separate" + ? "\(self.spendCoverage) · +\(hiddenCount) more currencies" + : "\(self.spendCoverage) · native currencies kept separate" + } + + private var spendCoverage: String { + "\(self.payload.spendReportingProviderCount)/\(self.payload.providers.count) report spend" } private var rankings: some View { diff --git a/Sources/CodexBar/ShareStatsPayload.swift b/Sources/CodexBar/ShareStatsPayload.swift index 8a8e2945e9..bfdd2354c6 100644 --- a/Sources/CodexBar/ShareStatsPayload.swift +++ b/Sources/CodexBar/ShareStatsPayload.swift @@ -11,6 +11,12 @@ struct ShareStatsProviderPayload: Sendable, Equatable { let coveredDayCount: Int } +struct ShareStatsProviderRosterEntry: Sendable, Equatable { + let provider: UsageProvider + let providerName: String + let currencyCode: String +} + struct ShareStatsModelPayload: Sendable, Equatable { let provider: UsageProvider let providerName: String @@ -85,6 +91,7 @@ struct ShareStatsCurrencyPayload: Sendable, Equatable, Identifiable { let currencyCode: String let estimatedCost: Double? let coveredDayCount: Int + var isPartial = false var id: String { self.currencyCode @@ -98,12 +105,17 @@ struct ShareStatsPayload: Sendable, Equatable { let topModels: [ShareStatsModelPayload] let currencies: [ShareStatsCurrencyPayload] let totalTokens: Int? + let totalTokensIsPartial: Bool var hasShareableData: Bool { !self.providers.isEmpty && self.providers.contains { provider in provider.totalTokens != nil || provider.estimatedCost != nil } } + + var spendReportingProviderCount: Int { + self.providers.count { $0.estimatedCost != nil } + } } struct ShareStatsSubscriptionName: Sendable, Equatable { @@ -221,9 +233,10 @@ enum ShareStatsSanitizer { enum ShareStatsBuilder { static func make( model: SpendDashboardModel, - subscriptionNames: [String: ShareStatsSubscriptionName] = [:]) -> ShareStatsPayload? + subscriptionNames: [String: ShareStatsSubscriptionName] = [:], + providerRoster: [ShareStatsProviderRosterEntry] = []) -> ShareStatsPayload? { - let providers = model.groups.flatMap { group in + let trackedProviders = model.groups.flatMap { group in group.providers.map { row in ShareStatsProviderPayload( provider: row.provider, @@ -235,6 +248,26 @@ enum ShareStatsBuilder { coveredDayCount: row.coveredDayCount) } } + let providers: [ShareStatsProviderPayload] + if providerRoster.isEmpty { + providers = trackedProviders + } else { + let trackedByProvider = trackedProviders.reduce(into: [UsageProvider: ShareStatsProviderPayload]()) { + if $0[$1.provider] == nil { + $0[$1.provider] = $1 + } + } + providers = providerRoster.map { entry in + trackedByProvider[entry.provider] ?? ShareStatsProviderPayload( + provider: entry.provider, + providerName: entry.providerName, + subscriptionName: nil, + currencyCode: entry.currencyCode, + totalTokens: nil, + estimatedCost: nil, + coveredDayCount: 0) + } + } let sanitizedModels = model.groups.filter { $0.modelHistoryCompleteness == .complete }.flatMap { group in @@ -278,13 +311,18 @@ enum ShareStatsBuilder { return lhs.modelName < rhs.modelName } } + let rosterHasUntrackedProviders = providers.count > trackedProviders.count let currencies = model.groups.map { ShareStatsCurrencyPayload( currencyCode: $0.currencyCode, - estimatedCost: self.finiteCost($0.totalCost), - coveredDayCount: $0.coveredDayCount) + estimatedCost: self.finiteCost($0.totalCost ?? $0.knownCost), + coveredDayCount: $0.coveredDayCount, + isPartial: rosterHasUntrackedProviders || $0.totalCost == nil) } - let totalTokens = self.combinedTotalTokens(model.groups.map(\.totalTokens)) + let knownTokenValues = trackedProviders.compactMap(\.totalTokens) + let totalTokens = self.safeTokenSum(knownTokenValues) + let totalTokensIsPartial = knownTokenValues.count < providers.count || + model.groups.contains { $0.totalTokens == nil } let periodEnd = model.groups.map(\.chartDomain.upperBound).max() ?? Date() let payload = ShareStatsPayload( days: model.requestedDays, @@ -292,7 +330,8 @@ enum ShareStatsBuilder { providers: providers, topModels: topModels, currencies: currencies, - totalTokens: totalTokens) + totalTokens: totalTokens, + totalTokensIsPartial: totalTokensIsPartial) return payload.hasShareableData ? payload : nil } @@ -311,6 +350,16 @@ enum ShareStatsBuilder { } return total } + + private static func safeTokenSum(_ values: [Int]) -> Int? { + var total = 0 + for value in values { + let result = total.addingReportingOverflow(value) + guard !result.overflow else { return nil } + total = result.partialValue + } + return values.isEmpty ? nil : total + } } enum ShareStatsFormatting { @@ -345,10 +394,16 @@ enum ShareStatsFormatting { static func text(_ payload: ShareStatsPayload) -> String { var lines = ["My AI subscriptions · last \(payload.days) days"] if let tokens = payload.totalTokens { - lines.append("\(self.compactCount(tokens)) tracked tokens") + let value = payload.totalTokensIsPartial ? "~\(self.compactCount(tokens))" : self.compactCount(tokens) + lines.append("\(value) tracked tokens") } + lines.append( + "\(payload.spendReportingProviderCount)/\(payload.providers.count) connected services report spend") lines.append(contentsOf: payload.currencies.map { currency in - let spend = currency.estimatedCost.map { "\(self.currency($0, code: currency.currencyCode)) estimated" } + let spend = currency.estimatedCost.map { + let value = self.currency($0, code: currency.currencyCode) + return "\(currency.isPartial ? "~" : "")\(value) estimated" + } ?? "Spend unavailable" return "\(currency.currencyCode): \(spend) · " + "coverage \(currency.coveredDayCount)/\(payload.days) days" diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index a0780767b5..e7ea651193 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -573,12 +573,54 @@ extension StatusItemController { guard !model.isOverviewErrorOnly else { return nil } return (provider: provider, model: model) } - guard !rows.isEmpty else { return false } - let t0 = CACurrentMediaTime() defer { self.logChartRenderDurationIfSlow("addOverviewRows(\(rows.count))", startedAt: t0) } + let spendModel = self.overviewSpendDashboardModel(providers: enabledProviders) + let spendSummary = OverviewSpendSummary( + model: spendModel, + connectedProviderCount: enabledProviders.count) + let fallbackCurrencyCode = spendModel.groups.first?.currencyCode ?? "USD" + let sharePayload = ShareStatsBuilder.make( + model: spendModel, + providerRoster: enabledProviders.map { + ShareStatsProviderRosterEntry( + provider: $0, + providerName: ProviderDefaults.metadata[$0]?.displayName ?? $0.rawValue, + currencyCode: fallbackCurrencyCode) + }) + let summaryItem = self.makeMenuCardItem( + OverviewSpendSummaryCardView( + summary: spendSummary, + days: spendModel.requestedDays, + width: menuWidth, + canShare: sharePayload != nil, + share: { [weak interactionMenu] in + guard let sharePayload else { return } + interactionMenu?.cancelTracking() + DispatchQueue.main.async { + ShareStatsPresenter.shared.present(payload: sharePayload) + } + }), + id: "overviewSpendSummary", + width: menuWidth, + heightCacheScope: "overviewSpendSummary", + heightCacheFingerprint: [ + spendSummary.primarySpendText, + spendSummary.coverageText, + spendSummary.tokenText ?? "", + ].joined(separator: "|"), + containsInteractiveControls: sharePayload != nil) + menu.addItem(summaryItem) + menu.addItem(.separator()) + + guard !rows.isEmpty else { + self.addOverviewEmptyState(to: menu, enabledProviders: enabledProviders) + return true + } + for (index, row) in rows.enumerated() { + let emphasis: OverviewMenuCardRowView.Emphasis = index == 0 ? .prominent : .compact let identifier = "\(Self.overviewRowIdentifierPrefix)\(row.provider.rawValue)" let storageText = self.store.storageFootprintText(for: row.provider) let submenu = self.makeOverviewRowSubmenu( @@ -586,13 +628,20 @@ extension StatusItemController { model: row.model, width: menuWidth) let item = self.makeMenuCardItem( - OverviewMenuCardRowView(model: row.model, storageText: storageText, width: menuWidth), + OverviewMenuCardRowView( + model: row.model, + storageText: storageText, + width: menuWidth, + emphasis: emphasis), id: identifier, width: menuWidth, heightCacheScope: row.provider.rawValue, heightCacheFingerprint: row.model.heightFingerprint( section: "overview", - additional: [UsageMenuCardView.Model.heightFingerprintField("storage", storageText)]), + additional: [ + UsageMenuCardView.Model.heightFingerprintField("storage", storageText), + "emphasis=\(emphasis == .prominent ? "prominent" : "compact")", + ]), submenu: submenu, containsInteractiveControls: row.model.subtitleStyle == .error || row.model.usesLiveSubtitle, usesGPUSelection: true, @@ -606,13 +655,32 @@ extension StatusItemController { item.action = #selector(self.selectOverviewProvider(_:)) } menu.addItem(item) - if index < rows.count - 1 { + if index == 0, rows.count > 1 { menu.addItem(.separator()) } } return true } + private func overviewSpendDashboardModel(providers: [UsageProvider]) -> SpendDashboardModel { + let inputs = providers.compactMap { provider -> SpendDashboardModel.ProviderInput? in + guard let snapshot = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot else { + return nil + } + return SpendDashboardModel.ProviderInput( + provider: provider, + displayName: ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue, + snapshot: snapshot) + } + let requestedDays = self.settings.effectiveCostUsageHistoryDays + let now = inputs.map(\.snapshot.updatedAt).max() ?? Date() + return SpendDashboardModel.build( + inputs: inputs, + requestedDays: requestedDays, + now: now, + preferredCurrencyCode: self.settings.preferredCurrencyCode) + } + private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) { let resolvedProviders = self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, diff --git a/Sources/CodexBar/StatusItemController+MenuTypes.swift b/Sources/CodexBar/StatusItemController+MenuTypes.swift index f5094e714e..5a6b781b77 100644 --- a/Sources/CodexBar/StatusItemController+MenuTypes.swift +++ b/Sources/CodexBar/StatusItemController+MenuTypes.swift @@ -33,14 +33,126 @@ extension ProviderSwitcherSelection { } } +struct OverviewSpendSummary: Equatable { + let primarySpendText: String + let coverageText: String + let tokenText: String? + let isPartial: Bool + + init(model: SpendDashboardModel, connectedProviderCount: Int) { + let connectedCount = max(0, connectedProviderCount) + let knownCostCount = model.groups.reduce(0) { $0 + $1.knownCostProviderCount } + let knownTokenRows = model.groups.flatMap(\.providers).compactMap(\.totalTokens) + let knownTokens = Self.safeTokenSum(knownTokenRows) + let tokenCoverageIsComplete = knownTokenRows.count == connectedCount + self.isPartial = knownCostCount < connectedCount || model.groups.contains { $0.totalCost == nil } + + let spendTexts = model.groups.compactMap { group -> String? in + guard let cost = group.totalCost ?? group.knownCost else { return nil } + let formatted = UsageFormatter.currencyString(cost, currencyCode: group.currencyCode) + let groupIsPartial = group.totalCost == nil || knownCostCount < connectedCount + return groupIsPartial ? "~\(formatted)" : formatted + } + self.primarySpendText = spendTexts.isEmpty ? L("Spend unavailable") : spendTexts.joined(separator: " · ") + self.coverageText = "\(codexBarLocalizedInteger(knownCostCount)) / " + + "\(codexBarLocalizedInteger(connectedCount)) \(L("Providers"))" + self.tokenText = knownTokens.map { + let formatted = ShareStatsFormatting.compactCount($0) + let value = tokenCoverageIsComplete ? formatted : "~\(formatted)" + return L("%@ tokens", value) + } + } + + private static func safeTokenSum(_ values: [Int]) -> Int? { + var total = 0 + for value in values { + let result = total.addingReportingOverflow(value) + guard !result.overflow else { return nil } + total = result.partialValue + } + return values.isEmpty ? nil : total + } +} + +struct OverviewSpendSummaryCardView: View { + static let rowHeight: CGFloat = 94 + + let summary: OverviewSpendSummary + let days: Int + let width: CGFloat + let canShare: Bool + let share: () -> Void + + var body: some View { + HStack(alignment: .top, spacing: 12) { + VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 6) { + Text(L("Usage & Spend")) + .font(.headline.weight(.semibold)) + Text("·") + Text(spendDashboardDayRangeText(self.days)) + } + .foregroundStyle(.secondary) + + Text(self.summary.primarySpendText) + .font(.system(.title2, design: .rounded, weight: .bold)) + .monospacedDigit() + .lineLimit(2) + + HStack(spacing: 8) { + Text(self.summary.coverageText) + if let tokenText = self.summary.tokenText { + Text("·") + Text(tokenText) + } + } + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + + Spacer(minLength: 6) + + Button(action: self.share) { + Image(systemName: "square.and.arrow.up") + .font(.body.weight(.semibold)) + .frame(width: 30, height: 30) + .background(.primary.opacity(0.07), in: Circle()) + } + .buttonStyle(.plain) + .menuCardInteractiveControl(isEnabled: self.canShare) + .disabled(!self.canShare) + .opacity(self.canShare ? 1 : 0.35) + .accessibilityLabel(L("Share Stats…")) + .help(L("Share Stats…")) + } + .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) + .padding(.vertical, 10) + .frame(width: self.width, alignment: .leading) + .frame(minHeight: Self.rowHeight, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color.accentColor.opacity(0.08)) + .padding(.horizontal, 6) + } + } +} + struct OverviewMenuCardRowView: View { + enum Emphasis: Equatable { + case prominent + case compact + } + static let showsSectionDividers = false static let rowHeight: CGFloat = 88 + static let compactRowHeight: CGFloat = 54 static let accessibilityRowHeight: CGFloat = 112 let model: UsageMenuCardView.Model let storageText: String? let width: CGFloat + var emphasis: Emphasis = .prominent @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor @Environment(\.dynamicTypeSize) private var dynamicTypeSize @@ -50,6 +162,30 @@ struct OverviewMenuCardRowView: View { let liveSubtitle = Self.liveSubtitle(for: liveModel, refreshMonitor: self.refreshMonitor) let metric = Self.primaryMetric(for: liveModel) let prioritizesStatus = Self.prioritizesStatus(for: liveSubtitle.style) + Group { + switch self.emphasis { + case .prominent: + self.prominentContent( + liveModel: liveModel, + liveSubtitle: liveSubtitle, + metric: metric, + prioritizesStatus: prioritizesStatus) + case .compact: + self.compactContent( + liveModel: liveModel, + liveSubtitle: liveSubtitle, + metric: metric, + prioritizesStatus: prioritizesStatus) + } + } + } + + private func prominentContent( + liveModel: UsageMenuCardView.Model, + liveSubtitle: MenuCardLiveSubtitle, + metric: UsageMenuCardView.Model.Metric?, + prioritizesStatus: Bool) -> some View + { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .firstTextBaseline, spacing: 10) { Text(liveModel.providerName) @@ -91,14 +227,14 @@ struct OverviewMenuCardRowView: View { accessibilityLabel: metric.percentStyle.accessibilityLabel) } } else { - Text(Self.spendReference(for: liveModel) ?? liveModel.subtitleText) + Text(Self.compactSpendText(for: liveModel)) .font(.subheadline.weight(.medium)) .lineLimit(1) } HStack(alignment: .firstTextBaseline, spacing: 6) { if metric != nil || prioritizesStatus { - Text(Self.spendReference(for: liveModel) ?? liveModel.subtitleText) + Text(Self.compactSpendText(for: liveModel)) .lineLimit(1) .truncationMode(.tail) } @@ -117,10 +253,62 @@ struct OverviewMenuCardRowView: View { .frame(minHeight: Self.rowHeight(for: self.dynamicTypeSize), alignment: .leading) } + private func compactContent( + liveModel: UsageMenuCardView.Model, + liveSubtitle: MenuCardLiveSubtitle, + metric: UsageMenuCardView.Model.Metric?, + prioritizesStatus: Bool) -> some View + { + VStack(alignment: .leading, spacing: 5) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(liveModel.providerName) + .font(.subheadline.weight(.semibold)) + .lineLimit(self.dynamicTypeSize.isAccessibilitySize ? 2 : 1) + if !liveModel.email.isEmpty { + Text(liveModel.email) + .font(.caption) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + .lineLimit(self.dynamicTypeSize.isAccessibilitySize ? 2 : 1) + .truncationMode(.middle) + } + Spacer(minLength: 6) + } + + HStack(alignment: .firstTextBaseline, spacing: 8) { + if prioritizesStatus { + Text(liveSubtitle.text) + .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) + } else if let metric { + let title = UsageMenuCardView.popupMetricTitle(provider: liveModel.provider, metric: metric) + Text(metric.linePresentation(title: title).titleText) + } else { + Text(liveSubtitle.text) + } + Spacer(minLength: 8) + Text(Self.compactSpendText(for: liveModel)) + .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) + } + .font(.caption) + .lineLimit(self.dynamicTypeSize.isAccessibilitySize ? 2 : 1) + } + .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) + .padding(.vertical, 6) + .frame(width: self.width, alignment: .leading) + .frame( + minHeight: self.dynamicTypeSize.isAccessibilitySize + ? Self.accessibilityRowHeight + : Self.compactRowHeight, + alignment: .leading) + } + static func primaryMetric(for model: UsageMenuCardView.Model) -> UsageMenuCardView.Model.Metric? { model.metrics.first } + static func compactSpendText(for model: UsageMenuCardView.Model) -> String { + self.spendReference(for: model) ?? L("Spend unavailable") + } + static func prioritizesStatus(for style: UsageMenuCardView.Model.SubtitleStyle) -> Bool { style != .info } diff --git a/Sources/CodexBar/StatusItemController+OverviewScroll.swift b/Sources/CodexBar/StatusItemController+OverviewScroll.swift index 5fb980afdd..288087cc1e 100644 --- a/Sources/CodexBar/StatusItemController+OverviewScroll.swift +++ b/Sources/CodexBar/StatusItemController+OverviewScroll.swift @@ -26,6 +26,12 @@ extension StatusItemController { self.overviewScrollAccumulatedDelta = 0 return false } + // Long overviews need AppKit's native viewport movement so offscreen rows can become visible. + // Keep legacy row-to-row wheel navigation only for short lists that fit without scrolling. + if self.overviewRowCount(in: menu) > SettingsStore.mergedOverviewLegacyWheelNavigationLimit { + self.overviewScrollAccumulatedDelta = 0 + return false + } guard !event.hasPreciseScrollingDeltas else { self.overviewScrollAccumulatedDelta = 0 return false @@ -61,7 +67,11 @@ extension StatusItemController { } func menuHasOverviewRows(_ menu: NSMenu) -> Bool { - menu.items.contains { item in + self.overviewRowCount(in: menu) > 0 + } + + func overviewRowCount(in menu: NSMenu) -> Int { + menu.items.count { item in (item.representedObject as? String)?.hasPrefix(Self.overviewRowIdentifierPrefix) == true } } diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index 0155c6397c..f94c817e3b 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -434,8 +434,8 @@ struct SettingsStoreTests { } @Test - func `resolved merged overview providers defaults to first six when selection empty`() throws { - let suite = "SettingsStoreTests-merged-overview-default-first-six" + func `resolved merged overview providers defaults to every active provider when selection empty`() throws { + let suite = "SettingsStoreTests-merged-overview-default-all" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) @@ -445,10 +445,11 @@ struct SettingsStoreTests { zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) - let activeProviders: [UsageProvider] = [.codex, .claude, .cursor, .opencode, .warp, .gemini, .grok] + let activeProviders = Array(UsageProvider.allCases.prefix(20)) let resolved = store.resolvedMergedOverviewProviders(activeProviders: activeProviders) - #expect(resolved == [.codex, .claude, .cursor, .opencode, .warp, .gemini]) + #expect(activeProviders.count == 20) + #expect(resolved == activeProviders) } @Test @@ -690,7 +691,7 @@ struct SettingsStoreTests { #expect(resolvedWhenEmpty == []) let resolvedAfterReenable = store.resolvedMergedOverviewProviders(activeProviders: activeProviders) - #expect(resolvedAfterReenable == [.codex, .claude, .cursor, .opencode, .warp, .gemini]) + #expect(resolvedAfterReenable == activeProviders) } @Test diff --git a/Tests/CodexBarTests/ShareStatsTests.swift b/Tests/CodexBarTests/ShareStatsTests.swift index 64a874f785..1be23ce51f 100644 --- a/Tests/CodexBarTests/ShareStatsTests.swift +++ b/Tests/CodexBarTests/ShareStatsTests.swift @@ -31,24 +31,65 @@ struct ShareStatsTests { subscriptionNames: subscriptionNames)) #expect(payload.days == 30) - #expect(payload.totalTokens == nil) + #expect(payload.totalTokens == 500) + #expect(payload.totalTokensIsPartial) #expect(payload.currencies == [ ShareStatsCurrencyPayload(currencyCode: "GBP", estimatedCost: 12, coveredDayCount: 10), - ShareStatsCurrencyPayload(currencyCode: "USD", estimatedCost: nil, coveredDayCount: 0), + ShareStatsCurrencyPayload(currencyCode: "USD", estimatedCost: 4, coveredDayCount: 0, isPartial: true), ]) #expect(payload.providers.map(\.providerName) == ["Claude", "Codex · #1", "Cursor"]) #expect(payload.providers.map(\.subscriptionName) == ["Max", "Pro 20x", "Cursor Pro"]) #expect(payload.providers.last?.estimatedCost == nil) + #expect(payload.spendReportingProviderCount == 2) #expect(payload.topModels.map(\.modelName).prefix(2) == ["Claude", "GPT"]) let text = ShareStatsFormatting.text(payload) #expect(text.contains("GBP: £12.00 estimated · coverage 10/30 days")) #expect(text.contains("Claude · Max: 300 tokens · ~£12.00 est · 10/30 days")) - #expect(text.contains("USD: Spend unavailable · coverage 0/30 days")) + #expect(text.contains("USD: ~$4.00 estimated · coverage 0/30 days")) #expect(text.contains("Cursor · Cursor Pro: Spend unavailable")) + #expect(text.contains("2/3 connected services report spend")) #expect(!text.contains("£12.00 +")) } + @Test + func `overview roster keeps every connected service in the flex card`() throws { + let roster = [ + ShareStatsProviderRosterEntry(provider: .codex, providerName: "Codex", currencyCode: "USD"), + ShareStatsProviderRosterEntry(provider: .claude, providerName: "Claude", currencyCode: "USD"), + ShareStatsProviderRosterEntry(provider: .openrouter, providerName: "OpenRouter", currencyCode: "USD"), + ShareStatsProviderRosterEntry(provider: .gemini, providerName: "Gemini", currencyCode: "USD"), + ShareStatsProviderRosterEntry(provider: .grok, providerName: "Grok", currencyCode: "USD"), + ShareStatsProviderRosterEntry(provider: .cursor, providerName: "Cursor", currencyCode: "USD"), + ] + let payload = try #require(ShareStatsBuilder.make(model: Self.dashboard, providerRoster: roster)) + + #expect(payload.providers.map(\.provider) == roster.map(\.provider)) + #expect(payload.providers.count == 6) + #expect(payload.spendReportingProviderCount == 2) + #expect(payload.providers.last?.estimatedCost == nil) + #expect(payload.totalTokens == 500) + #expect(payload.totalTokensIsPartial) + #expect(payload.currencies.allSatisfy(\.isPartial)) + #expect(ShareStatsFormatting.text(payload).contains("~500 tracked tokens")) + #expect(ShareStatsFormatting.text(payload).contains("2/6 connected services report spend")) + } + + @Test + func `twenty connected services remain accounted for without overflowing the flex card`() throws { + let roster = Array(UsageProvider.allCases.prefix(20)).map { provider in + ShareStatsProviderRosterEntry( + provider: provider, + providerName: ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue, + currencyCode: "USD") + } + let payload = try #require(ShareStatsBuilder.make(model: Self.dashboard, providerRoster: roster)) + + #expect(roster.count == 20) + #expect(payload.providers.count == 20) + #expect(ShareStatsCardView.providerDisplayLimit(for: payload.providers.count) == 4) + } + @Test func `payload sanitizer excludes emails identifiers paths and prompts`() throws { let model = Self.dashboard(models: [ @@ -309,6 +350,7 @@ struct ShareStatsTests { #expect(ShareStatsCardView.providerDisplayLimit(for: 5) == 5) #expect(ShareStatsCardView.providerDisplayLimit(for: 6) == 4) #expect(ShareStatsCardView.providerDisplayLimit(for: 12) == 4) + #expect(ShareStatsCardView.providerDisplayLimit(for: 20) == 4) } @Test @MainActor diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index 1e6ba01553..aa76341712 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -248,7 +248,7 @@ struct SpendDashboardModelTests { #expect(group.models.map(\.modelName) == ["test-model"]) #expect(group.models.map(\.totalCost) == [4]) #expect(spendDashboardModelHistoryPresentation(group) == .partial) - #expect(spendDashboardAggregateCostText(group) == "≥ $4.00") + #expect(spendDashboardAggregateCostText(group) == "~$4.00") #expect(spendDashboardCostCoverageText(group) == "1 / 2 Accounts") } diff --git a/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift b/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift index 2a4b7b2fde..fb41ee9ffc 100644 --- a/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift +++ b/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift @@ -28,17 +28,34 @@ struct StatusMenuOverviewScrollTests { statusBar: .system) } - private func makeOverviewMenu() -> NSMenu { + private func makeOverviewMenu(count: Int = 2) -> NSMenu { let menu = NSMenu() - for provider in ["claude", "codex"] { + for index in 0.. NSEvent? { guard let cgEvent = CGEvent( scrollWheelEvent2Source: nil, diff --git a/Tests/CodexBarTests/StatusMenuTests.swift b/Tests/CodexBarTests/StatusMenuTests.swift index a324d5414c..59f0b763cc 100644 --- a/Tests/CodexBarTests/StatusMenuTests.swift +++ b/Tests/CodexBarTests/StatusMenuTests.swift @@ -1573,7 +1573,8 @@ extension StatusMenuTests { let ids = self.representedIDs(in: menu) let overviewRows = ids.filter { $0.hasPrefix("overviewRow-") } #expect(Set(overviewRows) == Set(enabledProviders.map { "overviewRow-\($0.rawValue)" })) - #expect(menu.items.count(where: \.isSeparatorItem) == overviewRows.count + 1) + #expect(ids.contains("overviewSpendSummary")) + #expect(menu.items.count(where: \.isSeparatorItem) < overviewRows.count + 1) } @Test diff --git a/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift b/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift index 0288c31c5c..f97d4c44f3 100644 --- a/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift +++ b/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift @@ -77,6 +77,122 @@ struct UsageMenuCardLayoutTests { #expect(OverviewMenuCardRowView.primaryMetric(for: model)?.id == "session") } + @Test + func `overview keeps one prominent provider and compresses the remaining rows`() { + let model = Self.model(metrics: [ + UsageMenuCardView.Model.Metric( + id: "session", + title: "Session", + percent: 37, + percentStyle: .left, + resetText: "Resets in 41m", + detailText: nil, + detailLeftText: nil, + detailRightText: nil, + pacePercent: nil, + paceOnTop: true), + ]) + let width: CGFloat = 296 + + let prominent = NSHostingController(rootView: OverviewMenuCardRowView( + model: model, + storageText: nil, + width: width, + emphasis: .prominent)) + .sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) + let compact = NSHostingController(rootView: OverviewMenuCardRowView( + model: model, + storageText: nil, + width: width, + emphasis: .compact)) + .sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) + + #expect(prominent.height == OverviewMenuCardRowView.rowHeight) + #expect(compact.height == OverviewMenuCardRowView.compactRowHeight) + #expect(compact.height < prominent.height) + #expect(OverviewMenuCardRowView.compactSpendText(for: model) == "Spend unavailable") + + let accessibilityCompact = NSHostingController(rootView: OverviewMenuCardRowView( + model: model, + storageText: nil, + width: width, + emphasis: .compact) + .environment(\.dynamicTypeSize, .accessibility2)) + .sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) + #expect(accessibilityCompact.height >= OverviewMenuCardRowView.accessibilityRowHeight) + } + + @Test + func `overview spend summary keeps partial totals honest across connected services`() { + let group = SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + .init( + id: "codex", + rank: 1, + provider: .codex, + displayName: "Codex", + totalTokens: 4_820_000, + totalCost: 412.64, + coveredDayCount: 30), + .init( + id: "claude", + rank: 2, + provider: .claude, + displayName: "Claude", + totalTokens: nil, + totalCost: nil, + coveredDayCount: 8), + .init( + id: "openrouter", + rank: 3, + provider: .openrouter, + displayName: "OpenRouter", + totalTokens: 9_640_000, + totalCost: 282.74, + coveredDayCount: 30), + .init( + id: "gemini", + rank: 4, + provider: .gemini, + displayName: "Gemini", + totalTokens: nil, + totalCost: nil, + coveredDayCount: 0), + .init( + id: "grok", + rank: 5, + provider: .grok, + displayName: "Grok", + totalTokens: nil, + totalCost: nil, + coveredDayCount: 0), + .init( + id: "cursor", + rank: 6, + provider: .cursor, + displayName: "Cursor", + totalTokens: 1_250_000, + totalCost: 64.18, + coveredDayCount: 30), + ], + models: [], + dailyPoints: [], + totalTokens: nil, + totalCost: nil, + coveredDayCount: 30, + chartDomain: Date(timeIntervalSince1970: 1_783_036_800)...Date(timeIntervalSince1970: 1_785_628_800), + modelHistoryCompleteness: .incomplete) + let summary = OverviewSpendSummary( + model: SpendDashboardModel(requestedDays: 30, groups: [group]), + connectedProviderCount: 6) + + #expect(summary.primarySpendText == "~$759.56") + #expect(summary.coverageText == "3 / 6 Providers") + #expect(summary.tokenText == "~15.7M tokens") + #expect(summary.isPartial) + } + @Test func `overview prioritizes refresh status and expands for accessibility text`() { let metric = UsageMenuCardView.Model.Metric( diff --git a/docs/screenshots/spend-dashboard-proof/README.md b/docs/screenshots/spend-dashboard-proof/README.md index 73d5bd0223..89906ef0e5 100644 --- a/docs/screenshots/spend-dashboard-proof/README.md +++ b/docs/screenshots/spend-dashboard-proof/README.md @@ -1,11 +1,15 @@ # Spend dashboard proof -These PNGs are rendered by the production `SpendDashboardHeader` and -`SpendTrackedAccessPanel` SwiftUI components at wide and narrow settings widths. -The provider names and account labels are synthetic fixtures; no account data, -keys, or usage values are included. +The wide and narrow settings PNGs are rendered by the production +`SpendDashboardHeader` and `SpendTrackedAccessPanel` SwiftUI components. The +Overview capture uses the production `StatusItemController`, +`MenuRowContainerView`, and 310-point menu width. The share capture opens the +production `ShareStatsPresenter` from that Overview. -Regenerate them with a full Xcode toolchain: +All provider names, account labels, spend, and token values are synthetic. No +real account data, keys, or usage values are included. + +Regenerate the settings PNGs with a full Xcode toolchain: ```sh CODEXBAR_SPEND_DASHBOARD_PROOF_DIR="$PWD/docs/screenshots/spend-dashboard-proof" \ @@ -15,3 +19,10 @@ CODEXBAR_SPEND_DASHBOARD_PROOF_DIR="$PWD/docs/screenshots/spend-dashboard-proof" The narrow render verifies that the range controls wrap below the title and the tracked-source grid collapses to one column. The wide render uses two columns. Both states keep cost-history inclusion and exclusion explicit. + +`overview-all-providers.png` and `share-all-providers.png` are local runtime +acceptance captures rather than golden test outputs. They show the same +six-provider synthetic roster: three sources with known spend and three sources +whose spend is explicitly unavailable. The Overview keeps Codex prominent and +uses compact rows for the remaining providers; the share card preserves all six +connected sources while labeling partial spend and token totals as estimates. diff --git a/docs/screenshots/spend-dashboard-proof/overview-all-providers.png b/docs/screenshots/spend-dashboard-proof/overview-all-providers.png new file mode 100644 index 0000000000000000000000000000000000000000..59152df96a59375c698b2fd74b2b999b0333a0db GIT binary patch literal 38696 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/spend-dashboard-proof/share-all-providers.png b/docs/screenshots/spend-dashboard-proof/share-all-providers.png new file mode 100644 index 0000000000000000000000000000000000000000..9bce7990482639fb342fe0d3b5d1bd2da1c1f337 GIT binary patch literal 64974 zcmeFZ2Ut|ivM9PpA~|OXl941xk|-icBxi=8fy= zGXn~P3^RB9_u28Bch3IKdGCJrzHj%?YifG+s_v?;RbACx4SEi}29Q5iQC9)5umC^} z^8wIM;DIvO$sPbSHGw+-0NezyqMQI+41}oyFk&(QVCQ20xdewe7w69~w&3rZ=p#T% z$I;!}-OJJ4gYBNsT|nxgx+d;#44Cu>%=HIIKFu6Ip9Flx-Fr#!?hx@X1ig+;uCAnH zrK6{o3Ls96)Gm=VgPbio|4I8xK!!3?n!(u#TU%$8Q*u0cuZ7 zr!a8zZ`k2a@Q2^9^`GD`f7CJ1Q^M5g!NBA;j?Zi{@H7S%u>PBJhrfYcpZol7_jmfu zd15;^L;c_1n0yOR1oQwiKpW5j6#pOUiTzPe32+BI0Dr&}Z~`0vN5C6k!#wMO`8)^g zFt9q{2G{^XfB*&-1@2<9#Bcs$p2eiU^6jr>PwfCe1dQnt_Fv0fM**Ol6hoH9U(0y& z0e~m~06IrJo_Rj|6F$rp>m1k1{+{R`zp;qw0RS%zjXpC50DL|GxSmC$ujmmTa zSpa~Jb94tlaT8k*hZqNo9l)l*!lA%I_W-OIJa|}tq(39Z!p6bHyMcd`fRG4Np_UxL z#=^nD#>K(I`z?4_!I=91E(IRtZ4t#ARJzab*VJkiA_#l>R}+dI2^`|#7VA3x785SPDx(}e}#{D~~g^-q-j16>pty0CF^ad7c}(}jiYhsih; zxOlfkZcr-f;y-&q#V#6plltMCk5!!n9QX8KG}fLIgtVMuP%ijy(*B_ApCc^nzeU+! z2>UBti+~b<{YS#V#*7jU4rZiqF$oVJ?{~t#iT_8s`EQBvk3{r4k^Cj0F-Wj5I4}?2 z!2FRA;1m4i-aieXmobV;1ib)YqzNNTI23>aaLsjHrSQC+IAo}arY^_Hm8Ee*)A}BF zR$)3H_1*BArDf66DxBA`@=hk?cX32fgvf{2b?r{8F_NAmW#ALtsq*ZHdv^H>=XNbm zf9ReEC+^GmC+c^pE5Bpv@*j!i~|C}n&BCQ<~XEHa=;%y1KXh6Lia*w3aGt;tKjvhml<$Kt#Q zo_b!;msB`C)-36@T&8lZ|Eh7UUJIo}+9Mi0)N}eWk*|KPmYPeR1%#t$S9^tjG1ays z>9x^7?`(UJErR*uZWAtw2lhz}d-z5+wrBIbAyOIpHu-~&a?zH_MCp{z8XAsM%M9=x zEPHdFq(q{-&gMgzqH%7b#`Ol7PRE&N-^ErxqJgeOL?Ieb@~uY$;RI*^(WC%%Me3n} zkIf)7(D@1tAT2@)kW|+RA-DGA$*ZPOYSF-uTMtnrb zWTbGA&+^;C85d6h$ZQqFXcHtq44cDG{E(m5oUkft5dhNqga-WKIY`0wSgRkBE)c}s z95fJ@|Bqb3!4>wB^@S6+RFvc|C*lSCEROVck$R<#z0?j@=hoUd6a`qa4$fUT){aLZ z|M2hx8X$6CProKvSVa-@pn>%RnOcnhQs;b_E`F8?8_hL63zYu8qC3+4g|3D4DKb2XxyYf)IyhL`WQ zB7uqnEyO+7QbgXY($vp6;iJmuLMQCm%i8bCof}2ese-=sY*boKo8ry)LH>zxa#%vj zz+1D69rDe0XVn8kA~hTz+Z8Hrz6p;fMZ#B7+nB*nAN%pd1fTPt^^*2LqYd_JqWD`h zgE(OrlD=Yw`akFJcasnX#Q8d?;r96P@}UV=2cf*m9aeWstx3`wTKp+hQ$U97lQOJUjEl=_Fr=OcWU%M&=voB z$N$cL`uCIl{|TCiB18yjpp*)aY#=L{=3%{(b8~{!(DiFlZp)1Duj9wK7RH40Y3l7T z+=?$vaboEqcj@0Pw7oTaZGHU#vQ}b6ffTMV-DPFvWZ{M#^?asos`217OAV`Md1hj$ zw=KTmnM!HHd5@eLh1_GT%c$Hl3fPYGp*hZh;QC3_-@OYiiZmonjF-CeP4Py1?Bi=R zfCj?6QR6e2ki!GkNZDtewh*!dn}*ETFY&RfN;Tc7@)F#V z70~0z!r2p$-mDL^a^iMVbGCKxlSGh0dGJS=()wzY0+hZM*4OBrW^5MV|AK*(R;_d6 zgAYSx#6{&r)Q-vz6byNAYxPcHQbFm+e7U}ScXtkXRSQQ_wlJBd!K+z;yyJ>W?}P-9s?ee!Em`~6;r zho1M^@k)7Ik2?mhMN*_Ng{J;1Isxm7!%E*p-)46|yS&E>E?1Q>)$=q&ARMRj9N%C@ z(0jyfVMXf1RcOoP{qO@UZ^0VTDhB$>4+^fV3sjv8tKm6NNchS6cQ;|T+sUXv2Qp&a zj&)_tZni2P%cxMRw5zGB?z8*=2sp$Cajdx}q@C{IG4jz;>jP&op|6?P3DdU=XQ3G* zZDrq|Z4GFEq6$%l`^B^9Z9$`*ppT~M-b8I%-Rz^9+RqwMHBFJn@@5RQia#ITL>V3# zWfo;Y$g>x~9UNTD-z~^K;wEOd@^@YQLGK^zya>!c%Ql`nX|XC>SXO3vZ@;dSv^}uz z+5GbM137p~vQ!1>-|$Fz+_SB}5Ge#FO@+3T4+Vc(U3GePR1nKFM0~H8!-i=b2LIsM z{`kTVF}UVpMYG#f;4X0Hv|yC);`d3_`HQ-j+_%u1r?aeJh*&oG1wW4?&*J zUmgQKuf2+yfL;c|q_L|5+b}H---23W6Eav`S(--baN_Z$x{UI+?3bgWf5P`ic%x#lD zWEve%)6+(mxq;%Z9^TL%xy_Z)zdX>VYId3Y4|3LBQuOx%@@1c|`7dwEAMBBib^CjQ zNb~-Mp{qy7xz+Enc5sXmN#z= zbBpdbx&Oyk()-=RCzw9x{?26RsXwS}tsWx07waVd_CreOVE#1TvUZOGN; z#JNV!3Hki*{e8n|}JcSHQoP++Y9BQwaqN1t&Z@(`F^AGD4&IZxINs894 zz<{XZU@2vTbx zLSe(<-0_(?e?g==?*`dPK?AGYakV@F%Ui|LfpSp1|nD?*7{>8?B3CRCX-?PC# zG{~t~aPn^jvgu?lFyN=RY0z%lbVQYdAShYkvq1wU@$DzryyHfMd@t8?W)favwDnU6 zdK#Hjl(LYeI}6F!im=&V_)s^=#IG_p)bo|BHRm{dj`U|eImsQqT@$jI2GFdqzf5*H zf6j1bUrnZ}B_+XN{dj1`hpj=IeJ!UNUGx4tjIIa%Z^<;CvpUVyuHyp_j*C+{#uHPu zVzE9S`%P#v4^u2Zk|@jgh6a41bhYtm((`WZP4b}_FkVzBFB%}6s#tJ?Yi27umW&jr zD~kv7eFrhHqQ4^(oL%{S3MwC*4&GL1kA~E>k2WwRArR@;=O~{QMKr)lfl)%3LYY4o zj#zzM4%=5!rZ&R&z|5Ce#rcj4#nC<*u5^!Tlh1#hDpJ$JDt6wCJ-s7mtTF8^_jpJ3 z`_X1iYjx_!hA_hg_&^kKK|r|Ga|9KXq`2EXW)-hR(X~W9U_~fQ|ktkM?y8OGF ze4Jk4mV5qi<8wYlDt^2yU1^Yb6m;aR1f3^Xqi4bU!dS9ze_p*l@tmBKF|8PNwpRScZSoNF;?ic+-fuyK(Lz~o*Q~H#irFN|%6s{t2=VfA7Rd`E$ zTYPh-srh;YL!zh6vmB-5TMj!vX@0$c`<;tT{7g!)qVcg{GMNZr3c3$VEHB=By<|nO zqte>?HO$N1mH44LlY2(%=>x+THRMe0DHfc$wJnKPsB$8{_GJ>P)soI=yf zuiER(Lc3oir6J!Wqv5r06Z*7Y6Tepb^yt1R9FOMLcNpFIqe)qY4ejV`GWcjhUXT_X z5``piEnjpbkd32BtMcqk>++{rw~};xQg`U;p>i*C_&UGcK8e&43|d15xQG5g1Fs+y zEicL}oYH@pPG<+R%h+;q4bKvv_EG>GuT(_Iw>%E>(rb;sT$*0$DhER=Lb?L3TNA^} z!T0(@Sm3@XpTJGsoqn@TPjiN*YMCE8uBp22;*RqVWPk@px9;4$&%$@56m+BZItQuj z4&@7S{fMYGCh=k^bjcU1Sg`5(Mj=G>sU-P9|0cdC&C~9WVnx}}n)h}-@#d8%DpTuV zOu+vw87~gxk-XZkQ1#+uK_6Ot zToCWZieo6UpQdyzhm?#C(X9;oY##j9d3NU9-Xr^2ycSf__1kfuSA6`hNt>$F1g5K5 ztBY@t1p0~gRv(PzRg9PgLCJ2+9n>+NHUYX5oqq--`z27$+y9&HYH`YDSEt_cwQ_nXUNQg=CE_R}4 z`bv<_S;NY|bQQB_gZR28U7dqN4reEgE1H$%h0Spf0uHl{yFztZrysycOU69Y+7$ zH8*j4`%$WSZU5V|t`llyz~_|)pU~axR{Y2|tX1&)ZbsIavRZ@1QaMCx9eIk}TcgNF z>gTjK)UwsiWrEs3(%7U>_37A&s*?JzvI_rEqpC?ziO)P;{_;GRiP%e7b=m5-%V=h9 zG@W_XSZ}=w6v-|hH&$KG3G+a>D^Aun{`5bZ(rK7va}qvm8+$(VY2muCsP2t@>Q3V9 z@spsMZW{(!{L$vP)N@(m|EM+ZX?fh}mS+WEE6wShOgZ~mz3hc!7cebfVh(QdXJr9N z{lyl2Y8-h-$FsANwK_$Fpx?9L!UdOKVe^9hQ70`*C1QNciy%ci@g8ON)1rBqhETQ3 zX>5cwc8bP=YQv?K%|O+k`I&DY57{c847_w)y^@}-+_NH*6)y#MKN#0)4rqeJdjD*n za!c1%5=EUmX39isetX4xeo8WM%(TV!6uw~=>jYmJ&U{j>?Z|W?YX0cUx1s%k% zgK^5&%2ehu>u^b9gy&WTUk zOlh(eXNcJy)uwPUdpV2$0yCRYV&rsnw0Q{f6aJsfYsTLdh@Z=nNFE-++0V?PG=n|) zTzliw`6*fyPhC%mLSS}r4i1)Hy+P&5FE+j;QD`cv_l*i`b7!?V6sB$y;Tsv6Q&%#m zj9pDQqXGV<>$}rI+R#3@_92vgKg~{{rVf-wRmA2{U7PSIM{+=;)aw1MFd0U~=Z%r0 zqL1*D-itJ<{uj8JWmjA=;rUP3ppl9U%%2B&D-{F>Il@1E;#-+`vDmWI4r1x^>!liY zlol<@_g4X}LW>!F(ZHZvV9p^FUafPTjS}94PzyYt)lJfNde%~HKYt#l@mA!?r`0Yf z$dmFW=?z5&njhJPaMg2aSOE(kT)Vpx3-R6p&J$hc?pkxr-zzdF5tXMIKz8!XWWkgn zm*?U>aR>1YmCLA|5t<_@?&F}Z4*L#K3N9z}ZC8c}KX}qQPh<;5i%_B%!6781qhn9f zX0NDpCDiYLGzF6HW{WZ!dD4(hzKD4kzvGlsd(vUWyl06GXYEhG?-fN%K`R=AKkq?c zNwvl2KBvp8M4O2lN0b85RnqLwR32COG4}{>bJ56uJk>^d*%!dZJ-rB5-t z>>x$BdyHUQh;HgS_u1t`2P3FHdDr3X3+-q6D~h4;o2O625=-}+SEU+t4}oe+yuoxN4nR^0*fD>eZQJ26Dju?yn}h#TlXgtyt2^^SrR&BuPH*IaMU<9N;*eHb*s+U-cWo*K zrC9iLUPXaX4Jn#Se|UahfJ2W}T(&6uIPMl~4|RK1_$R-~v7wP&}qeFov*6*uSkN{CQR3|I2R@3&Eh1TGLW&jdv0%WPZl#z^~V@$O6QI ztqfo*)ehx*qVHXmG#e7vS02W0-~agZ+hZv^sb>?R_zh(hA7ZCabM0&d7To8Z3nwemE&a}+;i^|<(t3Z7T1CR4*>aCp-6x;=H`x5@K@XEe26S5= zJfM{rb{VC%-F~%7F01X8%Svpa?jWDDz4RnTD93^G%b<5>;oaf!jE^e@wkrEl8?ifR zp!OII>||An{~t{S7r zzE7-|YmHMh!*DP~ysB8cO`6;wt6K`wCE^N;6T2QTiaD9$RpePM8t70{KrFK^TtLo7 zXVCx_8yeW~1`QaWNiUwn%1gAj)=qP_MluNGx5nbueyy#IxRKA0xYtr*tu{1VrgYz8 z`Tm-Xq$YeP_D5X8odfN*)6EH2hpMl`y3$;=LnEA3($X}koKBldoWwVudjrJUM}h5^ zJ1%J82klaE6WK)kpHWevGG4TMy+h(WMsY z`h=CNCF&Ae{`c5!R0R}!hdgqf&##Z54<^iI&or9TOv+%KQcomMG!?R=a+RgH8$KNI?zF{7u|JCZxdB_hcvpCZL9wb;34Opb3 z0Y&kWR+ozOt|Wdcq>6O5v_9WPp)_jB9|CX}r*>G~(i zr*jF?-_w3le^%kVOnOMC$H<%aMJB2*fuqbNzKT%v0fT%2Oo8!S&S6cshZ(ItJ1VF`ceKqmql64O2-Q;Y$Q!C$fB1!-{!N6=+Yv; zy-BhaXTIn;8Iq@sRQlGVN+0WdS2sh*%T+4XD5_oxr*Ul;?hKYM?2+dS8dcBkM466! z9k8FT*+gdC+WA5N7)~3k5R1>b@vtP{mBd5RuTAW4G?uMsK<|6M(E9AJ?7h_tf2GHZ&XP2bYx{ZGxb&r5458J8VXllfH9P$&yQ# zft?D3NT-b}jST<^XGbuW=LdlPc*jY-2DeGmSD_13R5-CZj@2$w)uY45*9nX*HvnfXU@4z;V5`=%US zd>k(r`WY(o#aJm31PYEatT7VV-1QApvie2NlA=Y`_l1isM%)EdCXe3JN6XU+dDwI? zV40SHd5kc7g|FHj$?y&qmQTXGm*shVa5Eu9quZmR=hc0YTZQ(7hsV^nLKACR3)-v47{R``cJ)dL1-69{JhdM?=QUwv(Xkanf2*F*;gUpW=8u&-S)wSfdbYv(=9jk)~GZB=*M(2;*-< z*pxWR-$sDelOl|pwkjik$~{Dl8P+sSbKW(zC~x3BpZbP1A0I=-7S>(IKU>4fjNs{p zP{`9F+=89^_3B|Nsilgg2Shx-sH>N|`J*{fJ65S5MNd?PHv_(;zUK4Bceb<+<7XsfY>2n6yNAn5!amf^EPRFVgwZAa&eRrGLyALMZEJv(D_-HJj-ar3c%J|CNQ*X6Mz=@F+XYr(I-E zeQNB)6}n#Qtq{6O8KexI3(_dqs~U3mA-<4!Ej~@)??ciS)E_qgLGw<<)P=!Loy&f6 zWxPBd{1F8_U23Jl-TOrRmUc`X7}K7r<_H`=UROr zx~!v?O}3ZBRYCiTY+J!1ljUG_0U=q#qJGGyLFj?0&};cZ%9bSi0yx{vZ#z!?Y&*AR zmF(4aWgTF}eIdB@H7&D(xycLX{({!oefvyvZBw+8T_ZRRrp6pwxr!~=VOYlzzj zrbjGnaMGMxMsb`TOa7ANOX^MnLv3P>YFU}IcCzj`A4m%bFWgzc#ohAEu;Q?qgSatL zOJ_#1m~>qUOKRWbzU@BoB=3vBNRp-aP!*G`tSE7tb?(NsYo=}tXMF;NsZCtU;oQHz zx!(EqO^I_{nR&3;uSJFMsYvPR^sTz%4hQ;A$>S4U(mD6Q9MIWVdA7ljY^K=d^d}Jx zO=kLuwQ%Z$@4be*)p%*M=vn2JE-HZ zU>V86{MTFWt#wZNkP!zxfEA*ui+1bHw$mL*J~Wkg)YUsgp58a1>)dh926%q-YteJGlu40GNY zt%u-+aF`(A@{Hztp|VPX%@)coJ3WuylxYeNK);DB!~?j=9f`GL3qdmQ)Z?&h&iW}k z7d{`gA12iiX)c#o?DXqAfnWMau{zhzEHB_z>k5%U7SMJAFTu~f`^`>&U9oOxm9Jf$;OtYaS|OZwdg+?VL_r!*z9O~Cqjdkkgoo2q z*qUdyRAHm%{?3AOnW+}jM!?>qnuSqeE>CCYBA$CF_~w*H3()c;P;2|IQl<3{cxK^t=TUDZ!!;0KIZh2c*= zH|B#fj6^$bVH-$1sLb`TdMPbfne>Ad&;QqP676R%f2`ISS3zhZ2&ugR;BsbI`pJU#Uw+1 zOn{W&DO_#S#!1IzG=vx7-ql79cRh)e74a*vgxD=HN*yeTK6Unjh|6IboUFNiAFoF_rG(>RIAs zUs{V|oR|;Kbq|f{j42q5?4ys_1k{FoKiI;}AY^SgyOzQe0&d5ZKQn}CEdoxI>QbIe zwN1DJ^(N7cjg(@ZsuX9U^T4O@H?N(ftCS2D<*oACK2FMGJ5_@zS_}ggwu_}#oYRu7 zBV6!CYB;o>X4@tSRMidMXL||8hliwINlBFBzZB=U(B1wDe>{&_|G2rKFOhLR&#A`h zc9MBNsMye^?+4pY>?9ZRb5D6tUR(Tl2ql8{L<#mGn{Bq1pIN)l_#%y#n@Fm-CQQR6 zwN77RFF(cQUMZJXXpUA8e-ESJYUIx&jqavUeV@V=ZM*wQsb_+lj;l@^9G)Kx`HBu- zt)%yASu8=q;mDRPU)&;niS<%} zw*_;21kb)Y3EEo}2g~WKcCzHT;xsyvFR%`l?#RQlalhqe_OzkLT9N)1tr3pXuOSq{ z#|;QAQW?Rp&Ps>aF>0EreUAp{S8IE>mLk=eXnk)R_!{VT<-Pp$`*>c3bjy*&`|fnx z5WbKl76Mp*_t-*QKMNhq=ardd3Q2Ea53TP(9lhD{B*CW1Y+i8&2YEhWF7yF_bs7S(UcRf(W^_Rw+F`=-~ zJ{*mKD$I3v8hhpl3bxQ@#<41=Emx=`bHtH5$otoC+Z}V_GM~cnLuE-ySND>U6PDjN zs_Wjdz|}qB0{L=}X>nRTd%Yw!N8W!`=lFF4LAZWww<(-@Gnvg?W0s6|)jS$X|A=Vn6E=`#$ zhyE4(?_x(%4#9`c>An*ir~r-UlLaA*-*#$)&;UrkD5c5o-rM&RxtOpjYqEv*IabT- z=pUFfXV|Q<-JRozX{%+3llpptYqoKGo9jsP3cqmbmMfkO#}2WH1MYP{8X#~-QdOct zLNGjC&0<9Z(Un_$v3&+^Ctk{W-)0m(<&Z3ebhe4i9Lb79HQXbb+RB4^1EDeQ&~wvIv*)GKwkcmJlrWqpaErAT>>01wq>HHkEv`04v zjOe9IRd0VDP+0o}Va>#Kfgi?(XA$RGN_c255EX{$%>FQ}dP`#L_qo30!0 zc-bRqh#OKCvZ(NNEL>hFh-TIuDU<7To_!Z~^nPDjbF21iw`|zBf3g{EH1>hmU0I9@|BO^b4wPw34+Oml_@^CE{-k9ArX|<0DTmmbELK`5&Y=h#Dd#IFp1zt~t{Y~J_n{~bT`8!0!YqNWm z5#E*fNJ2y)lni*V=Wb^&c0xLLhs~(5IsGP+Bi#E?!?h0Vnp$Q9Fb z$d9zsh7E~*^ZCAsD&#K4vRioi--(-W2ZMU(chmhli2!PgbG?og^)JYT?T5VcsV8>Ow201eVA9L=)hMTWTqzP{Lz@NV!6j@Hs+w zz#VgaKN~$4rP&r^I7X@Tc!8~%1n*3#Pa-+^M~dc@IL}J_?Mw@V1y0rZ-E4IeIshr` zMpwT+f^|Xdo`uo;LbKMB=!%hrktUrPp)-aL4{;)Hch31mUIsipGdHB><@$2}-b@<{ zQm+n1efKh(Zn8FRhzVLUXJvjlzO*=s4G^cDs@O$S6LSQWJkQ>)m%RsHH5{KE{w^kW z&%CMjdj;lr=Q`ZZXzJuR+R0LcD%aND&|UDf`7I+XlTZ3ZV1jyn{`)=_f$Y8mjTF=N zE0Q0Ta=m#=YIT!?9PajxN;d}u2QLF=%fU$-+aD`H&PR685gv{2K36w?jA$6XTvGQD z|0LjOqbpEO{cMuUXyN6XlMyft^%4~xcOd5c!V%p@@Iz*OwT8qw1VncN4OHb%c&ysg z#5_~WPo~IPSDVKv6$r5oWRV@RfLDC1MCfNF9~zx$9JF$~l`ASxh4-?VlNDrNk)@}Q5iGpW!VM{B_8jS*JXU6R_!rawqYK+?!4|v`s-?^(q&zDMn?DEYqKUuQD zOD@T3QvS|@8JK;jG>yAu!79#w!)QxI5@+flD~T{<-106A z65S$)31c8~N4(yR$*ukDe(El0lVZ}qX)(4`FST*`9_c{80*o{32y90h+ z>1Lhm66kN2csqlc{A(^6a10GajN_T{=Doyhq`s6cr$Whcqk)Pc%2gPC;68BaBQ zVcfcjVJN{4<(;oThHPH+U%Z+1yq@=G<(`LhO0;meUU69<4dC+SpPg3ZqPqR$xg1%o zO+nGmTxx65mNzIPPYpQZZSib5*CG}m%r58e&oSHPVNnR8b^PwYlUME7$HKz1R)$eV zGn-)`-sGYCfnZ9VlzW;9$!W@p6n7-)WE5y!9-Gg!#AG$$K~x|x^|4Ed1Q4q4S_uuP z3%FbkSMH9X2=1c+?upNkxz!TL9#?{ZYKJr^2NQY$)78F|C1XYdOTuVCbLKmo>p!&p zN(4Gp^*s31f_O9B#tvlGP7*_?$mYE|f2HM@JK1!lt&mTZRdTjubA_^p4c^Ws2)}j1 zqAW4$Lzsxw4U92@BHVzI;P+5Eu@WN*gxd4X)HpOBo}z&($s>wlhR7w*lYuCN#_PF0 z9TxntYRBE(SPTuNNOyXL!Ziv_|4eUg=@@&5M~@Q=Fk;_ z-;RK<^Sd?`$EKFmxb_^!J&x(m;Fjua$dVe>8ki1`x*waemGCXz?J&3=4(eMWzJ3F7 z;4e2;ua8{m?#eVb^No5sOm`${_sD1h;y=H-^ESS2{5XKhYVf$V!g+;f<|i4&!K|L; zzFFb)Y~Ic5OE5nBj*NF6Bbx((@eCjF1pT znTVQ4D)+)Tom8Y5kzYza67jbc8l)~K&5|KFC6TFPw;*$ktfQ4hvV3m1y0-7~n%(VS+s8GJT)S|Qt- z3&TwWpPZ5*Df8Fn;gH@V{q>n*%DSwz_f~hN0(L@*OZZhHJVmZba9>>z?<$8BqJ~4H zC)=IJ=I@Ly_CC3|x4LI6PXnhtC*v^hC3_=K7=3m2XisX4t>GYkCg<=BR;hBGfClUr zVQx@Bze6TKKV^E9$xSzEmT|WwXV&Y;G8%;Lx=S4NQW* zAwOSdwFH(Sb+4O)_~2w#Z&t2)SOi`6a2g?kb}M>ZJP3Bps@z?g2e$~XF_9i&Um=6_ zj%eUqVh#=L32qq+3#>;3_EukxV;r2aoZJ}rsG=d30N$~DLc!8wNbSN9&bG_jw`DUf#^hs@q-=9JRWemBIF)#_1HBhPYh zo=pQkiIUX}sW@-a`fL{{ojdeyEra{;xr}x<-400lS|{tkq`$+Z6{2lA-q6OHurYC#CY*+ zFNzyn-!0wO3YCI3r1zX#-kKDsIk>7nb7j)KJ0egKJxL}`&Kbt(`v76SQOG;0xgW(_ zP%nGX%|ZeSM4Pv)wzJLmQwqUhR$iTcZuI-^oyJN*j>&g%IHKm8Wh z+nF*yBWF}y{K;PrtO+T_-zr{NgLhW6LlcX07gqPROI>I)4c}|KdSq&SNE^xg(JP|F4#{u#WYMoT0^|v>&~MWF*k9rNL5&6v{cC0TgOai#6Y;P zqL)5Cu%os@*H7R?vfdEY;- zr{la~Q8n4n8}7l=5mlE1SZa^5g|6R+$bu<{9!<+ytOeUV3GdNUy76IhG1)GAEh3iI zP+;Q8)=QJZo12(Gq~h0mW?3_FpG*&Exs0~^*7@IQ@Y+#6?3_-OrFbn$cB5@_ynQWC zq4q|^`iOgPlvVvSq~1(2U2|FxHBk(bWfO39!g_J;nM-{5^cHfg@r$$F$hh;e_S5Td zc_xIFIwB4(+@(PABbZVlHk0MuGgx-&`geWDrcW{9$1yRc47R5Pd`*fB_DS)X=L!hk z)|CxKNp(E_n{rB~iyE?<#oN>*WdF7Ew-}im~y6O^G>Cz6Y+NO1LP|DHn)PauADxdRr4jLY!J^%nb6t~floLzD87(h|OJB376VY&x#A#=V zes6rNPz5=1xY7@@M5IE6!!v!3->^ph0I7lWKkSuMk|q>=;t1M*K*lytN!~!Zwg$LV zAt*K~34=l~TJjwtrGG&@wRta7;!RMd?IL+?w~2rR`-$9w&hfx1SjFI$O;Qqy4(Wz) z#hkcSguA_2E7uu=JFJUuI0eK*95R@mOym>4Pqq%b*Cr7}{krZlwEy^XuYz3;HIfll z*>jP;QCs^Xn|`{cd0G7U$7Kb3c9%=4A?USWhMfnsC=F8^>Zm+k2O3xt{@yku9W}mC zc~rHo@G@Jlu?_Cmy~+r7WAcsbrIn}Q^9w9mEIqr`EQ)tMuDwDdGmH0@oQUYWjSbNW z<2ctvh?JuG+sLEL<@swsW3arLh|jM=`aqc87>u zrz_aGN3c>IwJ{+dHy~2h-J>%Fnk2FuNHgS8TeDgQ1zfnsGG*($&*CK1Z0eo+a74-~ zLs!HR)bd1*+03w$epc!ra=2af2v3ab)X{8aNQr6my~P3n_E>%T2Zao*VP#1#f0P$3 z1d#s1oG9zmlBbyi8~3o1weboiIpw-wrdv0r9<$)rR$StNx82SLQ=#(-Gkq^ET3Y+L zVr1H3n$4`VPOGFS3Gi44)$X}GZ*Bef^O_MQkWXUYh;7=|abd%o>aR5M{B>B%U!GC| zm9p2lm@y#0Tt$%g5rKj0f>w_aN^7q7f7HW?zi7NDS(1H&BRJEas6Joh$QIx7=H8BX z{D<`hvaqR;!6YZR0#vdmiQGBBFuN8U2eo4ACxPm_JMdq!I!{44t+mr0e!UHSN1Kpp zV$_mm-L%~#pKx9E<78o|-|_}5NP!%w3?*YgP#Me~>CJ**K`!F0ZTUi1jy;oyMmHZ- zU_I1*_bl2qukC6E$q$cPTa9kvht;14P0O=_B|CoE6}IR&t#E%(E(PaZC;ifreV6^@ z>mdny$?I>uUk)NzGjb?VcY`Pqf>%O3hpRA+KB}MxGqu7LN|nyit8;~ze*x#>|11NS z@9urh#h#CbA0TD+Pt_j(6lMuh!dQpStc+le^Dy{W&(JDxfMTxsqUqP2>@+JW$Zzt* z4ckZys(loo>r(6?pr21e~CCNM?Qr0w$%NSQFec3?O;)@Ns{;c zEDI7f{QQglw^HDgB&gMb0%17(C-;26=5KcZ%SQgeAO3Slm3($9-`k&iX8AanhuQC(z?aSU&de{4``{{lB0zM#E|diTbB!R%%M|1|%*O~Syp&lcLV z2`8p}fB7WxO|;Nu_G17 z5Bsw6z&WN6eEWlAfDuSj~=XB%h>Em_<*Qu>}p?>!LOR?e(o#kRXm`+?B$^s<%kE z8Gki{oz7!|9h)Z2#*T%Ir!R9d#hLZ>XM{4ZjGME6ZM`HBA1)VoMP}rwePD2>baKlt zFjl`U>i7gn>8^xOEeTS9D=(!j1g|Dh8QL||@g`gJ7W#|aBv$O?Cwe>?#TQ?P1EXxi z#c1`YxggOgh*PWcEHuod`+1#j&cc_y&x~fhpI&zP7{+ZHFK~#4j%8Z;jWo4!);BiR zq>R?4LSJJ`_Vt4B!$fb&PID%E`g3O%J9(LFnVB4gFd`t2;FyqIB7{}XLfD?PN@dxc z$?9tL9l`SQ;k{ecw2(Kx9JE70`3$ngKV`0zkRaIDVfB^5Dmd1$;U-q+Mwye>O!$>T)r-{5gPr!UmY zxo3RK&RS4Iw{1q8socsS>$p<5IC*>BU)>?i$)vfNYS7+VU2gD`fv(x>kV;4=$IoEV zp$)4zvF7G?ADgCSrIgFGI?cRDhPC!z=47OXSl`eZ*M8t4j3WGAI=2!s-p2B!HiIj> zyUVxKGgeVVHa$K0PMx%ficu$u7XM@A&?4ybXQo%pUo$oQ*YB%aTPGGzi>`(LC}?VG zYHLV7K6eu0d3&5}is=c<;jlW-f$5b3GRTx?EY`bKCS&GKz2tdH&|Gee`Ea?x=Mo5PURW${kHyL?eyZv_pK+iAj3Z zV>a`tw~l666PGJnr-x_!!6^7u14r#FZtiHby5jPeVvl__&f}?=9=uLMq=NpX(;I{d zwsBpj=wl2ROa^o0EGvD!pCpme)%86y0oE{x|Aj9i$%Vlj|2pu(r3&H_py}f@ZS6@_Iu5%vGxcxR|X`rrJRepKf}rTEz#-XToH6?^E|)TiGRv^ zxp{d}iuVP5=RvM;tACOp#yX0WjM86|v6e($2Tj}N{yq}QRl`1kXYt`)Xm$E&fMMOY7JzQdq2W-5&m1?kiVfo1wjDag+H8KXeQm$1#^W7gX-fGl~p4; zFrBRz*3VdU;2jNPjPZNi`p<<5;P6u#h?=^X-JYJL4&3(;!EwOYewY^{J4=quy zXF01`rqPOBiJ9efjqed8TihJJHD%o$3oM-r65Q8hzPtCL&5I>TV(&m_Dy|CP^cam% zNcRWBTqGaT-k*-JEB>*i71TUo)JsxQhho0OSF?IdQ0^x)(YQPc<-Gz&HE7+IU(e7r zC0`DboRj6h@bpB)+HRXmoBG!Mx~iJxXc4~{O zO>m`C@cY)*W`e#UPQXb|BfI{q%+3L3065-yau3hQYGzIf6=7-6QQyLC;EWzo7)y{XJsz*VCD61EldKId~KJB#3Gd1NqBoU|P>%iRBH_ z1{05>UsUs#Q#xDl?Gf4Ibvzyb!0l#**=ZinU-Wt2HDrD7Pcuy zH>ZB`BS8W7sQz(_78Serq$lx=Cng)kL9?r|h9ZVK<0k7i#tO>*)CI{T*T2^a8ts6wsZeD!e5PW_A0y)mrHIE(kVm+&(c6V8eDo7$xK_TJ{0>{uw zW-DaKg4`cuy}W$9{OH@is{1Y_oA#Z!-v15dV+eN=o0VJoF&-w_KOX_8%Xuc2;Lx~| z#^yOo^ZAw8URrTe4fl0ILoiNn%~w^L@yhM$Vm{N{-jH#=6l842&DRW%ll3o6?QqW-Jf+fej$8>)axoSM z)A=3)GI?#_q?Jpr{G@Eku0Nb#J4*cY5q`+$(99LMF-;CLZt%?*FgUSGa>{6^%@d@pai1oEQ#>fpb;(C7do<2tO&rtF#{8!} z7aA#jij1U8ZSa557@}I{*azRq6`vUCi9=|{f-Hc%)i(kt!uny5!(A7f}@7#mP+}NKu?tNcC|YMkbINePi^VD;-3PO8@m<|wiw(O`l0+Dk*qDemvkU_ z;72Mt&`8-Yo2~V}!weu}Q%$<)m;`R2b zW_W||vKWS?H)8l-CRiA8M4vSXX-4MbNbtRXUx=tO zpNC1PRybUoVEa(NWJz;z`EXUxl1xk$X>Z1!tg&E6$Br^xsd$Q9gkHzoLHndpe&Z7% z&{W{WmnQ~Lv9h#EC->tb@>PWfv4FXvLDzlxiX;WTNLKEo63_1!iM?HzW*L28f4A~6 zTIhC{sabpIs5LMmLnCEhhssqy!iJ!{|G9Yduy)wLt)<Yi}qay50z3LKo6Zjtq@rNFVr=+!r%Pk4KXg$%6@hw%KVqdD53Ze2*>eST!< zLse(;+_8?EmuS6bp@0^^XoE-P6X2%YEITrlUtU-Kdoy$cJzIB1ipGhur7TYv{yY<} z5Pe3Oa290K_IXVn{6@Ol-5NzOYE}PhKna@hN4oD|gh~*Vum8}U`Ch7prIQH3u`rsW z?!M4XVwQRHHF%~pt6okt73B{ zr)X>`DWS49C%Rp#+_uNuaq2zh34y2vJRC&sNIU97d=gYRu3xW8jH0Hn%bESKu8OmYXnq(}C72f6%7x^}PF7ge3Lj=PzW;3__Oq8JsH-t*zSZ7ysOm*+J zM07aa+y%Zn)cH?8VIQ?$rxkM7MOjD__@?*W-M^c&og0a;Yye0E0nDmTb&MrK4f#N$ zXg+nJx;mQg1*ehWUO6mIVEU!WD2)Zy-_({Fm^j~#9ZSk1M_ed14+OaL{}yk zhMyz=+a2k7hl4(e*5!&USj!R_0*YWZeX7nffFB?=hghn>;3i7y1%j?Gh!LK9)>L&& zMoWB=G&>_T!XOs@d!?E|C5<9uRKYSc^GE&{6jau5n05Uv#bp=C?}@tJp66@62|unS zHMO(*K4wG+;lElTeXuDCJnxoz{gAEeP)Ov)>kkE;dJf zoAol6O(>Mi9jLxGm{TY12emyTi^9acvM%Gtb)&y)0q>&Lk=>}uSJSl18 zYOhbZ3XSe)D4@(p{}X60ci1V1{g|#fzLrPXpH^Pc=Wi&l0vLf?iDYIDq}Ez0#R=@_ zCZ#>gwcGq`*2JFz613n4%9pA)8DVA^IzEPSPAh*WePtFh%yn@<3`C=`DPv zSZQ|b^J=XZOz||4!b#O2p`^+G18$Sf`B_JpV8e88kDa1;MM0EeThI{sIGQ{~hHZo^H!KV@J|nu5sE6CI^&s z+SF+^W|?6QJq@=1ZcqE}xd+YnTvrBmVO5DwzNIkhHKYlvi)0b8+7H;~4pJxPDT{c! z>)zgy=h;zUW}ofBHNYcAvzFjymS*C!K(r7V!&dg6e1_IFcK^wOWbWBb>}Qm}!}t5o z(L1EBc7dGk|KbzoYuzZtibJJamrt)7OTjFgCe`P6TJ$TPJWwd*B)=zl0 zRM*jJjZZ59*to2tMJVAT(<3M3#{;N{rK$i)gxd~pw8d*$k_%9Y2w5lv4}Q)_3_~nx zW^V5=7v_A-Frh&%n^>B8%Hp|ofDjhsm_v|ZpWPv4p-Fcjn5Gl3qb^|B+&bIr4?uSG zUW}Hy%9Yho8rZ49K33#-4b~YO_N`mlDbC#7?r_xh%#udSOtS-+OvVx?UjIWYaPxF4 zfYghImz(uKvhKb-dTddUMqoe_DyA zx({aTR&oM>6T}|GKqX{iJkfrYj(0#u(yWQWD#(MdE#;eHqA24O;gtlq0Na^I)>S~= z!&TzbV;w>}4Y}{-hBVnAmxx#a4X2tC35N=ZiJcm(e)U<^@8EKLcb&Um)d@SE@WIM|C@_#E zXW{g(CS8@u{%Of{$7uTxlaCC7ss5;V0UoWjFAhQ~&DKxc5y*~MbBOO4$oZUw{WTI_ zBU8>GO0mQ!>5&l>5;kUE>bm@os^&zpq>*;X9^ey+uzBS8l-!b(&`JA%Nb<@N zrBW(SO)$8^Y3`>yziRoKsc5pqIlm)`?zs351xB5}p@^73q=(OG)>bx+*0^)hmo^wJ zUqxk!Vy$kxoak4^Kj*KPh~)a*aQ1>J^}z3++>yA=`e(qUsG3egEDqQT=EBE^!(;fZF(nf7w!Y)-p5&H?}| zm-l~y*qg?-8R6~zaJUExqt^IGSNuQ(QGc7t;hw@2B;dI)FtZ5^vM-x_Pcawv0wF~!txK#>SbWVe^A_dh|o`*%?b`S1J z5HFGkCAvLk&^2FYoc@9^&G-kqJF0u-n@UZV=m(M7l_T8~Lt%G$dr;tGwqVM893Ov* zhcY?&2&_0wa`N7T(&aOupvc1Y2fHPk`3F9~GDj1QWZP}d?Fz>`P#iyZMcWmTk{{s5 zm5SL=K`QGyxtU?k$p-zRgn9*_3h3iuzNR+5T9&-GLY}@-Iz~_|fde<1*D6Z?zG`3p zE0{XKZF>SEK)j@@Q#ed9UtXFq{mqob)$Q_Th!|BgAvMY~K60j1U1fOxK%~*45U#Su z>>ny#0u!UfwW7OSKwM7E zB_7zgw^&7v@wi$t@A6lsR+JQBWU@$sl?0VY!90ZsCi?k^hFZZZ|mebeuGJx(|fs^`-V zj4B;Lb5|=yakDTtSs@LtL}VMbOgD>rcm?|2(|_i#@J7k8jne+kn!?C!Na-AY z?82CFI{0#ze+uMSZ&aBdEqT9LF*B<~@BTc6YD_p9rvl#(ZD8|Z45&u4#w4dtuWpv} zIo2+!ohmyfUG!dwA8c+g+3*Y9@R4hn*e9rKi0XG{dOj?_SfNXxt!TL*h-Abep`L#* z6x&)ns6$1GY;nZ4ckPsDk5vk=hz+h|w}T0MWBdVulzqVT?(RtSkWZ_xHq3%6k@_@- z4yz=JNR+%!3*zc5cXxpDni&yedFFxMJ(cTWQjuMMo&$<)LoR0Nl*)fur(lgxHaU!$~Tv9yzY~J;bP~|F(!A@zIz&7XryGsm&D& zm|INHbP|T5`q^5Qw=ton)4RLL5SLdOw)lv7?cD{Q+HuAjTTp0ieM#Zh&xlKS!5HId z68LcKLm4N0ISO=|DY>eAMXfG;&Lxf-&I@VTy9!JAf~xU%F18KVQi1?gWI$(BSSkofyH=1Mt!^xHzdcq% z$Q)5@sAApGMfj2wWsLmtgGvT!X5zYRPU?gQv0iy;{aW_gwIp!YG78c~igYODzngvE zwEzQgK_pGcnaGyvjmgIW?2aZ#f|8WEj@ho=PN`3q7XNUnn>^NWHCIW}GE^2V4%SiuOK07mosKOlZTYy7{r zjsDxA;(r+t7fYHC|AYVNX?ehThX2#!3BbJh_^B4@_V?-vkaTkMdMR0g{0RpBS@blf zffyF;&J%aziT5F*Frw!@==+JDJV~76Zx}Xk)QJSJ{FtiTie}D#Z~ElvXBhP$_{9HM z@FyQj`3+PI8e0d#8L%HC+GtN7Qc??5q1c0)i^M!p1@P3@*Ta=kD}e@CSSK0nq$S|j z5j(#Cj&xrezbD@^Iv1k>FF#*u@L3=UnRY;MtOXPNd8E6JQKw?CpQ8u1JB3T`ZZdfg zI%VWg{;SUSyNDXtgQ^#HKZpzZk_6H$+|YcrJCV6aEORL(YBzdv?6Lkit}9$x@s^gy zmaWs={_!Zt_B<{KYfg&hL1j^brdVBaFfR}EP{E^?8I`xep0E>BslreAof_+p=Ca2& zG*rT^8ZkAnNeHrA5adPi)zpRyt|&H7fO$%pYX=h0xPm3zTmmOger(x?0+G~565nrT z@Y*h~9Wdbmr~pa=vGgxa_j@+wXtpbA`p9( zTMpI3I@sDHM5tz*DUqK-IQ~0pII@q|+JN+vH6zf|My1+tX zq^!Nyb2Zu~T~f&h0);+J;xU_sNjN9uy8#YQqW~uG_;? z5T+Wl4_$c2xO!||rsXCEDY|bJaRO%Ii(S@ors!Zb@V8DkiEf1mlaqe3ETNXr02-;U z6Wel6aAa_k5Fp;-&|+>V zi)hX~)ewUY1~0CKyJYL2l4U^jT-MxG9TVVEa!9|F+_S&Rh5w6VyvV*ha^%*^4ktd2 zgSH>E{c{mp?>2##A5FdYFlFf3aJZK0mSG?~xR)0H!hADwDpW|k#!_S9cdGA_k6QG> z6z={)P9jmk?RyVU_nX15oB_iU_+l3~QGd=~9#5ZiQ+6W_ZuEGJtx)$Awq&(8=-#$W zguj5(Nb2XeAq)PNdI&sn88SF0*ij?Awo%+>(2=~h?uHDYmy~Ul{|&{HgGVn(HF6?Q znB8Q!!T07m;G*ZV1g1!g`s6qKpAnxf%_UlHq&_;qBXhd*DV5zmfF|Vvi{w}sQ9>2v z3j-(+?{`ekF()uK+DaUs7#Ge#M4U(TDQ7o;rjQb`I8=J=XB1>(m+Q8N;^r+`S3Lv) z0dU@^-zTf$k93NrxmHJiLs>PP>3!3-psUU{T{>jXSD7CFE>BItEF{(N&-n_Y`BH%9 z``y>u`S?aaF>g}NY?V608*G>BG0Y6+y<{ei#H5Cz?Jv~3uA(cY@x0jCNyrfI5i(;^ zYK?FDlM3rav_rG+_9|4{VS^>bx|QfXfhIO?t7zs+Msjeji>LaBG4+N;p3}pKVO;@i z&PX}NR^d}3OYU~zA$nmkTF$w;Zc)UE-VDREkSKR8sPN+6*M%RRbmB1E&z_Xj@A)(I z?M;%>07~4Ig`Dk|%0h3B&uJ8Ibv|x+PdJkBHrun(Mt%ca1P_}C!II%EiRHdNs(C=6 z_W5&eGg%?m&Ool`Q?mBdV7i!e@;pgXBjKmy>I z%GRFYH$*X#`f=SQl`0&6ZHW_X^K{8P^MSPEEDsgLG_FkFDQ_kFS?^Zn_}!?+@gOS| zr4%p1?V_$fPwb*L6IknXV?n;;`4wE!gAB8tInp|AJ&tqDRJ;9{4$WT9BxziY8Z@B8 zdmyuhTMqQ#vxHeJmNhPHs?7*K3T?+6pH{=DB$n`TqUq8o&)!DjeEg!NvfJqh4<4qK z4t!=wY!O(~DUK2`-*TS&IYOSR%{r56-+$?Ws?)hwjeCddr1#nIO3G*rL^$&=x zP#_o;s)ZrEWk0v}5?}mA&yDr;<3@WP!d?gR@)rki zi^AOm4ZVRcGLuzs2_uAw8Vl@kC_9_f5XHzAGz%NBAf5T0ONn&no__!u*||21z6b@# zA5O3X4b@kZqd%B;J6qxU|D7iF_UaV>^U7u)X}c|Cum+O%nJJV6FlqpdA2Mc19&cnI zq8OnfW<$}MKQCaVa6p+Y=uG>24U2jCIm3=^fsIeBuD@rPoUjhWuk57`a_v-ix3c!KPVc?5x{aC|e=E`kSgk+RJ7(dMRRGXDd8AGTRj03`t< z&Sv1JEw{z)@Qd@IsYsu8p6Cih@gvy)xJGEQerJA7VXiFO4fwob|MYS!nkjl;Hk~Iz zQAbmnc*b}#E4{jPq9(DN6%n$n4cFbC?5H|nuW9+z5?cqX0`u3*4f}9)w5SrP^QK}m z_{Ed^3IE=*FPoDRcg)I<Tk)58zmKCdas~?_cLV3 zFbi=Iqwz(4Mc`~X=*@!=G31rmLoa3eL|gXq7wH0>ci(dx9;p5?9mgO5$_yca`N0Rad#wf%TwO*G8F{p0pC zne)ZVK_8zUYAGUQJlRK>O!#^QR9k56=IF#-M;plVqJ}xfhQpdbPK3Y0)Z>m^NoTzFV+7^BOIG_fmg@4NE*c5H z(Eb>$=WuVgw1b?>&~B==fM30<_-2MEq7v`{3J8^Cg?N-5)9Pt`l?hkXVWhQDd{VhS zq}bp@jC~v22267VRNEuAFN@At|g2w#_l8X5(clX;Etz1Mfm?zTl+2{%Sb% zTl@5bw8K;dl3k5G!Kj*{aZwwO_H)fCC7%@7;?8Ew?ZvIbG}we!u}A;m6*T+yBho%9 zhTu80Y@GYuWh4^GOE<7<9C_-F-U|4+dFsDVw6@iT;dX=1N^()vF*3OKVcd@?Csn<8 znUkr1MSPu31Nr7lEk;=Pr<({j)Qy%Z+U`oyndU90@9(xXe1_$)g^Ch4zDba)Ykni6 zmyW8bXbsQqzgvp1O&!WgF$2mi4Ww%|8I##UJB2mv$wp!#Bu}IUxSpXH?xWl9QL?vd zyX@%GR0kTvn|64^I!%MO#~yi&C_i7DhY7GbU43b^WfhW6NKss3*0^i`CI_d_p0CrxB+S zXT66}T<3p!;y_@&yRiTKJJ-k5NF&*&^vCtI>Dse8a7E2{jbvM>srNG80h!jj3_RhX znY0nkf(P4+?kdUd(w(lrf(2(=IFo@P3&s0OSnbMab+O}COO}K6&K9Fgx3kd-7N-Z? zoY6a3?3n z8p6!`p0K_8K4@w;GW<_0NXgva)JSNt8%`{SSH=7#g@$G0!W2>xHWt;colv_ z<7&Z~438X?L^HhTi3<=dLqy1OKjwXU*GCmazsOsiT-=jpZS}CH#E7J z3-#Aavqd3~Ngg8Clm#*hmfA4?;HjG7$E&}gEEfOAVNpzWVZ>jq_}8l|5}Tud!MA@} zk>b+eyZOHyYAa4Z|3801*2(|(D}u1|N#_jc==+3B5CY$6*iSo1E)A*%`k5!~tCA!K z#L|qRlcRH5c~X6$J5hhbfmCeBfDj)2E6|uf9h&$Ku8&1`f36j>L-A1U?Cf6a>&x1G zkU<8rY8R!DD}42Y>rW=a-ar1Du^Xfp&`8#c$dFSZ6=&9# zh0I#83xkN#l`b@4S>-Z~ z8TEOai=ShXbIASz9tYz*0568Rs*R4GSe{R*d1iXGfz7jF-i;0|V$S7xIwLS~fP-z~ zTa1?d5cI@17n_6eJgdV`tnmEkY8MwWix`^-aYr)Y)mnxu!DW^YzB3GjmFr#$vRX^Am4&jMRPI(jGy6WlOk`zFyG zwyTNjbb8}NyRP_oZ@kn8jePm)S?8X80`SBJ(R6XIe=*s!lRcqc4N-+XAZ^&i6iimT zrX48(T%x1uCP$DNA=_%G2hK#v zlV4I~p(vewze-g#n#FRp6HR^EYNpn3AVDn0ZxX2!7$PS*m12hg@BRvI!hB%7`&?7K z9PIG3nm__3S@4#>a)^A(Rl`+=L#3ZxqLb3D)H7uarJxpCg?c7VNqfZiriL*yg zaH+D>orR={)Zrdw1(wD?9+5xe*p9@|&nR`Lr9*t% z?yd8MYQb$23f(|E>KdB{nvH;GYs!)}b7qxomnp({)K!f|S0fe+YC6Z!&Hj1n(w{r^ zfuQTvTq>=YwRNGSbbzseG=!7n%^7E+;2ZifpJ#kAO@BjqArjxyTsG6QO}7xV3C3Sx zf!q^s1i0qkI>1lYDxPM@>x#iW4!kcn+uAy3g~T%xx@b#2t(CS#%u1>uRSoPcP{NU0 zJh_tWhqjlVZr^DlBc6C3MBy3*AR)l=gUGj^vc+FZ(>0qkCE7ucutm&e&_UwZswaOW zZW=k4l2c#_tii0|ECA@Solj)^bF9+*NMzdBbWNxP%5SQYH0&CQ`$|-+ik=Zebq(Li zJ8>KQ3K79xhQyvcYKXg|PuYXGPAE9WMZPYC?rSGXe#sl^{sI>nN_>f^9lGi(F*lMg zw=nzJbu8}!O33x9g50*cUUZGP)}>MsrfV%@a=1%TB9J*%EHI`azmU0YL{oT)k4zFW zF}{RSWq%b!CYRwCtR8I|V=72;UAB_hB{92%z$zpfx)0+37dL~wTpMHRt@A;vWwq59 zar|cFAWBunO*TdGH#03B-^mXeTslQ|IJx;HU9Qx+!Zb3|7(3E86rkQ-c!f3`1nKgv zD1H_n4Hsxh9*a9UOOFj!%IXR?GLc4$roCq?Mk|unJ|d}&QG2&248PC=z-9zkXA^$G zQBSd*vLG-aGDn<~Z;vI6|8kc1%Xys~iiO+8$t)f}W*so#>q zk(A#ONa?vqZ?a-BNDXgI*}+~S9GHO_P;mL-=p}reB1w}DeXs^{uaSw7e#1#8e0i1v z5KXJu;h3b=(jwN0nrN#Ru8>5n^Fo3q0_plE$~IXtkp2h9Z@&yYl)0k!ntm5RA< z`6Y+Mg+XrjbR`tw{xWc+5>AOih(=m<-cqeO>(~n#*KZH@@)EpFm}SN(hP@8m9_7$< zEzrsRBj4#+d407~$ zD>~*g-5jMhNR=$9q8Ugj_g!S+jI=kvDO6=5P=4N(y)7~rj2R%5q5JA6_8 zmdo#rLaI^)mkW;5z3tT3*w+0d5*>BqYv}rO^K_lU}sr@=f23dn&T5htsI?QD4;O%SWrkOR~s;}5= zlSa|rySiiLAqYi6J8d#VS~Lrefo|Uv4>#nT4~R?^lze`42vF|k)T5D{h|rP#7U!iw zAQNl}L3Jz5)+SoTY8Nc(wPfmK6=S8-2n;zq-BWSpwr!{-R;Q2k2VUOh?5~vi z_5MH)oJ65nr`Id5UMSdFM@{Z?k<;?!goFfd^^hOAX2)yF&BGXsi=q6Q_JV3GY4RmD zji_Do5&Fln=x&y>2#?*v$Y$AMGeDW;n1@Y(z;)q)k~L%hm>UJ3J-Xj0M~;XRW(%i? zEaDIDXOnl76WztZBoP^9#%iIkVt&o{$h2S%nQ6UiCH?oM?;|j~*$(opkfW8vChkKZ zO>KprbR>kFVdizru9m=RCpVZF)*@`1#Q8o^0_`$%)02ojxQ}pG6Na?!pW^Dm=6bKc z)^rbxH~u+(am=||{UqQ3gj)|hiFY=2Q`$$lQ%*=k zQ)G!bEKUtt*Za)%81 z2PgC(6WI_f1BLG}x=TOYb=4e)PJqavc~94U2U;~?Vs|le7!5^ktQ0(roM|)lI{aec z6bgm5yHwFk!dzEsx&}VH>}i3KuCKsejCHzngy>Exd+i z%hspLmg`MpN4An-+jhU0zOXB@GNL4#h|s=(mlmx{E{0UfweY>HjZ4>M3uREjXVG%j z(feeIMt^5mi{RZkj|dPzYBBpn#BBGn(gw@w9GXg^F6dxvX(SCSCfwmDwsE)!oPo4w z?NPxFu|!YoZS9aAGRnM;iW5rD*lXiVohQ1c($1j+_LdUuPpENpX}NBpFI;rHqg0iv zqypw1$ac&>^v&KY)VI!0>}3#ElWGAZ#;&bj*x4#DIy%lMy%c@>_QyfN#kUw7vZdgk zH#@lemMnL=Al?ihi(8}JYSgO4`+DGPrwCkQv;XN?+eMD|4YrR^UX4=*-&=i5oeWJ^ ze_;|3wMgyux%s!)r?-gU!9~L%fVth!g%G@n)X))!3{9&g;$_f^aFc2ng*5M`9Pw@q z^;~za!>Y$Xr}~!WD_>pn&WopFN2aD_!R->Lvab!m`L*wol1w^JkM@Z6BOvDI57rzrB;XIFTiB0x1Av>$+?CU_9aT;E!oa{IW~h7y@a8Q zr>A^^mUaZkw^nntvx74~<2bD1usApIj8+Iw+$0l>oEOV%h=NHSdhlplwz>b z;|PizrPw_{xwmVLxw}_?EDjLItH9VOw61RcXi%1M{d~nA=-p~rFK;!oUZOQjp|2t< z6DDpnq)I1x3(yf$O~F6)YSp`!kOMb2Hc`bFPq5eHD4B|*9@1zy6|##|vZ#6rlo!QJ z|J_^g-wqPb{Hy+&e-9FC{#ztC`w4(C^%%m_^V^vOF4=_M8l5O}thog|u3PpQwje`z z!KKvg2XFGN8Zy+|ny<#e4ApV)_E1IxXPVFUG;S8>I0oMU!u|7AOhqNN8`0DhCCn1c zDud=f55dhK&H>A2^Xsr$)qTgi$T%L*nW^{Yy6VRt{XQ4zm=v&_rM8o9tl2Z3-|A|- zk)&`S_l~-$V)HbEBa)B=fkwVDduNn;g8rF;$-IgoECo|DMZ&FW1=uYDf8O;?fC?n9 z-_;Qk#W5sW<|C)9<*H0mDFXi%XFFf>Ct7G-y$;66XSz-`8h0@yi=m@_pQgfOMIc)D zitc>Jo~?^2I9uHHJpIdu#TMnl2jAJ>>U2X%M|Z-xq`6FDf$dHVvlLbh*;+a~e%z$s zuf0hmMy`XZH12`<*X%Q%+2_QFs{v)@7C2^f0Q*+Fq1nYywjWAcgu2ep0mh6GD}`A( z>PF>I)xCfpFK)?_A46$z&ZyCpS$ozF;R^Pg=}Yd&N$VsL(mmuTRmb zUD4gJLq`cT3ExX66EjZVxm1>Z--+r%kCOA^00mPE#Tn-}iM8HiLD1WikJJ_v3!OeY z*j*fi&Wz0&TA4+b4I_=KN!YG4Dp4crdgb&N9DZ~%?NX}<&^^+#F?a`COx&Rms(EdV zsH*hUpkU&pQZIIF_gX(c>mNWfQv)rahjED5J04l|mpx5dz6j!a;6)hQmL0VZe|``J z;wJ&A0|*D;9m9g+GBbNEdZZ$6+S z=4rHSp2ZsMh1JM3le?@+Vji(Q#VR-fxDH1gR-K61M?s`%eM1j*XKeF8VrS}d)dP6&m5a*h$!r4o_66y?2JuLgMN~`pL9? zRGCE2Qf)>$P`?{8ouNgHPAKfGOqUDheVbjfY*PO0g{$}=u)S`r7iQ3q-7k0Tk6;ki zB#Y`SrInS7vmniQYkl=erdLP9<=X}8c3Gx}PsM1h6aTMk6PDr%O^oIiL!W4=Y=5Bh z%x%4Z>5f<%|T%dduVNZ!r;(got9O39ddL$Pb{eyyCfR4(qW zajkHM&In<-h2I~DJ)9)~yfLY5_~w+yQa|TeWdn^WR^%?NEl?_)TDNI48c{C^_xUBf zG(`#S&o=i_T=J^Yb!BaUD`gf9f}df^J}+{)9I$HHchJ^N5~bfuj+5lv#T?|uGCr8; z)UPs@oc{FgeS5M*uX8N1X}znrQcUq155B|%1 z^yJP83i^iLV}7^!IqiHmQRRl;lNI-p4^ner)BeP2Ag0N@i-h6xbOkLc1*BWi1^B6K z@8WPOfja0kpw?q9CUEL>d+SkZP7Y*kFz@VJzuF{WVH#?K2-^H&kzn^Bsjo=G`eOGRA_pRO0F1_DbE zeJ46CJpgV#$lJp7AIn^Ikl5(O4O0KMS3KOi%9q*}suWS?1$I!EpcfA5S?WvSSsUVA z`p(YI#a%7X$J@4GlC`5menu5Em5U+bXZ?v8^d6Rc%K;{c+_K}l^d4l4TOT^_mB$+0O7g zi0^rq`}V36VVM#FnZrAlH%7eETA;Y(Be+A!o-D%=}?`WAKgQ6cD||K2B-bTyt~m` z-VF14owUBHG$Sv3dKlo=peXre%S}}-aCi!DSQZZao`oMOM8n-FR1WzQQD=QUpvnSm zOJ7ene(kX0Z{W*3n&@i77#529yi->jIejXA+6%sWw&b`IOy*lWlohqmXQ%pVkdhbKzng&n-~Rj$OvV2? z=)R>8cpVQIriyd55jRTM9C-NvZ9R9EN-a0O>C2bdNLJ~-@;4Z-lmY~5ya0nqla7%Cr?(PuWAwYoO zP6u}h)(|XchUhvs+L^?r#$nF};FPZT_nVj^ed! zKP;M(7V=D@HS_RltP=KuE1UuZBj1WpM$Z!FYJaxuvY>~KD2t5&qQX(_*Jr+nrr%zx zpwr_92!>AsBAubmM}&Jsi&U^)U}MQLrA%dU_&ae!Ly{lcOKHFA_u@>beh-8nYVba5 z|AV~t0o}oKh%YvHbqwjba;?w>b%bp8VCP#gl=f5EH{K0zPNd@E;@0&h2Hk>yy|zrg zThKW`_P@IQ56PSeKi_-+sY3311#A((oqm^4mg@Lz1%7+YoU=^5<9pJ(baN$!)cegP zfwCRW&-RNoTY%{%>xLudH`n3J4QQ7JNO(PPSTk}9B9{4WLW)2a^|yBaT_fa#bD-R$ z03Zfy08j($Job$KJ|*?vs+v!I1^*v>TxXuMbD}YK{%0*J*6OpnYIRK^s)56#vUC#^ z6OxbY+-M{Gvi_)L$a;}D3q9yIb2|1&6eDCQ)&LR1NkhWt(;?f>fq~x$Q;IP4>YK8~ zRK8?QrQJya@$bJP{hvnj{o5R&zgaHrA5C}N$Zr74C7WNDOHLegX#zM>?|kVD z#J#W)>1MDP2>Yt{%VC`>3$}t9^JNo_?tD@$1v__f1ld?*qNTKw(MH1(iUE1-D(sWy z$=gISh31O{-34<#u^PpNt?)Z_2{@4)^erd%*N@dtBZze=`_R~)_4R?XTNGC#uF_j^ z4n#R&2hi|UXsu_Im@;v4IaXT~{qtzf8uZQRg>|ND>Otyedi+RxxJ=k-7DBc~q0}vK z;JCf!sKQpbWy@@q-hyvy&V?1bv7|>{G4n?2!Dy#SK5t2+>Zu&HxX)-)W_qbxwNz78 zTgau;54P;K4gt$lct z4d=q>FEeXWMQa*gZXHNXf*Z*5O}NGPY5j*69T5G;nHv#JDS$qbaM5enXbXSSMTTQnJ~-$>W4V0e=q zYjPZB%Nc9I?JBJs;?xF=f<=6>9qgkgj4#L5y&;3nK7#O@PAlrTU8=_h@AqTtRf zZ$r)3I<+_ck3SO4Ht1%`vB@zpHVQJAqyJCu{y$=toPpi67P*;e)Ql4E2aQwikAT8rvc=EQytpIh3L{8ry;hV=E!h$r`>;&V{ZK( z^X+H8ojf%f)B%TQX_p$^dWzO3^}<|y`E@g>>G6o4m1=ije3%SKdL2=jt>he`u2 ztUg30JdVYGsAnqsea-5^GuC>w<-9kIIG_a$rqR3j5LS_GRmS@>&Fo{^F>M)qc40AS z{GcB#R6*^;m{LL!LXj0525h6I$;+pFT2P|PPG0K5ycBkUrkFEEiPh?B5)&PhYVyYt z_Qw&im!DWOREe-I2tPA}rAWg8A_QL)E5$NBU(0MjSVpbN;>^AWPcFgUA&SjC`9-EO zMiA3^V6=~APK|xL3V!8fR!IiY_-ba9YuSP#|Hn;OK?zSQ)ut70skWuz;$(R)SA?m2&RYmx#h1kJf^?)hjW~oe`r#w|I?qBC@>W!AKjM$Mwl_ z1+{)`qt{<}($`}=-#;?|^|Qs-UFc_XWh|4FY+p%F(YMD;gKg72AO0e#$_i za8yQpp1MJL1>e`Sj~@stGh2)jS-F83-I;VO3{ycaclZ|hc+4zF>A>wXmh9z77uaS! zzye#e;Kf_ns9E09z;Sp~FxE?3X{SunY-%{l6%!jx0AoE_jGSz8RmaP$7frb?A{KoD zTd%+4#yJwomfKo2VXP3PJ!_t+)|)Me$Jh{OPve4qqeEk?i-{};!igcHw;%_xYZNP` zO{4h)M1q#aKG_b;(XSLzQiOI&nE!l6Gfb#CaU0m|`d?$=|5sS~|LgDn>9@hZPsUf5 z6R=8v4&|i2dXNl(#&Z8G8&FRFBz^=$SPNbip5eg?Lt2dBy*iwP7RhO?Wn>Ho-*uK= zraZdHk$v+l`o(uyEpk!{{Irv5uiobrl&_2@C-7a4j7m|1+{z@M!d)C(L^7bQA;T##%r^jtImQDHGwLlUJoiKU$pz;%Obk(SP92_AeID&YAd_pbq`N~!H;R0D zB9XsGMb@UMq12Q)Uo!Ae?h)4J<983M0^ED;>^BsE(_4}CCWjsV-orZqn)hl#GzWv8 z+*fQ)tfaw=mRk*`sSN#lZGKxx4YMx@jBby^*4%9+l(F_Y=jTpEw$8ksI?NS}iIQG3 zsj^a0(Z=;eJ&vCO!yY`ho!jwp`A*V zq(*DSa4pZYu_I9R40gF8R@Qf7F%#s!DT^C|1|D(AQq@YEo74jg*ok>@&tz05xIW_| z$TTo;|H!zn|ImPh<2d&biQ8jt{vULAMzv+U=~rS)bO!9Kar+kHnw**Mb1#v*=i5~d z9{i^L|K4tD<@o@qL`}+=h5Cp?lXdQr9s%UAJl8}zc0C$ZFfcOHLREZn{f8-BP`hz# zxjDR7A(M}NuJ0JAS!%cC@>lQPz1=vpb9H8Nntm1 z)Jpn^-#vboEBpWZ>|%d&=K&ntPr922z`C7b_vof)Zt0rZ+8|T8J%v_QEI80d?eG?K zzm3x^Yq>BoG}?^w_|R#p8;;g=kYky-;5_MRlspaPRbIW`$CB4*;jhh5Kmyz;@89)-a1 zhD^71T5`)I$5v~@lRTERmBHv2Td0l{>KY0B=j1mRymKL4$I8J1Ht_}y0+v&)a`GZN zX4PIc@WP-L`_k>SnBZGb4Q08Lm4Z;C^8?8$o=VOIrh%8kjY8||0sv(9$kzmj_&|s0 zm#+cZ#}X3^qdW#5scY5>Y7QONLm*E`;Gm-cj!tPi4Zep)s2GK?^jcDto-UNh(stU#Mo*IxR7=J%$H5G{mkMkLoXgUQ?!>HXPB_*`7C97ln><| z6@^y0O&Y7Gx1em6X4>6jN1{%OhR&Lx4F77aCD?T4ozPV=|DT_PO=u8qFvPxF!N5M0 zamuJvcUNUPY~|@;Vt?D62K8x@z=DX3Sr*1-vbdmyuzPp21W12JRe4@p~g?)V*+H9QB$BL8Bw+`rW|RVG!l z=sRSGDMr{HcG5LFYu+Y0$;PWE$kIaYXz>N|?ug^_OGnd zJ@%PT5tGa5o&(ng00KLswtEJwKig4+ym9rFsac4d6^>Id{lG!N1 zQIlwH!s>nVBO(_QYrp1;lJ26BVXFI+Ef0##+ZcYd$bfas74288S#|0fnra&|IM_c7 ztqzf0;L3I?)<67SL8u9nU60B1`iHt(#_K$o_;A&%jaXT>;LRJYxt5=4;Vsc~TRAPd z>9n^eL_$?|JO?B;aT{~pI&VRxMkm$+y`f<~TwYH`os=+dL7hS+GGNiLatkHOmb(3D zp5&^`xi^uLY}W7VPEbLk^a%lT#`8M7Fr44_H-+hLpUEs=AezDK8~CAH{F*#KD0b#e z-gTxeom!|bj3<6w2+CaDQ%Oqwz_EER?-NVV$7B-{vxgv(`Yb#jtn|_-t`X%BsB9ct zdFMC7{V-dc2pijJHl+vB4~%I02osA{tM66S!l1t|R%yCoj5N5HqE#)pfh0>S zsLs~g)lk-w|EjM)yIr3=zDvvKXF_zQ`6(pE)5uPG#~^d@ABGL*`$uPG=eDz`JwBvx z+mB8WLw7dk-Za#YO9}>`Hl_}BuUg2bGG@*WZ90F4sZL!PTh{jw2%(dGX z+PzE^4bvhz8`R~7`tsGf+=Z;2fx84KSrEe7=QCQQ^{x)J1j^cfRiFLWb@rJ51x>#H zykySQzZ*ROP-=a_xHL^E+T*X>nN@2^aa9Rr!Bl9RrcL>PvRxu~MmUSGUFkUw=LzFw z#nO>i$i_PD);3O%JDV{a&p9cyE5?VA%*1DYh&!S=7-sPyMaoVB(XgRct_lGdvG9I^ zuR{b%4P0DNV0s6FSsf-Dev}%Rl}FdA4J>Sq{wa-ST%XPG;G)c$NlYAfG`iV9KB=f9#O zh6yRGj?m%6)h10|E&#Nf_uRf<)xU?t8ZAU-T=Z&?Rty`=;;|-&93H)hJ90u4>O3F< zoY9vGWz^dz4jl4dr)$}8J20jtryx3H9r^5qz*Q~JG_(bi(z>jTBp-9W5Q~> za8f58yrF|;aTFNdGwMaTI)bOYsE_cKX`3kjxv}KRUfw(y%kf|!=`QC(B@utH_m6f9 zrZHYUi8m?Y+zc~6@`H(1@n8cv+8AmVg*Bh8Q|=A-$UVwRx=h%*AxC5zmN>$>(<7PD0>S-oJM&VOrG@~(7Vz2MUh87qPWlFs8oU>)7TdiDk^5qGnc(G^roxq z5zqM*eX~u(V>?$bb2mRKwH6>HbLL#^NGO^2&{aCJ^VlMpTeIuTyc;wZf|v54CX@!n zUw7VsOkV{5<>;I|7hgEf8&j)H(;G;0TO))?3s-Kann@gLuzfTMZoxEG=o)V8?9UC5`>D5Y#~1}%?o#WdKz-Kp4u{&A*sb> zB~35x(DXsPOLQ$D&$sw8KB~qHLyAB>=)d~p@88Yu|HjGTzs;%f%Y^r@0RR8H+=E>T zgb4grIDGJTf)!0WUd+dpV%=< zDLsz7>QsCy>HwwfO`F&`^a6?ouryNR0ZWvn;pS%FQ#f$dKHQ)XgFvKQpWQeQ8DuX6 z>`DOB4VsAMaV1JZvL-G;QqA2-(wfU1=Pc=`YwQefPq*-Ot|94`;i!BZQ@prz z7cP9#$pzq|7paI~3bn2k`t);$i)=wRcy_)Db%HKa3*}+=osQUN_z|95x%BnCNR;fI zMH)mzwwuQV<`n;Bd289nGI6)OL>82kxLA*9t9F=D*A7%_g0q$rZ3z*o8TzKO=BD4I zvEGfHBU$wfklaZ;a_csSoLl<93}1jBhHJe`V)UOCpnKQ1EnoF@{gDh!_>Y_Usi-c! zjF)>O#>azgjz=ulV>81`Hi-bI?>C;90-H8MgrmGpUkm@r-V%8!B0*!+IE_3ds^-cH z7uWy1Q#e2V#o?x0pw!2a?;s=1SR}(JT30!-T7!9S8eI@^XLanLD4er+GVInRga zG<|UQ6H0I|cM`}8ZtLr9Mp3D)tMKr3vx(+HtZZ8(%0*~pLHa;d&)_i;b7exydXACh z=~ZdXJ0}vVB;oi&QIqL?r0WW643^uH$|yrkx_lK`#vqZcJ4I2JsoV^17gw^A+BY^- zJP}1Fg9k0tU#u3NHBmiDCjE(py=c|EbtPLJvZ6LcqHIHd-05B|`-#0KE;FR1>0D*P znMh-TqoB$7E=!tpY>|k4!)G0;H5M%UPG&9%s)iO*h-+^3NVY znN$s}jUxCa)eS+msh(@&youkg|g!B}-JiW9XjG?rKSSZKqWfQm1b$zHakC-{>wA z?pF3I9~lHKqxL@GZEsVM{;CkB4$zRjqzwAdmCgDY|46x?V{KC}rUtE(I+S})pSm)< zmy`~mLHpXMpb!Cd0O!}Phq=-B&gj1^;H=Il^M@?zV&f>lsKUI9t+;g%dbVcOjbr7i zuO%n5128-%KM%OmYsOy(v{;bLK18Jvdi_F-uOPn&!jw)q@%<|9oGp^v;ZDg6nt&QT z>NLf}PgdG}P5hlVTL)~-n2U}V!D3B10+2fMGlxS_=jltsrR9+1G_y#f2BI$Y&^Hs- zH5lp>sEuSyM-@rZ(`5Uyq5bNb6QL@yx-fH5LKtzF@cR`?N-u6(tNW!Za|f0ij~m(( z`-S<=%PHJW7M0jUn)vXyY^2?Lyc+6VW&8Sh!VQNzDFT|C*2YEPQ(0Kor!AwpI`awy zUn`~;D3jt|yQyHmSci!yQp7YgrWnoC_@ZrljP!(g2{;#$dm7g^WMXkm>*??=bQ4(% zs}GzFL7&(#y!niTxX|quG%LflI>2aiEvoPBR|Jl{1lZS;U!NGgY6JojI%f}(t;gN* z*zz+dDrZS!j~gxkAmCg?R^7a=0(-CcNybdx_n$&xBP45?EN1O(P04ZQ9e^HSQA^c#Tw@n|vi%p`v8C~L+&_#H z{O8l?i0a?1H_-|*=hV>rD=WKy=blR)DTL>jU22@4882;dbf35UI4ysDVi2v>IWzJO zO7-$N{oH%p~87xk)zYL1EG0vkCT z23sEptVefG+29=-aP50b8Nr%6+ZZb0L1%S*m)7u>=5b+eB0S_w^@>7*u~at)$`r1*A-K91Me|MixsQ6ofeuY z*20g2TU)j8)EW|nZS+V4?&&qMVY|_v(&j{4 z9XTMQcwWz8vSXPSHjp{r73`YHn~u9zQ@a|$+O|()*ov`z6c;Y-^|Mw-0^6}x;ig&n zJ1*gL*0$Yb9^VRe=f5@zs#OixnsG1O>38Spmi8(uWeo`Wj0@M8tx(d++W-!GfK3@n z;_Fd=hboMPcc&5B%m_`}ODMP{Dm~;$q~p$_Xd4#;>r0#iUh* z@;j?HoH&KE^lAKc6XlQbAq4q_tn8(_lu^ysI)d;>CuJ`t*mcJBMhz8Jiuwc_pvyp3 z(>%WExlqShY;of}G0u?I@7#WXvEfHh_F>vY80ENe(pkcT7vJ5^1|o-?I+3w$&sY%I z`D*ErRNI}R4t4AiZVrm4kF2`P&% zdQUvxoy=cXkqfLu;R&oo5xjdw^BY&}u_LU?kXWee&8Vj?fiS^iFdWQ<=QeH^Kfql@tIHaZ~24rAyZa<(E&OMyUYibzjUpai1w9o)-I|iAy{~m`(X*gij! zc=UowOLcxhcX1fXpCBhZG9brF>~dL2pB7LPURBM|{52<><>PO+!IV9w)PH-7WchcI z8dZqAiLg`oD;z*LI?ASI*3+p#f@7 z+RZgTLiswO1UT={fLveN!g2<^1{$-Z%zce_+fTLtAKE7!$^6@!O)38T{ZIS+*+2dq zAODBX50>pG8^6qdSx6S&Dqe$&V}P-zUFn8EDmsn%2egeLbYFG9y zHX@VTrgc`i{unLZ_aWQ2VOE{AG_Iua)EM|6kP(XV4R1j!OL$xlnNH_33L{jJ*B%Ln zigJ3f>QOO_H5Ucz0eO@xf#ssb&7@hk6UdmN%eRR^rp+U4CQl<(x4)oo1E5f~$ z?jQQ9UX0oJ9#IcxNP+kP4nVgXC)>Xyg=olIZ$T}-g8c30FII)Ip(7y#yBBL$GcZMZ)t;-{XigZ)-&+^1|0TP!Rg{nSNptd zfBiE+lFdmr>=?-U{IP3@~L0?lor2=t(&3OJAHa01A3WyUs&mvI zvRGESKR-RUq_l!ylNu^>EhXIgI7XVh_>N3~6jMOWnWk7oobc+t$x6)KTVrS$O595|j(pi$eKa-`IT(6Z>2ziZNJ{c=@4G9-pjzKprP_ZXO36mNKZJ7Xq% zw^?uiM+^>b^?B^|eEjE}8JsE5rtFBPh`l`X2E%jvh@mpm^jVVO7oy0vc~Q^+iaw`) zh`|c+y*2YIOCAQB(o9#S>T>|G5~@w%c7%rjKhT&_D+7-nOAoo~A3}x=V7awG?Ztns zy*c0?k{9{@P{FzSxwK}GH>=bUgK?eG)npcqG`b(PYCt$6;ajNLf(h#({K2?%!0q($ z`%@Wc+OcLE5%NW2W!vP38lmP=N6+fl4Farjx+Jz(NU@6bT*Xr7 z#9b+&x)jy~dHJ2vp4R9=8A|(nLSmLepYW#%>T*~8jH~db5%Wc}jxqB6wy20k zj*Uud#%-nh8P{NXPi*zBcPR&GH>vxN>pjR0LxWl{_$J<^>)f1a>v&$xYObY(QM@Pk zoHu!PJv()!vmh$_J9EhV?s2Y^I+t)by)>4uUAvncYf=OiZ{iBOjKdlaO(rvqU$m{{ zfUvJhCA}~aEhf<@D*n8+f@|-4ly!e3%^S`+TeaZX9aqo>mC^v;(-=q1pr|_Qf z#&n5M?|Mj0wZ8wdr1ai+tn8#r#*##ADsX8XGUR8!SUB27=p_v&N}y`Yaq|E;D^wPB z;ga&AgNLlU_pz1a$9xFvDDWM_<%Gl*2V`ch<}OIPx^12%$g|f?LJy}(o6cQMM7e%v zk5|6_v!BpR3vHCLqe4%4&+w2U@MeTLqQ^<6D<_!4)~kBs1DehL>0BniQw3j>g&Y4j z=f)&5jq2)JIU&)J#nB7yOmMian*8EaenkZsy|iLav1KGwSYoABwTt)`G;^6}S)t(| zNZGfyTZr1O>t}^=2bjFq(y#zU^?u?PH&hx3sNXX5IFuT8iWwu;ZQ^D+PcsQ_%_sT3 z#5#Qwy*#*EXPxk7kTtK6{>Zu_Z_PKO!g_8yir!Gpz;q=RG;&%>J^c(=>!35kph#zsh<$u&b_O$?QV94 z>3cE(1QF9A&2PTN+<0VNt@}p#By2;vmS%jY+h7MnQIHTbSpIs7AV+>drx}%`hKGlK zb56||Tgq9loQtR-wpd7;ETVk*nz%C!0Mo6c_q2|-h!=8{8g|4RNeG{fNy?$RriiqJ zlcPHL$=+Sfm*03kr`mN3`f3EA;tFprsMlov{uEYFQBpTF6-36GH}^L|-BBHdBH`de zrsv`4G*Ja<8h~OlowU(jq(%;+dWAX&zjNFzYb7QEzlVC}GKkFjG+n2*7Mki9M-VGL zKY-ux1bvI%{`Hu8W|tc6hG|DTGds!!{?itoIa+gawIg)sdnFeDJ_q8SD@1u|FW0w z+QuswY8yYERB39KVn#R6l@pUIIz0#ffz8j`gQzic5UHh>@LjU7v6^m%qZj#NWCNXPRjjAGDG}3 zkq>pro|>aPW$wuTmV4jtKm13^hS%WmCj|F2GZL z3Hc-juSWgL6DdcgxuFSH%*P6nU*n%=@O$`kXXcdHd(0~1|ATWc}05AD!)Ga6lKGA~J-vO3Kb zgF6B+ZmYgQkk8}s!o~1TNmg*sB3E_h*sL3k z7RS~V3zY{otuNca#>mLw+&d<;AD1dk7~Dg4%pG8jN(7(@nqmFZGpN4qQNMAFa6(S^ zk}Y{QZsp3%1l|`1`!~B{=twk`Xk7l8EZ>v&;vai&c=DB}%wXQm?RV&G zB)FvtAruv(y%5KY=x+ZXoUIa`u-xv4&KOb8>1KA|I@SHc5%@ww<|!tR@K2sfGojY~ z`gA~TE3UezP$rN%1>U)`U3zt2>MY2s^4zX?&e?kU*DxNOm9tB%;6&DJzt5DcNivHc z#qV#7R!T;!KETGoZ6v`O=Q$)YOIZfGSUOXu5=mDPZ_wx*ZF$-fd2)le^}28Yp{T`$ zgYmr=dvPlK-#O9T+Y>wZ4drPuWVn{vs!d{JY|eRQ~ApSzfbB{FZ(5n7%s zpGUKCva^BNv%@N8ov_E5qgmKv<(|mTbP5*V4IKL0Q=JNB1&Oj&SGH!Z?eDpolxSOs zy~jhfXM+}RN*Ymj|AKBdjz%e%_o9Ng=5JRw2B62z$vN3%IuQ*}sGu7-6>i)!Hc=5= zkJ07`7(Ctw8uCXiIMkyyG=>32lqt2a?|FYeSpS{YIz!VvDf;9c|Fm{%m~XGE2KQWV zvj2Mpt-C`G0i^0bn>vgf?5<|{l%v-O^zNl@OlU~hu2|b_U8%hfUUPPZryz*3HxGj*l2;n9ufBHa}v=+paIjB^+PRS z?LFsBpVM9u!LM^zx1G+hp7?Pvg=qaS{%kZ5+%Izf2_#6AY?q21S{s9{qu0U7(<-`p zEXLk4eMs+@^Dzz@pxcJBlcoU-B4)5kJwmN~!OcJdj;SJDwRa9Bs;fJuX8X9T@~R9H ztfCANTgW#EqT;!-?uqEK#eZQvA;-{3ugW`bs;SOIk7gzwdyiP$)ZI?oSs*Dc{OLE< z0FfR(e;~38*jJzW>q_z~xb0hC-;u!`jTVIO+7s;^xVBYtyduvN{9!-vFdC?;?x{T# zEz9zHuH%O5dzB!fNg)5Vl-kRo0%-G)(}AZ!`s*|im-e+Ac37QJ`dF9Rz{>DWqn@6@ z;>M&t6ydyAd|L14C1bQ=i^ow|+{1R}sJW}{-T5tsN2*v$$#g&cYjd0#8_IKf!*&^A zVpzmWxLF8Jn3vdEhG-|GNAP}Jj_#a31a+P<-NFK^r31jCH8m}e_RbfbZp*A1AUWD` z0M|6?XoLC}rh(rl4NC2m2<(Qfm?t|;M{|YfmzV_Yl~OshSQ+!nM}`|5X-1B>o7_Iv zsl9g@+g!J({k|~Xnl=X6Cq6)dS*;j!fCL@6vlQxS231OKSDvR58{U2awi)@Prp zy_!D!DfS>WMZ7jDufFAEqghJ&oy%j>3uA22W$jg@-0I{gAZ&h3d!-t&f5QR47mv_K zn-}gE7O>)GRKcybYhh@ zzn{(hIsju0kGA?|SNgiFntnDdy~!j0Nqi(>{CJvm^9jF|OTC^h8sYcPJCHkLb{pE| zvq8EUbI{}mtPZp1N4^a=SV&12G~g$@e3xFbCcvV@8SMPL+dk#~l3Zh^PQ2{4nx0E^ z`E1jUDF3v`=h$12l|d@?swSMnkqH^l@m$1X>qhG)>0=YT~S|rv)LU$l|C>dPpClGlYu7(oH&-U~I0`*1)sbY;P<0Mg&PR zTFb&a>dd2(A=8~OLNJwr-_l<^VmGx(@jRSl(1G()zN?wQDf!X^vflI8FwSmk^}}<; zI@nQg#m^nO0JrE*{dZkiHiu3^IP3(i!3&oj&t+PB=fB3>GHTo zGZI6}rtDfGeGT9`VnR4tn_0Oe3no%wQIRAl2|5lQ_kH~&hkp_Au!m`hzT#B0pp6_1 zsI334-|dvctHn{*zqT+f4NzY_1Apy9KmxduRCgOTj+&bn&PTObX7r9+H@(_yyC`&0 zn|w7p#hkUYmwc)$FP*B6pWO&hIVs0yDlG z%L$u=?alckjtP;atD^3~!)`J7pO!Or`g<5B!2{^MXG=PI20Auz*MuE2qI9f%Fa(84 z&1%Nf*=9mgn@07C2wtnza-J=J)S-2@%JICLRgaxyV6wZ(kJ`JE$_uHVm?`CGG=g-j zp1RA|gG!6O(HN7B*1%~k&Yvw#>nd8?W7tG%D$sxIYO0DAKPQA8P;1wVHX?8B)5wlW;~Jlv1oGi&(ew>|B&_$|PKkdHcK`v#c-x zoihM^7t29xj!$e6z#@%pOszSLL28i%@Wpi4(JEGEdxbbdZxV0oW=0Xcu)>+%%S8ou zQk?Txxdx9l%Do9M&QjzjCGg8u6`D>0~ZzQ&p#gyJ^MEGjleUTV9O%2B?22q@=y=_8DC5%T0Bw%znh;|PEi{#u;O*-*@R z`ppGT<0DUL@1YW;9>TfA3kc&3veKC3;1#jK!Pd4Q^J1}J&+jRlIt(31Huk0Ud<#05 zZ;qkdTb&)anW?-v@m*q!-QyE22X=WTne!BqzH8rtkY52q-Gbz|tR&vFUVX?;w3Z@y z<1yte^qA_S04ew_f}0IvEy0?nvU4^hY?Ynwep0~W#tEK?IUQ#&`kT-fo*%HSelefZR@nWGQUy#}|8cW&SA1q$gF&N{gSqZ$3Qp>z(JyY{i3;c(xBY2U4c z8^f5te#cALZ2b_s3j{|1Wcw*8O?9nB2t)S|#vuc$H-msPzHqO-dcFENJpJG_{9hABGes}YA>Iu(l zHV%$f>bth%mhrmPZW*prRRbUeM4=b1WS2vL#pk^qPz|KBw%XuX7kz zGl&vpWb=R8PUde$RER^XF>7iCcZpaYL)9mp_nKL1pOxxazUeYbtZBd=G9hq#?wn5N zP9bxot6pukQtHhqlO;L9*xUw$4^Av6Q?s+1;XRcEkdPZWpJ3f*OoUs2>KoALBTUF1 z>c3rRu<-L+1vYOU8UY%h)E8-UDSG$NL%7H*jm8?s6!0MN?mYWJ3;^Uf7H}>9`3EIh zsIWL^C`ZL%POvGzhxcKo1Rh`cJ`T$J=v`_W*B%cEFH<1$h0STm_jTx-04y$Vj{Q_J z0glyVO%oYYP*Oa%E%ngwzT;Gjez>|s<38NWu#qfn*Ky}*vyK&+X}M_x>yvNSsbu05 zdf2Dt%5M__$%+e1u%BYCQ7_g%_-W_?*VmGwDB>8g zg+aRK@SU7)LAV{27AbgpT=Va>-_uOC4fpb)ViHo#$eQ%`@4OXOfMdKC!!v~2hO$d& zO>50*>2zL1a*3PPH9{CN*_o>&CkestA=vR9aXY85Qd7;Fj%RryAK8v6bo0T0rCU7-@U?efC)d)=~7C zetkk4s3?cdfTOSN$^oqqD|tV!I|ZrMtX!>TOGtX$@xhCSg|AL|#gH6Oy;qiDU*;Zg z*yc^t(iodwsm;v|*wE{Jm(-CQ-UWRS7h?N#|pLaP2(*sArjX%A?JqI}5u2tG@6+_lF;QgA)M{QTxtUWGJ*~9O+{`aP_ zaw%i(*-*w|a&Qm$FvN2Nn+_1cr-0L=9h=YiX? z0fx4WFQ%~wC#*6Y(i*@?#X&pup6z6-EifOe;+ws;dqe;Hp6%asf+cT7rt zj5f2r*ukOymdOz$AOPmc$$Em38S1@OYOuf8$|OEUJ9bcEJidWqVxgzsG5qb*SMtYOwPz$wT-+>%mJl+E1>W~2u$R7<=3clBEdpB8Vy4QN6?utI{ zsCT7_)_wOuX;Wwqsf{)9tEzF@n#y{?ceO$C!-g@6@vHChKi-Rs*KIWNkA9}F{A<^J z52NlT$HHG_%wH?Fq4}}Ee%+;ck0S+d9uMBBWTGYCwst+DU1$VXG+! zSN*IhqVL;kj2g~aWWm#YubGGO()_OAC%=qWv&JEKUZp~W^+OVy^_Gy!YHHuiD zr;IS_(H{;Ppo;NNwXAiDaGP;Y;J; z3DTjpaq7#&#%Q@zYwf_$y_`ziF7jD&y||6nV|l3(?=rTY7oGY>!Vm#wdX8N`^k<5z zJU!hxC_W^A$G=nk{Ee<4Zll|}omCfePM<((Zjs)sUW7r_EPHkc3Pq1*eFlxG(c_VY zzQ=aYFRTQCc)DMLk8h!WTg&H!GlTv={D%s_n-Y@wLNvm9NlA*SmREBtnjx33($Hdr z<+5Kj8;{p|?nxU+2e`fzQG6Wf%*G_5y7GxuSKw^ zrYO6nM=BAiCMNF1`r?HaKNOr@ou5eDeUu%qWfB`KDfvb#^^+gyZoD1qW#M&%C`b-Q zyi66dB8EE$xo$5S;HWNRL1`%+s1@HZW8=nr1|ffBrzJ;e8M=RUh2e#Homq>Hl*!T7 ywYp)55LK@%<5J{pd?$V-Y#v5HA0WM4@CuJVuE#q?bE4E6cT_%xqe|>{=6?VH$koFD literal 0 HcmV?d00001 From d0d9548a6be5f06bc9918a75d1f100ba246061f5 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 21:52:40 -0700 Subject: [PATCH 11/51] Keep overview spend code within lint limits --- .../CodexBar/StatusItemController+Menu.swift | 19 ---------------- .../StatusItemController+OverviewSpend.swift | 22 +++++++++++++++++++ .../StatusMenuOverviewScrollTests.swift | 7 ++++++ Tests/CodexBarTests/StatusMenuTests.swift | 3 +-- 4 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 Sources/CodexBar/StatusItemController+OverviewSpend.swift diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index e7ea651193..28d415b64f 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -662,25 +662,6 @@ extension StatusItemController { return true } - private func overviewSpendDashboardModel(providers: [UsageProvider]) -> SpendDashboardModel { - let inputs = providers.compactMap { provider -> SpendDashboardModel.ProviderInput? in - guard let snapshot = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot else { - return nil - } - return SpendDashboardModel.ProviderInput( - provider: provider, - displayName: ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue, - snapshot: snapshot) - } - let requestedDays = self.settings.effectiveCostUsageHistoryDays - let now = inputs.map(\.snapshot.updatedAt).max() ?? Date() - return SpendDashboardModel.build( - inputs: inputs, - requestedDays: requestedDays, - now: now, - preferredCurrencyCode: self.settings.preferredCurrencyCode) - } - private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) { let resolvedProviders = self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, diff --git a/Sources/CodexBar/StatusItemController+OverviewSpend.swift b/Sources/CodexBar/StatusItemController+OverviewSpend.swift new file mode 100644 index 0000000000..bd8be60d60 --- /dev/null +++ b/Sources/CodexBar/StatusItemController+OverviewSpend.swift @@ -0,0 +1,22 @@ +import Foundation + +extension StatusItemController { + func overviewSpendDashboardModel(providers: [UsageProvider]) -> SpendDashboardModel { + let inputs = providers.compactMap { provider -> SpendDashboardModel.ProviderInput? in + guard let snapshot = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot else { + return nil + } + return SpendDashboardModel.ProviderInput( + provider: provider, + displayName: ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue, + snapshot: snapshot) + } + let requestedDays = self.settings.effectiveCostUsageHistoryDays + let now = inputs.map(\.snapshot.updatedAt).max() ?? Date() + return SpendDashboardModel.build( + inputs: inputs, + requestedDays: requestedDays, + now: now, + preferredCurrencyCode: self.settings.preferredCurrencyCode) + } +} diff --git a/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift b/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift index fb41ee9ffc..24f1130780 100644 --- a/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift +++ b/Tests/CodexBarTests/StatusMenuOverviewScrollTests.swift @@ -4,6 +4,13 @@ import Foundation import Testing @testable import CodexBar +extension StatusMenuTests { + func expectCondensedOverview(menu: NSMenu, ids: [String], rows: [String]) { + #expect(ids.contains("overviewSpendSummary")) + #expect(menu.items.count(where: \.isSeparatorItem) < rows.count + 1) + } +} + @MainActor struct StatusMenuOverviewScrollTests { private func makeController(suiteName: String) -> StatusItemController { diff --git a/Tests/CodexBarTests/StatusMenuTests.swift b/Tests/CodexBarTests/StatusMenuTests.swift index 59f0b763cc..11340ea193 100644 --- a/Tests/CodexBarTests/StatusMenuTests.swift +++ b/Tests/CodexBarTests/StatusMenuTests.swift @@ -1573,8 +1573,7 @@ extension StatusMenuTests { let ids = self.representedIDs(in: menu) let overviewRows = ids.filter { $0.hasPrefix("overviewRow-") } #expect(Set(overviewRows) == Set(enabledProviders.map { "overviewRow-\($0.rawValue)" })) - #expect(ids.contains("overviewSpendSummary")) - #expect(menu.items.count(where: \.isSeparatorItem) < overviewRows.count + 1) + self.expectCondensedOverview(menu: menu, ids: ids, rows: overviewRows) } @Test From 6ecfc05960fa33b187b409802a3e065db16c1ea2 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 21:57:35 -0700 Subject: [PATCH 12/51] Import provider types in overview spend helper --- Sources/CodexBar/StatusItemController+OverviewSpend.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CodexBar/StatusItemController+OverviewSpend.swift b/Sources/CodexBar/StatusItemController+OverviewSpend.swift index bd8be60d60..7f20a2649a 100644 --- a/Sources/CodexBar/StatusItemController+OverviewSpend.swift +++ b/Sources/CodexBar/StatusItemController+OverviewSpend.swift @@ -1,3 +1,4 @@ +import CodexBarCore import Foundation extension StatusItemController { From 1d4553783c8d60e5a54b624bfad344930263ad34 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 22:48:41 -0700 Subject: [PATCH 13/51] Add OpenRouter model spend to shared usage --- .../OpenRouterProviderImplementation.swift | 3 +- Sources/CodexBar/ShareStatsCardView.swift | 14 +- Sources/CodexBar/ShareStatsPayload.swift | 114 +++++++- Sources/CodexBar/SpendDashboardModel.swift | 11 +- Sources/CodexBar/UsageStore+Refresh.swift | 7 +- .../CodexBar/UsageStore+TokenAccounts.swift | 7 +- Sources/CodexBar/UsageStore+TokenCost.swift | 15 +- .../ProviderPluginSnapshotMapper.swift | 77 ++++++ .../OpenRouterActivityUsageSnapshot.swift | 251 ++++++++++++++++++ .../OpenRouterProviderDescriptor.swift | 6 +- .../Resources/Plugins/openrouter.js | 24 ++ Sources/CodexBarCore/UsageFetcher.swift | 14 + .../OpenRouterActivityUsageTests.swift | 226 ++++++++++++++++ .../OpenRouterUsageStatsTests.swift | 6 +- .../ProviderArchitectureGatekeeperTests.swift | 66 ++++- .../ProviderPluginDetailsParityTests.swift | 8 +- Tests/CodexBarTests/ShareStatsTests.swift | 142 +++++++++- .../SpendDashboardDateTruthTests.swift | 27 ++ ...ashboardIntegratedModelCoverageTests.swift | 1 + .../SpendDashboardModelTests.swift | 12 +- .../spend-dashboard-proof/README.md | 15 +- .../share-all-providers.png | Bin 64974 -> 116606 bytes 22 files changed, 986 insertions(+), 60 deletions(-) create mode 100644 Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift create mode 100644 Tests/CodexBarTests/OpenRouterActivityUsageTests.swift diff --git a/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift b/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift index b285e6710c..bcf16bca05 100644 --- a/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift +++ b/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift @@ -39,7 +39,8 @@ struct OpenRouterProviderImplementation: ProviderImplementation { title: "API key", subtitle: "Stored in ~/.codexbar/config.json. " + "Get your key from openrouter.ai/settings/keys and set a key spending limit " - + "there to enable API key quota tracking.", + + "there to enable API key quota tracking. A Management API key also enables " + + "per-model spend and token history for the last 30 completed UTC days.", kind: .secure, placeholder: "sk-or-v1-...", binding: context.providerConfigBinding(.apiKey), diff --git a/Sources/CodexBar/ShareStatsCardView.swift b/Sources/CodexBar/ShareStatsCardView.swift index 5fc5666ce2..2a085784f6 100644 --- a/Sources/CodexBar/ShareStatsCardView.swift +++ b/Sources/CodexBar/ShareStatsCardView.swift @@ -5,11 +5,15 @@ struct ShareStatsCardView: View { let payload: ShareStatsPayload - static func providerDisplayLimit(for providerCount: Int) -> Int { + nonisolated static func providerDisplayLimit(for providerCount: Int) -> Int { providerCount > 5 ? 4 : min(providerCount, 5) } - static func providerPaletteIndex( + nonisolated static func modelSectionDetail(for modelCount: Int) -> String { + modelCount > 3 ? "3 OF \(modelCount) · BY TOKENS" : "BY TOKENS" + } + + nonisolated static func providerPaletteIndex( for model: ShareStatsModelPayload, providers: [ShareStatsProviderPayload]) -> Int? { @@ -148,9 +152,11 @@ struct ShareStatsCardView: View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 6) { - self.sectionHeader("TOP MODELS", detail: "BY USAGE") + self.sectionHeader( + "TOP MODELS", + detail: Self.modelSectionDetail(for: self.payload.topModels.count)) if self.payload.topModels.isEmpty { - Text("No model-level history in this local snapshot") + Text("No share-safe model ranking in this snapshot") .font(.system(size: 18, weight: .medium, design: .rounded)) .foregroundStyle(self.secondary) .padding(.top, 4) diff --git a/Sources/CodexBar/ShareStatsPayload.swift b/Sources/CodexBar/ShareStatsPayload.swift index bfdd2354c6..18f039ea11 100644 --- a/Sources/CodexBar/ShareStatsPayload.swift +++ b/Sources/CodexBar/ShareStatsPayload.swift @@ -20,6 +20,7 @@ struct ShareStatsProviderRosterEntry: Sendable, Equatable { struct ShareStatsModelPayload: Sendable, Equatable { let provider: UsageProvider let providerName: String + let modelIdentity: String let modelName: String let currencyCode: String let totalTokens: Int? @@ -29,6 +30,7 @@ struct ShareStatsModelPayload: Sendable, Equatable { private struct ShareStatsModelFamilyKey: Hashable { let provider: UsageProvider let providerName: String + let modelIdentity: String let modelName: String let currencyCode: String } @@ -80,6 +82,7 @@ private struct ShareStatsModelFamilyAccumulator { return ShareStatsModelPayload( provider: self.key.provider, providerName: self.key.providerName, + modelIdentity: self.key.modelIdentity, modelName: self.key.modelName, currencyCode: self.key.currencyCode, totalTokens: totalTokens, @@ -154,16 +157,23 @@ enum ShareStatsSanitizer { static func modelName(_ rawValue: String) -> String? { guard let value = self.safeLabel( rawValue, - maximumLength: 72, + maximumLength: 96, maximumWords: 3, requireModelShape: true) else { return nil } let normalized = value.lowercased() + guard !normalized.contains("://"), !normalized.contains("\\") else { return nil } + let pathComponents = normalized.split(separator: "/", omittingEmptySubsequences: false) + guard pathComponents.count <= 2, + pathComponents.allSatisfy({ !$0.isEmpty }) + else { return nil } + + let routedModelName = String(pathComponents.last ?? "") let regionalPrefixes = ["us.", "eu.", "apac.", "global."] - let familyName = regionalPrefixes.first { normalized.hasPrefix($0) }.map { - String(normalized.dropFirst($0.count)) - } ?? normalized + let familyName = regionalPrefixes.first { routedModelName.hasPrefix($0) }.map { + String(routedModelName.dropFirst($0.count)) + } ?? routedModelName let publicModelFamilies: [(prefixes: [String], label: String)] = [ (["amazon.nova-", "nova-"], "Amazon Nova"), (["anthropic.claude-", "claude-", "claude "], "Claude"), @@ -190,13 +200,74 @@ enum ShareStatsSanitizer { (["tts-"], "OpenAI TTS"), (["whisper-"], "Whisper"), ] - guard !normalized.contains("://"), - !normalized.contains("/"), - !normalized.contains("\\") - else { return nil } - return publicModelFamilies.first { family in - family.prefixes.contains(where: familyName.hasPrefix) - }?.label + guard let match = publicModelFamilies.lazy.compactMap({ family -> (String, String)? in + guard let prefix = family.prefixes.first(where: familyName.hasPrefix) else { return nil } + return (prefix, family.label) + }).first else { return nil } + + let remainder = String(familyName.dropFirst(match.0.count)) + guard !remainder.isEmpty else { return match.1 } + guard let detail = self.publicModelDetail(remainder) else { return nil } + return "\(match.1) \(detail)" + } + + private static func publicModelDetail(_ rawValue: String) -> String? { + let displayTokens: [String: String] = [ + "air": "Air", "chat": "Chat", "code": "Code", "coder": "Coder", "codex": "Codex", + "fable": "Fable", "fast": "Fast", "flash": "Flash", "free": "Free", "haiku": "Haiku", + "instruct": "Instruct", "large": "Large", "lite": "Lite", "max": "Max", + "latest": "Latest", "luna": "Luna", "medium": "Medium", "mini": "Mini", "nano": "Nano", + "opus": "Opus", + "oss": "OSS", "preview": "Preview", "pro": "Pro", "reasoning": "Reasoning", + "small": "Small", "sol": "Sol", "sonnet": "Sonnet", "terra": "Terra", "thinking": "Thinking", + "turbo": "Turbo", "vision": "Vision", + ] + let tokens = rawValue.split(whereSeparator: { "-_:".contains($0) }).map(String.init) + var details: [String] = [] + var numericVersion: [String] = [] + + func flushNumericVersion() { + guard !numericVersion.isEmpty else { return } + details.append(numericVersion.joined(separator: ".")) + numericVersion.removeAll(keepingCapacity: true) + } + + for token in tokens { + if token.allSatisfy(\.isNumber), token.count <= 3 { + numericVersion.append(token) + continue + } + flushNumericVersion() + if token.range(of: #"^v[0-9]+$"#, options: .regularExpression) != nil || + token.range(of: #"^[0-9]{8}$"#, options: .regularExpression) != nil + { + break + } + if token.range(of: #"^[0-9]+(?:\.[0-9]+)+$"#, options: .regularExpression) != nil { + details.append(token) + continue + } + if let displayToken = displayTokens[token] { + details.append(displayToken) + continue + } + if token.range(of: #"^[a-z][0-9]+(?:\.[0-9]+)*$"#, options: .regularExpression) != nil { + details.append(token.uppercased()) + continue + } + if token.range(of: #"^[0-9]+b$"#, options: .regularExpression) != nil { + details.append(token.uppercased()) + continue + } + if token.range(of: #"^[0-9]+o$"#, options: .regularExpression) != nil { + details.append(token) + continue + } + return nil + } + flushNumericVersion() + guard !details.isEmpty else { return nil } + return details.joined(separator: " ") } private static func safeLabel( @@ -279,6 +350,7 @@ enum ShareStatsBuilder { return ShareStatsModelPayload( provider: row.provider, providerName: row.providerName, + modelIdentity: modelName.lowercased(), modelName: modelName, currencyCode: group.currencyCode, totalTokens: row.totalTokens, @@ -290,6 +362,7 @@ enum ShareStatsBuilder { let key = ShareStatsModelFamilyKey( provider: row.provider, providerName: row.providerName, + modelIdentity: row.modelIdentity, modelName: row.modelName, currencyCode: row.currencyCode) if var existing = modelFamilies[key] { @@ -308,10 +381,19 @@ enum ShareStatsBuilder { if lhs.providerName != rhs.providerName { return lhs.providerName < rhs.providerName } - return lhs.modelName < rhs.modelName + if lhs.modelName != rhs.modelName { + return lhs.modelName < rhs.modelName + } + if lhs.currencyCode != rhs.currencyCode { + return lhs.currencyCode < rhs.currencyCode + } + return lhs.modelIdentity < rhs.modelIdentity } } - let rosterHasUntrackedProviders = providers.count > trackedProviders.count + let trackedProviderKinds = Set(trackedProviders.map(\.provider)) + let rosterHasUntrackedProviders = providerRoster.contains { + !trackedProviderKinds.contains($0.provider) + } let currencies = model.groups.map { ShareStatsCurrencyPayload( currencyCode: $0.currencyCode, @@ -321,7 +403,8 @@ enum ShareStatsBuilder { } let knownTokenValues = trackedProviders.compactMap(\.totalTokens) let totalTokens = self.safeTokenSum(knownTokenValues) - let totalTokensIsPartial = knownTokenValues.count < providers.count || + let totalTokensIsPartial = trackedProviders.contains { $0.totalTokens == nil } || + rosterHasUntrackedProviders || model.groups.contains { $0.totalTokens == nil } let periodEnd = model.groups.map(\.chartDomain.upperBound).max() ?? Date() let payload = ShareStatsPayload( @@ -436,6 +519,9 @@ enum ShareStatsFormatting { } return "\(model.modelName) (\(model.providerName)): \(metrics.joined(separator: " · "))" }) + if payload.topModels.count > 5 { + lines.append("+\(payload.topModels.count - 5) more models ranked in local stats") + } } lines.append("Generated locally by CodexBar · Data through \(self.dataThrough(payload.periodEnd))") return lines.joined(separator: "\n") diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index 7b0d1f5aeb..a8cac51668 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -597,7 +597,10 @@ struct SpendDashboardModel: Equatable, Sendable { displayCalendar: Calendar) -> ClosedRange { let bucketCalendar = Self.bucketCalendar(for: input.provider, displayCalendar: displayCalendar) - let bucketEnd = bucketCalendar.startOfDay(for: input.snapshot.updatedAt) + let snapshotDay = bucketCalendar.startOfDay(for: input.snapshot.updatedAt) + let bucketEnd = input.provider == .openrouter + ? bucketCalendar.date(byAdding: .day, value: -1, to: snapshotDay) ?? snapshotDay + : snapshotDay let scanEnd = displayCalendar.startOfDay(for: bucketEnd) let scanDays = max(1, input.snapshot.historyDays) let bucketStart = bucketCalendar.date(byAdding: .day, value: -(scanDays - 1), to: bucketEnd) ?? bucketEnd @@ -652,9 +655,9 @@ struct SpendDashboardModel: Equatable, Sendable { } private static func bucketCalendar(for provider: UsageProvider, displayCalendar: Calendar) -> Calendar { - guard provider == .mistral else { return displayCalendar } - // Mistral labels both daily buckets and snapshot coverage by UTC day. Map each UTC boundary into the - // containing local dashboard day instead of reinterpreting the label as a local date. + guard provider == .mistral || provider == .openrouter else { return displayCalendar } + // Mistral and OpenRouter label daily buckets and snapshot coverage by UTC day. Map each UTC boundary + // into the containing local dashboard day instead of reinterpreting the label as a local date. return self.gregorianCalendar(timeZone: TimeZone(secondsFromGMT: 0) ?? .gmt) } diff --git a/Sources/CodexBar/UsageStore+Refresh.swift b/Sources/CodexBar/UsageStore+Refresh.swift index 8d828965d8..9d056043cd 100644 --- a/Sources/CodexBar/UsageStore+Refresh.swift +++ b/Sources/CodexBar/UsageStore+Refresh.swift @@ -702,8 +702,13 @@ extension UsageStore { self.lastKnownResetSnapshots[provider.instanceID] } let profileStable = self.preservingDeepSeekProfileCatalog(in: accountScoped, provider: provider) + let historyStable = provider == .openrouter + ? self.preservingOpenRouterActivityIfCurrent( + profileStable, + previous: self.snapshots[provider.instanceID]) + : profileStable let stabilized = Self.commandCodeSnapshotResolvingDepletionOnEnrichmentFailure( - current: profileStable, + current: historyStable, previous: self.snapshots[provider.instanceID]) let backfilled = stabilized.backfillingResetTimes(from: resetBackfillSource) let warningAccountDiscriminator = Self.warningAccountDiscriminator( diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift index fdd16e3f66..6d8283c17a 100644 --- a/Sources/CodexBar/UsageStore+TokenAccounts.swift +++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift @@ -1452,7 +1452,12 @@ extension UsageStore { ? labeled.preservingDeepSeekPlatformProfiles( from: self.presentationSnapshot(for: .deepseek)) : labeled - let backfilled = profileStable.backfillingResetTimes( + let historyStable = provider == .openrouter + ? self.preservingOpenRouterActivityIfCurrent( + profileStable, + previous: self.snapshots[provider.instanceID]) + : profileStable + let backfilled = historyStable.backfillingResetTimes( from: self.lastKnownResetSnapshots[provider.instanceID]) let warningAccountDiscriminator = Self.warningTokenAccountDiscriminator(account) self.handleQuotaWarningTransitions( diff --git a/Sources/CodexBar/UsageStore+TokenCost.swift b/Sources/CodexBar/UsageStore+TokenCost.swift index 646b2fdbc0..b3c8fe453b 100644 --- a/Sources/CodexBar/UsageStore+TokenCost.swift +++ b/Sources/CodexBar/UsageStore+TokenCost.swift @@ -434,6 +434,8 @@ extension UsageStore { switch provider { case .openai: snapshot?.openAIAPIUsage?.toCostUsageTokenSnapshot() + case .openrouter: + snapshot?.openRouterActivityUsage?.toCostUsageTokenSnapshot() case .mistral: snapshot?.mistralUsage?.toCostUsageTokenSnapshot(historyDays: self.settings.effectiveCostUsageHistoryDays) case .opencodego: @@ -451,7 +453,7 @@ extension UsageStore { nonisolated static func tokenCostRequiresProviderSnapshot(_ provider: UsageProvider) -> Bool { switch provider { - case .mistral, .openai, .opencodego: + case .mistral, .openai, .openrouter, .opencodego: true default: false @@ -501,4 +503,15 @@ extension UsageStore { nonisolated static func tokenCostNoDataMessage(for provider: UsageProvider) -> String { ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.noDataMessage() } + + func preservingOpenRouterActivityIfCurrent( + _ snapshot: UsageSnapshot, + previous: UsageSnapshot?) -> UsageSnapshot + { + guard snapshot.openRouterActivityUsage == nil, + self.tokenSnapshotPublicationForCurrentProviderConfig(for: .openrouter)?.snapshot != nil, + let activity = previous?.openRouterActivityUsage + else { return snapshot } + return snapshot.withOpenRouterActivityUsage(activity) + } } diff --git a/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift b/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift index 365f5d40d5..0eb839bb3c 100644 --- a/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift +++ b/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift @@ -17,6 +17,9 @@ enum ProviderPluginSnapshotMapper { let tertiary = try self.window(value, property: "tertiary") let extraRateWindows = try self.extraWindows(value) let providerCost = try self.cost(value, now: now) + let openRouterActivityUsage = provider == .openrouter + ? self.openRouterActivityUsage(value, now: now) + : nil let details = try self.details(value) let identity = try self.identity(value, provider: provider) let subscriptionRenewsAt = try self.optionalDate(value, property: "subscriptionRenewsAt") @@ -37,6 +40,7 @@ enum ProviderPluginSnapshotMapper { extraRateWindows: extraRateWindows, providerCost: providerCost, details: details, + openRouterActivityUsage: openRouterActivityUsage, subscriptionExpiresAt: subscriptionExpiresAt, subscriptionRenewsAt: subscriptionRenewsAt, updatedAt: now, @@ -44,6 +48,55 @@ enum ProviderPluginSnapshotMapper { dataConfidence: dataConfidence) } + /// Activity is an optional management-key enrichment. Malformed or unsupported + /// activity must not invalidate an otherwise healthy provider snapshot. + private static func openRouterActivityUsage( + _ root: any ProviderPluginValue, + now: Date) -> OpenRouterActivityUsageSnapshot? + { + guard let value = root.property("openRouterActivityUsage"), + value.isObject, + !value.isArray, + !value.isNull, + let rows = value.property("data"), + rows.isArray, + let rawCount = rows.property("length")?.int32Value(), + rawCount >= 0, + rawCount <= 10_000 + else { return nil } + + do { + let items: [[String: Any]] = try (0.. UsageDataConfidence { guard let value = root.property("dataConfidence"), !value.isUndefined, !value.isNull else { return .unknown @@ -291,6 +344,30 @@ enum ProviderPluginSnapshotMapper { return Int(number) } + private static func requiredNonnegativeInteger( + _ value: any ProviderPluginValue, + property: String, + path: String) throws -> Int + { + let number = try self.requiredFiniteNumber(value, property: property, path: path) + guard number.rounded() == number, number >= 0, number <= Double(Int.max) else { + throw ProviderPluginError.invalidSnapshot("\(path).\(property) must be a nonnegative integer") + } + return Int(number) + } + + private static func requiredNonnegativeNumber( + _ value: any ProviderPluginValue, + property: String, + path: String) throws -> Double + { + let number = try self.requiredFiniteNumber(value, property: property, path: path) + guard number >= 0 else { + throw ProviderPluginError.invalidSnapshot("\(path).\(property) must be nonnegative") + } + return number + } + private static func requiredString( _ value: any ProviderPluginValue, property: String, diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift new file mode 100644 index 0000000000..fb5b9fc4c8 --- /dev/null +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift @@ -0,0 +1,251 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// OpenRouter's account activity for completed UTC days, aggregated by routed model slug. +public struct OpenRouterActivityUsageSnapshot: Codable, Equatable, Sendable { + public struct ModelBreakdown: Codable, Equatable, Sendable, Identifiable { + public let model: String + public let promptTokens: Int + public let completionTokens: Int + /// Informational only. OpenRouter includes these tokens in `completionTokens`. + public let reasoningTokens: Int + public let requests: Int + public let usageUSD: Double + + public var id: String { + self.model + } + + public var totalTokens: Int { + self.promptTokens + self.completionTokens + } + } + + public struct DailyBucket: Codable, Equatable, Sendable, Identifiable { + public let date: String + public let promptTokens: Int + public let completionTokens: Int + /// Informational only. OpenRouter includes these tokens in `completionTokens`. + public let reasoningTokens: Int + public let requests: Int + public let usageUSD: Double + public let models: [ModelBreakdown] + + public var id: String { + self.date + } + + public var totalTokens: Int { + self.promptTokens + self.completionTokens + } + } + + public let daily: [DailyBucket] + public let historyDays: Int + public let updatedAt: Date + + /// Decodes the documented `/api/v1/activity` envelope and keeps only the requested + /// completed UTC-day window. OpenRouter currently exposes at most 30 completed days. + public init(data: Data, now: Date, historyDays: Int = 30) throws { + let response = try JSONDecoder().decode(Response.self, from: data) + let days = max(1, min(30, historyDays)) + let calendar = Self.utcCalendar + let today = calendar.startOfDay(for: now) + guard let firstDay = calendar.date(byAdding: .day, value: -days, to: today) else { + throw DecodeError.invalidDateWindow + } + + var accumulators: [BucketKey: Accumulator] = [:] + for item in response.data { + guard let date = Self.date(from: item.date) else { throw DecodeError.invalidActivityItem } + guard date >= firstDay, date < today else { continue } + let model = item.model.trimmingCharacters(in: .whitespacesAndNewlines) + guard !model.isEmpty, + item.promptTokens >= 0, + item.completionTokens >= 0, + item.reasoningTokens >= 0, + item.requests >= 0, + item.usage.isFinite, + item.usage >= 0 + else { + throw DecodeError.invalidActivityItem + } + let key = BucketKey(date: item.date, model: model) + try accumulators[key, default: Accumulator()].add(item) + } + + let modelBuckets = Dictionary(grouping: accumulators, by: { $0.key.date }) + let daily = try modelBuckets.map { date, values in + let models = try values.map { key, accumulator in + try accumulator.modelBreakdown(model: key.model) + }.sorted(by: Self.modelSort) + let promptTokens = try Self.sum(models.map(\.promptTokens)) + let completionTokens = try Self.sum(models.map(\.completionTokens)) + _ = try Self.add(promptTokens, completionTokens) + return try DailyBucket( + date: date, + promptTokens: promptTokens, + completionTokens: completionTokens, + reasoningTokens: Self.sum(models.map(\.reasoningTokens)), + requests: Self.sum(models.map(\.requests)), + usageUSD: Self.sum(models.map(\.usageUSD)), + models: models) + }.sorted { $0.date < $1.date } + _ = try Self.sum(daily.map(\.totalTokens)) + _ = try Self.sum(daily.map(\.requests)) + _ = try Self.sum(daily.map(\.usageUSD)) + self.daily = daily + self.historyDays = days + self.updatedAt = now + } + + public func toCostUsageTokenSnapshot() -> CostUsageTokenSnapshot { + let daily = self.daily.map { bucket in + CostUsageDailyReport.Entry( + date: bucket.date, + inputTokens: bucket.promptTokens, + outputTokens: bucket.completionTokens, + totalTokens: bucket.totalTokens, + requestCount: bucket.requests, + costUSD: bucket.usageUSD, + modelsUsed: bucket.models.map(\.model), + modelBreakdowns: bucket.models.map { model in + CostUsageDailyReport.ModelBreakdown( + modelName: model.model, + costUSD: model.usageUSD, + totalTokens: model.totalTokens, + requestCount: model.requests) + }) + } + return CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + sessionRequests: nil, + last30DaysTokens: self.daily.reduce(0) { $0 + $1.totalTokens }, + last30DaysCostUSD: self.daily.reduce(0) { $0 + $1.usageUSD }, + last30DaysRequests: self.daily.reduce(0) { $0 + $1.requests }, + currencyCode: "USD", + historyDays: self.historyDays, + historyCoverageIsEstablished: true, + historyLabel: "Last \(self.historyDays) completed UTC days", + daily: daily, + updatedAt: self.updatedAt) + } + + private struct Response: Decodable { + let data: [Item] + } + + private struct Item: Decodable { + let date: String + let model: String + let promptTokens: Int + let completionTokens: Int + let reasoningTokens: Int + let requests: Int + let usage: Double + + private enum CodingKeys: String, CodingKey { + case date + case model + case promptTokens = "prompt_tokens" + case completionTokens = "completion_tokens" + case reasoningTokens = "reasoning_tokens" + case requests + case usage + } + } + + private struct BucketKey: Hashable { + let date: String + let model: String + } + + private struct Accumulator { + var promptTokens = 0 + var completionTokens = 0 + var reasoningTokens = 0 + var requests = 0 + var usageUSD = 0.0 + + mutating func add(_ item: Item) throws { + self.promptTokens = try OpenRouterActivityUsageSnapshot.add(self.promptTokens, item.promptTokens) + self.completionTokens = try OpenRouterActivityUsageSnapshot.add( + self.completionTokens, + item.completionTokens) + self.reasoningTokens = try OpenRouterActivityUsageSnapshot.add( + self.reasoningTokens, + item.reasoningTokens) + self.requests = try OpenRouterActivityUsageSnapshot.add(self.requests, item.requests) + self.usageUSD += item.usage + guard self.usageUSD.isFinite else { throw DecodeError.numericOverflow } + } + + func modelBreakdown(model: String) throws -> ModelBreakdown { + _ = try OpenRouterActivityUsageSnapshot.add(self.promptTokens, self.completionTokens) + return ModelBreakdown( + model: model, + promptTokens: self.promptTokens, + completionTokens: self.completionTokens, + reasoningTokens: self.reasoningTokens, + requests: self.requests, + usageUSD: self.usageUSD) + } + } + + private enum DecodeError: Error { + case invalidDateWindow + case invalidActivityItem + case numericOverflow + } + + private static let utcCalendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.locale = Locale(identifier: "en_US_POSIX") + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + return calendar + }() + + private static func date(from key: String) -> Date? { + guard key.range(of: #"^\d{4}-\d{2}-\d{2}$"#, options: .regularExpression) != nil else { return nil } + let parts = key.split(separator: "-").compactMap { Int($0) } + guard parts.count == 3, + let date = self.utcCalendar.date(from: DateComponents( + timeZone: self.utcCalendar.timeZone, + year: parts[0], + month: parts[1], + day: parts[2])), + self.dateKey(date) == key + else { return nil } + return date + } + + private static func dateKey(_ date: Date) -> String { + let components = self.utcCalendar.dateComponents([.year, .month, .day], from: date) + return String(format: "%04d-%02d-%02d", components.year ?? 0, components.month ?? 0, components.day ?? 0) + } + + private static func add(_ lhs: Int, _ rhs: Int) throws -> Int { + let result = lhs.addingReportingOverflow(rhs) + guard !result.overflow else { throw DecodeError.numericOverflow } + return result.partialValue + } + + private static func sum(_ values: [Int]) throws -> Int { + try values.reduce(0) { try self.add($0, $1) } + } + + private static func sum(_ values: [Double]) throws -> Double { + let total = values.reduce(0, +) + guard total.isFinite else { throw DecodeError.numericOverflow } + return total + } + + private static func modelSort(_ lhs: ModelBreakdown, _ rhs: ModelBreakdown) -> Bool { + if lhs.usageUSD != rhs.usageUSD { return lhs.usageUSD > rhs.usageUSD } + if lhs.totalTokens != rhs.totalTokens { return lhs.totalTokens > rhs.totalTokens } + return lhs.model.localizedStandardCompare(rhs.model) == .orderedAscending + } +} diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift index 6fe897049b..9388daec09 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift @@ -63,8 +63,10 @@ public enum OpenRouterProviderDescriptor { ], widgetColor: ProviderColor(red: 111 / 255, green: 66 / 255, blue: 193 / 255)), tokenCost: ProviderTokenCostConfig( - supportsTokenCost: false, - noDataMessage: { "OpenRouter cost summary is not yet supported." }), + supportsTokenCost: true, + noDataMessage: { "OpenRouter activity history requires a Management API key." }, + menuHintLines: [.literal("Reported by OpenRouter for completed UTC days.")], + primaryValue: .latestDaily), presentation: ProviderUsagePresentation( menuCard: ProviderMenuCardPresentation( showsCreditsSection: false, diff --git a/Sources/CodexBarCore/Resources/Plugins/openrouter.js b/Sources/CodexBarCore/Resources/Plugins/openrouter.js index f72029f8e5..55604f3b75 100644 --- a/Sources/CodexBarCore/Resources/Plugins/openrouter.js +++ b/Sources/CodexBarCore/Resources/Plugins/openrouter.js @@ -68,6 +68,29 @@ defineProvider({ } } catch (_) {} + // /activity is management-key-only and optional. Credits and key quota remain valid + // when analytics is forbidden, unsupported by an endpoint override, slow, or malformed. + let openRouterActivityUsage = null; + try { + const activityResponse = await ctx.http.get(`${base}/activity`, { timeoutSeconds: 5 }); + if (activityResponse.status === 200) { + const activityPayload = JSON.parse(activityResponse.bodyText); + if (activityPayload && Array.isArray(activityPayload.data)) { + openRouterActivityUsage = { + data: activityPayload.data.map(item => ({ + date: item.date, + model: item.model, + prompt_tokens: item.prompt_tokens, + completion_tokens: item.completion_tokens, + reasoning_tokens: item.reasoning_tokens, + requests: item.requests, + usage: item.usage, + })), + }; + } + } + } catch (_) {} + function resetWindowUsage(reset) { const windowKey = reset === "daily" ? "usage_daily" : @@ -163,6 +186,7 @@ defineProvider({ details, }; if (primary) result.primary = primary; + if (openRouterActivityUsage) result.openRouterActivityUsage = openRouterActivityUsage; return result; }, }); diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index 389470e7bd..7a9f088c9d 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -151,6 +151,7 @@ public struct UsageSnapshot: Codable, Sendable { public let deepseekPlatformProfiles: [DeepSeekPlatformProfile] public let opencodegoUsage: OpenCodeGoUsageSnapshot? public let openAIAPIUsage: OpenAIAPIUsageSnapshot? + public let openRouterActivityUsage: OpenRouterActivityUsageSnapshot? public let codexResetCredits: CodexRateLimitResetCreditsSnapshot? public let mistralUsage: MistralUsageSnapshot? /// Live-only marker for optional Command Code subscription lookup failure. @@ -173,6 +174,7 @@ public struct UsageSnapshot: Codable, Sendable { case providerCost case details case openAIAPIUsage + case openRouterActivityUsage case codexResetCredits case mistralUsage case subscriptionExpiresAt @@ -196,6 +198,7 @@ public struct UsageSnapshot: Codable, Sendable { deepseekPlatformProfiles: [DeepSeekPlatformProfile] = [], opencodegoUsage: OpenCodeGoUsageSnapshot? = nil, openAIAPIUsage: OpenAIAPIUsageSnapshot? = nil, + openRouterActivityUsage: OpenRouterActivityUsageSnapshot? = nil, codexResetCredits: CodexRateLimitResetCreditsSnapshot? = nil, mistralUsage: MistralUsageSnapshot? = nil, commandCodeSubscriptionEnrichmentUnavailable: Bool = false, @@ -220,6 +223,7 @@ public struct UsageSnapshot: Codable, Sendable { self.deepseekPlatformProfiles = deepseekPlatformProfiles self.opencodegoUsage = opencodegoUsage self.openAIAPIUsage = openAIAPIUsage + self.openRouterActivityUsage = openRouterActivityUsage self.codexResetCredits = codexResetCredits self.mistralUsage = mistralUsage self.commandCodeSubscriptionEnrichmentUnavailable = commandCodeSubscriptionEnrichmentUnavailable @@ -269,6 +273,9 @@ public struct UsageSnapshot: Codable, Sendable { self.deepseekPlatformProfiles = [] // Live-only browser profile catalog self.opencodegoUsage = nil // Not persisted, fetched fresh each time self.openAIAPIUsage = try container.decodeIfPresent(OpenAIAPIUsageSnapshot.self, forKey: .openAIAPIUsage) + self.openRouterActivityUsage = try container.decodeIfPresent( + OpenRouterActivityUsageSnapshot.self, + forKey: .openRouterActivityUsage) self.codexResetCredits = try container.decodeIfPresent( CodexRateLimitResetCreditsSnapshot.self, forKey: .codexResetCredits) @@ -314,6 +321,7 @@ public struct UsageSnapshot: Codable, Sendable { try container.encode(self.details, forKey: .details) } try container.encodeIfPresent(self.openAIAPIUsage, forKey: .openAIAPIUsage) + try container.encodeIfPresent(self.openRouterActivityUsage, forKey: .openRouterActivityUsage) try container.encodeIfPresent(self.codexResetCredits, forKey: .codexResetCredits) try container.encodeIfPresent(self.mistralUsage, forKey: .mistralUsage) try container.encodeIfPresent(self.subscriptionExpiresAt, forKey: .subscriptionExpiresAt) @@ -388,6 +396,10 @@ public struct UsageSnapshot: Codable, Sendable { self.replacing(dataConfidence: .value(dataConfidence)) } + public func withOpenRouterActivityUsage(_ activity: OpenRouterActivityUsageSnapshot) -> UsageSnapshot { + self.replacing(openRouterActivityUsage: .value(activity)) + } + public func scoped(to provider: UsageProvider) -> UsageSnapshot { guard let identity else { return self } let scopedIdentity = identity.scoped(to: provider) @@ -467,6 +479,7 @@ public struct UsageSnapshot: Codable, Sendable { details: Replacement<[ProviderDetailSection]> = .unchanged, deepseekDetailedUsageState: Replacement = .unchanged, deepseekPlatformProfiles: Replacement<[DeepSeekPlatformProfile]> = .unchanged, + openRouterActivityUsage: Replacement = .unchanged, codexResetCredits: Replacement = .unchanged, subscriptionExpiresAt: Replacement = .unchanged, subscriptionRenewsAt: Replacement = .unchanged, @@ -484,6 +497,7 @@ public struct UsageSnapshot: Codable, Sendable { deepseekPlatformProfiles: deepseekPlatformProfiles.resolving(self.deepseekPlatformProfiles), opencodegoUsage: self.opencodegoUsage, openAIAPIUsage: self.openAIAPIUsage, + openRouterActivityUsage: openRouterActivityUsage.resolving(self.openRouterActivityUsage), codexResetCredits: codexResetCredits.resolving(self.codexResetCredits), mistralUsage: self.mistralUsage, commandCodeSubscriptionEnrichmentUnavailable: self.commandCodeSubscriptionEnrichmentUnavailable, diff --git a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift new file mode 100644 index 0000000000..fbef153b6f --- /dev/null +++ b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift @@ -0,0 +1,226 @@ +import Foundation +import Testing +@testable import CodexBar +@testable import CodexBarCore + +struct OpenRouterActivityUsageTests { + @Test + func `activity rows become thirty day daily and model cost history`() throws { + let activity = try OpenRouterActivityUsageSnapshot( + data: Data(Self.activityFixture.utf8), + now: Self.now, + historyDays: 30) + + let snapshot = activity.toCostUsageTokenSnapshot() + + #expect(snapshot.historyDays == 30) + #expect(snapshot.currencyCode == "USD") + #expect(snapshot.last30DaysTokens == 325) + #expect(abs((snapshot.last30DaysCostUSD ?? -1) - 0.05) < 0.000_000_001) + #expect(snapshot.last30DaysRequests == 10) + #expect(snapshot.daily.map(\.date) == ["2026-08-05", "2026-08-06"]) + + let firstDay = try #require(snapshot.daily.first) + #expect(firstDay.inputTokens == 60) + #expect(firstDay.outputTokens == 145) + // OpenRouter documents reasoning_tokens as a subset of completion_tokens. + // Counting it again would overstate this day by 30 tokens. + #expect(firstDay.totalTokens == 205) + #expect(firstDay.requestCount == 7) + #expect(abs((firstDay.costUSD ?? -1) - 0.02) < 0.000_000_001) + #expect(firstDay.modelsUsed == ["anthropic/claude-sonnet-4-6"]) + + let model = try #require(firstDay.modelBreakdowns?.only) + #expect(model.modelName == "anthropic/claude-sonnet-4-6") + #expect(model.totalTokens == 205) + #expect(model.requestCount == 7) + #expect(abs((model.costUSD ?? -1) - 0.02) < 0.000_000_001) + } + + @Test + func `duplicate routed models aggregate without adding estimated BYOK spend`() throws { + let activity = try OpenRouterActivityUsageSnapshot( + data: Data(Self.activityFixture.utf8), + now: Self.now) + + let snapshot = activity.toCostUsageTokenSnapshot() + let firstDay = try #require(snapshot.daily.first) + let claudeRows = try #require(firstDay.modelBreakdowns?.filter { + $0.modelName == "anthropic/claude-sonnet-4-6" + }) + + #expect(claudeRows.count == 1) + #expect(claudeRows.only?.totalTokens == 205) + #expect(claudeRows.only?.requestCount == 7) + #expect(abs((claudeRows.only?.costUSD ?? -1) - 0.02) < 0.000_000_001) + // The fixture also carries $1.25 of byok_usage_inference. `usage` is the + // OpenRouter spend field; combining the two would fabricate $1.27. + #expect(abs((firstDay.costUSD ?? -1) - 0.02) < 0.000_000_001) + } + + @Test @MainActor + func `transient activity failure preserves only current credential history`() throws { + let activity = try OpenRouterActivityUsageSnapshot( + data: Data(Self.activityFixture.utf8), + now: Self.now) + let previous = UsageSnapshot( + primary: nil, + secondary: nil, + openRouterActivityUsage: activity, + updatedAt: Self.now) + let current = UsageSnapshot(primary: nil, secondary: nil, updatedAt: Self.now) + let settings = testSettingsStore( + suiteName: "OpenRouterActivityUsageTests-\(UUID().uuidString)", + tokenAccountStore: InMemoryTokenAccountStore()) + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + store.publishTokenSnapshot(activity.toCostUsageTokenSnapshot(), for: .openrouter) + + let preserved = store.preservingOpenRouterActivityIfCurrent(current, previous: previous) + #expect(preserved.openRouterActivityUsage == activity) + + settings[providerConfig: .openrouter, field: .apiKey] = "changed-scope-fixture" + let rejected = store.preservingOpenRouterActivityIfCurrent(current, previous: previous) + #expect(rejected.openRouterActivityUsage == nil) + } + + @Test + func `bundled plugin projects activity into provider snapshot`() async throws { + let requests = OpenRouterActivityRequestRecorder() + let transport = ProviderHTTPTransportHandler { request in + await requests.append(request) + switch request.url?.lastPathComponent { + case "activity": + return try Self.response(request, body: Self.activityFixture) + case "key": + return try Self.response(request, body: #"{"data":{}}"#) + default: + return try Self.response(request, body: #"{"data":{"total_credits":100,"total_usage":40}}"#) + } + } + let runtime = try ProviderPluginRuntime(bundledPlugin: "openrouter", transport: transport) + + let usage = try await runtime.fetchUsage( + settings: [ + OpenRouterSettingsReader.apiURLEnvironmentKey: "https://openrouter.test/api/v1", + ], + secrets: [OpenRouterSettingsReader.envKey: "fixture-management-key"], + now: Self.now) + + let paths = await requests.requests.compactMap(\.url?.path) + #expect(paths.contains("/api/v1/activity")) + let activity = try #require(usage.openRouterActivityUsage) + #expect(activity.toCostUsageTokenSnapshot().last30DaysTokens == 325) + #expect(abs((activity.toCostUsageTokenSnapshot().last30DaysCostUSD ?? -1) - 0.05) < 0.000_000_001) + } + + @Test + func `bundled plugin treats activity forbidden as optional`() async throws { + let requests = OpenRouterActivityRequestRecorder() + let transport = ProviderHTTPTransportHandler { request in + await requests.append(request) + switch request.url?.lastPathComponent { + case "activity": + return try Self.response( + request, + body: #"{"error":{"message":"management key required"}}"#, + statusCode: 403) + case "key": + return try Self.response(request, body: #"{"data":{}}"#) + default: + return try Self.response(request, body: #"{"data":{"total_credits":100,"total_usage":40}}"#) + } + } + let runtime = try ProviderPluginRuntime(bundledPlugin: "openrouter", transport: transport) + + let usage = try await runtime.fetchUsage( + settings: [ + OpenRouterSettingsReader.apiURLEnvironmentKey: "https://openrouter.test/api/v1", + ], + secrets: [OpenRouterSettingsReader.envKey: "ordinary-api-key"]) + + let paths = await requests.requests.compactMap(\.url?.path) + #expect(paths.contains("/api/v1/activity")) + #expect(usage.openRouterActivityUsage == nil) + #expect(usage.detailRow(label: "Remaining")?.value == "$60.00") + } + + private static let now = Date(timeIntervalSince1970: 1_785_974_400) // 2026-08-07 00:00:00 UTC + + private static let activityFixture = #""" + { + "data": [ + { + "byok_usage_inference": 1.0, + "completion_tokens": 125, + "date": "2026-08-05", + "endpoint_id": "endpoint-claude-a", + "model": "anthropic/claude-sonnet-4-6", + "model_permaslug": "anthropic/claude-sonnet-4-6-20260219", + "prompt_tokens": 50, + "provider_name": "Anthropic", + "reasoning_tokens": 25, + "requests": 5, + "usage": 0.015 + }, + { + "byok_usage_inference": 0.25, + "completion_tokens": 20, + "date": "2026-08-05", + "endpoint_id": "endpoint-claude-b", + "model": "anthropic/claude-sonnet-4-6", + "model_permaslug": "anthropic/claude-sonnet-4-6-20260219", + "prompt_tokens": 10, + "provider_name": "Anthropic", + "reasoning_tokens": 5, + "requests": 2, + "usage": 0.005 + }, + { + "byok_usage_inference": 0, + "completion_tokens": 80, + "date": "2026-08-06", + "endpoint_id": "endpoint-gpt", + "model": "openai/gpt-5.4-mini", + "model_permaslug": "openai/gpt-5.4-mini-20260801", + "prompt_tokens": 40, + "provider_name": "OpenAI", + "reasoning_tokens": 20, + "requests": 3, + "usage": 0.03 + } + ] + } + """# + + private static func response( + _ request: URLRequest, + body: String, + statusCode: Int = 200) throws -> (Data, URLResponse) + { + let response = try #require(try HTTPURLResponse( + url: #require(request.url), + statusCode: statusCode, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])) + return (Data(body.utf8), response) + } +} + +private actor OpenRouterActivityRequestRecorder { + private(set) var requests: [URLRequest] = [] + + func append(_ request: URLRequest) { + self.requests.append(request) + } +} + +extension Array { + fileprivate var only: Element? { + self.count == 1 ? self[0] : nil + } +} diff --git a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift index 77b3c3cb16..1179c99123 100644 --- a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift +++ b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift @@ -74,13 +74,17 @@ struct OpenRouterPluginGoldenTests { secrets: [OpenRouterSettingsReader.envKey: "sk-or-v1-test"]) let recorded = await requests.requests - #expect(recorded.count == 2) + #expect(recorded.count == 3) #expect(recorded[0].timeoutInterval == 15) #expect(recorded[0].value(forHTTPHeaderField: "HTTP-Referer") == "https://codexbar.example") #expect(recorded[0].value(forHTTPHeaderField: "X-Title") == "CodexBar QA") #expect(recorded[1].timeoutInterval == 1) #expect(recorded[1].value(forHTTPHeaderField: "HTTP-Referer") == nil) #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) + #expect(recorded[2].url?.path.hasSuffix("/activity") == true) + #expect(recorded[2].timeoutInterval == 5) + #expect(recorded[2].value(forHTTPHeaderField: "HTTP-Referer") == nil) + #expect(recorded[2].value(forHTTPHeaderField: "X-Title") == nil) #expect(usage.detailRow(label: "Today")?.value == "$0.12") #expect(usage.detailRow(label: "This week")?.value == "$0.74") #expect(usage.detailRow(label: "This month")?.value == "$4.56") diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 29142ffe9f..6a4699ede7 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2363,11 +2363,27 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 639, - anchor: "guard provider == .mistral else { return displayCalendar }", - expectedProviderIDs: ["mistral"], + line: 601, + anchor: "let bucketEnd = input.provider == .openrouter", + expectedProviderIDs: ["openrouter"], expectedReferenceCount: 1, - expectedReferenceFingerprint: ["mistral@0"], + expectedReferenceFingerprint: ["openrouter@0"], + reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), + AllowedProviderConstruct( + path: "Sources/CodexBar/SpendDashboardModel.swift", + line: 658, + anchor: "guard provider == .mistral || provider == .openrouter else { return displayCalendar }", + expectedProviderIDs: ["mistral", "openrouter"], + expectedReferenceCount: 2, + expectedReferenceFingerprint: ["mistral@0", "openrouter@0"], + reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), + AllowedProviderConstruct( + path: "Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift", + line: 18, + anchor: "let openRouterActivityUsage = provider == .openrouter", + expectedProviderIDs: ["openrouter"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["openrouter@0"], reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+AccountMenuDisplay.swift", @@ -2809,6 +2825,14 @@ struct ProviderArchitectureGatekeeperTests { expectedReferenceCount: 2, expectedReferenceFingerprint: ["codex@0", "codex@10"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), + AllowedProviderConstruct( + path: "Sources/CodexBar/UsageStore+Refresh.swift", + line: 705, + anchor: "let historyStable = provider == .openrouter", + expectedProviderIDs: ["openrouter"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["openrouter@0"], + reason: "This exact app-runtime bridge preserves current-scope optional provider history."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", line: 721, @@ -3020,6 +3044,14 @@ struct ProviderArchitectureGatekeeperTests { expectedReferenceCount: 1, expectedReferenceFingerprint: ["deepseek@0"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), + AllowedProviderConstruct( + path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", + line: 1455, + anchor: "let historyStable = provider == .openrouter", + expectedProviderIDs: ["openrouter"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["openrouter@0"], + reason: "This exact app-runtime bridge preserves current-scope optional provider history."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", line: 1466, @@ -3044,6 +3076,14 @@ struct ProviderArchitectureGatekeeperTests { expectedReferenceCount: 1, expectedReferenceFingerprint: ["deepseek@0"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), + AllowedProviderConstruct( + path: "Sources/CodexBar/UsageStore+TokenCost.swift", + line: 512, + anchor: "self.tokenSnapshotPublicationForCurrentProviderConfig(for: .openrouter)?.snapshot != nil,", + expectedProviderIDs: ["openrouter"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["openrouter@0"], + reason: "This exact app-runtime bridge preserves current-scope optional provider history."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", line: 218, @@ -3104,19 +3144,19 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 422, + line: 435, anchor: "case .openai:", - expectedProviderIDs: ["mistral", "openai", "opencodego"], - expectedReferenceCount: 3, - expectedReferenceFingerprint: ["openai@0", "mistral@2", "opencodego@4"], + expectedProviderIDs: ["mistral", "openai", "openrouter", "opencodego"], + expectedReferenceCount: 4, + expectedReferenceFingerprint: ["openai@0", "openrouter@2", "mistral@4", "opencodego@6"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 441, - anchor: "case .mistral, .openai, .opencodego:", - expectedProviderIDs: ["mistral", "openai", "opencodego"], - expectedReferenceCount: 3, - expectedReferenceFingerprint: ["mistral@0", "openai@0", "opencodego@0"], + line: 456, + anchor: "case .mistral, .openai, .openrouter, .opencodego:", + expectedProviderIDs: ["mistral", "openai", "openrouter", "opencodego"], + expectedReferenceCount: 4, + expectedReferenceFingerprint: ["mistral@0", "openai@0", "openrouter@0", "opencodego@0"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", diff --git a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift index 96fe8dd3e3..1ae64aa3c7 100644 --- a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift +++ b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift @@ -55,6 +55,7 @@ struct ProviderPluginDetailsParityTests { switch request.url?.path { case "/api/v1/credits": Self.openRouterCredits case "/api/v1/key": Self.openRouterKey + case "/api/v1/activity": Self.openRouterActivity default: throw FixtureError.unexpectedURL(request.url) } } @@ -146,17 +147,21 @@ struct ProviderPluginDetailsParityTests { secrets: [OpenRouterSettingsReader.envKey: "fixture-key"]) let recorded = await requests.requests - #expect(recorded.count == 2) + #expect(recorded.count == 3) #expect(recorded[0].url?.absoluteString == (overridden ? "https://router.example.test/gateway/v1/credits" : "https://openrouter.ai/api/v1/credits")) #expect(recorded[1].url?.absoluteString == (overridden ? "https://router.example.test/gateway/v1/key" : "https://openrouter.ai/api/v1/key")) + #expect(recorded[2].url?.absoluteString == (overridden + ? "https://router.example.test/gateway/v1/activity" + : "https://openrouter.ai/api/v1/activity")) #expect(recorded[0].value(forHTTPHeaderField: "X-Title") == (overridden ? "CodexBar QA" : "CodexBar")) #expect(recorded[0].value(forHTTPHeaderField: "HTTP-Referer") == (overridden ? "https://codexbar.example" : nil)) #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) + #expect(recorded[2].value(forHTTPHeaderField: "X-Title") == nil) } @Test @@ -494,6 +499,7 @@ struct ProviderPluginDetailsParityTests { "usage_daily":1,"usage_weekly":2,"usage_monthly":4, "rate_limit":{"requests":120,"interval":"10s"}}} """# + private static let openRouterActivity = #"{"data":[]}"# private static let poeBalance = #"{"current_point_balance":2500}"# private static let poeHistory = #""" {"data":[ diff --git a/Tests/CodexBarTests/ShareStatsTests.swift b/Tests/CodexBarTests/ShareStatsTests.swift index 1be23ce51f..59e0c4bb2c 100644 --- a/Tests/CodexBarTests/ShareStatsTests.swift +++ b/Tests/CodexBarTests/ShareStatsTests.swift @@ -41,7 +41,7 @@ struct ShareStatsTests { #expect(payload.providers.map(\.subscriptionName) == ["Max", "Pro 20x", "Cursor Pro"]) #expect(payload.providers.last?.estimatedCost == nil) #expect(payload.spendReportingProviderCount == 2) - #expect(payload.topModels.map(\.modelName).prefix(2) == ["Claude", "GPT"]) + #expect(payload.topModels.map(\.modelName).prefix(2) == ["Claude Sonnet 4", "GPT 5.4"]) let text = ShareStatsFormatting.text(payload) #expect(text.contains("GBP: £12.00 estimated · coverage 10/30 days")) @@ -70,11 +70,59 @@ struct ShareStatsTests { #expect(payload.providers.last?.estimatedCost == nil) #expect(payload.totalTokens == 500) #expect(payload.totalTokensIsPartial) - #expect(payload.currencies.allSatisfy(\.isPartial)) + #expect(payload.currencies.allSatisfy { currency in currency.isPartial }) #expect(ShareStatsFormatting.text(payload).contains("~500 tracked tokens")) #expect(ShareStatsFormatting.text(payload).contains("2/6 connected services report spend")) } + @Test + func `duplicate accounts cannot mask a connected provider with unavailable usage`() throws { + let model = SpendDashboardModel(requestedDays: 30, groups: [ + SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + SpendDashboardModel.ProviderRow( + id: "codex:one", + rank: 1, + provider: .codex, + displayName: "Codex · #1", + totalTokens: 200, + totalCost: 4, + coveredDayCount: 30), + SpendDashboardModel.ProviderRow( + id: "codex:two", + rank: 2, + provider: .codex, + displayName: "Codex · #2", + totalTokens: 300, + totalCost: 6, + coveredDayCount: 30), + ], + models: [], + dailyPoints: [], + totalTokens: 500, + totalCost: 10, + coveredDayCount: 30, + chartDomain: Self.date...Self.date, + modelHistoryCompleteness: .complete), + ]) + let roster = [ + ShareStatsProviderRosterEntry(provider: .codex, providerName: "Codex", currencyCode: "USD"), + ShareStatsProviderRosterEntry( + provider: .openrouter, + providerName: "OpenRouter", + currencyCode: "USD"), + ] + + let payload = try #require(ShareStatsBuilder.make(model: model, providerRoster: roster)) + + #expect(payload.totalTokens == 500) + #expect(payload.totalTokensIsPartial) + #expect(payload.currencies.only?.isPartial == true) + #expect(payload.spendReportingProviderCount == 1) + #expect(payload.providers.count == 2) + } + @Test func `twenty connected services remain accounted for without overflowing the flex card`() throws { let roster = Array(UsageProvider.allCases.prefix(20)).map { provider in @@ -118,9 +166,9 @@ struct ShareStatsTests { subscriptionNames: subscriptionNames)) let text = ShareStatsFormatting.text(payload) - #expect(payload.topModels.map(\.modelName) == ["Claude", "GPT"]) - #expect(payload.topModels.last?.totalTokens == 400) - #expect(payload.topModels.last?.estimatedCost == 8) + #expect(payload.topModels.map(\.modelName) == ["Claude Sonnet 4", "GPT 5.4"]) + #expect(payload.topModels.last?.totalTokens == 200) + #expect(payload.topModels.last?.estimatedCost == 4) #expect(payload.providers.map(\.subscriptionName) == ["Max", nil, nil]) #expect(!text.contains("person@example.com")) #expect(!text.contains("/Users/")) @@ -165,8 +213,82 @@ struct ShareStatsTests { @Test func `bedrock regional model identifiers map to public families`() { - #expect(ShareStatsSanitizer.modelName("us.amazon.nova-2-lite-v1:0") == "Amazon Nova") - #expect(ShareStatsSanitizer.modelName("global.anthropic.claude-sonnet-4-v1:0") == "Claude") + #expect(ShareStatsSanitizer.modelName("us.amazon.nova-2-lite-v1:0") == "Amazon Nova 2 Lite") + #expect(ShareStatsSanitizer.modelName("global.anthropic.claude-sonnet-4-v1:0") == "Claude Sonnet 4") + } + + @Test + func `share model labels preserve first class variants and routed OpenRouter ids`() { + #expect(ShareStatsSanitizer.modelName("claude-fable-5") == "Claude Fable 5") + #expect(ShareStatsSanitizer.modelName("claude-opus-4-6") == "Claude Opus 4.6") + #expect(ShareStatsSanitizer.modelName("claude-sonnet-4-6") == "Claude Sonnet 4.6") + #expect(ShareStatsSanitizer.modelName("anthropic/claude-sonnet-4-6") == "Claude Sonnet 4.6") + #expect(ShareStatsSanitizer.modelName("openai/gpt-5.4-mini") == "GPT 5.4 Mini") + #expect(ShareStatsSanitizer.modelName("openai/gpt-5.6-sol") == "GPT 5.6 Sol") + #expect(ShareStatsSanitizer.modelName("openai/gpt-5.6-terra") == "GPT 5.6 Terra") + #expect(ShareStatsSanitizer.modelName("openai/gpt-5.6-luna") == "GPT 5.6 Luna") + #expect(ShareStatsSanitizer.modelName("openai/gpt-4o") == "GPT 4o") + #expect(ShareStatsSanitizer.modelName("google/gemini-2.5-pro") == "Gemini 2.5 Pro") + #expect(ShareStatsSanitizer.modelName("moonshotai/kimi-k2.5") == "Kimi K2.5") + #expect(ShareStatsSanitizer.modelName("acme/private-model-v2") == nil) + #expect(ShareStatsSanitizer.modelName("acme/gpt-secret-project") == nil) + } + + @Test @MainActor + func `routed OpenRouter models remain distinct in the share ranking`() throws { + let modelRows = [ + ("anthropic/claude-fable-5", 900), + ("anthropic/claude-opus-4-6", 800), + ("anthropic/claude-sonnet-4-6", 700), + ("openai/gpt-5.4-mini", 600), + ("gpt-5.4-mini", 100), + ("google/gemini-2.5-pro", 500), + ("x-ai/grok-4-fast", 400), + ] + let dashboard = SpendDashboardModel(requestedDays: 30, groups: [ + SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + SpendDashboardModel.ProviderRow( + id: "openrouter", + rank: 1, + provider: .openrouter, + displayName: "OpenRouter", + totalTokens: 4000, + totalCost: 40, + coveredDayCount: 30), + ], + models: modelRows.enumerated().map { index, row in + SpendDashboardModel.ModelRow( + rank: index + 1, + provider: .openrouter, + providerName: "OpenRouter", + modelName: row.0, + totalTokens: row.1, + totalCost: Double(row.1) / 100) + }, + dailyPoints: [], + totalTokens: 4000, + totalCost: 40, + coveredDayCount: 30, + chartDomain: Self.date...Self.date, + modelHistoryCompleteness: .complete), + ]) + + let payload = try #require(ShareStatsBuilder.make(model: dashboard)) + #expect(payload.topModels.map(\.modelName) == [ + "Claude Fable 5", + "Claude Opus 4.6", + "Claude Sonnet 4.6", + "GPT 5.4 Mini", + "Gemini 2.5 Pro", + "Grok 4 Fast", + ]) + #expect(payload.topModels.allSatisfy { $0.provider == .openrouter }) + #expect(payload.topModels.first { $0.modelName == "GPT 5.4 Mini" }?.totalTokens == 700) + #expect(ShareStatsCardView.modelSectionDetail(for: payload.topModels.count) == "3 OF 6 · BY TOKENS") + #expect(ShareStatsFormatting.text(payload).contains("+1 more models ranked in local stats")) + #expect(try #require(ShareStatsRenderer.pngData(for: payload)).isEmpty == false) } @Test @@ -176,21 +298,21 @@ struct ShareStatsTests { rank: 1, provider: .codex, providerName: "Codex", - modelName: "gpt-5.4", + modelName: "gpt-5.4-mini", totalTokens: Int.max, totalCost: Double.greatestFiniteMagnitude), SpendDashboardModel.ModelRow( rank: 2, provider: .codex, providerName: "Codex", - modelName: "gpt-5.4-mini", + modelName: "openai/gpt-5.4-mini", totalTokens: 1, totalCost: Double.greatestFiniteMagnitude), SpendDashboardModel.ModelRow( rank: 3, provider: .codex, providerName: "Codex", - modelName: "gpt-5.4-nano", + modelName: "chatgpt-5.4-mini", totalTokens: 5, totalCost: 5), ] diff --git a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift index ea29bd0f58..2c94181052 100644 --- a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift @@ -13,6 +13,33 @@ struct SpendDashboardDateTruthTests { let chartCost: Double? } + @Test + func `OpenRouter completed UTC history never fabricates the current day`() throws { + var utc = Calendar(identifier: .gregorian) + utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) + let august6 = try #require(utc.date(from: DateComponents(year: 2026, month: 8, day: 6))) + let snapshot = Self.snapshot( + currency: "USD", + entries: [ + Self.entry(day: "2026-08-05", cost: 1, tokens: 10), + Self.entry(day: "2026-08-06", cost: 2, tokens: 20), + ], + historyDays: 2, + updatedAt: now) + let group = try #require(SpendDashboardModel.build( + inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], + requestedDays: 2, + now: now, + calendar: utc).groups.first) + + #expect(group.providers.first?.coveredDayCount == 1) + #expect(group.totalCost == 2) + #expect(group.totalTokens == 20) + #expect(group.dailyPoints.map(\.day) == [august6]) + #expect(group.coveredDayCount == 1) + } + @Test func `Mistral UTC buckets map into Pacific dashboard days at midnight UTC`() throws { var pacific = Calendar(identifier: .gregorian) diff --git a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift index f85256a492..5c2b79295f 100644 --- a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift +++ b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift @@ -32,6 +32,7 @@ struct SpendDashboardIntegratedModelCoverageTests { .cursor, .mistral, .openai, + .openrouter, .opencodego, .vertexai, ]) diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index aa76341712..bcd7f3affd 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -47,7 +47,17 @@ struct SpendDashboardModelTests { let providers = Set(ProviderDescriptorRegistry.all .filter(\.tokenCost.supportsTokenCost) .map(\.id)) - #expect(providers == [.codex, .claude, .vertexai, .openai, .mistral, .bedrock, .cursor, .opencodego]) + #expect(providers == [ + .codex, + .claude, + .vertexai, + .openai, + .openrouter, + .mistral, + .bedrock, + .cursor, + .opencodego, + ]) } @Test diff --git a/docs/screenshots/spend-dashboard-proof/README.md b/docs/screenshots/spend-dashboard-proof/README.md index 89906ef0e5..52772f144b 100644 --- a/docs/screenshots/spend-dashboard-proof/README.md +++ b/docs/screenshots/spend-dashboard-proof/README.md @@ -20,9 +20,12 @@ The narrow render verifies that the range controls wrap below the title and the tracked-source grid collapses to one column. The wide render uses two columns. Both states keep cost-history inclusion and exclusion explicit. -`overview-all-providers.png` and `share-all-providers.png` are local runtime -acceptance captures rather than golden test outputs. They show the same -six-provider synthetic roster: three sources with known spend and three sources -whose spend is explicitly unavailable. The Overview keeps Codex prominent and -uses compact rows for the remaining providers; the share card preserves all six -connected sources while labeling partial spend and token totals as estimates. +`overview-all-providers.png` and `share-all-providers.png` are local synthetic +runtime captures rather than deterministic golden outputs, so they follow the +Mac's active appearance. They show the same six-provider roster: three sources +with known spend and three sources whose spend is explicitly unavailable. The +Overview is captured at the production 310-point menu width, keeps Codex +prominent, and uses compact rows for the remaining providers. The share capture +uses the production `ShareStatsWindowController`, preserves all six connected +sources, labels partial spend with its reporting denominator, and ranks routed +OpenRouter models alongside first-class Claude and Codex variants. diff --git a/docs/screenshots/spend-dashboard-proof/share-all-providers.png b/docs/screenshots/spend-dashboard-proof/share-all-providers.png index 9bce7990482639fb342fe0d3b5d1bd2da1c1f337..39e735fa6d656f539e192dad720882c61571b9a2 100644 GIT binary patch literal 116606 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

dbk@@dTboX|Q@Z|pX<+CU$w9588|gFm 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 O9|ajz=_;UU*#869euS+6 literal 64974 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 From b03a03bd1a1d57b61c856789f0926953ff814a9f Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 23:06:11 -0700 Subject: [PATCH 14/51] Align OpenRouter spend with portable plugins --- .../ProviderPluginSnapshotMapper.swift | 18 ++-- .../OpenRouterActivityUsageSnapshot.swift | 89 ++++++++++++++++++- .../OpenRouterActivityUsageTests.swift | 58 +++++++++++- .../ProviderPluginDetailsParityTests.swift | 34 ++++++- .../SpendDashboardDateTruthTests.swift | 24 +++++ .../SpendDashboardTrackedSourceTests.swift | 9 +- docs/plugin-conversion-matrix.md | 2 +- docs/plugin-prototype.md | 5 +- 8 files changed, 215 insertions(+), 24 deletions(-) diff --git a/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift b/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift index 0eb839bb3c..1f603ca16f 100644 --- a/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift +++ b/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift @@ -62,7 +62,7 @@ enum ProviderPluginSnapshotMapper { rows.isArray, let rawCount = rows.property("length")?.int32Value(), rawCount >= 0, - rawCount <= 10_000 + rawCount <= 10000 else { return nil } do { @@ -71,23 +71,23 @@ enum ProviderPluginSnapshotMapper { throw ProviderPluginError.invalidSnapshot("openRouterActivityUsage.data must contain objects") } let path = "openRouterActivityUsage.data[\(index)]" - return [ - "date": try self.requiredString(row, property: "date", path: path), - "model": try self.requiredString(row, property: "model", path: path), - "prompt_tokens": try self.requiredNonnegativeInteger( + return try [ + "date": self.requiredString(row, property: "date", path: path), + "model": self.requiredString(row, property: "model", path: path), + "prompt_tokens": self.requiredNonnegativeInteger( row, property: "prompt_tokens", path: path), - "completion_tokens": try self.requiredNonnegativeInteger( + "completion_tokens": self.requiredNonnegativeInteger( row, property: "completion_tokens", path: path), - "reasoning_tokens": try self.requiredNonnegativeInteger( + "reasoning_tokens": self.requiredNonnegativeInteger( row, property: "reasoning_tokens", path: path), - "requests": try self.requiredNonnegativeInteger(row, property: "requests", path: path), - "usage": try self.requiredNonnegativeNumber(row, property: "usage", path: path), + "requests": self.requiredNonnegativeInteger(row, property: "requests", path: path), + "usage": self.requiredNonnegativeNumber(row, property: "usage", path: path), ] } let data = try JSONSerialization.data(withJSONObject: ["data": items]) diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift index fb5b9fc4c8..75b03b7ce2 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift @@ -46,6 +46,30 @@ public struct OpenRouterActivityUsageSnapshot: Codable, Equatable, Sendable { public let historyDays: Int public let updatedAt: Date + private enum CodingKeys: String, CodingKey { + case daily + case historyDays + case updatedAt + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let daily = try container.decode([DailyBucket].self, forKey: .daily) + let historyDays = try container.decode(Int.self, forKey: .historyDays) + let updatedAt = try container.decode(Date.self, forKey: .updatedAt) + do { + try Self.validate(daily: daily, historyDays: historyDays, updatedAt: updatedAt) + } catch { + throw DecodingError.dataCorruptedError( + forKey: .daily, + in: container, + debugDescription: "OpenRouter activity history is invalid") + } + self.daily = daily + self.historyDays = historyDays + self.updatedAt = updatedAt + } + /// Decodes the documented `/api/v1/activity` envelope and keeps only the requested /// completed UTC-day window. OpenRouter currently exposes at most 30 completed days. public init(data: Data, now: Date, historyDays: Int = 30) throws { @@ -93,9 +117,7 @@ public struct OpenRouterActivityUsageSnapshot: Codable, Equatable, Sendable { usageUSD: Self.sum(models.map(\.usageUSD)), models: models) }.sorted { $0.date < $1.date } - _ = try Self.sum(daily.map(\.totalTokens)) - _ = try Self.sum(daily.map(\.requests)) - _ = try Self.sum(daily.map(\.usageUSD)) + try Self.validate(daily: daily, historyDays: days, updatedAt: now) self.daily = daily self.historyDays = days self.updatedAt = now @@ -129,7 +151,7 @@ public struct OpenRouterActivityUsageSnapshot: Codable, Equatable, Sendable { currencyCode: "USD", historyDays: self.historyDays, historyCoverageIsEstablished: true, - historyLabel: "Last \(self.historyDays) completed UTC days", + historyLabel: "OpenRouter account activity · Last \(self.historyDays) completed UTC days", daily: daily, updatedAt: self.updatedAt) } @@ -243,6 +265,65 @@ public struct OpenRouterActivityUsageSnapshot: Codable, Equatable, Sendable { return total } + private static func validate( + daily: [DailyBucket], + historyDays: Int, + updatedAt: Date) throws + { + guard (1...30).contains(historyDays), daily.count <= historyDays else { + throw DecodeError.invalidActivityItem + } + let today = self.utcCalendar.startOfDay(for: updatedAt) + guard let firstDay = self.utcCalendar.date(byAdding: .day, value: -historyDays, to: today) else { + throw DecodeError.invalidDateWindow + } + var dates = Set() + for bucket in daily { + guard let date = self.date(from: bucket.date), + date >= firstDay, + date < today, + dates.insert(bucket.date).inserted, + bucket.promptTokens >= 0, + bucket.completionTokens >= 0, + bucket.reasoningTokens >= 0, + bucket.requests >= 0, + bucket.usageUSD.isFinite, + bucket.usageUSD >= 0, + !bucket.models.isEmpty, + bucket.models.count <= 10000 + else { throw DecodeError.invalidActivityItem } + _ = try self.add(bucket.promptTokens, bucket.completionTokens) + + var modelNames = Set() + for model in bucket.models { + guard !model.model.isEmpty, + modelNames.insert(model.model).inserted, + model.promptTokens >= 0, + model.completionTokens >= 0, + model.reasoningTokens >= 0, + model.requests >= 0, + model.usageUSD.isFinite, + model.usageUSD >= 0 + else { throw DecodeError.invalidActivityItem } + _ = try self.add(model.promptTokens, model.completionTokens) + } + let promptTokens = try self.sum(bucket.models.map(\.promptTokens)) + let completionTokens = try self.sum(bucket.models.map(\.completionTokens)) + let reasoningTokens = try self.sum(bucket.models.map(\.reasoningTokens)) + let requests = try self.sum(bucket.models.map(\.requests)) + let usageUSD = try self.sum(bucket.models.map(\.usageUSD)) + guard bucket.promptTokens == promptTokens, + bucket.completionTokens == completionTokens, + bucket.reasoningTokens == reasoningTokens, + bucket.requests == requests, + bucket.usageUSD == usageUSD + else { throw DecodeError.invalidActivityItem } + } + _ = try self.sum(daily.map { try self.add($0.promptTokens, $0.completionTokens) }) + _ = try self.sum(daily.map(\.requests)) + _ = try self.sum(daily.map(\.usageUSD)) + } + private static func modelSort(_ lhs: ModelBreakdown, _ rhs: ModelBreakdown) -> Bool { if lhs.usageUSD != rhs.usageUSD { return lhs.usageUSD > rhs.usageUSD } if lhs.totalTokens != rhs.totalTokens { return lhs.totalTokens > rhs.totalTokens } diff --git a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift index fbef153b6f..65316abc27 100644 --- a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift +++ b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift @@ -58,6 +58,58 @@ struct OpenRouterActivityUsageTests { #expect(abs((firstDay.costUSD ?? -1) - 0.02) < 0.000_000_001) } + @Test + func `persisted activity validates before entering spend totals`() throws { + let activity = try OpenRouterActivityUsageSnapshot( + data: Data(Self.activityFixture.utf8), + now: Self.now) + let usage = UsageSnapshot( + primary: nil, + secondary: nil, + openRouterActivityUsage: activity, + updatedAt: Self.now) + let encoded = try JSONEncoder().encode(usage) + + let decoded = try JSONDecoder().decode(UsageSnapshot.self, from: encoded) + #expect(decoded.openRouterActivityUsage == activity) + + let encodedText = try #require(String(data: encoded, encoding: .utf8)) + let invalidText = encodedText.replacingOccurrences( + of: #""promptTokens":60"#, + with: #""promptTokens":-1"#) + #expect(invalidText != encodedText) + let invalid = Data(invalidText.utf8) + #expect(throws: DecodingError.self) { + _ = try JSONDecoder().decode(UsageSnapshot.self, from: invalid) + } + } + + @Test + func `non OpenRouter plugins cannot inject activity history`() async throws { + let runtime = try ProviderPluginRuntime(source: #""" + defineProvider({ + id: "synthetic", + name: "Synthetic fixture", + endpoints: ["https://fixture.example"], + settings: [], + async fetchUsage() { + return { + primary: { usedPercent: 1 }, + openRouterActivityUsage: { data: [{ + date: "2026-08-06", model: "openai/gpt-5.6-sol", + prompt_tokens: 10, completion_tokens: 20, reasoning_tokens: 5, + requests: 1, usage: 0.01, + }] }, + }; + }, + }); + """#) + + let snapshot = try await runtime.fetchUsage(now: Self.now) + + #expect(snapshot.openRouterActivityUsage == nil) + } + @Test @MainActor func `transient activity failure preserves only current credential history`() throws { let activity = try OpenRouterActivityUsageSnapshot( @@ -118,8 +170,8 @@ struct OpenRouterActivityUsageTests { #expect(abs((activity.toCostUsageTokenSnapshot().last30DaysCostUSD ?? -1) - 0.05) < 0.000_000_001) } - @Test - func `bundled plugin treats activity forbidden as optional`() async throws { + @Test(arguments: [403, 404]) + func `bundled plugin treats unavailable activity as optional`(statusCode: Int) async throws { let requests = OpenRouterActivityRequestRecorder() let transport = ProviderHTTPTransportHandler { request in await requests.append(request) @@ -128,7 +180,7 @@ struct OpenRouterActivityUsageTests { return try Self.response( request, body: #"{"error":{"message":"management key required"}}"#, - statusCode: 403) + statusCode: statusCode) case "key": return try Self.response(request, body: #"{"data":{}}"#) default: diff --git a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift index 1ae64aa3c7..25c8f1def1 100644 --- a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift +++ b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift @@ -65,6 +65,13 @@ struct ProviderPluginDetailsParityTests { #expect(script.primary?.usedPercent == 25) #expect(script.identity?.loginMethod == "Balance: $60.00") + let activity = try #require(script.openRouterActivityUsage) + let activityCost = activity.toCostUsageTokenSnapshot() + #expect(activityCost.last30DaysTokens == 45) + #expect(activityCost.last30DaysRequests == 3) + #expect(abs((activityCost.last30DaysCostUSD ?? -1) - 0.015) < 0.000_000_001) + #expect(activityCost.daily.count == 1) + #expect(activityCost.daily.first?.modelBreakdowns?.count == 1) #expect(try script.details == [ Self.section("Credits", rows: [ Self.row("Remaining", "$60.00"), @@ -89,6 +96,24 @@ struct ProviderPluginDetailsParityTests { ]) } + @Test + func `OpenRouter malformed optional activity preserves credits on both plugin engines`() async throws { + let transport = Self.transport { request in + switch request.url?.path { + case "/api/v1/credits": Self.openRouterCredits + case "/api/v1/key": Self.openRouterKey + case "/api/v1/activity": #"{"data":[{"date":"2026-08-01","usage":"free"}]}"# + default: throw FixtureError.unexpectedURL(request.url) + } + } + + let script = try await ProviderPluginRuntime(bundledPlugin: "openrouter", transport: transport) + .fetchUsage(secrets: ["OPENROUTER_API_KEY": "fixture-key"]) + + #expect(script.openRouterActivityUsage == nil) + #expect(script.identity?.loginMethod == "Balance: $60.00") + } + @Test func `ClawRouter fixture matches stable cut-over details`() async throws { let transport = Self.transport { request in @@ -499,7 +524,14 @@ struct ProviderPluginDetailsParityTests { "usage_daily":1,"usage_weekly":2,"usage_monthly":4, "rate_limit":{"requests":120,"interval":"10s"}}} """# - private static let openRouterActivity = #"{"data":[]}"# + private static let openRouterActivity = #""" + {"data":[ + {"date":"2026-08-01","model":"anthropic/claude-sonnet-4-6","prompt_tokens":10, + "completion_tokens":20,"reasoning_tokens":5,"requests":2,"usage":0.01}, + {"date":"2026-08-01","model":"anthropic/claude-sonnet-4-6","prompt_tokens":5, + "completion_tokens":10,"reasoning_tokens":2,"requests":1,"usage":0.005} + ]} + """# private static let poeBalance = #"{"current_point_balance":2500}"# private static let poeHistory = #""" {"data":[ diff --git a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift index 2c94181052..cd0c68b05a 100644 --- a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift @@ -40,6 +40,30 @@ struct SpendDashboardDateTruthTests { #expect(group.coveredDayCount == 1) } + @Test + func `OpenRouter thirty completed days stay partial in annual view`() throws { + var utc = Calendar(identifier: .gregorian) + utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) + let snapshot = Self.snapshot( + currency: "USD", + entries: [Self.entry(day: "2026-08-06", cost: 2, tokens: 20)], + historyDays: 30, + updatedAt: now) + let group = try #require(SpendDashboardModel.build( + inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], + requestedDays: 365, + now: now, + calendar: utc).groups.first) + + #expect(group.providers.first?.coveredDayCount == 30) + #expect(group.coveredDayCount == 30) + #expect(group.totalCost == nil) + #expect(group.knownCost == 2) + #expect(group.totalTokens == nil) + #expect(group.knownTokens == 20) + } + @Test func `Mistral UTC buckets map into Pacific dashboard days at midnight UTC`() throws { var pacific = Calendar(identifier: .gregorian) diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index cae5ce4965..62a6a20154 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -53,6 +53,7 @@ struct SpendDashboardTrackedSourceTests { #expect(rows[.grok]?.state == .connected) #expect(rows[.gemini]?.state == .needsAttention) #expect(rows[.openrouter]?.state == .awaitingUsage) + #expect(rows[.openrouter]?.supportsCostHistory == true) #expect(rows.values.filter(\.contributesCostHistory).map(\.provider) == [.cursor]) } @@ -84,7 +85,7 @@ struct SpendDashboardTrackedSourceTests { let openRouterSources = credentialSources.filter { $0.provider == .openrouter } #expect(openRouterSources.count == 2) #expect(openRouterSources.allSatisfy { $0.state == .configured }) - #expect(openRouterSources.allSatisfy { !$0.supportsCostHistory }) + #expect(openRouterSources.allSatisfy(\.supportsCostHistory)) #expect(openRouterSources.allSatisfy { !$0.contributesCostHistory }) } @@ -96,7 +97,7 @@ struct SpendDashboardTrackedSourceTests { providerName: "OpenRouter", accountName: "Work", state: .connected, - supportsCostHistory: false, + supportsCostHistory: true, contributesCostHistory: false) #expect(spendDashboardTrackedSourceStatusText(source) == "Usage connected · not in cost total") @@ -117,7 +118,7 @@ struct SpendDashboardTrackedSourceTests { providerName: "OpenRouter", accountName: nil, state: .awaitingUsage, - supportsCostHistory: false, + supportsCostHistory: true, contributesCostHistory: false) #expect(spendDashboardTrackedSourceStatusText(setup) == "No usage yet") } @@ -187,7 +188,7 @@ struct SpendDashboardTrackedSourceTests { providerName: "OpenRouter", accountName: "Research", state: .awaitingUsage, - supportsCostHistory: false, + supportsCostHistory: true, contributesCostHistory: false), SpendDashboardTrackedSource( id: "cursor:account:work", diff --git a/docs/plugin-conversion-matrix.md b/docs/plugin-conversion-matrix.md index 27ebb8377d..1518a793f7 100644 --- a/docs/plugin-conversion-matrix.md +++ b/docs/plugin-conversion-matrix.md @@ -69,7 +69,7 @@ that remain cheap to convert. Remaining buckets name the next blocker after this | ollama | `needs-cookie-import` | No | Skipped: hosted parity requires HTML bootstrap/state extraction plus API-key fallback arbitration. | | synthetic | `cut-over` | Yes | Cut over on both engines: fixed-origin bearer GET with generic windows, cost, dates, and identity; the native fetch twin is deleted. | | warp | `needs-pty/webview/native` | No | Warp sends a POST GraphQL operation, which the GET-only HTTP broker cannot express. | -| openrouter | `cut-over` | Yes | Cut over on JavaScriptCore: endpoint and client-header overrides plus one-second best-effort key enrichment match native behavior; the native fetch core is Linux-only. | +| openrouter | `cut-over` | Yes | Cut over on both engines: endpoint and client-header overrides, best-effort key enrichment, and management-key completed-day model activity are plugin-owned; the native fetch core is deleted. | | elevenlabs | `convertible-now` | No | Verified `xi-api-key` GET; heterogeneous character/minute quotas map to named generic windows. | | windsurf | `needs-files/subprocess/oauth-broker` | No | Chromium localStorage, IDE databases, and binary protobuf decoding supply the current session. | | zed | `needs-files/subprocess/oauth-broker` | No | Zed server settings and a named Keychain credential must be read locally. | diff --git a/docs/plugin-prototype.md b/docs/plugin-prototype.md index 48ff47042d..4530d9c58e 100644 --- a/docs/plugin-prototype.md +++ b/docs/plugin-prototype.md @@ -189,7 +189,8 @@ prototype flag, so cut-over providers retain timeout and fresh-context recovery ## Current limitations The remaining bundled-conversion flag is macOS-only and compiled out when JavaScriptCore is unavailable. It supports bundled -first-party IDs and the generic snapshot and declarative details only: no provider-specific Swift payloads, +first-party IDs, the generic snapshot, declarative details, and explicitly allowlisted behavioral payload projections: +no arbitrary provider-specific Swift payloads, OAuth/refresh broker, local files or databases, subprocesses, arbitrary/form POST bodies, PTY, WebView, binary/protobuf responses, private-network HTTP, or unvalidated dynamic origins. The separate user-plugin path adds local `.js`/`.ts` discovery, approval, and settings without changing these @@ -201,4 +202,4 @@ first-party flag semantics. Browser cookies remain restricted to declared domain Display-only provider payloads now use `details` on both the Swift and JavaScript paths. The remaining bespoke `UsageSnapshot` fields drive behavior rather than presentation: Codex reset-credit actions, Command Code refresh stabilization, DeepSeek profile selection/transition state, and provider-derived token-cost pipelines for OpenAI API, -Mistral, and OpenCode Go. They are not plugin compatibility shims. +OpenRouter completed-day activity, Mistral, and OpenCode Go. They are not plugin compatibility shims. From 194634e7718c04210d23900638b8406bb4642f6b Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 23:11:20 -0700 Subject: [PATCH 15/51] Satisfy converged lint budgets --- Sources/CodexBar/UsageStore+Refresh.swift | 20 +++-- .../ProviderArchitectureGatekeeperTests.swift | 15 ++-- .../SpendDashboardDateTruthTests.swift | 51 ----------- ...endDashboardOpenRouterDateTruthTests.swift | 84 +++++++++++++++++++ 4 files changed, 105 insertions(+), 65 deletions(-) create mode 100644 Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift diff --git a/Sources/CodexBar/UsageStore+Refresh.swift b/Sources/CodexBar/UsageStore+Refresh.swift index 9d056043cd..68acb06bc9 100644 --- a/Sources/CodexBar/UsageStore+Refresh.swift +++ b/Sources/CodexBar/UsageStore+Refresh.swift @@ -701,14 +701,9 @@ extension UsageStore { } else { self.lastKnownResetSnapshots[provider.instanceID] } - let profileStable = self.preservingDeepSeekProfileCatalog(in: accountScoped, provider: provider) - let historyStable = provider == .openrouter - ? self.preservingOpenRouterActivityIfCurrent( - profileStable, - previous: self.snapshots[provider.instanceID]) - : profileStable + let profileStable = self.preservingProviderEnrichment(in: accountScoped, provider: provider) let stabilized = Self.commandCodeSnapshotResolvingDepletionOnEnrichmentFailure( - current: historyStable, + current: profileStable, previous: self.snapshots[provider.instanceID]) let backfilled = stabilized.backfillingResetTimes(from: resetBackfillSource) let warningAccountDiscriminator = Self.warningAccountDiscriminator( @@ -860,6 +855,17 @@ extension UsageStore { return snapshot.preservingDeepSeekPlatformProfiles(from: self.presentationSnapshot(for: .deepseek)) } + private func preservingProviderEnrichment( + in snapshot: UsageSnapshot, + provider: UsageProvider) -> UsageSnapshot + { + let profileStable = self.preservingDeepSeekProfileCatalog(in: snapshot, provider: provider) + guard provider == .openrouter else { return profileStable } + return self.preservingOpenRouterActivityIfCurrent( + profileStable, + previous: self.snapshots[provider.instanceID]) + } + private func bindCodexFailurePublicationOwner( provider: UsageProvider, expectedGuard: CodexAccountScopedRefreshGuard?) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 6a4699ede7..6465d82532 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2861,17 +2861,18 @@ struct ProviderArchitectureGatekeeperTests { path: "Sources/CodexBar/UsageStore+Refresh.swift", line: 854, anchor: "guard provider == .deepseek else { return snapshot }", - expectedProviderIDs: ["codex", "deepseek"], - expectedReferenceCount: 8, + expectedProviderIDs: ["codex", "deepseek", "openrouter"], + expectedReferenceCount: 9, expectedReferenceFingerprint: [ "deepseek@0", "deepseek@1", - "codex@8", - "codex@16", + "openrouter@9", + "codex@19", "codex@27", - "codex@28", - "codex@33", - "codex@35", + "codex@38", + "codex@39", + "codex@44", + "codex@46", ], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( diff --git a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift index cd0c68b05a..ea29bd0f58 100644 --- a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift @@ -13,57 +13,6 @@ struct SpendDashboardDateTruthTests { let chartCost: Double? } - @Test - func `OpenRouter completed UTC history never fabricates the current day`() throws { - var utc = Calendar(identifier: .gregorian) - utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) - let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) - let august6 = try #require(utc.date(from: DateComponents(year: 2026, month: 8, day: 6))) - let snapshot = Self.snapshot( - currency: "USD", - entries: [ - Self.entry(day: "2026-08-05", cost: 1, tokens: 10), - Self.entry(day: "2026-08-06", cost: 2, tokens: 20), - ], - historyDays: 2, - updatedAt: now) - let group = try #require(SpendDashboardModel.build( - inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], - requestedDays: 2, - now: now, - calendar: utc).groups.first) - - #expect(group.providers.first?.coveredDayCount == 1) - #expect(group.totalCost == 2) - #expect(group.totalTokens == 20) - #expect(group.dailyPoints.map(\.day) == [august6]) - #expect(group.coveredDayCount == 1) - } - - @Test - func `OpenRouter thirty completed days stay partial in annual view`() throws { - var utc = Calendar(identifier: .gregorian) - utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) - let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) - let snapshot = Self.snapshot( - currency: "USD", - entries: [Self.entry(day: "2026-08-06", cost: 2, tokens: 20)], - historyDays: 30, - updatedAt: now) - let group = try #require(SpendDashboardModel.build( - inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], - requestedDays: 365, - now: now, - calendar: utc).groups.first) - - #expect(group.providers.first?.coveredDayCount == 30) - #expect(group.coveredDayCount == 30) - #expect(group.totalCost == nil) - #expect(group.knownCost == 2) - #expect(group.totalTokens == nil) - #expect(group.knownTokens == 20) - } - @Test func `Mistral UTC buckets map into Pacific dashboard days at midnight UTC`() throws { var pacific = Calendar(identifier: .gregorian) diff --git a/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift new file mode 100644 index 0000000000..75ebc521a5 --- /dev/null +++ b/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift @@ -0,0 +1,84 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct SpendDashboardOpenRouterDateTruthTests { + @Test + func `completed UTC history never fabricates the current day`() throws { + var utc = Calendar(identifier: .gregorian) + utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) + let august6 = try #require(utc.date(from: DateComponents(year: 2026, month: 8, day: 6))) + let snapshot = Self.snapshot( + entries: [ + Self.entry(day: "2026-08-05", cost: 1, tokens: 10), + Self.entry(day: "2026-08-06", cost: 2, tokens: 20), + ], + historyDays: 2, + updatedAt: now) + let group = try #require(SpendDashboardModel.build( + inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], + requestedDays: 2, + now: now, + calendar: utc).groups.first) + + #expect(group.providers.first?.coveredDayCount == 1) + #expect(group.totalCost == 2) + #expect(group.totalTokens == 20) + #expect(group.dailyPoints.map(\.day) == [august6]) + #expect(group.coveredDayCount == 1) + } + + @Test + func `thirty completed days stay partial in annual view`() throws { + var utc = Calendar(identifier: .gregorian) + utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) + let snapshot = Self.snapshot( + entries: [Self.entry(day: "2026-08-06", cost: 2, tokens: 20)], + historyDays: 30, + updatedAt: now) + let group = try #require(SpendDashboardModel.build( + inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], + requestedDays: 365, + now: now, + calendar: utc).groups.first) + + #expect(group.providers.first?.coveredDayCount == 30) + #expect(group.coveredDayCount == 30) + #expect(group.totalCost == nil) + #expect(group.knownCost == 2) + #expect(group.totalTokens == nil) + #expect(group.knownTokens == 20) + } + + private static func snapshot( + entries: [CostUsageDailyReport.Entry], + historyDays: Int, + updatedAt: Date) -> CostUsageTokenSnapshot + { + CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: nil, + last30DaysCostUSD: nil, + currencyCode: "USD", + historyDays: historyDays, + daily: entries, + updatedAt: updatedAt) + } + + private static func entry(day: String, cost: Double, tokens: Int) -> CostUsageDailyReport.Entry { + CostUsageDailyReport.Entry( + date: day, + inputTokens: nil, + outputTokens: nil, + totalTokens: tokens, + costUSD: cost, + modelsUsed: nil, + modelBreakdowns: [ + .init(modelName: "fixture-model", costUSD: cost, totalTokens: tokens), + ]) + } +} From 65327296f8850f00b96ce5885431f244b716ae43 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 23:22:46 -0700 Subject: [PATCH 16/51] Fix all-provider CI regressions --- Tests/CodexBarTests/OpenRouterActivityUsageTests.swift | 3 ++- Tests/CodexBarTests/SettingsStoreTests.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift index 65316abc27..d3887061ea 100644 --- a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift +++ b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift @@ -254,8 +254,9 @@ struct OpenRouterActivityUsageTests { body: String, statusCode: Int = 200) throws -> (Data, URLResponse) { + let url = try #require(request.url) let response = try #require(try HTTPURLResponse( - url: #require(request.url), + url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "application/json"])) diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index f94c817e3b..8cbcbaa653 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -404,7 +404,7 @@ struct SettingsStoreTests { .gemini, .grok, ] - let expectedProviders: [UsageProvider] = [.opencode, .codex, .claude, .cursor, .warp, .gemini] + let expectedProviders: [UsageProvider] = [.opencode, .codex, .claude, .cursor, .warp, .gemini, .grok] #expect(storeA.mergedOverviewSelectedProviders == expectedProviders) let defaultsB = try #require(UserDefaults(suiteName: suite)) From af955f11f3f096c30fff9186a4e25699bb98ee5e Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 23:32:55 -0700 Subject: [PATCH 17/51] Stabilize OpenRouter plugin coverage --- Sources/CodexBarCore/Resources/Plugins/openrouter.js | 2 +- Tests/CodexBarTests/ShareStatsTests.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarCore/Resources/Plugins/openrouter.js b/Sources/CodexBarCore/Resources/Plugins/openrouter.js index 55604f3b75..bd46f52728 100644 --- a/Sources/CodexBarCore/Resources/Plugins/openrouter.js +++ b/Sources/CodexBarCore/Resources/Plugins/openrouter.js @@ -48,7 +48,7 @@ defineProvider({ const balance = Math.max(0, totalCredits - totalUsage); let keyData = null; try { - const keyResponse = await ctx.http.get(`${base}/key`, { timeoutSeconds: 1 }); + const keyResponse = await ctx.http.get(`${base}/key`, { timeoutSeconds: 5 }); const keyPayload = keyResponse.status === 200 ? JSON.parse(keyResponse.bodyText) : null; if (keyPayload && keyPayload.data && typeof keyPayload.data === "object" && !Array.isArray(keyPayload.data)) { diff --git a/Tests/CodexBarTests/ShareStatsTests.swift b/Tests/CodexBarTests/ShareStatsTests.swift index 59e0c4bb2c..681eb74588 100644 --- a/Tests/CodexBarTests/ShareStatsTests.swift +++ b/Tests/CodexBarTests/ShareStatsTests.swift @@ -118,7 +118,8 @@ struct ShareStatsTests { #expect(payload.totalTokens == 500) #expect(payload.totalTokensIsPartial) - #expect(payload.currencies.only?.isPartial == true) + #expect(payload.currencies.count == 1) + #expect(payload.currencies.first?.isPartial == true) #expect(payload.spendReportingProviderCount == 1) #expect(payload.providers.count == 2) } From a075e7496d7c08c6e2d154c9f2014296cd913a98 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 23:43:28 -0700 Subject: [PATCH 18/51] Fix spend dashboard test compilation --- .../CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift | 1 - Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift index 75ebc521a5..f4824358ee 100644 --- a/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift @@ -50,7 +50,6 @@ struct SpendDashboardOpenRouterDateTruthTests { #expect(group.totalCost == nil) #expect(group.knownCost == 2) #expect(group.totalTokens == nil) - #expect(group.knownTokens == 20) } private static func snapshot( diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index 62a6a20154..fb48cc2784 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -83,9 +83,10 @@ struct SpendDashboardTrackedSourceTests { #expect(Set(credentialSources.map(\.id)).count == credentialSources.count) let openRouterSources = credentialSources.filter { $0.provider == .openrouter } + let allSupportCostHistory = openRouterSources.allSatisfy(\.supportsCostHistory) #expect(openRouterSources.count == 2) #expect(openRouterSources.allSatisfy { $0.state == .configured }) - #expect(openRouterSources.allSatisfy(\.supportsCostHistory)) + #expect(allSupportCostHistory) #expect(openRouterSources.allSatisfy { !$0.contributesCostHistory }) } From 341f8e291a6b1c3e1bc5f0488bb2e6379eb85de9 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Fri, 7 Aug 2026 23:59:25 -0700 Subject: [PATCH 19/51] Correct OpenRouter completed-day fixtures --- Sources/CodexBarCore/Resources/Plugins/openrouter.js | 2 +- Tests/CodexBarTests/OpenRouterActivityUsageTests.swift | 2 +- Tests/CodexBarTests/OpenRouterUsageStatsTests.swift | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/CodexBarCore/Resources/Plugins/openrouter.js b/Sources/CodexBarCore/Resources/Plugins/openrouter.js index bd46f52728..d24f2a33a3 100644 --- a/Sources/CodexBarCore/Resources/Plugins/openrouter.js +++ b/Sources/CodexBarCore/Resources/Plugins/openrouter.js @@ -48,7 +48,7 @@ defineProvider({ const balance = Math.max(0, totalCredits - totalUsage); let keyData = null; try { - const keyResponse = await ctx.http.get(`${base}/key`, { timeoutSeconds: 5 }); + const keyResponse = await ctx.http.get(`${base}/key`, { timeoutSeconds: 2 }); const keyPayload = keyResponse.status === 200 ? JSON.parse(keyResponse.bodyText) : null; if (keyPayload && keyPayload.data && typeof keyPayload.data === "object" && !Array.isArray(keyPayload.data)) { diff --git a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift index d3887061ea..816c474361 100644 --- a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift +++ b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift @@ -201,7 +201,7 @@ struct OpenRouterActivityUsageTests { #expect(usage.detailRow(label: "Remaining")?.value == "$60.00") } - private static let now = Date(timeIntervalSince1970: 1_785_974_400) // 2026-08-07 00:00:00 UTC + private static let now = Date(timeIntervalSince1970: 1_786_060_800) // 2026-08-07 00:00:00 UTC private static let activityFixture = #""" { diff --git a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift index 1179c99123..9755de8cad 100644 --- a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift +++ b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift @@ -51,7 +51,7 @@ struct OpenRouterPluginGoldenTests { } @Test - func `requests preserve credits headers and one second enrichment deadline`() async throws { + func `requests preserve credits headers and bounded enrichment deadline`() async throws { let requests = OpenRouterRequestRecorder() let transport = Self.transport(requests: requests, keyBody: #""" {"data":{ @@ -78,7 +78,7 @@ struct OpenRouterPluginGoldenTests { #expect(recorded[0].timeoutInterval == 15) #expect(recorded[0].value(forHTTPHeaderField: "HTTP-Referer") == "https://codexbar.example") #expect(recorded[0].value(forHTTPHeaderField: "X-Title") == "CodexBar QA") - #expect(recorded[1].timeoutInterval == 1) + #expect(recorded[1].timeoutInterval == 2) #expect(recorded[1].value(forHTTPHeaderField: "HTTP-Referer") == nil) #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) #expect(recorded[2].url?.path.hasSuffix("/activity") == true) @@ -194,7 +194,7 @@ struct OpenRouterPluginGoldenTests { let transport = ProviderHTTPTransportHandler { request in if request.url?.path.hasSuffix("/key") == true { await withCheckedContinuation { continuation in - DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) { + DispatchQueue.global().asyncAfter(deadline: .now() + 2.5) { continuation.resume() } } @@ -210,7 +210,7 @@ struct OpenRouterPluginGoldenTests { let usage = try await runtime.fetchUsage(secrets: [OpenRouterSettingsReader.envKey: "fixture-key"]) - #expect(ContinuousClock.now - startedAt < .seconds(1.4)) + #expect(ContinuousClock.now - startedAt < .seconds(2.4)) #expect(usage.primary == nil) #expect(usage.detailRow(label: "API key budget")?.value == "Unavailable right now") try await Task.sleep(for: .milliseconds(600)) From 42d156517abca99d0bd8d98222e4b32d1c3ece95 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 00:20:53 -0700 Subject: [PATCH 20/51] Refresh provider selection migration gates --- Sources/CodexBar/SettingsStore+Defaults.swift | 5 +- .../ProviderArchitectureGatekeeperTests.swift | 144 ++++++++---------- 2 files changed, 66 insertions(+), 83 deletions(-) diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 01ca0d6dbf..be164768fe 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -855,7 +855,7 @@ extension SettingsStore { guard self.hasMergedOverviewSelectionPreference else { return Array(normalizedActive.prefix(maxVisibleProviders)) } - if normalizedActive.count <= maxVisibleProviders, + if normalizedActive.count <= Self.mergedOverviewLegacyWheelNavigationLimit, !self.mergedOverviewSelectionApplies(to: normalizedActive) { return normalizedActive @@ -881,7 +881,8 @@ extension SettingsStore { return [] } - let shouldPersistResolvedSelection = normalizedActive.count > maxVisibleProviders || + let shouldPersistResolvedSelection = + normalizedActive.count > Self.mergedOverviewLegacyWheelNavigationLimit || self.mergedOverviewSelectionApplies(to: normalizedActive) if self.hasMergedOverviewSelectionPreference, shouldPersistResolvedSelection { diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 6465d82532..d197ef3abd 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -896,37 +896,37 @@ struct ProviderArchitectureGatekeeperTests { reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 321, + line: 347, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 323, + line: 349, anchor: "modelProviderName: ProviderDescriptorRegistry.descriptor(for: .codex).metadata.displayName,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 378, + line: 404, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 380, + line: 406, anchor: "modelProviderName: ProviderDescriptorRegistry.descriptor(for: .codex).metadata.displayName,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 411, + line: 437, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 440, + line: 584, anchor: "let providerName = store.metadata(for: .codex).displayName", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), @@ -962,7 +962,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "The memory-pressure debug fixture installs its synthetic entry in the Codex cache slot."), SuppressedProviderReference( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1087, + line: 1136, anchor: "controller.refreshOpenMenuIfStillVisible(menu, provider: .codex)", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), @@ -1070,25 +1070,25 @@ struct ProviderArchitectureGatekeeperTests { reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 921, + line: 932, anchor: "let snapshotEmail = CodexIdentityResolver.normalizeEmail(snapshot.accountEmail(for: .codex)),", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 929, + line: 940, anchor: "let identity = snapshot.identity(for: .codex)", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 931, + line: 942, anchor: "providerID: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1462, + line: 1473, anchor: "let currentAccount = self.uniqueTokenAccount(provider: .claude, accountID: fetchedAccount.id),", expectedProviderIDs: ["claude"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), @@ -1118,19 +1118,19 @@ struct ProviderArchitectureGatekeeperTests { reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift", - line: 56, + line: 57, anchor: "providerConfigRevision: self.settings.providerConfigRevision(for: .codex),", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift", - line: 248, + line: 250, anchor: "&& self.settings.isCostUsageEffectivelyEnabled(for: .codex)", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift", - line: 249, + line: 251, anchor: "&& self.isEnabled(.codex)", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), @@ -1196,68 +1196,68 @@ struct ProviderArchitectureGatekeeperTests { reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 73, + line: 74, anchor: "allowVertexClaudeFallback: !self.isEnabled(.claude),", expectedProviderIDs: ["claude"], reason: "The local transcript scan permits Vertex fallback only when Claude is disabled to avoid " + "double-counting the same logs."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 209, + line: 219, anchor: "let scope = self.tokenCostScope(for: .codex)", expectedProviderIDs: ["codex"], reason: "The Codex-only cache hydration path passes its fixed provider identity to shared state helpers."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 211, + line: 221, anchor: "let publicationRevision = self.providerPublicationRevision(for: .codex)", expectedProviderIDs: ["codex"], reason: "The Codex-only cache hydration path passes its fixed provider identity to shared state helpers."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 212, + line: 222, anchor: "let providerConfigRevision = self.settings.providerConfigRevision(for: .codex)", expectedProviderIDs: ["codex"], reason: "The Codex-only cache hydration path passes its fixed provider identity to shared state helpers."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 214, + line: 224, anchor: "let tokenSnapshotScopeSignature = self.tokenSnapshotScopeSignature(for: .codex)", expectedProviderIDs: ["codex"], reason: "The Codex-only cache hydration path passes its fixed provider identity to shared state helpers."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 215, + line: 225, anchor: "let tokenSnapshotPublicationRevision = self.tokenSnapshotPublicationRevision(for: .codex)", expectedProviderIDs: ["codex"], reason: "The Codex-only cache hydration path passes its fixed provider identity to shared state helpers."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 244, + line: 254, anchor: "self.settings.isCostUsageEffectivelyEnabled(for: .codex),", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 245, + line: 255, anchor: "self.isEnabled(.codex),", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 254, + line: 264, anchor: "self.installCachedTokenSnapshot(result.snapshot, for: .codex)", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 335, + line: 348, anchor: "let credentialFingerprint = CookieHeaderCache.loadForDisplay(provider: .cursor)", expectedProviderIDs: ["cursor"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 348, + line: 361, anchor: "let scope = self.tokenCostScope(for: .cursor)", expectedProviderIDs: ["cursor"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), @@ -1521,7 +1521,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This tagged diagnostic payload encodes MiniMax details under the matching wire key."), SuppressedProviderReference( path: "Sources/CodexBarCore/UsageFetcher.swift", - line: 1479, + line: 1493, anchor: "providerID: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific core branch passes its already-selected identity to a shared helper."), @@ -2194,7 +2194,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesSpendDashboardPane.swift", - line: 254, + line: 270, anchor: "self.configuration.providerIDs.contains(UsageProvider.codex.rawValue)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2202,7 +2202,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesSpendDashboardPane.swift", - line: 365, + line: 395, anchor: ".count { $0.provider == .codex }", expectedProviderIDs: ["codex"], expectedReferenceCount: 3, @@ -2218,7 +2218,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared provider integration dispatches a capability owned by the provider descriptor or adapter."), AllowedProviderConstruct( path: "Sources/CodexBar/ShareStatsPayload.swift", - line: 163, + line: 185, anchor: "([\"codestral-\", \"devstral-\", \"magistral-\", \"mistral-\", \"mistral \", \"mistral.\", \"mixtral-\"], \"Mistral\"),", expectedProviderIDs: ["mistral"], expectedReferenceCount: 1, @@ -2282,7 +2282,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/SettingsStore.swift", - line: 1020, + line: 1022, anchor: "if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {", expectedProviderIDs: ["factory", "minimax", "zai"], expectedReferenceCount: 8, @@ -2299,7 +2299,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 135, + line: 156, anchor: "let codexRequests = providers.contains(.codex)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2307,7 +2307,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 184, + line: 208, anchor: "let providerBaselines = initialProviders.filter { $0 != .codex }.map { provider in", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2315,7 +2315,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 203, + line: 227, anchor: "let codexRequests = providers.contains(.codex)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2323,7 +2323,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 224, + line: 249, anchor: "for provider in providers where provider != .codex {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2331,7 +2331,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 466, + line: 610, anchor: "if providers.contains(.codex) {", expectedProviderIDs: ["codex"], expectedReferenceCount: 2, @@ -2339,7 +2339,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 521, + line: 614, anchor: "guard provider != .codex else { return nil }", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2347,7 +2347,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 1142, + line: 1286, anchor: "guard input.provider == .codex,", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2358,8 +2358,8 @@ struct ProviderArchitectureGatekeeperTests { line: 113, anchor: "guard summary.input.provider == .codex else { return false }", expectedProviderIDs: ["codex"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["codex@0"], + expectedReferenceCount: 2, + expectedReferenceFingerprint: ["codex@0", "codex@9"], reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", @@ -2379,7 +2379,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift", - line: 18, + line: 20, anchor: "let openRouterActivityUsage = provider == .openrouter", expectedProviderIDs: ["openrouter"], expectedReferenceCount: 1, @@ -2499,7 +2499,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1120, + line: 1169, anchor: "return .provider((self.resolvedMenuProvider(enabledProviders: enabledProviders) ?? .codex).instanceID)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2507,7 +2507,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1133, + line: 1182, anchor: "return self.store.enabledFirstPartyProvidersForDisplay().first ?? .codex", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2825,14 +2825,6 @@ struct ProviderArchitectureGatekeeperTests { expectedReferenceCount: 2, expectedReferenceFingerprint: ["codex@0", "codex@10"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), - AllowedProviderConstruct( - path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 705, - anchor: "let historyStable = provider == .openrouter", - expectedProviderIDs: ["openrouter"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["openrouter@0"], - reason: "This exact app-runtime bridge preserves current-scope optional provider history."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", line: 721, @@ -2862,7 +2854,7 @@ struct ProviderArchitectureGatekeeperTests { line: 854, anchor: "guard provider == .deepseek else { return snapshot }", expectedProviderIDs: ["codex", "deepseek", "openrouter"], - expectedReferenceCount: 9, + expectedReferenceCount: 7, expectedReferenceFingerprint: [ "deepseek@0", "deepseek@1", @@ -2871,13 +2863,11 @@ struct ProviderArchitectureGatekeeperTests { "codex@27", "codex@38", "codex@39", - "codex@44", - "codex@46", ], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 981, + line: 992, anchor: "guard provider == .claude, !hasSelectedTokenAccount else { return false }", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -2885,7 +2875,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1321, + line: 1332, anchor: "if provider == .gemini, Self.isGeminiConsumerTierDeprecationError(error) {", expectedProviderIDs: ["claude", "gemini"], expectedReferenceCount: 2, @@ -2893,7 +2883,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1365, + line: 1376, anchor: "if provider == .claude,", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -2901,7 +2891,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1381, + line: 1392, anchor: "if provider == .claude,", expectedProviderIDs: ["claude"], expectedReferenceCount: 5, @@ -2909,7 +2899,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1463, + line: 1474, anchor: "cached.cacheKey == self.tokenAccountSnapshotCacheKey(provider: .claude, account: currentAccount)", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -2941,7 +2931,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+SpendDashboardCodexCostCatchUp.swift", - line: 246, + line: 248, anchor: "&& self.settings.providerConfigRevision(for: .codex) == context.providerConfigRevision", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -3041,21 +3031,13 @@ struct ProviderArchitectureGatekeeperTests { path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", line: 1451, anchor: "let profileStable = provider == .deepseek", - expectedProviderIDs: ["deepseek"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["deepseek@0"], + expectedProviderIDs: ["deepseek", "openrouter"], + expectedReferenceCount: 2, + expectedReferenceFingerprint: ["deepseek@0", "openrouter@4"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", - line: 1455, - anchor: "let historyStable = provider == .openrouter", - expectedProviderIDs: ["openrouter"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["openrouter@0"], - reason: "This exact app-runtime bridge preserves current-scope optional provider history."), - AllowedProviderConstruct( - path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", - line: 1466, + line: 1471, anchor: "accountDiscriminatorOverride: provider == .claude ? warningAccountDiscriminator : nil)", expectedProviderIDs: ["claude", "deepseek"], expectedReferenceCount: 2, @@ -3063,7 +3045,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", - line: 1487, + line: 1492, anchor: "if provider == .claude,", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -3071,7 +3053,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", - line: 1512, + line: 1517, anchor: "if provider == .deepseek {", expectedProviderIDs: ["deepseek"], expectedReferenceCount: 1, @@ -3087,7 +3069,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge preserves current-scope optional provider history."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 218, + line: 228, anchor: "guard self.tokenSnapshotPublicationForCurrentProviderConfig(for: .codex) == nil else { return }", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -3095,7 +3077,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 241, + line: 251, anchor: "guard self.providerPublicationRevisionIsCurrent(publicationRevision, for: .codex),", expectedProviderIDs: ["codex"], expectedReferenceCount: 9, @@ -3113,7 +3095,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 278, + line: 288, anchor: "return provider == .codex && self.codexCostCatchUpActivity?.phase == .indexing", expectedProviderIDs: ["claude", "codex", "vertexai"], expectedReferenceCount: 4, @@ -3121,7 +3103,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 324, + line: 337, anchor: "guard provider == .cursor else {", expectedProviderIDs: ["cursor"], expectedReferenceCount: 1, @@ -3129,7 +3111,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 386, + line: 399, anchor: "if provider == .cursor,", expectedProviderIDs: ["cursor"], expectedReferenceCount: 1, @@ -3137,7 +3119,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 406, + line: 419, anchor: "guard provider == .cursor,", expectedProviderIDs: ["cursor"], expectedReferenceCount: 1, @@ -3157,11 +3139,11 @@ struct ProviderArchitectureGatekeeperTests { anchor: "case .mistral, .openai, .openrouter, .opencodego:", expectedProviderIDs: ["mistral", "openai", "openrouter", "opencodego"], expectedReferenceCount: 4, - expectedReferenceFingerprint: ["mistral@0", "openai@0", "openrouter@0", "opencodego@0"], + expectedReferenceFingerprint: ["mistral@0", "openai@0", "opencodego@0", "openrouter@0"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 483, + line: 498, anchor: "self.tokenFailureGates[.codex]?.reset()", expectedProviderIDs: ["claude", "codex"], expectedReferenceCount: 2, From 5ddd3c1baba7760caca2eb096dce8d00e6008c8f Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 00:43:32 -0700 Subject: [PATCH 21/51] Complete all-provider architecture coverage --- .../ProviderArchitectureGatekeeperTests.swift | 32 +++++++++++++++++++ Tests/CodexBarTests/SettingsStoreTests.swift | 1 + 2 files changed, 33 insertions(+) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index d197ef3abd..64d94a179b 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2224,6 +2224,14 @@ struct ProviderArchitectureGatekeeperTests { expectedReferenceCount: 1, expectedReferenceFingerprint: ["mistral@0"], reason: "This public model-family sanitizer is independent of the provider registry; Mistral is also a provider ID."), + AllowedProviderConstruct( + path: "Sources/CodexBar/ShareStatsPayload.swift", + line: 216, + anchor: "\"air\": \"Air\", \"chat\": \"Chat\", \"code\": \"Code\", \"coder\": \"Coder\", \"codex\": \"Codex\",", + expectedProviderIDs: ["codex"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["codex@0"], + reason: "This privacy allowlist recognizes Codex as a public model-name token, not as a provider dispatch."), AllowedProviderConstruct( path: "Sources/CodexBar/SessionQuotaNotifications.swift", line: 198, @@ -2329,6 +2337,14 @@ 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/CodexBar/SpendDashboardController.swift", + line: 479, + anchor: "if provider == .codex {", + expectedProviderIDs: ["codex"], + expectedReferenceCount: 3, + expectedReferenceFingerprint: ["codex@0", "codex@9", "codex@11"], + reason: "The tracked-source roster projects Codex's first-party multi-account state before generic token accounts."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", line: 610, @@ -2345,6 +2361,14 @@ 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/CodexBar/SpendDashboardController.swift", + line: 665, + anchor: "guard provider != .codex else { return nil }", + expectedProviderIDs: ["codex"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["codex@0"], + reason: "Codex ownership is represented by its dedicated account projection rather than generic provider config."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", line: 1286, @@ -2865,6 +2889,14 @@ struct ProviderArchitectureGatekeeperTests { "codex@39", ], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), + AllowedProviderConstruct( + path: "Sources/CodexBar/UsageStore+Refresh.swift", + line: 898, + anchor: "let identity = usage.identity(for: .codex)", + expectedProviderIDs: ["codex"], + expectedReferenceCount: 2, + expectedReferenceFingerprint: ["codex@0", "codex@2"], + reason: "This Codex-only identity repair preserves the expected account email on a provider-owned snapshot."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", line: 992, diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index 8cbcbaa653..487e75d79a 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -685,6 +685,7 @@ struct SettingsStoreTests { .opencode, .warp, .gemini, + .grok, ]) let resolvedWhenEmpty = store.reconcileMergedOverviewSelectedProviders(activeProviders: []) From f82fdfee8cfd5813554dbcbe968e4d178046f9bf Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 01:07:14 -0700 Subject: [PATCH 22/51] Keep bounded OpenRouter history partial --- Sources/CodexBar/SpendDashboardModel.swift | 10 +++++++--- .../ProviderArchitectureGatekeeperTests.swift | 20 +++++++++---------- ...ashboardIntegratedModelCoverageTests.swift | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index a8cac51668..401a715215 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -239,6 +239,9 @@ struct SpendDashboardModel: Equatable, Sendable { calendar: calendar) } let providers = Self.providerRows(summaries) + let coversRequestedHorizon = summaries.allSatisfy { + $0.input.provider != .openrouter || $0.input.snapshot.historyDays >= days + } let modelSummaries = summaries.filter { summary in guard summary.totalCost != nil else { return false } let summaryModelHistory = Self.modelSummary(summaries: [summary]) @@ -249,7 +252,8 @@ struct SpendDashboardModel: Equatable, Sendable { // A Codex session can have valid priced rows alongside model-less or unpriced rows. // Keep only the directly priced portion, but mark the aggregate partial and remove ranking. let modelSummary = Self.modelSummary(summaries: modelSummaries) - let modelHistoryCompleteness = modelSummaries.count == summaries.count && + let modelHistoryCompleteness = coversRequestedHorizon && + modelSummaries.count == summaries.count && modelSummary.completeness == .complete ? ModelHistoryCompleteness.complete : ModelHistoryCompleteness.incomplete @@ -259,8 +263,8 @@ struct SpendDashboardModel: Equatable, Sendable { providers: providers, models: modelSummary.rows, dailyPoints: dailyPoints, - totalTokens: Self.completeIntSum(providers.map(\.totalTokens)), - totalCost: Self.completeCostSum(providers.map(\.totalCost)), + totalTokens: coversRequestedHorizon ? Self.completeIntSum(providers.map(\.totalTokens)) : nil, + totalCost: coversRequestedHorizon ? Self.completeCostSum(providers.map(\.totalCost)) : nil, coveredDayCount: Self.commonCoverageDayCount(summaries: summaries, calendar: calendar), chartDomain: Self.chartDomain(bounds: bounds, calendar: calendar), modelHistoryCompleteness: modelHistoryCompleteness) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 64d94a179b..cc50df72e0 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2353,14 +2353,6 @@ struct ProviderArchitectureGatekeeperTests { expectedReferenceCount: 2, expectedReferenceFingerprint: ["codex@0", "codex@4"], reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), - AllowedProviderConstruct( - path: "Sources/CodexBar/SpendDashboardController.swift", - line: 614, - anchor: "guard provider != .codex else { return nil }", - expectedProviderIDs: ["codex"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["codex@0"], - reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", line: 665, @@ -2387,7 +2379,15 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 601, + line: 243, + anchor: "$0.input.provider != .openrouter || $0.input.snapshot.historyDays >= days", + expectedProviderIDs: ["openrouter"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["openrouter@0"], + reason: "OpenRouter activity has a bounded completed-day horizon, so longer requested totals remain partial."), + AllowedProviderConstruct( + path: "Sources/CodexBar/SpendDashboardModel.swift", + line: 605, anchor: "let bucketEnd = input.provider == .openrouter", expectedProviderIDs: ["openrouter"], expectedReferenceCount: 1, @@ -2395,7 +2395,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 658, + line: 662, anchor: "guard provider == .mistral || provider == .openrouter else { return displayCalendar }", expectedProviderIDs: ["mistral", "openrouter"], expectedReferenceCount: 2, diff --git a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift index 5c2b79295f..5998979283 100644 --- a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift +++ b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift @@ -109,7 +109,7 @@ struct SpendDashboardIntegratedModelCoverageTests { totalTokens: 20), ] let entry = CostUsageDailyReport.Entry( - date: "2026-08-05", + date: "2026-08-04", inputTokens: nil, outputTokens: nil, totalTokens: 30, From 99119ac5a8eed0d906ed53d2d6386ab86b3774ec Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 01:27:08 -0700 Subject: [PATCH 23/51] Expect OpenRouter tracked cost history --- Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index fb48cc2784..0774857343 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -54,7 +54,7 @@ struct SpendDashboardTrackedSourceTests { #expect(rows[.gemini]?.state == .needsAttention) #expect(rows[.openrouter]?.state == .awaitingUsage) #expect(rows[.openrouter]?.supportsCostHistory == true) - #expect(rows.values.filter(\.contributesCostHistory).map(\.provider) == [.cursor]) + #expect(Set(rows.values.filter(\.contributesCostHistory).map(\.provider)) == [.cursor, .openrouter]) } @Test From 78b57369ac9917be1fc934952a61c5d1ad698828 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:00:10 -0700 Subject: [PATCH 24/51] Fix overview spend window clock --- .../StatusItemController+OverviewSpend.swift | 6 +- .../StatusMenuOverviewSubmenuTests.swift | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/StatusItemController+OverviewSpend.swift b/Sources/CodexBar/StatusItemController+OverviewSpend.swift index 7f20a2649a..67742a4085 100644 --- a/Sources/CodexBar/StatusItemController+OverviewSpend.swift +++ b/Sources/CodexBar/StatusItemController+OverviewSpend.swift @@ -2,7 +2,10 @@ import CodexBarCore import Foundation extension StatusItemController { - func overviewSpendDashboardModel(providers: [UsageProvider]) -> SpendDashboardModel { + func overviewSpendDashboardModel( + providers: [UsageProvider], + now: Date = Date()) -> SpendDashboardModel + { let inputs = providers.compactMap { provider -> SpendDashboardModel.ProviderInput? in guard let snapshot = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot else { return nil @@ -13,7 +16,6 @@ extension StatusItemController { snapshot: snapshot) } let requestedDays = self.settings.effectiveCostUsageHistoryDays - let now = inputs.map(\.snapshot.updatedAt).max() ?? Date() return SpendDashboardModel.build( inputs: inputs, requestedDays: requestedDays, diff --git a/Tests/CodexBarTests/StatusMenuOverviewSubmenuTests.swift b/Tests/CodexBarTests/StatusMenuOverviewSubmenuTests.swift index 5c1b7af07d..e282c54bee 100644 --- a/Tests/CodexBarTests/StatusMenuOverviewSubmenuTests.swift +++ b/Tests/CodexBarTests/StatusMenuOverviewSubmenuTests.swift @@ -4,6 +4,63 @@ import Testing @testable import CodexBar extension StatusMenuTests { + @Test + func `overview spend window uses current time instead of cached snapshot time`() throws { + let settings = self.makeSettings() + settings.costUsageHistoryDays = 7 + + let fetcher = UsageFetcher() + let store = UsageStore( + fetcher: fetcher, + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings) + let cachedAt = try #require(Calendar.current.date(from: DateComponents( + year: 2025, + month: 1, + day: 7, + hour: 12))) + store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 1000, + last30DaysCostUSD: 9, + historyDays: 7, + daily: [ + CostUsageDailyReport.Entry( + date: "2025-01-07", + inputTokens: 600, + outputTokens: 400, + totalTokens: 1000, + costUSD: 9, + modelsUsed: nil, + modelBreakdowns: nil), + ], + updatedAt: cachedAt), provider: .claude) + + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let currentTime = try #require(Calendar.current.date(from: DateComponents( + year: 2025, + month: 2, + day: 7, + hour: 12))) + let model = controller.overviewSpendDashboardModel(providers: [.claude], now: currentTime) + let group = try #require(model.groups.first) + + #expect(group.providers.first?.totalCost == nil) + #expect(group.dailyPoints.isEmpty) + #expect(Calendar.current.isDate( + group.chartDomain.upperBound, + inSameDayAs: currentTime.addingTimeInterval(86400))) + } + @Test func `overview rows expose provider detail submenus`() throws { self.disableMenuCardsForTesting() From 38e0f1621807b338cf099cd6eea76bca5ed061c1 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:13:53 -0700 Subject: [PATCH 25/51] Keep partial spend coverage truthful --- .../PreferencesSpendDashboardPane.swift | 51 ++++++++-- .../CodexBar/SpendDashboardController.swift | 32 ++++++- Sources/CodexBar/SpendDashboardModel.swift | 4 +- .../PreferencesSpendDashboardShareTests.swift | 92 +++++++++++++++++++ .../SpendDashboardCoverageTruthTests.swift | 90 ++++++++++++++++++ ...endDashboardOpenRouterDateTruthTests.swift | 5 +- .../SpendDashboardTrackedSourceTests.swift | 50 +++++++++- 7 files changed, 305 insertions(+), 19 deletions(-) create mode 100644 Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift create mode 100644 Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 1fd0b74fee..b8b6fe3af6 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -59,7 +59,7 @@ func spendDashboardTrackedSourceStatusText(_ source: SpendDashboardTrackedSource break } if source.contributesCostHistory { - return source.state == .connected + return source.costHistoryAvailable ? L("Cost history connected") : L("Cost history pending") } @@ -79,6 +79,16 @@ func spendDashboardCostCoverageText(_ group: SpendDashboardModel.CurrencyGroup) "\(codexBarLocalizedInteger(group.providers.count)) \(L("Accounts"))" } +func spendDashboardProviderCostText( + _ row: SpendDashboardModel.ProviderRow, + currencyCode: String, + requestedDays: Int) -> String +{ + guard let cost = row.totalCost else { return L("Spend unavailable") } + let formatted = UsageFormatter.currencyString(cost, currencyCode: currencyCode) + return row.coveredDayCount < requestedDays ? "~\(formatted)" : formatted +} + enum SpendDashboardModelHistoryPresentation: Equatable { case unavailable case empty @@ -383,9 +393,30 @@ struct SpendDashboardPane: View { } private var sharePayload: ShareStatsPayload? { - ShareStatsBuilder.make( + Self.makeSharePayload( model: self.controller.model, - subscriptionNames: self.subscriptionNames) + subscriptionNames: self.subscriptionNames, + trackedSources: self.configuration.trackedSources) + } + + static func makeSharePayload( + model: SpendDashboardModel, + subscriptionNames: [String: ShareStatsSubscriptionName], + trackedSources: [SpendDashboardTrackedSource]) -> ShareStatsPayload? + { + let fallbackCurrencyCode = model.groups.first?.currencyCode ?? "USD" + var seenProviders: Set = [] + let roster = trackedSources.compactMap { source -> ShareStatsProviderRosterEntry? in + guard seenProviders.insert(source.provider).inserted else { return nil } + return ShareStatsProviderRosterEntry( + provider: source.provider, + providerName: source.providerName, + currencyCode: fallbackCurrencyCode) + } + return ShareStatsBuilder.make( + model: model, + subscriptionNames: subscriptionNames, + providerRoster: roster) } private var subscriptionNames: [String: ShareStatsSubscriptionName] { @@ -585,7 +616,7 @@ private struct SpendTrackedSourceRow: View { break } if self.source.contributesCostHistory { - return self.source.state == .connected ? "checkmark.circle.fill" : "clock.fill" + return self.source.costHistoryAvailable ? "checkmark.circle.fill" : "clock.fill" } return self.source.state == .connected ? "minus.circle.fill" : "minus.circle" } @@ -595,7 +626,7 @@ private struct SpendTrackedSourceRow: View { return .orange } if self.source.contributesCostHistory { - return self.source.state == .connected ? .green : .orange + return self.source.costHistoryAvailable ? .green : .orange } return .secondary } @@ -625,7 +656,7 @@ private struct SpendCurrencySection: View { VStack(alignment: .leading, spacing: 12) { SpendCurrencySummaryView(group: self.group, requestedDays: self.requestedDays) - SpendProviderPanel(group: self.group) + SpendProviderPanel(group: self.group, requestedDays: self.requestedDays) SpendModelPanel(group: self.group) SpendDailyChart(group: self.group) } @@ -694,6 +725,7 @@ private struct SpendSummaryValue: View { private struct SpendProviderPanel: View { let group: SpendDashboardModel.CurrencyGroup + let requestedDays: Int var body: some View { SpendDashboardPanel { @@ -711,9 +743,10 @@ private struct SpendProviderPanel: View { SpendProviderIcon(provider: row.provider) Text(row.displayName).lineLimit(1) Spacer() - Text(row.totalCost.map { - UsageFormatter.currencyString($0, currencyCode: self.group.currencyCode) - } ?? L("Spend unavailable")) + Text(spendDashboardProviderCostText( + row, + currencyCode: self.group.currencyCode, + requestedDays: self.requestedDays)) .foregroundStyle(row.totalCost == nil ? .secondary : .primary) .monospacedDigit() } diff --git a/Sources/CodexBar/SpendDashboardController.swift b/Sources/CodexBar/SpendDashboardController.swift index c7e234cab9..d83be538e7 100644 --- a/Sources/CodexBar/SpendDashboardController.swift +++ b/Sources/CodexBar/SpendDashboardController.swift @@ -18,6 +18,27 @@ struct SpendDashboardTrackedSource: Identifiable, Equatable, Sendable { let state: State let supportsCostHistory: Bool let contributesCostHistory: Bool + let costHistoryAvailable: Bool + + init( + id: String, + provider: UsageProvider, + providerName: String, + accountName: String?, + state: State, + supportsCostHistory: Bool, + contributesCostHistory: Bool, + costHistoryAvailable: Bool = false) + { + self.id = id + self.provider = provider + self.providerName = providerName + self.accountName = accountName + self.state = state + self.supportsCostHistory = supportsCostHistory + self.contributesCostHistory = contributesCostHistory + self.costHistoryAvailable = costHistoryAvailable + } } struct SpendDashboardConfiguration: Equatable, Sendable { @@ -476,6 +497,8 @@ enum SpendDashboardSource { let providerName = store.metadata(for: provider).displayName let supportsCostHistory = ProviderDescriptorRegistry.descriptor(for: provider) .tokenCost.supportsTokenCost + let currentCostHistoryAvailable = store + .tokenSnapshotPublicationForCurrentProviderConfig(for: provider)?.snapshot != nil if provider == .codex { let accounts = settings.codexVisibleAccountProjection.visibleAccounts let snapshotsByID = Dictionary(uniqueKeysWithValues: store.codexAccountSnapshots.map { @@ -498,7 +521,8 @@ enum SpendDashboardSource { hasConfiguredCredential: true, hasError: hasError), supportsCostHistory: supportsCostHistory, - contributesCostHistory: supportsCostHistory && enabled.contains(provider)) + contributesCostHistory: supportsCostHistory && enabled.contains(provider), + costHistoryAvailable: account.isActive && currentCostHistoryAvailable) }) continue } @@ -528,7 +552,8 @@ enum SpendDashboardSource { hasConfiguredCredential: true, hasError: hasError), supportsCostHistory: supportsCostHistory, - contributesCostHistory: supportsCostHistory && isActive && enabled.contains(provider)) + contributesCostHistory: supportsCostHistory && isActive && enabled.contains(provider), + costHistoryAvailable: isActive && currentCostHistoryAvailable) }) continue } @@ -551,7 +576,8 @@ enum SpendDashboardSource { hasConfiguredCredential: hasConfiguredCredential, hasError: hasError), supportsCostHistory: supportsCostHistory, - contributesCostHistory: supportsCostHistory && enabled.contains(provider))) + contributesCostHistory: supportsCostHistory && enabled.contains(provider), + costHistoryAvailable: currentCostHistoryAvailable)) } return sources diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index 401a715215..9fce6d8dca 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -239,9 +239,7 @@ struct SpendDashboardModel: Equatable, Sendable { calendar: calendar) } let providers = Self.providerRows(summaries) - let coversRequestedHorizon = summaries.allSatisfy { - $0.input.provider != .openrouter || $0.input.snapshot.historyDays >= days - } + let coversRequestedHorizon = summaries.allSatisfy { $0.coveredDayCount >= days } let modelSummaries = summaries.filter { summary in guard summary.totalCost != nil else { return false } let summaryModelHistory = Self.modelSummary(summaries: [summary]) diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift new file mode 100644 index 0000000000..41882cffa3 --- /dev/null +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -0,0 +1,92 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct PreferencesSpendDashboardShareTests { + @Test + func `settings share includes every tracked provider once even without cost history`() throws { + let date = Date(timeIntervalSince1970: 1_785_974_400) + let model = SpendDashboardModel(requestedDays: 30, groups: [ + SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + SpendDashboardModel.ProviderRow( + id: "codex:personal", + rank: 1, + provider: .codex, + displayName: "Codex · Personal", + totalTokens: 200, + totalCost: 4, + coveredDayCount: 30), + ], + models: [], + dailyPoints: [], + totalTokens: 200, + totalCost: 4, + coveredDayCount: 30, + chartDomain: date...date, + modelHistoryCompleteness: .complete), + ]) + let trackedSources = [ + Self.source( + id: "codex:personal", + provider: .codex, + providerName: "Codex", + accountName: "Personal", + state: .connected, + contributesCostHistory: true), + Self.source( + id: "codex:work", + provider: .codex, + providerName: "Codex", + accountName: "Work", + state: .configured, + contributesCostHistory: true), + Self.source( + id: "openrouter:current", + provider: .openrouter, + providerName: "OpenRouter", + accountName: nil, + state: .awaitingUsage, + contributesCostHistory: true), + Self.source( + id: "gemini:current", + provider: .gemini, + providerName: "Gemini", + accountName: nil, + state: .needsAttention, + contributesCostHistory: false), + ] + + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: trackedSources)) + + #expect(payload.providers.map(\.provider) == [.codex, .openrouter, .gemini]) + #expect(payload.providers.map(\.estimatedCost) == [4, nil, nil]) + #expect(payload.spendReportingProviderCount == 1) + #expect(payload.currencies.allSatisfy(\.isPartial)) + #expect(payload.totalTokensIsPartial) + #expect(ShareStatsFormatting.text(payload).contains("1/3 connected services report spend")) + } + + private static func source( + id: String, + provider: UsageProvider, + providerName: String, + accountName: String?, + state: SpendDashboardTrackedSource.State, + contributesCostHistory: Bool) -> SpendDashboardTrackedSource + { + SpendDashboardTrackedSource( + id: id, + provider: provider, + providerName: providerName, + accountName: accountName, + state: state, + supportsCostHistory: contributesCostHistory, + contributesCostHistory: contributesCostHistory) + } +} diff --git a/Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift b/Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift new file mode 100644 index 0000000000..8d181af39b --- /dev/null +++ b/Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift @@ -0,0 +1,90 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct SpendDashboardCoverageTruthTests { + @Test + func `short non OpenRouter history stays a qualified lower bound in a longer window`() throws { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) + let snapshot = CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 20, + last30DaysCostUSD: 2, + currencyCode: "USD", + historyDays: 30, + daily: [ + CostUsageDailyReport.Entry( + date: "2026-08-07", + inputTokens: 10, + outputTokens: 10, + totalTokens: 20, + costUSD: 2, + modelsUsed: ["claude-sonnet-4"], + modelBreakdowns: [ + .init(modelName: "claude-sonnet-4", costUSD: 2, totalTokens: 20), + ]), + ], + updatedAt: now) + let group = try #require(SpendDashboardModel.build( + inputs: [.init(provider: .claude, displayName: "Claude", snapshot: snapshot)], + requestedDays: 365, + now: now, + calendar: calendar).groups.first) + let row = try #require(group.providers.first) + + #expect(row.coveredDayCount == 30) + #expect(row.totalCost == 2) + #expect(group.totalCost == nil) + #expect(group.knownCost == 2) + #expect(group.totalTokens == nil) + #expect(group.modelHistoryCompleteness == .incomplete) + #expect(spendDashboardProviderCostText( + row, + currencyCode: group.currencyCode, + requestedDays: 365) == "~$2.00") + } + + @Test + func `full requested history keeps provider and aggregate amounts exact`() throws { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) + let snapshot = CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 20, + last30DaysCostUSD: 2, + currencyCode: "USD", + historyDays: 30, + daily: [ + CostUsageDailyReport.Entry( + date: "2026-08-07", + inputTokens: 10, + outputTokens: 10, + totalTokens: 20, + costUSD: 2, + modelsUsed: ["claude-sonnet-4"], + modelBreakdowns: [ + .init(modelName: "claude-sonnet-4", costUSD: 2, totalTokens: 20), + ]), + ], + updatedAt: now) + let group = try #require(SpendDashboardModel.build( + inputs: [.init(provider: .claude, displayName: "Claude", snapshot: snapshot)], + requestedDays: 30, + now: now, + calendar: calendar).groups.first) + let row = try #require(group.providers.first) + + #expect(group.totalCost == 2) + #expect(group.totalTokens == 20) + #expect(spendDashboardProviderCostText( + row, + currencyCode: group.currencyCode, + requestedDays: 30) == "$2.00") + } +} diff --git a/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift index f4824358ee..e10560dd39 100644 --- a/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift @@ -24,8 +24,9 @@ struct SpendDashboardOpenRouterDateTruthTests { calendar: utc).groups.first) #expect(group.providers.first?.coveredDayCount == 1) - #expect(group.totalCost == 2) - #expect(group.totalTokens == 20) + #expect(group.totalCost == nil) + #expect(group.knownCost == 2) + #expect(group.totalTokens == nil) #expect(group.dailyPoints.map(\.day) == [august6]) #expect(group.coveredDayCount == 1) } diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index 0774857343..993c17627a 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -57,6 +57,50 @@ struct SpendDashboardTrackedSourceTests { #expect(Set(rows.values.filter(\.contributesCostHistory).map(\.provider)) == [.cursor, .openrouter]) } + @Test + @MainActor + func `quota usage does not claim cost history is connected`() throws { + let settings = testSettingsStore(suiteName: "SpendDashboardTrackedSourceTests-quota-only") + let metadata = try #require(ProviderRegistry.shared.metadata[.openrouter]) + try settings.setProviderEnabled(provider: .openrouter, metadata: metadata, enabled: true) + settings[providerConfig: .openrouter, field: .apiKey] = "fixture-key" + + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + store._setSnapshotForTesting( + UsageSnapshot( + primary: RateWindow( + usedPercent: 25, + windowMinutes: nil, + resetsAt: nil, + resetDescription: nil), + secondary: nil, + updatedAt: Date()), + provider: .openrouter) + + let source = try #require(SpendDashboardSource.trackedSources(settings: settings, store: store).first { + $0.provider == .openrouter + }) + + #expect(spendDashboardTrackedSourceStatusText(source) == "Cost history pending") + + store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 100, + last30DaysCostUSD: 1, + daily: [], + updatedAt: Date()), provider: .openrouter) + let sourceWithCost = try #require(SpendDashboardSource.trackedSources( + settings: settings, + store: store).first { $0.provider == .openrouter }) + #expect(spendDashboardTrackedSourceStatusText(sourceWithCost) == "Cost history connected") + } + @Test @MainActor func `tracked access includes every saved provider credential without inventing cost coverage`() { @@ -174,7 +218,8 @@ struct SpendDashboardTrackedSourceTests { accountName: "Personal", state: .connected, supportsCostHistory: true, - contributesCostHistory: true), + contributesCostHistory: true, + costHistoryAvailable: true), SpendDashboardTrackedSource( id: "claude:account:team", provider: .claude, @@ -182,7 +227,8 @@ struct SpendDashboardTrackedSourceTests { accountName: "Team", state: .connected, supportsCostHistory: true, - contributesCostHistory: true), + contributesCostHistory: true, + costHistoryAvailable: true), SpendDashboardTrackedSource( id: "openrouter:account:research", provider: .openrouter, From c013481e5b30766e84812131ad1c9622ebbb7b95 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:16:24 -0700 Subject: [PATCH 26/51] Satisfy share test lint --- .../PreferencesSpendDashboardShareTests.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift index 41882cffa3..ce19c8b0cb 100644 --- a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -33,28 +33,24 @@ struct PreferencesSpendDashboardShareTests { id: "codex:personal", provider: .codex, providerName: "Codex", - accountName: "Personal", state: .connected, contributesCostHistory: true), Self.source( id: "codex:work", provider: .codex, providerName: "Codex", - accountName: "Work", state: .configured, contributesCostHistory: true), Self.source( id: "openrouter:current", provider: .openrouter, providerName: "OpenRouter", - accountName: nil, state: .awaitingUsage, contributesCostHistory: true), Self.source( id: "gemini:current", provider: .gemini, providerName: "Gemini", - accountName: nil, state: .needsAttention, contributesCostHistory: false), ] @@ -76,7 +72,6 @@ struct PreferencesSpendDashboardShareTests { id: String, provider: UsageProvider, providerName: String, - accountName: String?, state: SpendDashboardTrackedSource.State, contributesCostHistory: Bool) -> SpendDashboardTrackedSource { @@ -84,7 +79,7 @@ struct PreferencesSpendDashboardShareTests { id: id, provider: provider, providerName: providerName, - accountName: accountName, + accountName: nil, state: state, supportsCostHistory: contributesCostHistory, contributesCostHistory: contributesCostHistory) From e6ceba494a63ab750463956d4f039443ee7c9867 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:23:38 -0700 Subject: [PATCH 27/51] Harden multi-account spend sharing --- .../PreferencesSpendDashboardPane.swift | 25 +++- Sources/CodexBar/ShareStatsPayload.swift | 72 ++++++++--- .../CodexBar/SpendDashboardController.swift | 15 ++- .../PreferencesSpendDashboardShareTests.swift | 120 +++++++++++++++++- .../ProviderArchitectureGatekeeperTests.swift | 48 +++---- .../SpendDashboardTrackedSourceTests.swift | 53 ++++++++ 6 files changed, 282 insertions(+), 51 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index b8b6fe3af6..c0be96c014 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -68,6 +68,19 @@ func spendDashboardTrackedSourceStatusText(_ source: SpendDashboardTrackedSource : L("Configured · not in cost total") } +func spendDashboardTrackedSourcesForPresentation( + _ sources: [SpendDashboardTrackedSource], + model: SpendDashboardModel) -> [SpendDashboardTrackedSource] +{ + let costedSourceIDs = Set(model.groups.flatMap(\.providers).compactMap { row in + row.totalCost == nil ? nil : row.id + }) + return sources.map { source in + source.withCostHistoryAvailable( + source.costHistoryAvailable || costedSourceIDs.contains(source.id)) + } +} + func spendDashboardAggregateCostText(_ group: SpendDashboardModel.CurrencyGroup) -> String { guard let cost = group.totalCost ?? group.knownCost else { return L("Spend unavailable") } let formatted = UsageFormatter.currencyString(cost, currencyCode: group.currencyCode) @@ -367,7 +380,9 @@ struct SpendDashboardPane: View { @ViewBuilder private var trackedAccess: some View { - let sources = self.configuration.trackedSources + let sources = spendDashboardTrackedSourcesForPresentation( + self.configuration.trackedSources, + model: self.controller.model) if !sources.isEmpty { SpendTrackedAccessPanel( sources: sources, @@ -405,13 +420,17 @@ struct SpendDashboardPane: View { trackedSources: [SpendDashboardTrackedSource]) -> ShareStatsPayload? { let fallbackCurrencyCode = model.groups.first?.currencyCode ?? "USD" + let sourcesByProvider = Dictionary(grouping: trackedSources, by: \.provider) var seenProviders: Set = [] let roster = trackedSources.compactMap { source -> ShareStatsProviderRosterEntry? in - guard seenProviders.insert(source.provider).inserted else { return nil } + guard seenProviders.insert(source.provider).inserted, + let sources = sourcesByProvider[source.provider] + else { return nil } return ShareStatsProviderRosterEntry( provider: source.provider, providerName: source.providerName, - currencyCode: fallbackCurrencyCode) + currencyCode: fallbackCurrencyCode, + expectedSourceCount: sources.count) } return ShareStatsBuilder.make( model: model, diff --git a/Sources/CodexBar/ShareStatsPayload.swift b/Sources/CodexBar/ShareStatsPayload.swift index 18f039ea11..9a505ebb7d 100644 --- a/Sources/CodexBar/ShareStatsPayload.swift +++ b/Sources/CodexBar/ShareStatsPayload.swift @@ -15,6 +15,19 @@ struct ShareStatsProviderRosterEntry: Sendable, Equatable { let provider: UsageProvider let providerName: String let currencyCode: String + let expectedSourceCount: Int + + init( + provider: UsageProvider, + providerName: String, + currencyCode: String, + expectedSourceCount: Int = 1) + { + self.provider = provider + self.providerName = providerName + self.currencyCode = currencyCode + self.expectedSourceCount = max(1, expectedSourceCount) + } } struct ShareStatsModelPayload: Sendable, Equatable { @@ -323,20 +336,33 @@ enum ShareStatsBuilder { if providerRoster.isEmpty { providers = trackedProviders } else { - let trackedByProvider = trackedProviders.reduce(into: [UsageProvider: ShareStatsProviderPayload]()) { - if $0[$1.provider] == nil { - $0[$1.provider] = $1 + let trackedByProvider = Dictionary(grouping: trackedProviders, by: \.provider) + var emittedProviders: Set = [] + providers = providerRoster.compactMap { entry in + guard emittedProviders.insert(entry.provider).inserted else { return nil } + guard let matches = trackedByProvider[entry.provider], !matches.isEmpty else { + return ShareStatsProviderPayload( + provider: entry.provider, + providerName: entry.providerName, + subscriptionName: nil, + currencyCode: entry.currencyCode, + totalTokens: nil, + estimatedCost: nil, + coveredDayCount: 0) } - } - providers = providerRoster.map { entry in - trackedByProvider[entry.provider] ?? ShareStatsProviderPayload( + let knownCosts = matches.compactMap(\.estimatedCost) + let currencyCodes = Set(matches.map(\.currencyCode)) + let coveredDayCount = matches.filter { $0.estimatedCost != nil || $0.totalTokens != nil } + .map(\.coveredDayCount) + .min() ?? 0 + return ShareStatsProviderPayload( provider: entry.provider, providerName: entry.providerName, - subscriptionName: nil, - currencyCode: entry.currencyCode, - totalTokens: nil, - estimatedCost: nil, - coveredDayCount: 0) + subscriptionName: matches.count == 1 ? matches[0].subscriptionName : nil, + currencyCode: matches[0].currencyCode, + totalTokens: self.combinedTotalTokens(matches.map(\.totalTokens)), + estimatedCost: currencyCodes.count == 1 ? self.safeCostSum(knownCosts) : nil, + coveredDayCount: coveredDayCount) } } let sanitizedModels = model.groups.filter { @@ -390,21 +416,25 @@ enum ShareStatsBuilder { return lhs.modelIdentity < rhs.modelIdentity } } - let trackedProviderKinds = Set(trackedProviders.map(\.provider)) - let rosterHasUntrackedProviders = providerRoster.contains { - !trackedProviderKinds.contains($0.provider) + let rosterHasIncompleteProviders = providerRoster.contains { entry in + let matches = trackedProviders.filter { $0.provider == entry.provider } + let knownCosts = matches.compactMap(\.estimatedCost) + return matches.count < entry.expectedSourceCount || + matches.contains { $0.totalTokens == nil || $0.estimatedCost == nil } || + Set(matches.map(\.currencyCode)).count > 1 || + self.safeCostSum(knownCosts) == nil } let currencies = model.groups.map { ShareStatsCurrencyPayload( currencyCode: $0.currencyCode, estimatedCost: self.finiteCost($0.totalCost ?? $0.knownCost), coveredDayCount: $0.coveredDayCount, - isPartial: rosterHasUntrackedProviders || $0.totalCost == nil) + isPartial: rosterHasIncompleteProviders || $0.totalCost == nil) } let knownTokenValues = trackedProviders.compactMap(\.totalTokens) let totalTokens = self.safeTokenSum(knownTokenValues) let totalTokensIsPartial = trackedProviders.contains { $0.totalTokens == nil } || - rosterHasUntrackedProviders || + rosterHasIncompleteProviders || model.groups.contains { $0.totalTokens == nil } let periodEnd = model.groups.map(\.chartDomain.upperBound).max() ?? Date() let payload = ShareStatsPayload( @@ -443,6 +473,16 @@ enum ShareStatsBuilder { } return values.isEmpty ? nil : total } + + private static func safeCostSum(_ values: [Double]) -> Double? { + guard !values.isEmpty else { return nil } + var total = 0.0 + for value in values { + total += value + guard total.isFinite else { return nil } + } + return total + } } enum ShareStatsFormatting { diff --git a/Sources/CodexBar/SpendDashboardController.swift b/Sources/CodexBar/SpendDashboardController.swift index d83be538e7..77ca72442e 100644 --- a/Sources/CodexBar/SpendDashboardController.swift +++ b/Sources/CodexBar/SpendDashboardController.swift @@ -39,6 +39,18 @@ struct SpendDashboardTrackedSource: Identifiable, Equatable, Sendable { self.contributesCostHistory = contributesCostHistory self.costHistoryAvailable = costHistoryAvailable } + + func withCostHistoryAvailable(_ available: Bool) -> Self { + Self( + id: self.id, + provider: self.provider, + providerName: self.providerName, + accountName: self.accountName, + state: self.state, + supportsCostHistory: self.supportsCostHistory, + contributesCostHistory: self.contributesCostHistory, + costHistoryAvailable: available) + } } struct SpendDashboardConfiguration: Equatable, Sendable { @@ -498,7 +510,8 @@ enum SpendDashboardSource { let supportsCostHistory = ProviderDescriptorRegistry.descriptor(for: provider) .tokenCost.supportsTokenCost let currentCostHistoryAvailable = store - .tokenSnapshotPublicationForCurrentProviderConfig(for: provider)?.snapshot != nil + .tokenSnapshotPublicationForCurrentProviderConfig(for: provider)? + .snapshot?.historyCoverageIsEstablished == true if provider == .codex { let accounts = settings.codexVisibleAccountProjection.visibleAccounts let snapshotsByID = Dictionary(uniqueKeysWithValues: store.codexAccountSnapshots.map { diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift index ce19c8b0cb..0acd9dfdf9 100644 --- a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -19,11 +19,19 @@ struct PreferencesSpendDashboardShareTests { totalTokens: 200, totalCost: 4, coveredDayCount: 30), + SpendDashboardModel.ProviderRow( + id: "codex:work", + rank: 2, + provider: .codex, + displayName: "Codex · Work", + totalTokens: 100, + totalCost: 3, + coveredDayCount: 30), ], models: [], dailyPoints: [], - totalTokens: 200, - totalCost: 4, + totalTokens: 300, + totalCost: 7, coveredDayCount: 30, chartDomain: date...date, modelHistoryCompleteness: .complete), @@ -61,13 +69,91 @@ struct PreferencesSpendDashboardShareTests { trackedSources: trackedSources)) #expect(payload.providers.map(\.provider) == [.codex, .openrouter, .gemini]) - #expect(payload.providers.map(\.estimatedCost) == [4, nil, nil]) + #expect(payload.providers.map(\.estimatedCost) == [7, nil, nil]) #expect(payload.spendReportingProviderCount == 1) #expect(payload.currencies.allSatisfy(\.isPartial)) #expect(payload.totalTokensIsPartial) #expect(ShareStatsFormatting.text(payload).contains("1/3 connected services report spend")) } + @Test + func `missing tracked account keeps settings share partial`() throws { + let model = SpendDashboardModel(requestedDays: 30, groups: [ + Self.group(currencyCode: "USD", providers: [Self.row(id: "codex:personal", tokens: 200, cost: 4)]), + ]) + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: [ + Self.source( + id: "codex:personal", + provider: .codex, + providerName: "Codex", + state: .connected, + contributesCostHistory: true), + Self.source( + id: "codex:work", + provider: .codex, + providerName: "Codex", + state: .configured, + contributesCostHistory: true), + ])) + + #expect(payload.providers.first?.estimatedCost == 4) + #expect(payload.currencies.allSatisfy(\.isPartial)) + #expect(payload.totalTokensIsPartial) + } + + @Test + func `provider accounts in unlike currencies never merge spend`() throws { + let model = SpendDashboardModel(requestedDays: 30, groups: [ + Self.group(currencyCode: "USD", providers: [Self.row(id: "codex:personal", tokens: 200, cost: 4)]), + Self.group(currencyCode: "EUR", providers: [Self.row(id: "codex:work", tokens: 100, cost: 3)]), + ]) + let trackedSources = ["codex:personal", "codex:work"].map { + Self.source( + id: $0, + provider: .codex, + providerName: "Codex", + state: .connected, + contributesCostHistory: true) + } + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: trackedSources)) + + #expect(payload.providers.first?.estimatedCost == nil) + #expect(payload.currencies.allSatisfy(\.isPartial)) + } + + @Test + func `provider family overflow fails closed`() throws { + let model = SpendDashboardModel(requestedDays: 30, groups: [ + Self.group(currencyCode: "USD", providers: [ + Self.row(id: "codex:personal", tokens: Int.max, cost: Double.greatestFiniteMagnitude), + Self.row(id: "codex:work", tokens: 1, cost: Double.greatestFiniteMagnitude), + ]), + ]) + let trackedSources = ["codex:personal", "codex:work"].map { + Self.source( + id: $0, + provider: .codex, + providerName: "Codex", + state: .connected, + contributesCostHistory: true) + } + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: trackedSources)) + + #expect(payload.providers.first?.totalTokens == nil) + #expect(payload.providers.first?.estimatedCost == nil) + #expect(payload.currencies.allSatisfy(\.isPartial)) + #expect(payload.totalTokensIsPartial) + } + private static func source( id: String, provider: UsageProvider, @@ -84,4 +170,32 @@ struct PreferencesSpendDashboardShareTests { supportsCostHistory: contributesCostHistory, contributesCostHistory: contributesCostHistory) } + + private static func row(id: String, tokens: Int, cost: Double) -> SpendDashboardModel.ProviderRow { + SpendDashboardModel.ProviderRow( + id: id, + rank: 1, + provider: .codex, + displayName: "Codex", + totalTokens: tokens, + totalCost: cost, + coveredDayCount: 30) + } + + private static func group( + currencyCode: String, + providers: [SpendDashboardModel.ProviderRow]) -> SpendDashboardModel.CurrencyGroup + { + let date = Date(timeIntervalSince1970: 1_785_974_400) + return SpendDashboardModel.CurrencyGroup( + currencyCode: currencyCode, + providers: providers, + models: [], + dailyPoints: [], + totalTokens: nil, + totalCost: nil, + coveredDayCount: 30, + chartDomain: date...date, + modelHistoryCompleteness: .incomplete) + } } diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 820359b61d..09fadb9372 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -896,37 +896,37 @@ struct ProviderArchitectureGatekeeperTests { reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 347, + line: 380, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 349, + line: 382, anchor: "modelProviderName: ProviderDescriptorRegistry.descriptor(for: .codex).metadata.displayName,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 404, + line: 437, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 406, + line: 439, anchor: "modelProviderName: ProviderDescriptorRegistry.descriptor(for: .codex).metadata.displayName,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 437, + line: 470, anchor: "provider: .codex,", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 584, + line: 623, anchor: "let providerName = store.metadata(for: .codex).displayName", expectedProviderIDs: ["codex"], reason: "This Codex account projection passes its fixed provider identity to shared spend infrastructure."), @@ -2194,7 +2194,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesSpendDashboardPane.swift", - line: 270, + line: 293, anchor: "self.configuration.providerIDs.contains(UsageProvider.codex.rawValue)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2202,7 +2202,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesSpendDashboardPane.swift", - line: 395, + line: 445, anchor: ".count { $0.provider == .codex }", expectedProviderIDs: ["codex"], expectedReferenceCount: 3, @@ -2218,7 +2218,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared provider integration dispatches a capability owned by the provider descriptor or adapter."), AllowedProviderConstruct( path: "Sources/CodexBar/ShareStatsPayload.swift", - line: 185, + line: 198, anchor: "([\"codestral-\", \"devstral-\", \"magistral-\", \"mistral-\", \"mistral \", \"mistral.\", \"mixtral-\"], \"Mistral\"),", expectedProviderIDs: ["mistral"], expectedReferenceCount: 1, @@ -2226,7 +2226,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This public model-family sanitizer is independent of the provider registry; Mistral is also a provider ID."), AllowedProviderConstruct( path: "Sources/CodexBar/ShareStatsPayload.swift", - line: 216, + line: 229, anchor: "\"air\": \"Air\", \"chat\": \"Chat\", \"code\": \"Code\", \"coder\": \"Coder\", \"codex\": \"Codex\",", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2307,7 +2307,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 156, + line: 189, anchor: "let codexRequests = providers.contains(.codex)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2315,7 +2315,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 208, + line: 241, anchor: "let providerBaselines = initialProviders.filter { $0 != .codex }.map { provider in", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2323,7 +2323,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 227, + line: 260, anchor: "let codexRequests = providers.contains(.codex)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2331,7 +2331,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 249, + line: 282, anchor: "for provider in providers where provider != .codex {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2339,7 +2339,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 479, + line: 515, anchor: "if provider == .codex {", expectedProviderIDs: ["codex"], expectedReferenceCount: 3, @@ -2347,7 +2347,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "The tracked-source roster projects Codex's first-party multi-account state before generic token accounts."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 610, + line: 649, anchor: "if providers.contains(.codex) {", expectedProviderIDs: ["codex"], expectedReferenceCount: 2, @@ -2355,7 +2355,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 665, + line: 704, anchor: "guard provider != .codex else { return nil }", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2363,7 +2363,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "Codex ownership is represented by its dedicated account projection rather than generic provider config."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardController.swift", - line: 1286, + line: 1325, anchor: "guard input.provider == .codex,", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2379,15 +2379,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 243, - anchor: "$0.input.provider != .openrouter || $0.input.snapshot.historyDays >= days", - expectedProviderIDs: ["openrouter"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["openrouter@0"], - reason: "OpenRouter activity has a bounded completed-day horizon, so longer requested totals remain partial."), - AllowedProviderConstruct( - path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 605, + line: 603, anchor: "let bucketEnd = input.provider == .openrouter", expectedProviderIDs: ["openrouter"], expectedReferenceCount: 1, @@ -2395,7 +2387,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 662, + line: 660, anchor: "guard provider == .mistral || provider == .openrouter else { return displayCalendar }", expectedProviderIDs: ["mistral", "openrouter"], expectedReferenceCount: 2, diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index 993c17627a..833ad71b43 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -88,6 +88,19 @@ struct SpendDashboardTrackedSourceTests { #expect(spendDashboardTrackedSourceStatusText(source) == "Cost history pending") + store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: nil, + last30DaysCostUSD: nil, + historyCoverageIsEstablished: false, + daily: [], + updatedAt: Date()), provider: .openrouter) + let sourceWithUnestablishedCost = try #require(SpendDashboardSource.trackedSources( + settings: settings, + store: store).first { $0.provider == .openrouter }) + #expect(spendDashboardTrackedSourceStatusText(sourceWithUnestablishedCost) == "Cost history pending") + store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( sessionTokens: nil, sessionCostUSD: nil, @@ -101,6 +114,46 @@ struct SpendDashboardTrackedSourceTests { #expect(spendDashboardTrackedSourceStatusText(sourceWithCost) == "Cost history connected") } + @Test + func `inactive Codex account with loaded spend presents cost history as connected`() throws { + let source = SpendDashboardTrackedSource( + id: "codex:work", + provider: .codex, + providerName: "Codex", + accountName: "Work", + state: .configured, + supportsCostHistory: true, + contributesCostHistory: true) + let date = Date(timeIntervalSince1970: 1_785_974_400) + let model = SpendDashboardModel(requestedDays: 30, groups: [ + SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + SpendDashboardModel.ProviderRow( + id: "codex:work", + rank: 1, + provider: .codex, + displayName: "Codex · Work", + totalTokens: 100, + totalCost: 2, + coveredDayCount: 30), + ], + models: [], + dailyPoints: [], + totalTokens: 100, + totalCost: 2, + coveredDayCount: 30, + chartDomain: date...date, + modelHistoryCompleteness: .complete), + ]) + + let presented = try #require(spendDashboardTrackedSourcesForPresentation( + [source], + model: model).first) + #expect(presented.costHistoryAvailable) + #expect(spendDashboardTrackedSourceStatusText(presented) == "Cost history connected") + } + @Test @MainActor func `tracked access includes every saved provider credential without inventing cost coverage`() { From a021ccbd960d260504a5c1dd76e09907a2db5ece Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:28:10 -0700 Subject: [PATCH 28/51] Reconcile shared account identities --- .../PreferencesSpendDashboardPane.swift | 2 +- Sources/CodexBar/ShareStatsPayload.swift | 80 +++++++++++++++---- .../PreferencesSpendDashboardShareTests.swift | 53 ++++++++++++ .../ProviderArchitectureGatekeeperTests.swift | 4 +- 4 files changed, 121 insertions(+), 18 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index c0be96c014..6e1cec78d9 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -430,7 +430,7 @@ struct SpendDashboardPane: View { provider: source.provider, providerName: source.providerName, currencyCode: fallbackCurrencyCode, - expectedSourceCount: sources.count) + expectedSourceIDs: Set(sources.map(\.id))) } return ShareStatsBuilder.make( model: model, diff --git a/Sources/CodexBar/ShareStatsPayload.swift b/Sources/CodexBar/ShareStatsPayload.swift index 9a505ebb7d..2e4ae903d6 100644 --- a/Sources/CodexBar/ShareStatsPayload.swift +++ b/Sources/CodexBar/ShareStatsPayload.swift @@ -2,6 +2,7 @@ import CodexBarCore import Foundation struct ShareStatsProviderPayload: Sendable, Equatable { + let sourceID: String? let provider: UsageProvider let providerName: String let subscriptionName: String? @@ -9,24 +10,44 @@ struct ShareStatsProviderPayload: Sendable, Equatable { let totalTokens: Int? let estimatedCost: Double? let coveredDayCount: Int + + init( + sourceID: String? = nil, + provider: UsageProvider, + providerName: String, + subscriptionName: String?, + currencyCode: String, + totalTokens: Int?, + estimatedCost: Double?, + coveredDayCount: Int) + { + self.sourceID = sourceID + self.provider = provider + self.providerName = providerName + self.subscriptionName = subscriptionName + self.currencyCode = currencyCode + self.totalTokens = totalTokens + self.estimatedCost = estimatedCost + self.coveredDayCount = coveredDayCount + } } struct ShareStatsProviderRosterEntry: Sendable, Equatable { let provider: UsageProvider let providerName: String let currencyCode: String - let expectedSourceCount: Int + let expectedSourceIDs: Set init( provider: UsageProvider, providerName: String, currencyCode: String, - expectedSourceCount: Int = 1) + expectedSourceIDs: Set = []) { self.provider = provider self.providerName = providerName self.currencyCode = currencyCode - self.expectedSourceCount = max(1, expectedSourceCount) + self.expectedSourceIDs = expectedSourceIDs } } @@ -323,6 +344,7 @@ enum ShareStatsBuilder { let trackedProviders = model.groups.flatMap { group in group.providers.map { row in ShareStatsProviderPayload( + sourceID: row.id, provider: row.provider, providerName: row.displayName, subscriptionName: subscriptionNames[row.id]?.displayName, @@ -340,7 +362,9 @@ enum ShareStatsBuilder { var emittedProviders: Set = [] providers = providerRoster.compactMap { entry in guard emittedProviders.insert(entry.provider).inserted else { return nil } - guard let matches = trackedByProvider[entry.provider], !matches.isEmpty else { + let allMatches = trackedByProvider[entry.provider] ?? [] + let matches = self.rosterMatches(entry: entry, candidates: allMatches) + guard !matches.isEmpty else { return ShareStatsProviderPayload( provider: entry.provider, providerName: entry.providerName, @@ -417,23 +441,30 @@ enum ShareStatsBuilder { } } let rosterHasIncompleteProviders = providerRoster.contains { entry in - let matches = trackedProviders.filter { $0.provider == entry.provider } + let allMatches = trackedProviders.filter { $0.provider == entry.provider } + let matches = self.rosterMatches(entry: entry, candidates: allMatches) let knownCosts = matches.compactMap(\.estimatedCost) - return matches.count < entry.expectedSourceCount || + return self.rosterIdentityIsIncomplete(entry: entry, candidates: allMatches) || matches.contains { $0.totalTokens == nil || $0.estimatedCost == nil } || Set(matches.map(\.currencyCode)).count > 1 || self.safeCostSum(knownCosts) == nil } - let currencies = model.groups.map { - ShareStatsCurrencyPayload( - currencyCode: $0.currencyCode, - estimatedCost: self.finiteCost($0.totalCost ?? $0.knownCost), - coveredDayCount: $0.coveredDayCount, - isPartial: rosterHasIncompleteProviders || $0.totalCost == nil) + let currencies = model.groups.map { group in + let rosterCosts = providers.filter { $0.currencyCode == group.currencyCode } + .compactMap(\.estimatedCost) + let estimatedCost = providerRoster.isEmpty + ? self.finiteCost(group.totalCost ?? group.knownCost) + : self.safeCostSum(rosterCosts) + return ShareStatsCurrencyPayload( + currencyCode: group.currencyCode, + estimatedCost: estimatedCost, + coveredDayCount: group.coveredDayCount, + isPartial: rosterHasIncompleteProviders || group.totalCost == nil) } - let knownTokenValues = trackedProviders.compactMap(\.totalTokens) + let tokenProviders = providerRoster.isEmpty ? trackedProviders : providers + let knownTokenValues = tokenProviders.compactMap(\.totalTokens) let totalTokens = self.safeTokenSum(knownTokenValues) - let totalTokensIsPartial = trackedProviders.contains { $0.totalTokens == nil } || + let totalTokensIsPartial = tokenProviders.contains { $0.totalTokens == nil } || rosterHasIncompleteProviders || model.groups.contains { $0.totalTokens == nil } let periodEnd = model.groups.map(\.chartDomain.upperBound).max() ?? Date() @@ -441,7 +472,7 @@ enum ShareStatsBuilder { days: model.requestedDays, periodEnd: periodEnd, providers: providers, - topModels: topModels, + topModels: rosterHasIncompleteProviders ? [] : topModels, currencies: currencies, totalTokens: totalTokens, totalTokensIsPartial: totalTokensIsPartial) @@ -483,6 +514,25 @@ enum ShareStatsBuilder { } return total } + + private static func rosterMatches( + entry: ShareStatsProviderRosterEntry, + candidates: [ShareStatsProviderPayload]) -> [ShareStatsProviderPayload] + { + guard !entry.expectedSourceIDs.isEmpty else { return candidates } + return candidates.filter { candidate in + candidate.sourceID.map(entry.expectedSourceIDs.contains) == true + } + } + + private static func rosterIdentityIsIncomplete( + entry: ShareStatsProviderRosterEntry, + candidates: [ShareStatsProviderPayload]) -> Bool + { + guard !entry.expectedSourceIDs.isEmpty else { return candidates.isEmpty } + let actualIDs = Set(candidates.compactMap(\.sourceID)) + return actualIDs != entry.expectedSourceIDs || candidates.count != entry.expectedSourceIDs.count + } } enum ShareStatsFormatting { diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift index 0acd9dfdf9..d8fe160020 100644 --- a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -154,6 +154,48 @@ struct PreferencesSpendDashboardShareTests { #expect(payload.totalTokensIsPartial) } + @Test + func `mismatched account identity is excluded and marks share partial`() throws { + let model = SpendDashboardModel(requestedDays: 30, groups: [ + Self.group(currencyCode: "USD", providers: [ + Self.row(id: "codex:a", tokens: 200, cost: 4), + Self.row(id: "codex:c", tokens: 900, cost: 9), + ]), + ]) + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: Self.codexSources(ids: ["codex:a", "codex:b"]))) + + #expect(payload.providers.first?.estimatedCost == 4) + #expect(payload.providers.first?.totalTokens == 200) + #expect(payload.currencies.first?.estimatedCost == 4) + #expect(payload.currencies.allSatisfy(\.isPartial)) + #expect(payload.totalTokensIsPartial) + #expect(payload.topModels.isEmpty) + } + + @Test + func `extra stale account row is excluded and marks share partial`() throws { + let model = SpendDashboardModel(requestedDays: 30, groups: [ + Self.group(currencyCode: "USD", providers: [ + Self.row(id: "codex:a", tokens: 200, cost: 4), + Self.row(id: "codex:b", tokens: 100, cost: 3), + Self.row(id: "codex:c", tokens: 900, cost: 9), + ]), + ]) + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: Self.codexSources(ids: ["codex:a", "codex:b"]))) + + #expect(payload.providers.first?.estimatedCost == 7) + #expect(payload.providers.first?.totalTokens == 300) + #expect(payload.currencies.first?.estimatedCost == 7) + #expect(payload.currencies.allSatisfy(\.isPartial)) + #expect(payload.totalTokensIsPartial) + } + private static func source( id: String, provider: UsageProvider, @@ -182,6 +224,17 @@ struct PreferencesSpendDashboardShareTests { coveredDayCount: 30) } + private static func codexSources(ids: [String]) -> [SpendDashboardTrackedSource] { + ids.map { + Self.source( + id: $0, + provider: .codex, + providerName: "Codex", + state: .connected, + contributesCostHistory: true) + } + } + private static func group( currencyCode: String, providers: [SpendDashboardModel.ProviderRow]) -> SpendDashboardModel.CurrencyGroup diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 09fadb9372..acd3d5aa02 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2218,7 +2218,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared provider integration dispatches a capability owned by the provider descriptor or adapter."), AllowedProviderConstruct( path: "Sources/CodexBar/ShareStatsPayload.swift", - line: 198, + line: 219, anchor: "([\"codestral-\", \"devstral-\", \"magistral-\", \"mistral-\", \"mistral \", \"mistral.\", \"mixtral-\"], \"Mistral\"),", expectedProviderIDs: ["mistral"], expectedReferenceCount: 1, @@ -2226,7 +2226,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This public model-family sanitizer is independent of the provider registry; Mistral is also a provider ID."), AllowedProviderConstruct( path: "Sources/CodexBar/ShareStatsPayload.swift", - line: 229, + line: 250, anchor: "\"air\": \"Air\", \"chat\": \"Chat\", \"code\": \"Code\", \"coder\": \"Coder\", \"codex\": \"Codex\",", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, From 25b797c9b005bd03849193790315361e7ee40b77 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:32:43 -0700 Subject: [PATCH 29/51] Preserve scoped OpenRouter history --- Sources/CodexBar/UsageStore+TokenCost.swift | 14 +++++++++++++- .../OpenRouterActivityUsageTests.swift | 3 +++ .../ProviderArchitectureGatekeeperTests.swift | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/UsageStore+TokenCost.swift b/Sources/CodexBar/UsageStore+TokenCost.swift index b3c8fe453b..46ec33fea6 100644 --- a/Sources/CodexBar/UsageStore+TokenCost.swift +++ b/Sources/CodexBar/UsageStore+TokenCost.swift @@ -509,9 +509,21 @@ extension UsageStore { previous: UsageSnapshot?) -> UsageSnapshot { guard snapshot.openRouterActivityUsage == nil, - self.tokenSnapshotPublicationForCurrentProviderConfig(for: .openrouter)?.snapshot != nil, + self.tokenSnapshotPublicationForCurrentCredentialScope(for: .openrouter)?.snapshot != nil, let activity = previous?.openRouterActivityUsage else { return snapshot } return snapshot.withOpenRouterActivityUsage(activity) } + + private func tokenSnapshotPublicationForCurrentCredentialScope( + for provider: UsageProvider) -> TokenSnapshotPublication? + { + guard let publication = self.tokenSnapshotPublications[provider.instanceID], + publication.providerConfigRevision == self.settings.providerConfigRevision(for: provider), + publication.scopeSignature == self.tokenSnapshotScopeSignature( + for: provider, + historyDays: publication.historyDays) + else { return nil } + return publication + } } diff --git a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift index 816c474361..1ad0817472 100644 --- a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift +++ b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift @@ -131,6 +131,9 @@ struct OpenRouterActivityUsageTests { startupBehavior: .testing, environmentBase: [:]) store.publishTokenSnapshot(activity.toCostUsageTokenSnapshot(), for: .openrouter) + settings.costUsageHistoryDays = 365 + + #expect(store.tokenSnapshotPublicationForCurrentProviderConfig(for: .openrouter) == nil) let preserved = store.preservingOpenRouterActivityIfCurrent(current, previous: previous) #expect(preserved.openRouterActivityUsage == activity) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 264f2c398b..fbe2c33bb7 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -3062,7 +3062,7 @@ struct ProviderArchitectureGatekeeperTests { AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", line: 512, - anchor: "self.tokenSnapshotPublicationForCurrentProviderConfig(for: .openrouter)?.snapshot != nil,", + anchor: "self.tokenSnapshotPublicationForCurrentCredentialScope(for: .openrouter)?.snapshot != nil,", expectedProviderIDs: ["openrouter"], expectedReferenceCount: 1, expectedReferenceFingerprint: ["openrouter@0"], From cd057ffd530f66972fbf2719ef58e7c652f8f449 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:34:21 -0700 Subject: [PATCH 30/51] Keep preservation independent of range --- Sources/CodexBar/UsageStore+TokenCost.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/CodexBar/UsageStore+TokenCost.swift b/Sources/CodexBar/UsageStore+TokenCost.swift index 46ec33fea6..d6142e75b0 100644 --- a/Sources/CodexBar/UsageStore+TokenCost.swift +++ b/Sources/CodexBar/UsageStore+TokenCost.swift @@ -519,10 +519,7 @@ extension UsageStore { for provider: UsageProvider) -> TokenSnapshotPublication? { guard let publication = self.tokenSnapshotPublications[provider.instanceID], - publication.providerConfigRevision == self.settings.providerConfigRevision(for: provider), - publication.scopeSignature == self.tokenSnapshotScopeSignature( - for: provider, - historyDays: publication.historyDays) + publication.providerConfigRevision == self.settings.providerConfigRevision(for: provider) else { return nil } return publication } From c27fad07981ea1ce9f1270fb2a6db83e949093da Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:46:07 -0700 Subject: [PATCH 31/51] Fix share test actor isolation --- Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift index a24a28725e..81c49f668a 100644 --- a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -3,6 +3,7 @@ import Foundation import Testing @testable import CodexBar +@MainActor struct PreferencesSpendDashboardShareTests { @Test func `settings share includes every tracked provider once even without cost history`() throws { From faa9f847355985ba6370349f065bbca88e3693db Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 02:57:06 -0700 Subject: [PATCH 32/51] Stabilize share test predicates --- .../PreferencesSpendDashboardShareTests.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift index 81c49f668a..34fd93d792 100644 --- a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -72,7 +72,8 @@ struct PreferencesSpendDashboardShareTests { #expect(payload.providers.map(\.provider) == [.codex, .openrouter, .gemini]) #expect(payload.providers.map(\.estimatedCost) == [7, nil, nil]) #expect(payload.spendReportingProviderCount == 1) - #expect(payload.currencies.allSatisfy(\.isPartial)) + let allCurrenciesArePartial = payload.currencies.allSatisfy(\.isPartial) + #expect(allCurrenciesArePartial) #expect(payload.totalTokensIsPartial) #expect(ShareStatsFormatting.text(payload).contains("1/3 connected services report spend")) } @@ -101,7 +102,8 @@ struct PreferencesSpendDashboardShareTests { ])) #expect(payload.providers.first?.estimatedCost == 4) - #expect(payload.currencies.allSatisfy(\.isPartial)) + let allCurrenciesArePartial = payload.currencies.allSatisfy(\.isPartial) + #expect(allCurrenciesArePartial) #expect(payload.totalTokensIsPartial) } @@ -125,7 +127,8 @@ struct PreferencesSpendDashboardShareTests { trackedSources: trackedSources)) #expect(payload.providers.first?.estimatedCost == nil) - #expect(payload.currencies.allSatisfy(\.isPartial)) + let allCurrenciesArePartial = payload.currencies.allSatisfy(\.isPartial) + #expect(allCurrenciesArePartial) } @Test @@ -151,7 +154,8 @@ struct PreferencesSpendDashboardShareTests { #expect(payload.providers.first?.totalTokens == nil) #expect(payload.providers.first?.estimatedCost == nil) - #expect(payload.currencies.allSatisfy(\.isPartial)) + let allCurrenciesArePartial = payload.currencies.allSatisfy(\.isPartial) + #expect(allCurrenciesArePartial) #expect(payload.totalTokensIsPartial) } @@ -171,7 +175,8 @@ struct PreferencesSpendDashboardShareTests { #expect(payload.providers.first?.estimatedCost == 4) #expect(payload.providers.first?.totalTokens == 200) #expect(payload.currencies.first?.estimatedCost == 4) - #expect(payload.currencies.allSatisfy(\.isPartial)) + let allCurrenciesArePartial = payload.currencies.allSatisfy(\.isPartial) + #expect(allCurrenciesArePartial) #expect(payload.totalTokensIsPartial) #expect(payload.topModels.isEmpty) } @@ -193,7 +198,8 @@ struct PreferencesSpendDashboardShareTests { #expect(payload.providers.first?.estimatedCost == 7) #expect(payload.providers.first?.totalTokens == 300) #expect(payload.currencies.first?.estimatedCost == 7) - #expect(payload.currencies.allSatisfy(\.isPartial)) + let allCurrenciesArePartial = payload.currencies.allSatisfy(\.isPartial) + #expect(allCurrenciesArePartial) #expect(payload.totalTokensIsPartial) } From 0a15a618775ba2808a6a7167d13ed0cc5df2c1be Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 03:13:10 -0700 Subject: [PATCH 33/51] Assert overflowed share payload fails closed --- .../PreferencesSpendDashboardShareTests.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift index 34fd93d792..4f84d0da0d 100644 --- a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -132,7 +132,7 @@ struct PreferencesSpendDashboardShareTests { } @Test - func `provider family overflow fails closed`() throws { + func `provider family overflow fails closed`() { let model = SpendDashboardModel(requestedDays: 30, groups: [ Self.group(currencyCode: "USD", providers: [ Self.row(id: "codex:personal", tokens: Int.max, cost: Double.greatestFiniteMagnitude), @@ -147,16 +147,12 @@ struct PreferencesSpendDashboardShareTests { state: .connected, contributesCostHistory: true) } - let payload = try #require(SpendDashboardPane.makeSharePayload( + let payload = SpendDashboardPane.makeSharePayload( model: model, subscriptionNames: [:], - trackedSources: trackedSources)) + trackedSources: trackedSources) - #expect(payload.providers.first?.totalTokens == nil) - #expect(payload.providers.first?.estimatedCost == nil) - let allCurrenciesArePartial = payload.currencies.allSatisfy(\.isPartial) - #expect(allCurrenciesArePartial) - #expect(payload.totalTokensIsPartial) + #expect(payload == nil) } @Test From 4c6bc5b5c40d5c915418097ddb164bedec4af7b3 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 03:32:22 -0700 Subject: [PATCH 34/51] Align date fixtures with coverage horizons --- .../SpendDashboardDateTruthTests.swift | 14 +++++++------- ...pendDashboardIntegratedModelCoverageTests.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift index ea29bd0f58..5d5eacc40f 100644 --- a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift @@ -30,7 +30,7 @@ struct SpendDashboardDateTruthTests { updatedAt: now) let group = try #require(SpendDashboardModel.build( inputs: [.init(provider: .mistral, displayName: "Mistral", snapshot: snapshot)], - requestedDays: 7, + requestedDays: 2, now: now, calendar: pacific).groups.first) @@ -140,7 +140,7 @@ struct SpendDashboardDateTruthTests { let snapshot = usage.toCostUsageTokenSnapshot() let group = try #require(SpendDashboardModel.build( inputs: [.init(provider: .mistral, displayName: "Mistral", snapshot: snapshot)], - requestedDays: 30, + requestedDays: 2, now: updatedAt, calendar: Self.calendar).groups.first) @@ -325,7 +325,7 @@ struct SpendDashboardDateTruthTests { Self.input(id: "healthy-cad", provider: .mistral, currency: "CAD", cost: 5), Self.input(id: "healthy-eur", provider: .bedrock, currency: "EUR", cost: 6), ], - requestedDays: 7, + requestedDays: 1, now: Self.now, calendar: Self.calendar).groups let cad = try #require(groups.first(where: { $0.currencyCode == "CAD" })) @@ -505,7 +505,7 @@ struct SpendDashboardDateTruthTests { historyDays: 2) let group = try #require(SpendDashboardModel.build( inputs: [.init(provider: .claude, displayName: "Claude", snapshot: snapshot)], - requestedDays: 7, + requestedDays: 2, now: Self.now, calendar: Self.calendar).groups.first) @@ -591,7 +591,7 @@ struct SpendDashboardDateTruthTests { historyDays: 1) let group = try #require(SpendDashboardModel.build( inputs: [.init(provider: .claude, displayName: "Claude", snapshot: snapshot)], - requestedDays: 7, + requestedDays: 1, now: Self.now, calendar: Self.calendar).groups.first) @@ -664,7 +664,7 @@ struct SpendDashboardDateTruthTests { #expect(snapshot.last30DaysTokens == 0) let group = try #require(SpendDashboardModel.build( inputs: [.init(provider: .mistral, displayName: "Mistral", snapshot: snapshot)], - requestedDays: 7, + requestedDays: 1, now: Self.now, calendar: Self.calendar).groups.first) @@ -726,7 +726,7 @@ struct SpendDashboardDateTruthTests { establishesCoverage: true)) let groups = SpendDashboardModel.build( inputs: [costOnly, completeUSD, tokenOnly], - requestedDays: 7, + requestedDays: 1, now: Self.now, calendar: Self.calendar).groups let eur = try #require(groups.first(where: { $0.currencyCode == "EUR" })) diff --git a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift index 5998979283..3c4fda6af0 100644 --- a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift +++ b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift @@ -39,7 +39,7 @@ struct SpendDashboardIntegratedModelCoverageTests { #expect(Set(group.models.map(\.id)) == expectedIDs) #expect(group.models.count == providers.count * 2) #expect(group.models.allSatisfy { $0.totalTokens != nil && $0.totalCost != nil }) - #expect(group.modelHistoryCompleteness == .complete) + #expect(group.modelHistoryCompleteness == .incomplete) } @Test From 170a5bd60422cfd8a70701a6ebe4a75b8aa5c210 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 03:33:15 -0700 Subject: [PATCH 35/51] Assert partial coverage lower bounds --- Tests/CodexBarTests/SpendDashboardModelTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index bcd7f3affd..5d3eacc81c 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -196,7 +196,8 @@ struct SpendDashboardModelTests { #expect(group.coveredDayCount == 0) #expect(group.providers.allSatisfy { $0.coveredDayCount == 7 }) - #expect(group.totalCost == 5) + #expect(group.totalCost == nil) + #expect(group.knownCost == 5) #expect(group.providers.map(\.id) == ["later", "earlier"]) #expect(group.dailyPoints.map(\.sourceID) == ["earlier", "later"]) } @@ -228,7 +229,8 @@ struct SpendDashboardModelTests { #expect(group.coveredDayCount == 3) #expect(group.providers.allSatisfy { $0.coveredDayCount == 7 }) - #expect(group.totalCost == 5) + #expect(group.totalCost == nil) + #expect(group.knownCost == 5) } @Test From 4ceeff6f9c75bef67e0785b2a20eb2012fbccdb2 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 03:55:01 -0700 Subject: [PATCH 36/51] Preserve date fixture validation semantics --- .../SpendDashboardDateTruthTests.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift index 5d5eacc40f..886bc622bd 100644 --- a/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardDateTruthTests.swift @@ -140,14 +140,16 @@ struct SpendDashboardDateTruthTests { let snapshot = usage.toCostUsageTokenSnapshot() let group = try #require(SpendDashboardModel.build( inputs: [.init(provider: .mistral, displayName: "Mistral", snapshot: snapshot)], - requestedDays: 2, + requestedDays: 30, now: updatedAt, calendar: Self.calendar).groups.first) #expect(snapshot.updatedAt == july15) #expect(group.coveredDayCount == 2) - #expect(group.totalCost == 3) - #expect(group.totalTokens == 30) + #expect(group.totalCost == nil) + #expect(group.totalTokens == nil) + #expect(group.knownCost == 3) + #expect(group.providers.first?.totalTokens == 30) #expect(group.dailyPoints.map(\.day) == [july14, july15]) #expect(group.dailyPoints.map(\.cost) == [1, 2]) } @@ -325,16 +327,18 @@ struct SpendDashboardDateTruthTests { Self.input(id: "healthy-cad", provider: .mistral, currency: "CAD", cost: 5), Self.input(id: "healthy-eur", provider: .bedrock, currency: "EUR", cost: 6), ], - requestedDays: 1, + requestedDays: 7, now: Self.now, calendar: Self.calendar).groups let cad = try #require(groups.first(where: { $0.currencyCode == "CAD" })) let eur = try #require(groups.first(where: { $0.currencyCode == "EUR" })) let usd = try #require(groups.first(where: { $0.currencyCode == "USD" })) - #expect(usd.totalCost == 7) + let coversRequestedHorizon = omission.historyDays >= 7 + #expect(usd.totalCost == (coversRequestedHorizon ? 7 : nil)) + #expect(usd.knownCost == 7) #expect(usd.totalTokens == nil) - #expect(usd.modelHistoryCompleteness == .complete) + #expect(usd.modelHistoryCompleteness == (coversRequestedHorizon ? .complete : .incomplete)) #expect(usd.models.map(\.totalCost) == [4, 3]) #expect(usd.models.first(where: { $0.provider == .claude })?.totalTokens == nil) #expect(usd.models.first(where: { $0.provider == .codex })?.totalTokens == 10) @@ -344,7 +348,7 @@ struct SpendDashboardDateTruthTests { aggregateTotal: usd.totalCost).content == .chart) #expect(cad.totalCost == nil) - #expect(cad.totalTokens == 30) + #expect(cad.totalTokens == (coversRequestedHorizon ? 30 : nil)) #expect(cad.modelHistoryCompleteness == .incomplete) #expect(cad.models.map(\.provider) == [.mistral]) #expect(cad.models.map(\.totalCost) == [5]) From 86fe5d87749c52ddfc7fdefed5beee8bd3b4ab25 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 04:14:02 -0700 Subject: [PATCH 37/51] Qualify stale provenance spend totals --- .../SpendDashboardTokenProvenanceTests.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift b/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift index c454d6ab1e..0a73478fbd 100644 --- a/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift @@ -127,12 +127,14 @@ struct SpendDashboardTokenProvenanceTests { let controller = Self.dashboardController(settings: settings, store: store, now: now) controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store)) await Self.waitUntil { !controller.isRefreshing } - #expect(controller.model.groups.first?.totalCost == 3) + #expect(controller.model.groups.first?.totalCost == nil) + #expect(controller.model.groups.first?.knownCost == 3) controller.refresh() await Self.waitUntil { !controller.isRefreshing } - #expect(controller.model.groups.first?.totalCost == 3) + #expect(controller.model.groups.first?.totalCost == nil) + #expect(controller.model.groups.first?.knownCost == 3) #expect(controller.failedSourceCount == 1) #expect(store.tokenSnapshotPublicationRevision(for: .mistral) == baselineRevision) } @@ -149,7 +151,8 @@ struct SpendDashboardTokenProvenanceTests { let controller = Self.dashboardController(settings: settings, store: store, now: now) controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store)) await Self.waitUntil { !controller.isRefreshing } - #expect(controller.model.groups.first?.totalCost == 4) + #expect(controller.model.groups.first?.totalCost == nil) + #expect(controller.model.groups.first?.knownCost == 4) controller.refresh() await Self.waitUntil { !controller.isRefreshing } From b35d6152268a602204a2aca5ac486c1c059b8197 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 04:57:35 -0700 Subject: [PATCH 38/51] Match saved accounts to provider spend --- .../PreferencesSpendDashboardPane.swift | 19 ++++++- .../PreferencesSpendDashboardShareTests.swift | 51 +++++++++++++++++++ .../ProviderArchitectureGatekeeperTests.swift | 2 +- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 6e1cec78d9..1af6fc854c 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -430,7 +430,9 @@ struct SpendDashboardPane: View { provider: source.provider, providerName: source.providerName, currencyCode: fallbackCurrencyCode, - expectedSourceIDs: Set(sources.map(\.id))) + expectedSourceIDs: self.shareExpectedSourceIDs( + provider: source.provider, + sources: sources)) } return ShareStatsBuilder.make( model: model, @@ -438,6 +440,21 @@ struct SpendDashboardPane: View { providerRoster: roster) } + private static func shareExpectedSourceIDs( + provider: UsageProvider, + sources: [SpendDashboardTrackedSource]) -> Set + { + let providerScopedSources = sources.filter { source in + source.contributesCostHistory && + (source.id == "\(provider.rawValue):current" || + source.id.hasPrefix("\(provider.rawValue):account:")) + } + if providerScopedSources.count == 1 { + return [provider.rawValue] + } + return Set(sources.map(\.id)) + } + private var subscriptionNames: [String: ShareStatsSubscriptionName] { var names: [String: ShareStatsSubscriptionName] = [:] let codexRowCount = self.controller.model.groups diff --git a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift index 4f84d0da0d..bab764db59 100644 --- a/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift +++ b/Tests/CodexBarTests/PreferencesSpendDashboardShareTests.swift @@ -243,6 +243,57 @@ struct PreferencesSpendDashboardShareTests { #expect(payload.topModels.isEmpty) } + @Test + func `selected saved account matches provider scoped spend`() throws { + let date = Date(timeIntervalSince1970: 1_785_974_400) + let model = SpendDashboardModel(requestedDays: 30, groups: [ + SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + SpendDashboardModel.ProviderRow( + id: "openrouter", + rank: 1, + provider: .openrouter, + displayName: "OpenRouter", + totalTokens: 900, + totalCost: 9, + coveredDayCount: 30), + ], + models: [], + dailyPoints: [], + totalTokens: 900, + totalCost: 9, + coveredDayCount: 30, + chartDomain: date...date, + modelHistoryCompleteness: .complete), + ]) + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: [ + Self.source( + id: "openrouter:account:00000000-0000-0000-0000-000000000001", + provider: .openrouter, + providerName: "OpenRouter", + state: .connected, + contributesCostHistory: true), + Self.source( + id: "openrouter:account:00000000-0000-0000-0000-000000000002", + provider: .openrouter, + providerName: "OpenRouter", + state: .configured, + contributesCostHistory: false), + ])) + + #expect(payload.providers.first?.estimatedCost == 9) + #expect(payload.providers.first?.totalTokens == 900) + #expect(payload.spendReportingProviderCount == 1) + #expect(payload.currencies.first?.estimatedCost == 9) + #expect(payload.currencies.first?.isPartial == false) + #expect(payload.totalTokens == 900) + #expect(payload.totalTokensIsPartial == false) + } + private static func source( id: String, provider: UsageProvider, diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index fbe2c33bb7..63c9eb7578 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2178,7 +2178,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesSpendDashboardPane.swift", - line: 445, + line: 462, anchor: ".count { $0.provider == .codex }", expectedProviderIDs: ["codex"], expectedReferenceCount: 3, From eccd74e7fa8e4509255f9e8b23165151ba5630f6 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 10:14:20 -0700 Subject: [PATCH 39/51] Qualify partial Overview token totals --- .../StatusItemController+MenuTypes.swift | 3 +- .../UsageMenuCardLayoutTests.swift | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBar/StatusItemController+MenuTypes.swift b/Sources/CodexBar/StatusItemController+MenuTypes.swift index 5a6b781b77..7030a09cb8 100644 --- a/Sources/CodexBar/StatusItemController+MenuTypes.swift +++ b/Sources/CodexBar/StatusItemController+MenuTypes.swift @@ -44,7 +44,8 @@ struct OverviewSpendSummary: Equatable { let knownCostCount = model.groups.reduce(0) { $0 + $1.knownCostProviderCount } let knownTokenRows = model.groups.flatMap(\.providers).compactMap(\.totalTokens) let knownTokens = Self.safeTokenSum(knownTokenRows) - let tokenCoverageIsComplete = knownTokenRows.count == connectedCount + let tokenCoverageIsComplete = knownTokenRows.count == connectedCount && + model.groups.allSatisfy { $0.totalTokens != nil } self.isPartial = knownCostCount < connectedCount || model.groups.contains { $0.totalCost == nil } let spendTexts = model.groups.compactMap { group -> String? in diff --git a/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift b/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift index e3f86fbe24..1a899591c8 100644 --- a/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift +++ b/Tests/CodexBarTests/UsageMenuCardLayoutTests.swift @@ -193,6 +193,51 @@ struct UsageMenuCardLayoutTests { #expect(summary.isPartial) } + @Test + func `overview token total reflects requested range coverage`() { + func model(coveredDayCount: Int) -> SpendDashboardModel { + let providers = [ + SpendDashboardModel.ProviderRow( + id: "codex", + rank: 1, + provider: .codex, + displayName: "Codex", + totalTokens: 4_000_000, + totalCost: 40, + coveredDayCount: coveredDayCount), + SpendDashboardModel.ProviderRow( + id: "openrouter", + rank: 2, + provider: .openrouter, + displayName: "OpenRouter", + totalTokens: 6_000_000, + totalCost: 60, + coveredDayCount: coveredDayCount), + ] + let group = SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: providers, + models: [], + dailyPoints: [], + totalTokens: coveredDayCount >= 365 ? 10_000_000 : nil, + totalCost: coveredDayCount >= 365 ? 100 : nil, + coveredDayCount: coveredDayCount, + chartDomain: Date(timeIntervalSince1970: 1_783_036_800)...Date(timeIntervalSince1970: 1_814_572_800), + modelHistoryCompleteness: coveredDayCount >= 365 ? .complete : .incomplete) + return SpendDashboardModel(requestedDays: 365, groups: [group]) + } + + let shortCoverage = OverviewSpendSummary( + model: model(coveredDayCount: 30), + connectedProviderCount: 2) + let fullCoverage = OverviewSpendSummary( + model: model(coveredDayCount: 365), + connectedProviderCount: 2) + + #expect(shortCoverage.tokenText == "~10M tokens") + #expect(fullCoverage.tokenText == "10M tokens") + } + @Test func `overview prioritizes refresh status and expands for accessibility text`() { let metric = UsageMenuCardView.Model.Metric( From 8f4b62284f585e61149eb838f2a18fa9e68a8a5a Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 10:55:13 -0700 Subject: [PATCH 40/51] Retain partial settings token totals --- .../PreferencesSpendDashboardPane.swift | 19 ++++++++++- .../ProviderArchitectureGatekeeperTests.swift | 4 +-- .../SpendDashboardCoverageTruthTests.swift | 34 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 1af6fc854c..72f1722d34 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -87,6 +87,23 @@ func spendDashboardAggregateCostText(_ group: SpendDashboardModel.CurrencyGroup) return group.totalCost == nil ? "~\(formatted)" : formatted } +func spendDashboardAggregateTokenText(_ group: SpendDashboardModel.CurrencyGroup) -> String { + if let totalTokens = group.totalTokens { + return UsageFormatter.tokenCountString(totalTokens) + } + + var knownTokens = 0 + var hasKnownTokens = false + for tokens in group.providers.compactMap(\.totalTokens) { + let addition = knownTokens.addingReportingOverflow(tokens) + guard !addition.overflow else { return "—" } + knownTokens = addition.partialValue + hasKnownTokens = true + } + guard hasKnownTokens else { return "—" } + return "~\(UsageFormatter.tokenCountString(knownTokens))" +} + func spendDashboardCostCoverageText(_ group: SpendDashboardModel.CurrencyGroup) -> String { "\(codexBarLocalizedInteger(group.knownCostProviderCount)) / " + "\(codexBarLocalizedInteger(group.providers.count)) \(L("Accounts"))" @@ -729,7 +746,7 @@ struct SpendCurrencySummaryView: View { value: spendDashboardAggregateCostText(self.group)) SpendSummaryValue( title: L("Tracked tokens"), - value: self.group.totalTokens.map(UsageFormatter.tokenCountString) ?? "—") + value: spendDashboardAggregateTokenText(self.group)) SpendSummaryValue( title: L("Coverage"), value: spendDashboardCostCoverageText(self.group)) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 63c9eb7578..99e5d864e2 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2170,7 +2170,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesSpendDashboardPane.swift", - line: 293, + line: 310, anchor: "self.configuration.providerIDs.contains(UsageProvider.codex.rawValue)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2178,7 +2178,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/PreferencesSpendDashboardPane.swift", - line: 462, + line: 479, anchor: ".count { $0.provider == .codex }", expectedProviderIDs: ["codex"], expectedReferenceCount: 3, diff --git a/Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift b/Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift index 8d181af39b..d6f84b3716 100644 --- a/Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift +++ b/Tests/CodexBarTests/SpendDashboardCoverageTruthTests.swift @@ -42,6 +42,7 @@ struct SpendDashboardCoverageTruthTests { #expect(group.knownCost == 2) #expect(group.totalTokens == nil) #expect(group.modelHistoryCompleteness == .incomplete) + #expect(spendDashboardAggregateTokenText(group) == "~20") #expect(spendDashboardProviderCostText( row, currencyCode: group.currencyCode, @@ -82,9 +83,42 @@ struct SpendDashboardCoverageTruthTests { #expect(group.totalCost == 2) #expect(group.totalTokens == 20) + #expect(spendDashboardAggregateTokenText(group) == "20") #expect(spendDashboardProviderCostText( row, currencyCode: group.currencyCode, requestedDays: 30) == "$2.00") } + + @Test + func `aggregate token text qualifies safe mixed coverage and rejects unknown or overflow`() { + let date = Date(timeIntervalSince1970: 1_785_974_400) + func group(tokens: [Int?]) -> SpendDashboardModel.CurrencyGroup { + let providers = tokens.enumerated().map { index, tokens in + SpendDashboardModel.ProviderRow( + id: "provider-\(index)", + rank: index + 1, + provider: index == 0 ? .codex : .openrouter, + displayName: index == 0 ? "Codex" : "OpenRouter", + totalTokens: tokens, + totalCost: nil, + coveredDayCount: index == 0 ? 365 : 30) + } + return SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: providers, + models: [], + dailyPoints: [], + totalTokens: nil, + totalCost: nil, + coveredDayCount: 30, + chartDomain: date...date, + modelHistoryCompleteness: .incomplete) + } + + #expect(spendDashboardAggregateTokenText(group(tokens: [4_000_000, 6_000_000])) == "~10M") + #expect(spendDashboardAggregateTokenText(group(tokens: [4_000_000, nil])) == "~4M") + #expect(spendDashboardAggregateTokenText(group(tokens: [nil, nil])) == "—") + #expect(spendDashboardAggregateTokenText(group(tokens: [.max, .max])) == "—") + } } From f3142c1988dc1e15414b9b8b244a3efbe8a6b7e9 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 12:02:12 -0700 Subject: [PATCH 41/51] Represent ambient token account ownership --- .../CodexBar/SpendDashboardController.swift | 2 +- .../SpendDashboardTrackedSourceTests.swift | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBar/SpendDashboardController.swift b/Sources/CodexBar/SpendDashboardController.swift index 77ca72442e..a124a55015 100644 --- a/Sources/CodexBar/SpendDashboardController.swift +++ b/Sources/CodexBar/SpendDashboardController.swift @@ -568,7 +568,7 @@ enum SpendDashboardSource { contributesCostHistory: supportsCostHistory && isActive && enabled.contains(provider), costHistoryAvailable: isActive && currentCostHistoryAvailable) }) - continue + if activeAccountID != nil { continue } } let config = settings.providerConfig(for: provider) diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index 833ad71b43..e7ef394fec 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -57,6 +57,91 @@ struct SpendDashboardTrackedSourceTests { #expect(Set(rows.values.filter(\.contributesCostHistory).map(\.provider)) == [.cursor, .openrouter]) } + @Test + @MainActor + func `automatic provider fetch with dormant saved account emits one ambient current source`() throws { + let settings = testSettingsStore(suiteName: "SpendDashboardTrackedSourceTests-ambient-account") + let metadata = try #require(ProviderRegistry.shared.metadata[.cursor]) + try settings.setProviderEnabled(provider: .cursor, metadata: metadata, enabled: true) + settings.addTokenAccount(provider: .cursor, label: "Dormant manual account", token: "fixture") + let account = try #require(settings.selectedTokenAccount(for: .cursor)) + settings.cursorCookieSource = .auto + #expect(settings.effectiveSelectedTokenAccount(for: .cursor) == nil) + + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + let sources = SpendDashboardSource.trackedSources(settings: settings, store: store) + .filter { $0.provider == .cursor } + let accountID = "cursor:account:\(account.id.uuidString.lowercased())" + + #expect(Set(sources.map(\.id)) == [accountID, "cursor:current"]) + #expect(sources.first { $0.id == accountID }?.contributesCostHistory == false) + #expect(sources.first { $0.id == "cursor:current" }?.contributesCostHistory == true) + + let date = Date(timeIntervalSince1970: 1_785_974_400) + let model = SpendDashboardModel(requestedDays: 30, groups: [ + SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + SpendDashboardModel.ProviderRow( + id: "cursor", + rank: 1, + provider: .cursor, + displayName: "Cursor", + totalTokens: 900, + totalCost: 9, + coveredDayCount: 30), + ], + models: [], + dailyPoints: [], + totalTokens: 900, + totalCost: 9, + coveredDayCount: 30, + chartDomain: date...date, + modelHistoryCompleteness: .complete), + ]) + let payload = try #require(SpendDashboardPane.makeSharePayload( + model: model, + subscriptionNames: [:], + trackedSources: sources)) + + #expect(payload.providers.first?.provider == .cursor) + #expect(payload.providers.first?.estimatedCost == 9) + #expect(payload.providers.first?.totalTokens == 900) + #expect(payload.currencies.first?.estimatedCost == 9) + #expect(payload.currencies.first?.isPartial == false) + #expect(payload.totalTokens == 900) + #expect(payload.totalTokensIsPartial == false) + } + + @Test + @MainActor + func `active saved account does not duplicate provider with an ambient current source`() throws { + let settings = testSettingsStore(suiteName: "SpendDashboardTrackedSourceTests-active-account") + let metadata = try #require(ProviderRegistry.shared.metadata[.cursor]) + try settings.setProviderEnabled(provider: .cursor, metadata: metadata, enabled: true) + settings.addTokenAccount(provider: .cursor, label: "Active manual account", token: "fixture") + let account = try #require(settings.effectiveSelectedTokenAccount(for: .cursor)) + + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + let sources = SpendDashboardSource.trackedSources(settings: settings, store: store) + .filter { $0.provider == .cursor } + + #expect(sources.count == 1) + #expect(sources.first?.id == "cursor:account:\(account.id.uuidString.lowercased())") + #expect(sources.first?.contributesCostHistory == true) + #expect(!sources.contains { $0.id == "cursor:current" }) + } + @Test @MainActor func `quota usage does not claim cost history is connected`() throws { From fe748cfd264e1f9c418cccd8a38991802bab324f Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 12:41:49 -0700 Subject: [PATCH 42/51] Bound optional OpenRouter enrichment latency --- .../Resources/Plugins/openrouter.js | 4 +- .../ProviderPluginDetailsParityTests.swift | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarCore/Resources/Plugins/openrouter.js b/Sources/CodexBarCore/Resources/Plugins/openrouter.js index d24f2a33a3..1f45bed9dc 100644 --- a/Sources/CodexBarCore/Resources/Plugins/openrouter.js +++ b/Sources/CodexBarCore/Resources/Plugins/openrouter.js @@ -48,7 +48,7 @@ defineProvider({ const balance = Math.max(0, totalCredits - totalUsage); let keyData = null; try { - const keyResponse = await ctx.http.get(`${base}/key`, { timeoutSeconds: 2 }); + const keyResponse = await ctx.http.get(`${base}/key`, { timeoutSeconds: 1 }); const keyPayload = keyResponse.status === 200 ? JSON.parse(keyResponse.bodyText) : null; if (keyPayload && keyPayload.data && typeof keyPayload.data === "object" && !Array.isArray(keyPayload.data)) { @@ -72,7 +72,7 @@ defineProvider({ // when analytics is forbidden, unsupported by an endpoint override, slow, or malformed. let openRouterActivityUsage = null; try { - const activityResponse = await ctx.http.get(`${base}/activity`, { timeoutSeconds: 5 }); + const activityResponse = await ctx.http.get(`${base}/activity`, { timeoutSeconds: 1 }); if (activityResponse.status === 200) { const activityPayload = JSON.parse(activityResponse.bodyText); if (activityPayload && Array.isArray(activityPayload.data)) { diff --git a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift index 25c8f1def1..9f25a75e38 100644 --- a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift +++ b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift @@ -114,6 +114,43 @@ struct ProviderPluginDetailsParityTests { #expect(script.identity?.loginMethod == "Balance: $60.00") } + @Test + func `OpenRouter delayed optional activity stays within the fast return budget`() async throws { + let requests = PluginRequestRecorder() + let transport = ProviderHTTPTransportHandler { request in + await requests.append(request) + if request.url?.path == "/api/v1/activity" { + try await Task.sleep(for: .seconds(3)) + throw URLError(.timedOut) + } + let body = switch request.url?.path { + case "/api/v1/credits": Self.openRouterCredits + case "/api/v1/key": Self.openRouterKey + default: throw FixtureError.unexpectedURL(request.url) + } + let response = try #require(HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"])) + return (Data(body.utf8), response) + } + let clock = ContinuousClock() + let startedAt = clock.now + let script = try await ProviderPluginRuntime(bundledPlugin: "openrouter", transport: transport) + .fetchUsage(secrets: ["OPENROUTER_API_KEY": "fixture-key"]) + let elapsed = startedAt.duration(to: clock.now) + let optionalRequests = await requests.requests.filter { + ["key", "activity"].contains($0.url?.lastPathComponent) + } + + #expect(optionalRequests.count == 2) + #expect(optionalRequests.allSatisfy { $0.timeoutInterval == 1 }) + #expect(elapsed < .seconds(2)) + #expect(script.openRouterActivityUsage == nil) + #expect(script.identity?.loginMethod == "Balance: $60.00") + } + @Test func `ClawRouter fixture matches stable cut-over details`() async throws { let transport = Self.transport { request in @@ -187,6 +224,8 @@ struct ProviderPluginDetailsParityTests { (overridden ? "https://codexbar.example" : nil)) #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) #expect(recorded[2].value(forHTTPHeaderField: "X-Title") == nil) + #expect(recorded[1].timeoutInterval == 1) + #expect(recorded[2].timeoutInterval == 1) } @Test From d31c88702835ec7cf48d60b563984598934a2a88 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 12:52:31 -0700 Subject: [PATCH 43/51] Keep OpenRouter activity responsive under load --- Sources/CodexBarCore/Resources/Plugins/openrouter.js | 2 +- .../CodexBarTests/ProviderPluginDetailsParityTests.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/CodexBarCore/Resources/Plugins/openrouter.js b/Sources/CodexBarCore/Resources/Plugins/openrouter.js index 1f45bed9dc..daee05ba76 100644 --- a/Sources/CodexBarCore/Resources/Plugins/openrouter.js +++ b/Sources/CodexBarCore/Resources/Plugins/openrouter.js @@ -72,7 +72,7 @@ defineProvider({ // when analytics is forbidden, unsupported by an endpoint override, slow, or malformed. let openRouterActivityUsage = null; try { - const activityResponse = await ctx.http.get(`${base}/activity`, { timeoutSeconds: 1 }); + const activityResponse = await ctx.http.get(`${base}/activity`, { timeoutSeconds: 2 }); if (activityResponse.status === 200) { const activityPayload = JSON.parse(activityResponse.bodyText); if (activityPayload && Array.isArray(activityPayload.data)) { diff --git a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift index 9f25a75e38..ec6206ad12 100644 --- a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift +++ b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift @@ -120,7 +120,7 @@ struct ProviderPluginDetailsParityTests { let transport = ProviderHTTPTransportHandler { request in await requests.append(request) if request.url?.path == "/api/v1/activity" { - try await Task.sleep(for: .seconds(3)) + try await Task.sleep(for: .seconds(4)) throw URLError(.timedOut) } let body = switch request.url?.path { @@ -145,8 +145,8 @@ struct ProviderPluginDetailsParityTests { } #expect(optionalRequests.count == 2) - #expect(optionalRequests.allSatisfy { $0.timeoutInterval == 1 }) - #expect(elapsed < .seconds(2)) + #expect(optionalRequests.map(\.timeoutInterval) == [1, 2]) + #expect(elapsed < .seconds(3)) #expect(script.openRouterActivityUsage == nil) #expect(script.identity?.loginMethod == "Balance: $60.00") } @@ -225,7 +225,7 @@ struct ProviderPluginDetailsParityTests { #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) #expect(recorded[2].value(forHTTPHeaderField: "X-Title") == nil) #expect(recorded[1].timeoutInterval == 1) - #expect(recorded[2].timeoutInterval == 1) + #expect(recorded[2].timeoutInterval == 2) } @Test From 614c5b2b523c6fa80b7dc7a5b2759caa0bf025c5 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sat, 8 Aug 2026 13:16:13 -0700 Subject: [PATCH 44/51] Update OpenRouter timeout golden --- Tests/CodexBarTests/OpenRouterUsageStatsTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift index acc0ad21c2..cf1cefa47c 100644 --- a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift +++ b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift @@ -79,11 +79,11 @@ struct OpenRouterPluginGoldenTests { #expect(recorded[0].timeoutInterval == 15) #expect(recorded[0].value(forHTTPHeaderField: "HTTP-Referer") == "https://codexbar.example") #expect(recorded[0].value(forHTTPHeaderField: "X-Title") == "CodexBar QA") - #expect(recorded[1].timeoutInterval == 2) + #expect(recorded[1].timeoutInterval == 1) #expect(recorded[1].value(forHTTPHeaderField: "HTTP-Referer") == nil) #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) #expect(recorded[2].url?.path.hasSuffix("/activity") == true) - #expect(recorded[2].timeoutInterval == 5) + #expect(recorded[2].timeoutInterval == 1) #expect(recorded[2].value(forHTTPHeaderField: "HTTP-Referer") == nil) #expect(recorded[2].value(forHTTPHeaderField: "X-Title") == nil) #expect(usage.detailRow(label: "Today")?.value == "$0.12") From b1a8742faee53e29fd25e4e212ee7ad9f4d685a1 Mon Sep 17 00:00:00 2001 From: Alec Gutman <44984861+Chipagosfinest@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:03:16 -0700 Subject: [PATCH 45/51] Fix OpenRouter plugin lint after merge --- Sources/CodexBarCore/Resources/Plugins/openrouter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarCore/Resources/Plugins/openrouter.js b/Sources/CodexBarCore/Resources/Plugins/openrouter.js index 30dc8779c2..6adb97212f 100644 --- a/Sources/CodexBarCore/Resources/Plugins/openrouter.js +++ b/Sources/CodexBarCore/Resources/Plugins/openrouter.js @@ -103,7 +103,7 @@ defineProvider({ const activityPayload = JSON.parse(activityResponse.bodyText); if (activityPayload && Array.isArray(activityPayload.data)) { openRouterActivityUsage = { - data: activityPayload.data.map(item => ({ + data: activityPayload.data.map((item) => ({ date: item.date, model: item.model, prompt_tokens: item.prompt_tokens, @@ -115,7 +115,7 @@ defineProvider({ }; } } - } catch (_) {} + } catch {} function resetWindowUsage(reset) { const windowKey = From 2194fc72c45a02be6f7de176025b518f2b0bbb98 Mon Sep 17 00:00:00 2001 From: Alec Gutman <44984861+Chipagosfinest@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:16:37 -0700 Subject: [PATCH 46/51] Defer OpenRouter activity enrichment --- .../OpenRouterProviderImplementation.swift | 3 +- Sources/CodexBar/SpendDashboardModel.swift | 8 +- Sources/CodexBar/UsageStore+Refresh.swift | 14 +- .../CodexBar/UsageStore+TokenAccounts.swift | 7 +- Sources/CodexBar/UsageStore+TokenCost.swift | 24 +- .../ProviderPluginSnapshotMapper.swift | 77 ---- .../OpenRouterActivityUsageSnapshot.swift | 332 ------------------ .../OpenRouterProviderDescriptor.swift | 6 +- .../Resources/Plugins/openrouter.js | 26 -- Sources/CodexBarCore/UsageFetcher.swift | 14 - .../OpenRouterActivityUsageTests.swift | 282 --------------- .../OpenRouterUsageStatsTests.swift | 12 +- .../ProviderArchitectureGatekeeperTests.swift | 105 ++---- .../ProviderPluginDetailsParityTests.swift | 78 +--- ...endDashboardOpenRouterDateTruthTests.swift | 84 ----- .../SpendDashboardTrackedSourceTests.swift | 22 +- docs/plugin-conversion-matrix.md | 2 +- docs/plugin-prototype.md | 2 +- .../spend-dashboard-proof/README.md | 4 +- 19 files changed, 67 insertions(+), 1035 deletions(-) delete mode 100644 Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift delete mode 100644 Tests/CodexBarTests/OpenRouterActivityUsageTests.swift delete mode 100644 Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift diff --git a/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift b/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift index bcf16bca05..b285e6710c 100644 --- a/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift +++ b/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift @@ -39,8 +39,7 @@ struct OpenRouterProviderImplementation: ProviderImplementation { title: "API key", subtitle: "Stored in ~/.codexbar/config.json. " + "Get your key from openrouter.ai/settings/keys and set a key spending limit " - + "there to enable API key quota tracking. A Management API key also enables " - + "per-model spend and token history for the last 30 completed UTC days.", + + "there to enable API key quota tracking.", kind: .secure, placeholder: "sk-or-v1-...", binding: context.providerConfigBinding(.apiKey), diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index 9fce6d8dca..e7bf4cb5ce 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -600,9 +600,7 @@ struct SpendDashboardModel: Equatable, Sendable { { let bucketCalendar = Self.bucketCalendar(for: input.provider, displayCalendar: displayCalendar) let snapshotDay = bucketCalendar.startOfDay(for: input.snapshot.updatedAt) - let bucketEnd = input.provider == .openrouter - ? bucketCalendar.date(byAdding: .day, value: -1, to: snapshotDay) ?? snapshotDay - : snapshotDay + let bucketEnd = snapshotDay let scanEnd = displayCalendar.startOfDay(for: bucketEnd) let scanDays = max(1, input.snapshot.historyDays) let bucketStart = bucketCalendar.date(byAdding: .day, value: -(scanDays - 1), to: bucketEnd) ?? bucketEnd @@ -657,8 +655,8 @@ struct SpendDashboardModel: Equatable, Sendable { } private static func bucketCalendar(for provider: UsageProvider, displayCalendar: Calendar) -> Calendar { - guard provider == .mistral || provider == .openrouter else { return displayCalendar } - // Mistral and OpenRouter label daily buckets and snapshot coverage by UTC day. Map each UTC boundary + guard provider == .mistral else { return displayCalendar } + // Mistral labels daily buckets and snapshot coverage by UTC day. Map each UTC boundary // into the containing local dashboard day instead of reinterpreting the label as a local date. return self.gregorianCalendar(timeZone: TimeZone(secondsFromGMT: 0) ?? .gmt) } diff --git a/Sources/CodexBar/UsageStore+Refresh.swift b/Sources/CodexBar/UsageStore+Refresh.swift index 68acb06bc9..523a563e7a 100644 --- a/Sources/CodexBar/UsageStore+Refresh.swift +++ b/Sources/CodexBar/UsageStore+Refresh.swift @@ -701,7 +701,7 @@ extension UsageStore { } else { self.lastKnownResetSnapshots[provider.instanceID] } - let profileStable = self.preservingProviderEnrichment(in: accountScoped, provider: provider) + let profileStable = self.preservingDeepSeekProfileCatalog(in: accountScoped, provider: provider) let stabilized = Self.commandCodeSnapshotResolvingDepletionOnEnrichmentFailure( current: profileStable, previous: self.snapshots[provider.instanceID]) @@ -855,17 +855,6 @@ extension UsageStore { return snapshot.preservingDeepSeekPlatformProfiles(from: self.presentationSnapshot(for: .deepseek)) } - private func preservingProviderEnrichment( - in snapshot: UsageSnapshot, - provider: UsageProvider) -> UsageSnapshot - { - let profileStable = self.preservingDeepSeekProfileCatalog(in: snapshot, provider: provider) - guard provider == .openrouter else { return profileStable } - return self.preservingOpenRouterActivityIfCurrent( - profileStable, - previous: self.snapshots[provider.instanceID]) - } - private func bindCodexFailurePublicationOwner( provider: UsageProvider, expectedGuard: CodexAccountScopedRefreshGuard?) @@ -889,6 +878,7 @@ extension UsageStore { usage: UsageSnapshot, expectedGuard: CodexAccountScopedRefreshGuard?) -> UsageSnapshot { + // Provider-specific by design: Codex identity repair preserves the expected account email on Codex snapshots. guard provider == .codex, CodexIdentityResolver.normalizeEmail(usage.accountEmail(for: .codex)) == nil, let accountEmail = CodexIdentityResolver.normalizeEmail(expectedGuard?.accountKey) diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift index 6d8283c17a..fdd16e3f66 100644 --- a/Sources/CodexBar/UsageStore+TokenAccounts.swift +++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift @@ -1452,12 +1452,7 @@ extension UsageStore { ? labeled.preservingDeepSeekPlatformProfiles( from: self.presentationSnapshot(for: .deepseek)) : labeled - let historyStable = provider == .openrouter - ? self.preservingOpenRouterActivityIfCurrent( - profileStable, - previous: self.snapshots[provider.instanceID]) - : profileStable - let backfilled = historyStable.backfillingResetTimes( + let backfilled = profileStable.backfillingResetTimes( from: self.lastKnownResetSnapshots[provider.instanceID]) let warningAccountDiscriminator = Self.warningTokenAccountDiscriminator(account) self.handleQuotaWarningTransitions( diff --git a/Sources/CodexBar/UsageStore+TokenCost.swift b/Sources/CodexBar/UsageStore+TokenCost.swift index d6142e75b0..646b2fdbc0 100644 --- a/Sources/CodexBar/UsageStore+TokenCost.swift +++ b/Sources/CodexBar/UsageStore+TokenCost.swift @@ -434,8 +434,6 @@ extension UsageStore { switch provider { case .openai: snapshot?.openAIAPIUsage?.toCostUsageTokenSnapshot() - case .openrouter: - snapshot?.openRouterActivityUsage?.toCostUsageTokenSnapshot() case .mistral: snapshot?.mistralUsage?.toCostUsageTokenSnapshot(historyDays: self.settings.effectiveCostUsageHistoryDays) case .opencodego: @@ -453,7 +451,7 @@ extension UsageStore { nonisolated static func tokenCostRequiresProviderSnapshot(_ provider: UsageProvider) -> Bool { switch provider { - case .mistral, .openai, .openrouter, .opencodego: + case .mistral, .openai, .opencodego: true default: false @@ -503,24 +501,4 @@ extension UsageStore { nonisolated static func tokenCostNoDataMessage(for provider: UsageProvider) -> String { ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.noDataMessage() } - - func preservingOpenRouterActivityIfCurrent( - _ snapshot: UsageSnapshot, - previous: UsageSnapshot?) -> UsageSnapshot - { - guard snapshot.openRouterActivityUsage == nil, - self.tokenSnapshotPublicationForCurrentCredentialScope(for: .openrouter)?.snapshot != nil, - let activity = previous?.openRouterActivityUsage - else { return snapshot } - return snapshot.withOpenRouterActivityUsage(activity) - } - - private func tokenSnapshotPublicationForCurrentCredentialScope( - for provider: UsageProvider) -> TokenSnapshotPublication? - { - guard let publication = self.tokenSnapshotPublications[provider.instanceID], - publication.providerConfigRevision == self.settings.providerConfigRevision(for: provider) - else { return nil } - return publication - } } diff --git a/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift b/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift index 1f603ca16f..365f5d40d5 100644 --- a/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift +++ b/Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift @@ -17,9 +17,6 @@ enum ProviderPluginSnapshotMapper { let tertiary = try self.window(value, property: "tertiary") let extraRateWindows = try self.extraWindows(value) let providerCost = try self.cost(value, now: now) - let openRouterActivityUsage = provider == .openrouter - ? self.openRouterActivityUsage(value, now: now) - : nil let details = try self.details(value) let identity = try self.identity(value, provider: provider) let subscriptionRenewsAt = try self.optionalDate(value, property: "subscriptionRenewsAt") @@ -40,7 +37,6 @@ enum ProviderPluginSnapshotMapper { extraRateWindows: extraRateWindows, providerCost: providerCost, details: details, - openRouterActivityUsage: openRouterActivityUsage, subscriptionExpiresAt: subscriptionExpiresAt, subscriptionRenewsAt: subscriptionRenewsAt, updatedAt: now, @@ -48,55 +44,6 @@ enum ProviderPluginSnapshotMapper { dataConfidence: dataConfidence) } - /// Activity is an optional management-key enrichment. Malformed or unsupported - /// activity must not invalidate an otherwise healthy provider snapshot. - private static func openRouterActivityUsage( - _ root: any ProviderPluginValue, - now: Date) -> OpenRouterActivityUsageSnapshot? - { - guard let value = root.property("openRouterActivityUsage"), - value.isObject, - !value.isArray, - !value.isNull, - let rows = value.property("data"), - rows.isArray, - let rawCount = rows.property("length")?.int32Value(), - rawCount >= 0, - rawCount <= 10000 - else { return nil } - - do { - let items: [[String: Any]] = try (0.. UsageDataConfidence { guard let value = root.property("dataConfidence"), !value.isUndefined, !value.isNull else { return .unknown @@ -344,30 +291,6 @@ enum ProviderPluginSnapshotMapper { return Int(number) } - private static func requiredNonnegativeInteger( - _ value: any ProviderPluginValue, - property: String, - path: String) throws -> Int - { - let number = try self.requiredFiniteNumber(value, property: property, path: path) - guard number.rounded() == number, number >= 0, number <= Double(Int.max) else { - throw ProviderPluginError.invalidSnapshot("\(path).\(property) must be a nonnegative integer") - } - return Int(number) - } - - private static func requiredNonnegativeNumber( - _ value: any ProviderPluginValue, - property: String, - path: String) throws -> Double - { - let number = try self.requiredFiniteNumber(value, property: property, path: path) - guard number >= 0 else { - throw ProviderPluginError.invalidSnapshot("\(path).\(property) must be nonnegative") - } - return number - } - private static func requiredString( _ value: any ProviderPluginValue, property: String, diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift deleted file mode 100644 index 75b03b7ce2..0000000000 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterActivityUsageSnapshot.swift +++ /dev/null @@ -1,332 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -/// OpenRouter's account activity for completed UTC days, aggregated by routed model slug. -public struct OpenRouterActivityUsageSnapshot: Codable, Equatable, Sendable { - public struct ModelBreakdown: Codable, Equatable, Sendable, Identifiable { - public let model: String - public let promptTokens: Int - public let completionTokens: Int - /// Informational only. OpenRouter includes these tokens in `completionTokens`. - public let reasoningTokens: Int - public let requests: Int - public let usageUSD: Double - - public var id: String { - self.model - } - - public var totalTokens: Int { - self.promptTokens + self.completionTokens - } - } - - public struct DailyBucket: Codable, Equatable, Sendable, Identifiable { - public let date: String - public let promptTokens: Int - public let completionTokens: Int - /// Informational only. OpenRouter includes these tokens in `completionTokens`. - public let reasoningTokens: Int - public let requests: Int - public let usageUSD: Double - public let models: [ModelBreakdown] - - public var id: String { - self.date - } - - public var totalTokens: Int { - self.promptTokens + self.completionTokens - } - } - - public let daily: [DailyBucket] - public let historyDays: Int - public let updatedAt: Date - - private enum CodingKeys: String, CodingKey { - case daily - case historyDays - case updatedAt - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let daily = try container.decode([DailyBucket].self, forKey: .daily) - let historyDays = try container.decode(Int.self, forKey: .historyDays) - let updatedAt = try container.decode(Date.self, forKey: .updatedAt) - do { - try Self.validate(daily: daily, historyDays: historyDays, updatedAt: updatedAt) - } catch { - throw DecodingError.dataCorruptedError( - forKey: .daily, - in: container, - debugDescription: "OpenRouter activity history is invalid") - } - self.daily = daily - self.historyDays = historyDays - self.updatedAt = updatedAt - } - - /// Decodes the documented `/api/v1/activity` envelope and keeps only the requested - /// completed UTC-day window. OpenRouter currently exposes at most 30 completed days. - public init(data: Data, now: Date, historyDays: Int = 30) throws { - let response = try JSONDecoder().decode(Response.self, from: data) - let days = max(1, min(30, historyDays)) - let calendar = Self.utcCalendar - let today = calendar.startOfDay(for: now) - guard let firstDay = calendar.date(byAdding: .day, value: -days, to: today) else { - throw DecodeError.invalidDateWindow - } - - var accumulators: [BucketKey: Accumulator] = [:] - for item in response.data { - guard let date = Self.date(from: item.date) else { throw DecodeError.invalidActivityItem } - guard date >= firstDay, date < today else { continue } - let model = item.model.trimmingCharacters(in: .whitespacesAndNewlines) - guard !model.isEmpty, - item.promptTokens >= 0, - item.completionTokens >= 0, - item.reasoningTokens >= 0, - item.requests >= 0, - item.usage.isFinite, - item.usage >= 0 - else { - throw DecodeError.invalidActivityItem - } - let key = BucketKey(date: item.date, model: model) - try accumulators[key, default: Accumulator()].add(item) - } - - let modelBuckets = Dictionary(grouping: accumulators, by: { $0.key.date }) - let daily = try modelBuckets.map { date, values in - let models = try values.map { key, accumulator in - try accumulator.modelBreakdown(model: key.model) - }.sorted(by: Self.modelSort) - let promptTokens = try Self.sum(models.map(\.promptTokens)) - let completionTokens = try Self.sum(models.map(\.completionTokens)) - _ = try Self.add(promptTokens, completionTokens) - return try DailyBucket( - date: date, - promptTokens: promptTokens, - completionTokens: completionTokens, - reasoningTokens: Self.sum(models.map(\.reasoningTokens)), - requests: Self.sum(models.map(\.requests)), - usageUSD: Self.sum(models.map(\.usageUSD)), - models: models) - }.sorted { $0.date < $1.date } - try Self.validate(daily: daily, historyDays: days, updatedAt: now) - self.daily = daily - self.historyDays = days - self.updatedAt = now - } - - public func toCostUsageTokenSnapshot() -> CostUsageTokenSnapshot { - let daily = self.daily.map { bucket in - CostUsageDailyReport.Entry( - date: bucket.date, - inputTokens: bucket.promptTokens, - outputTokens: bucket.completionTokens, - totalTokens: bucket.totalTokens, - requestCount: bucket.requests, - costUSD: bucket.usageUSD, - modelsUsed: bucket.models.map(\.model), - modelBreakdowns: bucket.models.map { model in - CostUsageDailyReport.ModelBreakdown( - modelName: model.model, - costUSD: model.usageUSD, - totalTokens: model.totalTokens, - requestCount: model.requests) - }) - } - return CostUsageTokenSnapshot( - sessionTokens: nil, - sessionCostUSD: nil, - sessionRequests: nil, - last30DaysTokens: self.daily.reduce(0) { $0 + $1.totalTokens }, - last30DaysCostUSD: self.daily.reduce(0) { $0 + $1.usageUSD }, - last30DaysRequests: self.daily.reduce(0) { $0 + $1.requests }, - currencyCode: "USD", - historyDays: self.historyDays, - historyCoverageIsEstablished: true, - historyLabel: "OpenRouter account activity · Last \(self.historyDays) completed UTC days", - daily: daily, - updatedAt: self.updatedAt) - } - - private struct Response: Decodable { - let data: [Item] - } - - private struct Item: Decodable { - let date: String - let model: String - let promptTokens: Int - let completionTokens: Int - let reasoningTokens: Int - let requests: Int - let usage: Double - - private enum CodingKeys: String, CodingKey { - case date - case model - case promptTokens = "prompt_tokens" - case completionTokens = "completion_tokens" - case reasoningTokens = "reasoning_tokens" - case requests - case usage - } - } - - private struct BucketKey: Hashable { - let date: String - let model: String - } - - private struct Accumulator { - var promptTokens = 0 - var completionTokens = 0 - var reasoningTokens = 0 - var requests = 0 - var usageUSD = 0.0 - - mutating func add(_ item: Item) throws { - self.promptTokens = try OpenRouterActivityUsageSnapshot.add(self.promptTokens, item.promptTokens) - self.completionTokens = try OpenRouterActivityUsageSnapshot.add( - self.completionTokens, - item.completionTokens) - self.reasoningTokens = try OpenRouterActivityUsageSnapshot.add( - self.reasoningTokens, - item.reasoningTokens) - self.requests = try OpenRouterActivityUsageSnapshot.add(self.requests, item.requests) - self.usageUSD += item.usage - guard self.usageUSD.isFinite else { throw DecodeError.numericOverflow } - } - - func modelBreakdown(model: String) throws -> ModelBreakdown { - _ = try OpenRouterActivityUsageSnapshot.add(self.promptTokens, self.completionTokens) - return ModelBreakdown( - model: model, - promptTokens: self.promptTokens, - completionTokens: self.completionTokens, - reasoningTokens: self.reasoningTokens, - requests: self.requests, - usageUSD: self.usageUSD) - } - } - - private enum DecodeError: Error { - case invalidDateWindow - case invalidActivityItem - case numericOverflow - } - - private static let utcCalendar: Calendar = { - var calendar = Calendar(identifier: .gregorian) - calendar.locale = Locale(identifier: "en_US_POSIX") - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - return calendar - }() - - private static func date(from key: String) -> Date? { - guard key.range(of: #"^\d{4}-\d{2}-\d{2}$"#, options: .regularExpression) != nil else { return nil } - let parts = key.split(separator: "-").compactMap { Int($0) } - guard parts.count == 3, - let date = self.utcCalendar.date(from: DateComponents( - timeZone: self.utcCalendar.timeZone, - year: parts[0], - month: parts[1], - day: parts[2])), - self.dateKey(date) == key - else { return nil } - return date - } - - private static func dateKey(_ date: Date) -> String { - let components = self.utcCalendar.dateComponents([.year, .month, .day], from: date) - return String(format: "%04d-%02d-%02d", components.year ?? 0, components.month ?? 0, components.day ?? 0) - } - - private static func add(_ lhs: Int, _ rhs: Int) throws -> Int { - let result = lhs.addingReportingOverflow(rhs) - guard !result.overflow else { throw DecodeError.numericOverflow } - return result.partialValue - } - - private static func sum(_ values: [Int]) throws -> Int { - try values.reduce(0) { try self.add($0, $1) } - } - - private static func sum(_ values: [Double]) throws -> Double { - let total = values.reduce(0, +) - guard total.isFinite else { throw DecodeError.numericOverflow } - return total - } - - private static func validate( - daily: [DailyBucket], - historyDays: Int, - updatedAt: Date) throws - { - guard (1...30).contains(historyDays), daily.count <= historyDays else { - throw DecodeError.invalidActivityItem - } - let today = self.utcCalendar.startOfDay(for: updatedAt) - guard let firstDay = self.utcCalendar.date(byAdding: .day, value: -historyDays, to: today) else { - throw DecodeError.invalidDateWindow - } - var dates = Set() - for bucket in daily { - guard let date = self.date(from: bucket.date), - date >= firstDay, - date < today, - dates.insert(bucket.date).inserted, - bucket.promptTokens >= 0, - bucket.completionTokens >= 0, - bucket.reasoningTokens >= 0, - bucket.requests >= 0, - bucket.usageUSD.isFinite, - bucket.usageUSD >= 0, - !bucket.models.isEmpty, - bucket.models.count <= 10000 - else { throw DecodeError.invalidActivityItem } - _ = try self.add(bucket.promptTokens, bucket.completionTokens) - - var modelNames = Set() - for model in bucket.models { - guard !model.model.isEmpty, - modelNames.insert(model.model).inserted, - model.promptTokens >= 0, - model.completionTokens >= 0, - model.reasoningTokens >= 0, - model.requests >= 0, - model.usageUSD.isFinite, - model.usageUSD >= 0 - else { throw DecodeError.invalidActivityItem } - _ = try self.add(model.promptTokens, model.completionTokens) - } - let promptTokens = try self.sum(bucket.models.map(\.promptTokens)) - let completionTokens = try self.sum(bucket.models.map(\.completionTokens)) - let reasoningTokens = try self.sum(bucket.models.map(\.reasoningTokens)) - let requests = try self.sum(bucket.models.map(\.requests)) - let usageUSD = try self.sum(bucket.models.map(\.usageUSD)) - guard bucket.promptTokens == promptTokens, - bucket.completionTokens == completionTokens, - bucket.reasoningTokens == reasoningTokens, - bucket.requests == requests, - bucket.usageUSD == usageUSD - else { throw DecodeError.invalidActivityItem } - } - _ = try self.sum(daily.map { try self.add($0.promptTokens, $0.completionTokens) }) - _ = try self.sum(daily.map(\.requests)) - _ = try self.sum(daily.map(\.usageUSD)) - } - - private static func modelSort(_ lhs: ModelBreakdown, _ rhs: ModelBreakdown) -> Bool { - if lhs.usageUSD != rhs.usageUSD { return lhs.usageUSD > rhs.usageUSD } - if lhs.totalTokens != rhs.totalTokens { return lhs.totalTokens > rhs.totalTokens } - return lhs.model.localizedStandardCompare(rhs.model) == .orderedAscending - } -} diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift index 9388daec09..6fe897049b 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift @@ -63,10 +63,8 @@ public enum OpenRouterProviderDescriptor { ], widgetColor: ProviderColor(red: 111 / 255, green: 66 / 255, blue: 193 / 255)), tokenCost: ProviderTokenCostConfig( - supportsTokenCost: true, - noDataMessage: { "OpenRouter activity history requires a Management API key." }, - menuHintLines: [.literal("Reported by OpenRouter for completed UTC days.")], - primaryValue: .latestDaily), + supportsTokenCost: false, + noDataMessage: { "OpenRouter cost summary is not yet supported." }), presentation: ProviderUsagePresentation( menuCard: ProviderMenuCardPresentation( showsCreditsSection: false, diff --git a/Sources/CodexBarCore/Resources/Plugins/openrouter.js b/Sources/CodexBarCore/Resources/Plugins/openrouter.js index 6adb97212f..5a38063f96 100644 --- a/Sources/CodexBarCore/Resources/Plugins/openrouter.js +++ b/Sources/CodexBarCore/Resources/Plugins/openrouter.js @@ -92,31 +92,6 @@ defineProvider({ } if (!keyData && !keyDegradation) keyDegradation = "Response was unavailable"; - // /activity is management-key-only and optional. Credits and key quota remain valid - // when analytics is forbidden, unsupported by an endpoint override, slow, or malformed. - let openRouterActivityUsage = null; - try { - const activityResponse = await ctx.http.get(`${base}/activity`, { - timeoutSeconds: optionalRequestTimeoutSeconds, - }); - if (activityResponse.status === 200) { - const activityPayload = JSON.parse(activityResponse.bodyText); - if (activityPayload && Array.isArray(activityPayload.data)) { - openRouterActivityUsage = { - data: activityPayload.data.map((item) => ({ - date: item.date, - model: item.model, - prompt_tokens: item.prompt_tokens, - completion_tokens: item.completion_tokens, - reasoning_tokens: item.reasoning_tokens, - requests: item.requests, - usage: item.usage, - })), - }; - } - } - } catch {} - function resetWindowUsage(reset) { const windowKey = reset === "daily" @@ -224,7 +199,6 @@ defineProvider({ details, }; if (primary) result.primary = primary; - if (openRouterActivityUsage) result.openRouterActivityUsage = openRouterActivityUsage; return result; }, }); diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index 7a9f088c9d..389470e7bd 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -151,7 +151,6 @@ public struct UsageSnapshot: Codable, Sendable { public let deepseekPlatformProfiles: [DeepSeekPlatformProfile] public let opencodegoUsage: OpenCodeGoUsageSnapshot? public let openAIAPIUsage: OpenAIAPIUsageSnapshot? - public let openRouterActivityUsage: OpenRouterActivityUsageSnapshot? public let codexResetCredits: CodexRateLimitResetCreditsSnapshot? public let mistralUsage: MistralUsageSnapshot? /// Live-only marker for optional Command Code subscription lookup failure. @@ -174,7 +173,6 @@ public struct UsageSnapshot: Codable, Sendable { case providerCost case details case openAIAPIUsage - case openRouterActivityUsage case codexResetCredits case mistralUsage case subscriptionExpiresAt @@ -198,7 +196,6 @@ public struct UsageSnapshot: Codable, Sendable { deepseekPlatformProfiles: [DeepSeekPlatformProfile] = [], opencodegoUsage: OpenCodeGoUsageSnapshot? = nil, openAIAPIUsage: OpenAIAPIUsageSnapshot? = nil, - openRouterActivityUsage: OpenRouterActivityUsageSnapshot? = nil, codexResetCredits: CodexRateLimitResetCreditsSnapshot? = nil, mistralUsage: MistralUsageSnapshot? = nil, commandCodeSubscriptionEnrichmentUnavailable: Bool = false, @@ -223,7 +220,6 @@ public struct UsageSnapshot: Codable, Sendable { self.deepseekPlatformProfiles = deepseekPlatformProfiles self.opencodegoUsage = opencodegoUsage self.openAIAPIUsage = openAIAPIUsage - self.openRouterActivityUsage = openRouterActivityUsage self.codexResetCredits = codexResetCredits self.mistralUsage = mistralUsage self.commandCodeSubscriptionEnrichmentUnavailable = commandCodeSubscriptionEnrichmentUnavailable @@ -273,9 +269,6 @@ public struct UsageSnapshot: Codable, Sendable { self.deepseekPlatformProfiles = [] // Live-only browser profile catalog self.opencodegoUsage = nil // Not persisted, fetched fresh each time self.openAIAPIUsage = try container.decodeIfPresent(OpenAIAPIUsageSnapshot.self, forKey: .openAIAPIUsage) - self.openRouterActivityUsage = try container.decodeIfPresent( - OpenRouterActivityUsageSnapshot.self, - forKey: .openRouterActivityUsage) self.codexResetCredits = try container.decodeIfPresent( CodexRateLimitResetCreditsSnapshot.self, forKey: .codexResetCredits) @@ -321,7 +314,6 @@ public struct UsageSnapshot: Codable, Sendable { try container.encode(self.details, forKey: .details) } try container.encodeIfPresent(self.openAIAPIUsage, forKey: .openAIAPIUsage) - try container.encodeIfPresent(self.openRouterActivityUsage, forKey: .openRouterActivityUsage) try container.encodeIfPresent(self.codexResetCredits, forKey: .codexResetCredits) try container.encodeIfPresent(self.mistralUsage, forKey: .mistralUsage) try container.encodeIfPresent(self.subscriptionExpiresAt, forKey: .subscriptionExpiresAt) @@ -396,10 +388,6 @@ public struct UsageSnapshot: Codable, Sendable { self.replacing(dataConfidence: .value(dataConfidence)) } - public func withOpenRouterActivityUsage(_ activity: OpenRouterActivityUsageSnapshot) -> UsageSnapshot { - self.replacing(openRouterActivityUsage: .value(activity)) - } - public func scoped(to provider: UsageProvider) -> UsageSnapshot { guard let identity else { return self } let scopedIdentity = identity.scoped(to: provider) @@ -479,7 +467,6 @@ public struct UsageSnapshot: Codable, Sendable { details: Replacement<[ProviderDetailSection]> = .unchanged, deepseekDetailedUsageState: Replacement = .unchanged, deepseekPlatformProfiles: Replacement<[DeepSeekPlatformProfile]> = .unchanged, - openRouterActivityUsage: Replacement = .unchanged, codexResetCredits: Replacement = .unchanged, subscriptionExpiresAt: Replacement = .unchanged, subscriptionRenewsAt: Replacement = .unchanged, @@ -497,7 +484,6 @@ public struct UsageSnapshot: Codable, Sendable { deepseekPlatformProfiles: deepseekPlatformProfiles.resolving(self.deepseekPlatformProfiles), opencodegoUsage: self.opencodegoUsage, openAIAPIUsage: self.openAIAPIUsage, - openRouterActivityUsage: openRouterActivityUsage.resolving(self.openRouterActivityUsage), codexResetCredits: codexResetCredits.resolving(self.codexResetCredits), mistralUsage: self.mistralUsage, commandCodeSubscriptionEnrichmentUnavailable: self.commandCodeSubscriptionEnrichmentUnavailable, diff --git a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift b/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift deleted file mode 100644 index 1ad0817472..0000000000 --- a/Tests/CodexBarTests/OpenRouterActivityUsageTests.swift +++ /dev/null @@ -1,282 +0,0 @@ -import Foundation -import Testing -@testable import CodexBar -@testable import CodexBarCore - -struct OpenRouterActivityUsageTests { - @Test - func `activity rows become thirty day daily and model cost history`() throws { - let activity = try OpenRouterActivityUsageSnapshot( - data: Data(Self.activityFixture.utf8), - now: Self.now, - historyDays: 30) - - let snapshot = activity.toCostUsageTokenSnapshot() - - #expect(snapshot.historyDays == 30) - #expect(snapshot.currencyCode == "USD") - #expect(snapshot.last30DaysTokens == 325) - #expect(abs((snapshot.last30DaysCostUSD ?? -1) - 0.05) < 0.000_000_001) - #expect(snapshot.last30DaysRequests == 10) - #expect(snapshot.daily.map(\.date) == ["2026-08-05", "2026-08-06"]) - - let firstDay = try #require(snapshot.daily.first) - #expect(firstDay.inputTokens == 60) - #expect(firstDay.outputTokens == 145) - // OpenRouter documents reasoning_tokens as a subset of completion_tokens. - // Counting it again would overstate this day by 30 tokens. - #expect(firstDay.totalTokens == 205) - #expect(firstDay.requestCount == 7) - #expect(abs((firstDay.costUSD ?? -1) - 0.02) < 0.000_000_001) - #expect(firstDay.modelsUsed == ["anthropic/claude-sonnet-4-6"]) - - let model = try #require(firstDay.modelBreakdowns?.only) - #expect(model.modelName == "anthropic/claude-sonnet-4-6") - #expect(model.totalTokens == 205) - #expect(model.requestCount == 7) - #expect(abs((model.costUSD ?? -1) - 0.02) < 0.000_000_001) - } - - @Test - func `duplicate routed models aggregate without adding estimated BYOK spend`() throws { - let activity = try OpenRouterActivityUsageSnapshot( - data: Data(Self.activityFixture.utf8), - now: Self.now) - - let snapshot = activity.toCostUsageTokenSnapshot() - let firstDay = try #require(snapshot.daily.first) - let claudeRows = try #require(firstDay.modelBreakdowns?.filter { - $0.modelName == "anthropic/claude-sonnet-4-6" - }) - - #expect(claudeRows.count == 1) - #expect(claudeRows.only?.totalTokens == 205) - #expect(claudeRows.only?.requestCount == 7) - #expect(abs((claudeRows.only?.costUSD ?? -1) - 0.02) < 0.000_000_001) - // The fixture also carries $1.25 of byok_usage_inference. `usage` is the - // OpenRouter spend field; combining the two would fabricate $1.27. - #expect(abs((firstDay.costUSD ?? -1) - 0.02) < 0.000_000_001) - } - - @Test - func `persisted activity validates before entering spend totals`() throws { - let activity = try OpenRouterActivityUsageSnapshot( - data: Data(Self.activityFixture.utf8), - now: Self.now) - let usage = UsageSnapshot( - primary: nil, - secondary: nil, - openRouterActivityUsage: activity, - updatedAt: Self.now) - let encoded = try JSONEncoder().encode(usage) - - let decoded = try JSONDecoder().decode(UsageSnapshot.self, from: encoded) - #expect(decoded.openRouterActivityUsage == activity) - - let encodedText = try #require(String(data: encoded, encoding: .utf8)) - let invalidText = encodedText.replacingOccurrences( - of: #""promptTokens":60"#, - with: #""promptTokens":-1"#) - #expect(invalidText != encodedText) - let invalid = Data(invalidText.utf8) - #expect(throws: DecodingError.self) { - _ = try JSONDecoder().decode(UsageSnapshot.self, from: invalid) - } - } - - @Test - func `non OpenRouter plugins cannot inject activity history`() async throws { - let runtime = try ProviderPluginRuntime(source: #""" - defineProvider({ - id: "synthetic", - name: "Synthetic fixture", - endpoints: ["https://fixture.example"], - settings: [], - async fetchUsage() { - return { - primary: { usedPercent: 1 }, - openRouterActivityUsage: { data: [{ - date: "2026-08-06", model: "openai/gpt-5.6-sol", - prompt_tokens: 10, completion_tokens: 20, reasoning_tokens: 5, - requests: 1, usage: 0.01, - }] }, - }; - }, - }); - """#) - - let snapshot = try await runtime.fetchUsage(now: Self.now) - - #expect(snapshot.openRouterActivityUsage == nil) - } - - @Test @MainActor - func `transient activity failure preserves only current credential history`() throws { - let activity = try OpenRouterActivityUsageSnapshot( - data: Data(Self.activityFixture.utf8), - now: Self.now) - let previous = UsageSnapshot( - primary: nil, - secondary: nil, - openRouterActivityUsage: activity, - updatedAt: Self.now) - let current = UsageSnapshot(primary: nil, secondary: nil, updatedAt: Self.now) - let settings = testSettingsStore( - suiteName: "OpenRouterActivityUsageTests-\(UUID().uuidString)", - tokenAccountStore: InMemoryTokenAccountStore()) - let store = UsageStore( - fetcher: UsageFetcher(environment: [:]), - browserDetection: BrowserDetection(cacheTTL: 0), - settings: settings, - startupBehavior: .testing, - environmentBase: [:]) - store.publishTokenSnapshot(activity.toCostUsageTokenSnapshot(), for: .openrouter) - settings.costUsageHistoryDays = 365 - - #expect(store.tokenSnapshotPublicationForCurrentProviderConfig(for: .openrouter) == nil) - - let preserved = store.preservingOpenRouterActivityIfCurrent(current, previous: previous) - #expect(preserved.openRouterActivityUsage == activity) - - settings[providerConfig: .openrouter, field: .apiKey] = "changed-scope-fixture" - let rejected = store.preservingOpenRouterActivityIfCurrent(current, previous: previous) - #expect(rejected.openRouterActivityUsage == nil) - } - - @Test - func `bundled plugin projects activity into provider snapshot`() async throws { - let requests = OpenRouterActivityRequestRecorder() - let transport = ProviderHTTPTransportHandler { request in - await requests.append(request) - switch request.url?.lastPathComponent { - case "activity": - return try Self.response(request, body: Self.activityFixture) - case "key": - return try Self.response(request, body: #"{"data":{}}"#) - default: - return try Self.response(request, body: #"{"data":{"total_credits":100,"total_usage":40}}"#) - } - } - let runtime = try ProviderPluginRuntime(bundledPlugin: "openrouter", transport: transport) - - let usage = try await runtime.fetchUsage( - settings: [ - OpenRouterSettingsReader.apiURLEnvironmentKey: "https://openrouter.test/api/v1", - ], - secrets: [OpenRouterSettingsReader.envKey: "fixture-management-key"], - now: Self.now) - - let paths = await requests.requests.compactMap(\.url?.path) - #expect(paths.contains("/api/v1/activity")) - let activity = try #require(usage.openRouterActivityUsage) - #expect(activity.toCostUsageTokenSnapshot().last30DaysTokens == 325) - #expect(abs((activity.toCostUsageTokenSnapshot().last30DaysCostUSD ?? -1) - 0.05) < 0.000_000_001) - } - - @Test(arguments: [403, 404]) - func `bundled plugin treats unavailable activity as optional`(statusCode: Int) async throws { - let requests = OpenRouterActivityRequestRecorder() - let transport = ProviderHTTPTransportHandler { request in - await requests.append(request) - switch request.url?.lastPathComponent { - case "activity": - return try Self.response( - request, - body: #"{"error":{"message":"management key required"}}"#, - statusCode: statusCode) - case "key": - return try Self.response(request, body: #"{"data":{}}"#) - default: - return try Self.response(request, body: #"{"data":{"total_credits":100,"total_usage":40}}"#) - } - } - let runtime = try ProviderPluginRuntime(bundledPlugin: "openrouter", transport: transport) - - let usage = try await runtime.fetchUsage( - settings: [ - OpenRouterSettingsReader.apiURLEnvironmentKey: "https://openrouter.test/api/v1", - ], - secrets: [OpenRouterSettingsReader.envKey: "ordinary-api-key"]) - - let paths = await requests.requests.compactMap(\.url?.path) - #expect(paths.contains("/api/v1/activity")) - #expect(usage.openRouterActivityUsage == nil) - #expect(usage.detailRow(label: "Remaining")?.value == "$60.00") - } - - private static let now = Date(timeIntervalSince1970: 1_786_060_800) // 2026-08-07 00:00:00 UTC - - private static let activityFixture = #""" - { - "data": [ - { - "byok_usage_inference": 1.0, - "completion_tokens": 125, - "date": "2026-08-05", - "endpoint_id": "endpoint-claude-a", - "model": "anthropic/claude-sonnet-4-6", - "model_permaslug": "anthropic/claude-sonnet-4-6-20260219", - "prompt_tokens": 50, - "provider_name": "Anthropic", - "reasoning_tokens": 25, - "requests": 5, - "usage": 0.015 - }, - { - "byok_usage_inference": 0.25, - "completion_tokens": 20, - "date": "2026-08-05", - "endpoint_id": "endpoint-claude-b", - "model": "anthropic/claude-sonnet-4-6", - "model_permaslug": "anthropic/claude-sonnet-4-6-20260219", - "prompt_tokens": 10, - "provider_name": "Anthropic", - "reasoning_tokens": 5, - "requests": 2, - "usage": 0.005 - }, - { - "byok_usage_inference": 0, - "completion_tokens": 80, - "date": "2026-08-06", - "endpoint_id": "endpoint-gpt", - "model": "openai/gpt-5.4-mini", - "model_permaslug": "openai/gpt-5.4-mini-20260801", - "prompt_tokens": 40, - "provider_name": "OpenAI", - "reasoning_tokens": 20, - "requests": 3, - "usage": 0.03 - } - ] - } - """# - - private static func response( - _ request: URLRequest, - body: String, - statusCode: Int = 200) throws -> (Data, URLResponse) - { - let url = try #require(request.url) - let response = try #require(try HTTPURLResponse( - url: url, - statusCode: statusCode, - httpVersion: "HTTP/1.1", - headerFields: ["Content-Type": "application/json"])) - return (Data(body.utf8), response) - } -} - -private actor OpenRouterActivityRequestRecorder { - private(set) var requests: [URLRequest] = [] - - func append(_ request: URLRequest) { - self.requests.append(request) - } -} - -extension Array { - fileprivate var only: Element? { - self.count == 1 ? self[0] : nil - } -} diff --git a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift index cf1cefa47c..dbd0834ca3 100644 --- a/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift +++ b/Tests/CodexBarTests/OpenRouterUsageStatsTests.swift @@ -52,7 +52,7 @@ struct OpenRouterPluginGoldenTests { } @Test - func `requests preserve credits headers and bounded enrichment deadline`() async throws { + func `requests preserve credits headers and one second enrichment deadline`() async throws { let requests = OpenRouterRequestRecorder() let transport = Self.transport(requests: requests, keyBody: #""" {"data":{ @@ -75,17 +75,13 @@ struct OpenRouterPluginGoldenTests { secrets: [OpenRouterSettingsReader.envKey: "sk-or-v1-test"]) let recorded = await requests.requests - #expect(recorded.count == 3) + #expect(recorded.count == 2) #expect(recorded[0].timeoutInterval == 15) #expect(recorded[0].value(forHTTPHeaderField: "HTTP-Referer") == "https://codexbar.example") #expect(recorded[0].value(forHTTPHeaderField: "X-Title") == "CodexBar QA") #expect(recorded[1].timeoutInterval == 1) #expect(recorded[1].value(forHTTPHeaderField: "HTTP-Referer") == nil) #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) - #expect(recorded[2].url?.path.hasSuffix("/activity") == true) - #expect(recorded[2].timeoutInterval == 1) - #expect(recorded[2].value(forHTTPHeaderField: "HTTP-Referer") == nil) - #expect(recorded[2].value(forHTTPHeaderField: "X-Title") == nil) #expect(usage.detailRow(label: "Today")?.value == "$0.12") #expect(usage.detailRow(label: "This week")?.value == "$0.74") #expect(usage.detailRow(label: "This month")?.value == "$4.56") @@ -196,7 +192,7 @@ struct OpenRouterPluginGoldenTests { let transport = ProviderHTTPTransportHandler { request in if request.url?.path.hasSuffix("/key") == true { await withCheckedContinuation { continuation in - DispatchQueue.global().asyncAfter(deadline: .now() + 2.5) { + DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) { continuation.resume() } } @@ -212,7 +208,7 @@ struct OpenRouterPluginGoldenTests { let usage = try await runtime.fetchUsage(secrets: [OpenRouterSettingsReader.envKey: "fixture-key"]) - #expect(ContinuousClock.now - startedAt < .seconds(2.4)) + #expect(ContinuousClock.now - startedAt < .seconds(1.4)) #expect(usage.primary == nil) #expect(usage.detailRow(label: "API key budget")?.value == "Unavailable right now") #expect(usage.detailRow(label: "API key budget")?.secondaryValue == "Request timed out") diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 63d4b856d4..81447c3cfd 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -1070,25 +1070,25 @@ struct ProviderArchitectureGatekeeperTests { reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 932, + line: 922, anchor: "let snapshotEmail = CodexIdentityResolver.normalizeEmail(snapshot.accountEmail(for: .codex)),", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 940, + line: 930, anchor: "let identity = snapshot.identity(for: .codex)", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 942, + line: 932, anchor: "providerID: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), SuppressedProviderReference( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1473, + line: 1463, anchor: "let currentAccount = self.uniqueTokenAccount(provider: .claude, accountID: fetchedAccount.id),", expectedProviderIDs: ["claude"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), @@ -1503,7 +1503,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This tagged diagnostic payload encodes MiniMax details under the matching wire key."), SuppressedProviderReference( path: "Sources/CodexBarCore/UsageFetcher.swift", - line: 1493, + line: 1479, anchor: "providerID: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific core branch passes its already-selected identity to a shared helper."), @@ -2355,27 +2355,11 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 603, - anchor: "let bucketEnd = input.provider == .openrouter", - expectedProviderIDs: ["openrouter"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["openrouter@0"], - reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), - AllowedProviderConstruct( - path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 660, - anchor: "guard provider == .mistral || provider == .openrouter else { return displayCalendar }", - expectedProviderIDs: ["mistral", "openrouter"], - expectedReferenceCount: 2, - expectedReferenceFingerprint: ["mistral@0", "openrouter@0"], - reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), - AllowedProviderConstruct( - path: "Sources/CodexBarCore/Plugins/ProviderPluginSnapshotMapper.swift", - line: 20, - anchor: "let openRouterActivityUsage = provider == .openrouter", - expectedProviderIDs: ["openrouter"], + line: 658, + anchor: "guard provider == .mistral else { return displayCalendar }", + expectedProviderIDs: ["mistral"], expectedReferenceCount: 1, - expectedReferenceFingerprint: ["openrouter@0"], + expectedReferenceFingerprint: ["mistral@0"], reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+AccountMenuDisplay.swift", @@ -2845,29 +2829,22 @@ struct ProviderArchitectureGatekeeperTests { path: "Sources/CodexBar/UsageStore+Refresh.swift", line: 854, anchor: "guard provider == .deepseek else { return snapshot }", - expectedProviderIDs: ["codex", "deepseek", "openrouter"], - expectedReferenceCount: 7, + expectedProviderIDs: ["codex", "deepseek"], + expectedReferenceCount: 8, expectedReferenceFingerprint: [ "deepseek@0", "deepseek@1", - "openrouter@9", - "codex@19", - "codex@27", - "codex@38", - "codex@39", + "codex@8", + "codex@16", + "codex@28", + "codex@29", + "codex@34", + "codex@36", ], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 898, - anchor: "let identity = usage.identity(for: .codex)", - expectedProviderIDs: ["codex"], - expectedReferenceCount: 2, - expectedReferenceFingerprint: ["codex@0", "codex@2"], - reason: "This Codex-only identity repair preserves the expected account email on a provider-owned snapshot."), - AllowedProviderConstruct( - path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 992, + line: 982, anchor: "guard provider == .claude, !hasSelectedTokenAccount else { return false }", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -2875,7 +2852,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1332, + line: 1322, anchor: "if provider == .gemini, Self.isGeminiConsumerTierDeprecationError(error) {", expectedProviderIDs: ["claude", "gemini"], expectedReferenceCount: 2, @@ -2883,7 +2860,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1376, + line: 1366, anchor: "if provider == .claude,", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -2891,7 +2868,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1392, + line: 1382, anchor: "if provider == .claude,", expectedProviderIDs: ["claude"], expectedReferenceCount: 5, @@ -2899,7 +2876,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+Refresh.swift", - line: 1474, + line: 1464, anchor: "cached.cacheKey == self.tokenAccountSnapshotCacheKey(provider: .claude, account: currentAccount)", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -3031,13 +3008,13 @@ struct ProviderArchitectureGatekeeperTests { path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", line: 1451, anchor: "let profileStable = provider == .deepseek", - expectedProviderIDs: ["deepseek", "openrouter"], - expectedReferenceCount: 2, - expectedReferenceFingerprint: ["deepseek@0", "openrouter@4"], + expectedProviderIDs: ["deepseek"], + expectedReferenceCount: 1, + expectedReferenceFingerprint: ["deepseek@0"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", - line: 1471, + line: 1466, anchor: "accountDiscriminatorOverride: provider == .claude ? warningAccountDiscriminator : nil)", expectedProviderIDs: ["claude", "deepseek"], expectedReferenceCount: 2, @@ -3045,7 +3022,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", - line: 1492, + line: 1487, anchor: "if provider == .claude,", expectedProviderIDs: ["claude"], expectedReferenceCount: 1, @@ -3053,20 +3030,12 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenAccounts.swift", - line: 1517, + line: 1512, anchor: "if provider == .deepseek {", expectedProviderIDs: ["deepseek"], expectedReferenceCount: 1, expectedReferenceFingerprint: ["deepseek@0"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), - AllowedProviderConstruct( - path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 512, - anchor: "self.tokenSnapshotPublicationForCurrentCredentialScope(for: .openrouter)?.snapshot != nil,", - expectedProviderIDs: ["openrouter"], - expectedReferenceCount: 1, - expectedReferenceFingerprint: ["openrouter@0"], - reason: "This exact app-runtime bridge preserves current-scope optional provider history."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", line: 228, @@ -3129,21 +3098,21 @@ struct ProviderArchitectureGatekeeperTests { path: "Sources/CodexBar/UsageStore+TokenCost.swift", line: 435, anchor: "case .openai:", - expectedProviderIDs: ["mistral", "openai", "openrouter", "opencodego"], - expectedReferenceCount: 4, - expectedReferenceFingerprint: ["openai@0", "openrouter@2", "mistral@4", "opencodego@6"], + expectedProviderIDs: ["mistral", "openai", "opencodego"], + expectedReferenceCount: 3, + expectedReferenceFingerprint: ["openai@0", "mistral@2", "opencodego@4"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 456, - anchor: "case .mistral, .openai, .openrouter, .opencodego:", - expectedProviderIDs: ["mistral", "openai", "openrouter", "opencodego"], - expectedReferenceCount: 4, - expectedReferenceFingerprint: ["mistral@0", "openai@0", "opencodego@0", "openrouter@0"], + line: 454, + anchor: "case .mistral, .openai, .opencodego:", + expectedProviderIDs: ["mistral", "openai", "opencodego"], + expectedReferenceCount: 3, + expectedReferenceFingerprint: ["mistral@0", "openai@0", "opencodego@0"], reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/UsageStore+TokenCost.swift", - line: 498, + line: 496, anchor: "self.tokenFailureGates[.codex]?.reset()", expectedProviderIDs: ["claude", "codex"], expectedReferenceCount: 2, diff --git a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift index 8b174c2347..c72c67918e 100644 --- a/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift +++ b/Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift @@ -55,7 +55,6 @@ struct ProviderPluginDetailsParityTests { switch request.url?.path { case "/api/v1/credits": Self.openRouterCredits case "/api/v1/key": Self.openRouterKey - case "/api/v1/activity": Self.openRouterActivity default: throw FixtureError.unexpectedURL(request.url) } } @@ -65,13 +64,6 @@ struct ProviderPluginDetailsParityTests { #expect(script.primary?.usedPercent == 25) #expect(script.identity?.loginMethod == "Balance: $60.00") - let activity = try #require(script.openRouterActivityUsage) - let activityCost = activity.toCostUsageTokenSnapshot() - #expect(activityCost.last30DaysTokens == 45) - #expect(activityCost.last30DaysRequests == 3) - #expect(abs((activityCost.last30DaysCostUSD ?? -1) - 0.015) < 0.000_000_001) - #expect(activityCost.daily.count == 1) - #expect(activityCost.daily.first?.modelBreakdowns?.count == 1) #expect(try script.details == [ Self.section("Credits", rows: [ Self.row("Remaining", "$60.00"), @@ -96,61 +88,6 @@ struct ProviderPluginDetailsParityTests { ]) } - @Test - func `OpenRouter malformed optional activity preserves credits on both plugin engines`() async throws { - let transport = Self.transport { request in - switch request.url?.path { - case "/api/v1/credits": Self.openRouterCredits - case "/api/v1/key": Self.openRouterKey - case "/api/v1/activity": #"{"data":[{"date":"2026-08-01","usage":"free"}]}"# - default: throw FixtureError.unexpectedURL(request.url) - } - } - - let script = try await Self.openRouterRuntime(transport: transport) - .fetchUsage(secrets: ["OPENROUTER_API_KEY": "fixture-key"]) - - #expect(script.openRouterActivityUsage == nil) - #expect(script.identity?.loginMethod == "Balance: $60.00") - } - - @Test - func `OpenRouter delayed optional activity stays within the fast return budget`() async throws { - let requests = PluginRequestRecorder() - let transport = ProviderHTTPTransportHandler { request in - await requests.append(request) - if request.url?.path == "/api/v1/activity" { - try await Task.sleep(for: .seconds(4)) - throw URLError(.timedOut) - } - let body = switch request.url?.path { - case "/api/v1/credits": Self.openRouterCredits - case "/api/v1/key": Self.openRouterKey - default: throw FixtureError.unexpectedURL(request.url) - } - let response = try #require(HTTPURLResponse( - url: request.url!, - statusCode: 200, - httpVersion: nil, - headerFields: ["Content-Type": "application/json"])) - return (Data(body.utf8), response) - } - let clock = ContinuousClock() - let startedAt = clock.now - let script = try await ProviderPluginRuntime(bundledPlugin: "openrouter", transport: transport) - .fetchUsage(secrets: ["OPENROUTER_API_KEY": "fixture-key"]) - let elapsed = startedAt.duration(to: clock.now) - let optionalRequests = await requests.requests.filter { - ["key", "activity"].contains($0.url?.lastPathComponent) - } - - #expect(optionalRequests.count == 2) - #expect(optionalRequests.allSatisfy { $0.timeoutInterval == 1 }) - #expect(elapsed < .seconds(3)) - #expect(script.openRouterActivityUsage == nil) - #expect(script.identity?.loginMethod == "Balance: $60.00") - } - @Test func `OpenRouter optional key timeout is an observable degradation`() async throws { let transport = ProviderHTTPTransportHandler { request in @@ -236,23 +173,18 @@ struct ProviderPluginDetailsParityTests { secrets: [OpenRouterSettingsReader.envKey: "fixture-key"]) let recorded = await requests.requests - #expect(recorded.count == 3) + #expect(recorded.count == 2) #expect(recorded[0].url?.absoluteString == (overridden ? "https://router.example.test/gateway/v1/credits" : "https://openrouter.ai/api/v1/credits")) #expect(recorded[1].url?.absoluteString == (overridden ? "https://router.example.test/gateway/v1/key" : "https://openrouter.ai/api/v1/key")) - #expect(recorded[2].url?.absoluteString == (overridden - ? "https://router.example.test/gateway/v1/activity" - : "https://openrouter.ai/api/v1/activity")) #expect(recorded[0].value(forHTTPHeaderField: "X-Title") == (overridden ? "CodexBar QA" : "CodexBar")) #expect(recorded[0].value(forHTTPHeaderField: "HTTP-Referer") == (overridden ? "https://codexbar.example" : nil)) #expect(recorded[1].value(forHTTPHeaderField: "X-Title") == nil) - #expect(recorded[2].value(forHTTPHeaderField: "X-Title") == nil) #expect(recorded[1].timeoutInterval == 15) - #expect(recorded[2].timeoutInterval == 15) } @Test @@ -633,14 +565,6 @@ struct ProviderPluginDetailsParityTests { "usage_daily":1,"usage_weekly":2,"usage_monthly":4, "rate_limit":{"requests":120,"interval":"10s"}}} """# - private static let openRouterActivity = #""" - {"data":[ - {"date":"2026-08-01","model":"anthropic/claude-sonnet-4-6","prompt_tokens":10, - "completion_tokens":20,"reasoning_tokens":5,"requests":2,"usage":0.01}, - {"date":"2026-08-01","model":"anthropic/claude-sonnet-4-6","prompt_tokens":5, - "completion_tokens":10,"reasoning_tokens":2,"requests":1,"usage":0.005} - ]} - """# private static let poeBalance = #"{"current_point_balance":2500}"# private static let poeHistory = #""" {"data":[ diff --git a/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift b/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift deleted file mode 100644 index e10560dd39..0000000000 --- a/Tests/CodexBarTests/SpendDashboardOpenRouterDateTruthTests.swift +++ /dev/null @@ -1,84 +0,0 @@ -import CodexBarCore -import Foundation -import Testing -@testable import CodexBar - -struct SpendDashboardOpenRouterDateTruthTests { - @Test - func `completed UTC history never fabricates the current day`() throws { - var utc = Calendar(identifier: .gregorian) - utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) - let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) - let august6 = try #require(utc.date(from: DateComponents(year: 2026, month: 8, day: 6))) - let snapshot = Self.snapshot( - entries: [ - Self.entry(day: "2026-08-05", cost: 1, tokens: 10), - Self.entry(day: "2026-08-06", cost: 2, tokens: 20), - ], - historyDays: 2, - updatedAt: now) - let group = try #require(SpendDashboardModel.build( - inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], - requestedDays: 2, - now: now, - calendar: utc).groups.first) - - #expect(group.providers.first?.coveredDayCount == 1) - #expect(group.totalCost == nil) - #expect(group.knownCost == 2) - #expect(group.totalTokens == nil) - #expect(group.dailyPoints.map(\.day) == [august6]) - #expect(group.coveredDayCount == 1) - } - - @Test - func `thirty completed days stay partial in annual view`() throws { - var utc = Calendar(identifier: .gregorian) - utc.timeZone = try #require(TimeZone(secondsFromGMT: 0)) - let now = try #require(ISO8601DateFormatter().date(from: "2026-08-07T12:00:00Z")) - let snapshot = Self.snapshot( - entries: [Self.entry(day: "2026-08-06", cost: 2, tokens: 20)], - historyDays: 30, - updatedAt: now) - let group = try #require(SpendDashboardModel.build( - inputs: [.init(provider: .openrouter, displayName: "OpenRouter", snapshot: snapshot)], - requestedDays: 365, - now: now, - calendar: utc).groups.first) - - #expect(group.providers.first?.coveredDayCount == 30) - #expect(group.coveredDayCount == 30) - #expect(group.totalCost == nil) - #expect(group.knownCost == 2) - #expect(group.totalTokens == nil) - } - - private static func snapshot( - entries: [CostUsageDailyReport.Entry], - historyDays: Int, - updatedAt: Date) -> CostUsageTokenSnapshot - { - CostUsageTokenSnapshot( - sessionTokens: nil, - sessionCostUSD: nil, - last30DaysTokens: nil, - last30DaysCostUSD: nil, - currencyCode: "USD", - historyDays: historyDays, - daily: entries, - updatedAt: updatedAt) - } - - private static func entry(day: String, cost: Double, tokens: Int) -> CostUsageDailyReport.Entry { - CostUsageDailyReport.Entry( - date: day, - inputTokens: nil, - outputTokens: nil, - totalTokens: tokens, - costUSD: cost, - modelsUsed: nil, - modelBreakdowns: [ - .init(modelName: "fixture-model", costUSD: cost, totalTokens: tokens), - ]) - } -} diff --git a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift index e7ef394fec..df64ed0cda 100644 --- a/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTrackedSourceTests.swift @@ -49,12 +49,12 @@ struct SpendDashboardTrackedSourceTests { let sources = SpendDashboardSource.trackedSources(settings: settings, store: store) let rows = Dictionary(uniqueKeysWithValues: sources.map { ($0.provider, $0) }) - #expect(Set(rows.keys) == [.cursor, .gemini, .grok, .openrouter]) + #expect(Set(rows.keys).isSuperset(of: [.cursor, .gemini, .grok, .openrouter])) #expect(rows[.grok]?.state == .connected) #expect(rows[.gemini]?.state == .needsAttention) #expect(rows[.openrouter]?.state == .awaitingUsage) - #expect(rows[.openrouter]?.supportsCostHistory == true) - #expect(Set(rows.values.filter(\.contributesCostHistory).map(\.provider)) == [.cursor, .openrouter]) + #expect(rows[.openrouter]?.supportsCostHistory == false) + #expect(Set(rows.values.filter(\.contributesCostHistory).map(\.provider)) == [.cursor]) } @Test @@ -171,7 +171,7 @@ struct SpendDashboardTrackedSourceTests { $0.provider == .openrouter }) - #expect(spendDashboardTrackedSourceStatusText(source) == "Cost history pending") + #expect(spendDashboardTrackedSourceStatusText(source) == "Usage connected · not in cost total") store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( sessionTokens: nil, @@ -184,7 +184,8 @@ struct SpendDashboardTrackedSourceTests { let sourceWithUnestablishedCost = try #require(SpendDashboardSource.trackedSources( settings: settings, store: store).first { $0.provider == .openrouter }) - #expect(spendDashboardTrackedSourceStatusText(sourceWithUnestablishedCost) == "Cost history pending") + #expect(spendDashboardTrackedSourceStatusText(sourceWithUnestablishedCost) == + "Usage connected · not in cost total") store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( sessionTokens: nil, @@ -196,7 +197,7 @@ struct SpendDashboardTrackedSourceTests { let sourceWithCost = try #require(SpendDashboardSource.trackedSources( settings: settings, store: store).first { $0.provider == .openrouter }) - #expect(spendDashboardTrackedSourceStatusText(sourceWithCost) == "Cost history connected") + #expect(spendDashboardTrackedSourceStatusText(sourceWithCost) == "Usage connected · not in cost total") } @Test @@ -265,10 +266,9 @@ struct SpendDashboardTrackedSourceTests { #expect(Set(credentialSources.map(\.id)).count == credentialSources.count) let openRouterSources = credentialSources.filter { $0.provider == .openrouter } - let allSupportCostHistory = openRouterSources.allSatisfy(\.supportsCostHistory) #expect(openRouterSources.count == 2) #expect(openRouterSources.allSatisfy { $0.state == .configured }) - #expect(allSupportCostHistory) + #expect(openRouterSources.allSatisfy { !$0.supportsCostHistory }) #expect(openRouterSources.allSatisfy { !$0.contributesCostHistory }) } @@ -280,7 +280,7 @@ struct SpendDashboardTrackedSourceTests { providerName: "OpenRouter", accountName: "Work", state: .connected, - supportsCostHistory: true, + supportsCostHistory: false, contributesCostHistory: false) #expect(spendDashboardTrackedSourceStatusText(source) == "Usage connected · not in cost total") @@ -301,7 +301,7 @@ struct SpendDashboardTrackedSourceTests { providerName: "OpenRouter", accountName: nil, state: .awaitingUsage, - supportsCostHistory: true, + supportsCostHistory: false, contributesCostHistory: false) #expect(spendDashboardTrackedSourceStatusText(setup) == "No usage yet") } @@ -373,7 +373,7 @@ struct SpendDashboardTrackedSourceTests { providerName: "OpenRouter", accountName: "Research", state: .awaitingUsage, - supportsCostHistory: true, + supportsCostHistory: false, contributesCostHistory: false), SpendDashboardTrackedSource( id: "cursor:account:work", diff --git a/docs/plugin-conversion-matrix.md b/docs/plugin-conversion-matrix.md index af39378884..76f8be2860 100644 --- a/docs/plugin-conversion-matrix.md +++ b/docs/plugin-conversion-matrix.md @@ -73,7 +73,7 @@ weakening the plugin network policy. | ollama | `needs-cookie-import` | No | Skipped: hosted parity requires HTML bootstrap/state extraction plus API-key fallback arbitration. | | synthetic | `cut-over` | Yes | Cut over on both engines: fixed-origin bearer GET with generic windows, cost, dates, and identity; the native fetch twin is deleted. | | warp | `needs-pty/webview/native` | No | Warp sends a POST GraphQL operation, which the GET-only HTTP broker cannot express. | -| openrouter | `cut-over` | Yes | Cut over on both engines: endpoint and client-header overrides, best-effort key enrichment, and management-key completed-day model activity are plugin-owned; the native fetch core is deleted. | +| openrouter | `cut-over` | Yes | Cut over on both engines: endpoint and client-header overrides plus best-effort key enrichment are plugin-owned; the native fetch core is deleted. | | elevenlabs | `convertible-now` | No | Verified `xi-api-key` GET; heterogeneous character/minute quotas map to named generic windows. | | windsurf | `needs-files/subprocess/oauth-broker` | No | Chromium localStorage, IDE databases, and binary protobuf decoding supply the current session. | | zed | `needs-files/subprocess/oauth-broker` | No | Zed server settings and a named Keychain credential must be read locally. | diff --git a/docs/plugin-prototype.md b/docs/plugin-prototype.md index 5a88ffb3c3..9093142747 100644 --- a/docs/plugin-prototype.md +++ b/docs/plugin-prototype.md @@ -244,4 +244,4 @@ first-party flag semantics. Browser cookies remain restricted to declared domain Display-only provider payloads now use `details` on both the Swift and JavaScript paths. The remaining bespoke `UsageSnapshot` fields drive behavior rather than presentation: Codex reset-credit actions, Command Code refresh stabilization, DeepSeek profile selection/transition state, and provider-derived token-cost pipelines for OpenAI API, -OpenRouter completed-day activity, Mistral, and OpenCode Go. They are not plugin compatibility shims. +Mistral and OpenCode Go. They are not plugin compatibility shims. diff --git a/docs/screenshots/spend-dashboard-proof/README.md b/docs/screenshots/spend-dashboard-proof/README.md index 52772f144b..74de562949 100644 --- a/docs/screenshots/spend-dashboard-proof/README.md +++ b/docs/screenshots/spend-dashboard-proof/README.md @@ -27,5 +27,5 @@ with known spend and three sources whose spend is explicitly unavailable. The Overview is captured at the production 310-point menu width, keeps Codex prominent, and uses compact rows for the remaining providers. The share capture uses the production `ShareStatsWindowController`, preserves all six connected -sources, labels partial spend with its reporting denominator, and ranks routed -OpenRouter models alongside first-class Claude and Codex variants. +sources, and labels partial spend with its reporting denominator. Its values and +model labels are synthetic UI fixtures rather than claims about live provider coverage. From e1c60a52f77a61f3dcc2125ae35b73905e25f15a Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sun, 9 Aug 2026 10:39:28 -0700 Subject: [PATCH 47/51] Keep share spend completeness independent from tokens --- Sources/CodexBar/ShareStatsPayload.swift | 32 +++++++++++------ Tests/CodexBarTests/ShareStatsTests.swift | 36 +++++++++++++++++++ ...ashboardIntegratedModelCoverageTests.swift | 3 +- .../SpendDashboardModelTests.swift | 1 - 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/Sources/CodexBar/ShareStatsPayload.swift b/Sources/CodexBar/ShareStatsPayload.swift index 064daec8c8..86a9c82eaf 100644 --- a/Sources/CodexBar/ShareStatsPayload.swift +++ b/Sources/CodexBar/ShareStatsPayload.swift @@ -444,15 +444,25 @@ enum ShareStatsBuilder { let rosterHasUnexpectedProviderFamilies = !providerRoster.isEmpty && trackedProviders.contains { !rosterProviderKinds.contains($0.provider) } - let rosterHasIncompleteProviders = rosterHasUnexpectedProviderFamilies || providerRoster.contains { entry in - let allMatches = trackedProviders.filter { $0.provider == entry.provider } - let matches = self.rosterMatches(entry: entry, candidates: allMatches) - let knownCosts = matches.compactMap(\.estimatedCost) - return self.rosterIdentityIsIncomplete(entry: entry, candidates: allMatches) || - matches.contains { $0.totalTokens == nil || $0.estimatedCost == nil } || - Set(matches.map(\.currencyCode)).count > 1 || - self.safeCostSum(knownCosts) == nil - } + let rosterHasIncompleteSpendProviders = rosterHasUnexpectedProviderFamilies || + providerRoster.contains { entry in + let allMatches = trackedProviders.filter { $0.provider == entry.provider } + let matches = self.rosterMatches(entry: entry, candidates: allMatches) + let knownCosts = matches.compactMap(\.estimatedCost) + return self.rosterIdentityIsIncomplete(entry: entry, candidates: allMatches) || + matches.contains { $0.estimatedCost == nil } || + Set(matches.map(\.currencyCode)).count > 1 || + self.safeCostSum(knownCosts) == nil + } + let rosterHasIncompleteTokenProviders = rosterHasUnexpectedProviderFamilies || + providerRoster.contains { entry in + let allMatches = trackedProviders.filter { $0.provider == entry.provider } + let matches = self.rosterMatches(entry: entry, candidates: allMatches) + return self.rosterIdentityIsIncomplete(entry: entry, candidates: allMatches) || + matches.contains { $0.totalTokens == nil } + } + let rosterHasIncompleteProviders = rosterHasIncompleteSpendProviders || + rosterHasIncompleteTokenProviders let currencies = model.groups.map { group in let rosterCosts = providers.filter { $0.currencyCode == group.currencyCode } .compactMap(\.estimatedCost) @@ -463,13 +473,13 @@ enum ShareStatsBuilder { currencyCode: group.currencyCode, estimatedCost: estimatedCost, coveredDayCount: group.coveredDayCount, - isPartial: rosterHasIncompleteProviders || group.totalCost == nil) + isPartial: rosterHasIncompleteSpendProviders || group.totalCost == nil) } let tokenProviders = providerRoster.isEmpty ? trackedProviders : providers let knownTokenValues = tokenProviders.compactMap(\.totalTokens) let totalTokens = self.safeTokenSum(knownTokenValues) let totalTokensIsPartial = tokenProviders.contains { $0.totalTokens == nil } || - rosterHasIncompleteProviders || + rosterHasIncompleteTokenProviders || model.groups.contains { $0.totalTokens == nil } let periodEnd = model.groups.map(\.chartDomain.upperBound).max() ?? Date() let payload = ShareStatsPayload( diff --git a/Tests/CodexBarTests/ShareStatsTests.swift b/Tests/CodexBarTests/ShareStatsTests.swift index 681eb74588..5d49d0b0a1 100644 --- a/Tests/CodexBarTests/ShareStatsTests.swift +++ b/Tests/CodexBarTests/ShareStatsTests.swift @@ -124,6 +124,42 @@ struct ShareStatsTests { #expect(payload.providers.count == 2) } + @Test + func `complete spend remains exact when a provider has no token counts`() throws { + let group = SpendDashboardModel.CurrencyGroup( + currencyCode: "USD", + providers: [ + SpendDashboardModel.ProviderRow( + id: "bedrock", + rank: 1, + provider: .bedrock, + displayName: "Bedrock", + totalTokens: nil, + totalCost: 12, + coveredDayCount: 30), + ], + models: [], + dailyPoints: [], + totalTokens: nil, + totalCost: 12, + coveredDayCount: 30, + chartDomain: Self.date...Self.date, + modelHistoryCompleteness: .complete) + let roster = [ + ShareStatsProviderRosterEntry(provider: .bedrock, providerName: "Bedrock", currencyCode: "USD"), + ] + + let payload = try #require(ShareStatsBuilder.make( + model: SpendDashboardModel(requestedDays: 30, groups: [group]), + providerRoster: roster)) + + #expect(payload.currencies.first?.estimatedCost == 12) + #expect(payload.currencies.first?.isPartial == false) + #expect(payload.totalTokens == nil) + #expect(payload.totalTokensIsPartial) + #expect(ShareStatsFormatting.text(payload).contains("USD: $12.00 estimated")) + } + @Test func `twenty connected services remain accounted for without overflowing the flex card`() throws { let roster = Array(UsageProvider.allCases.prefix(20)).map { provider in diff --git a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift index 3c4fda6af0..5ac3c11abc 100644 --- a/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift +++ b/Tests/CodexBarTests/SpendDashboardIntegratedModelCoverageTests.swift @@ -32,14 +32,13 @@ struct SpendDashboardIntegratedModelCoverageTests { .cursor, .mistral, .openai, - .openrouter, .opencodego, .vertexai, ]) #expect(Set(group.models.map(\.id)) == expectedIDs) #expect(group.models.count == providers.count * 2) #expect(group.models.allSatisfy { $0.totalTokens != nil && $0.totalCost != nil }) - #expect(group.modelHistoryCompleteness == .incomplete) + #expect(group.modelHistoryCompleteness == .complete) } @Test diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index e61ad96d29..57ba66550b 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -52,7 +52,6 @@ struct SpendDashboardModelTests { .claude, .vertexai, .openai, - .openrouter, .mistral, .bedrock, .cursor, From a573a7962f05e0bde29681ad25d3ec0f9248f0d7 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sun, 9 Aug 2026 11:04:22 -0700 Subject: [PATCH 48/51] Make Codex RPC timeouts win EOF races --- Sources/CodexBarCore/UsageFetcher.swift | 36 ++++++++++++++++--------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index 389470e7bd..0cee44af9e 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -831,6 +831,11 @@ enum RPCWireError: Error, LocalizedError { } } +private enum RPCRequestRaceResult: Sendable { + case value(Value) + case timedOut +} + /// RPC helper used on background tasks; safe because we confine it to the owning task. private final class CodexRPCClient: @unchecked Sendable { // Provider-specific by design: Codex RPC owns its dedicated subprocess log category. @@ -1012,24 +1017,29 @@ private final class CodexRPCClient: @unchecked Sendable { method: String, body: @escaping @Sendable () async throws -> T) async throws -> T { - try await withThrowingTaskGroup(of: T.self) { group in + try await withThrowingTaskGroup(of: RPCRequestRaceResult.self) { group in group.addTask { - try await body() + try await .value(body()) } - group.addTask { [weak self] in + group.addTask { try await Task.sleep(for: .seconds(seconds)) - self?.terminateProcessForTimeout(method: method) - throw RPCWireError.timeout(method: method) + return .timedOut } - do { - guard let result = try await group.next() else { - throw RPCWireError.timeout(method: method) - } - group.cancelAll() - return result - } catch { + + guard let result = try await group.next() else { group.cancelAll() - throw error + throw RPCWireError.timeout(method: method) + } + group.cancelAll() + + switch result { + case let .value(value): + return value + case .timedOut: + // Terminating the process closes stdout. Classify that expected EOF as a + // timeout by selecting the timer before requesting process termination. + self.terminateProcessForTimeout(method: method) + throw RPCWireError.timeout(method: method) } } } From 8bcaf4e74cac92719d0cabd753853fd991bc029e Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Sun, 9 Aug 2026 12:08:00 -0700 Subject: [PATCH 49/51] Refresh Codex provider gatekeeper anchor --- Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 81447c3cfd..7ab3910dd6 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -1503,7 +1503,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This tagged diagnostic payload encodes MiniMax details under the matching wire key."), SuppressedProviderReference( path: "Sources/CodexBarCore/UsageFetcher.swift", - line: 1479, + line: 1489, anchor: "providerID: .codex,", expectedProviderIDs: ["codex"], reason: "This provider-specific core branch passes its already-selected identity to a shared helper."), From 05b58b9d6ba2309ab159b5891ae1825d318209fe Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Mon, 10 Aug 2026 19:16:28 -0700 Subject: [PATCH 50/51] Keep integrated files within lint limits --- .../CodexBar/StatusItemController+Menu.swift | 13 -------- .../StatusItemController+OverviewSpend.swift | 14 +++++++++ .../MergedOverviewProviderLimitTests.swift | 30 +++++++++++++++++++ .../ProviderArchitectureGatekeeperTests.swift | 6 ++-- Tests/CodexBarTests/SettingsStoreTests.swift | 22 -------------- 5 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 Tests/CodexBarTests/MergedOverviewProviderLimitTests.swift diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 5504bf0c64..c8d4a13575 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -662,19 +662,6 @@ extension StatusItemController { return true } - private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) { - let resolvedProviders = self.settings.resolvedMergedOverviewProviders( - activeProviders: enabledProviders, - maxVisibleProviders: Self.maxOverviewProviders) - let message = resolvedProviders.isEmpty - ? L("No providers selected for Overview.") - : L("No overview data available.") - let item = NSMenuItem(title: message, action: nil, keyEquivalent: "") - item.isEnabled = false - item.representedObject = "overviewEmptyState" - menu.addItem(item) - } - private func addMenuCards(to menu: NSMenu, context: MenuCardContext, captureMenu: NSMenu? = nil) -> Bool { let fleetProjection = self.fleetAccountProjection(for: context.currentProvider) if self.addFleetFallback(fleetProjection, to: menu, context: context) { diff --git a/Sources/CodexBar/StatusItemController+OverviewSpend.swift b/Sources/CodexBar/StatusItemController+OverviewSpend.swift index 67742a4085..381d4d64ec 100644 --- a/Sources/CodexBar/StatusItemController+OverviewSpend.swift +++ b/Sources/CodexBar/StatusItemController+OverviewSpend.swift @@ -1,7 +1,21 @@ +import AppKit import CodexBarCore import Foundation extension StatusItemController { + func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) { + let resolvedProviders = self.settings.resolvedMergedOverviewProviders( + activeProviders: enabledProviders, + maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit) + let message = resolvedProviders.isEmpty + ? L("No providers selected for Overview.") + : L("No overview data available.") + let item = NSMenuItem(title: message, action: nil, keyEquivalent: "") + item.isEnabled = false + item.representedObject = "overviewEmptyState" + menu.addItem(item) + } + func overviewSpendDashboardModel( providers: [UsageProvider], now: Date = Date()) -> SpendDashboardModel diff --git a/Tests/CodexBarTests/MergedOverviewProviderLimitTests.swift b/Tests/CodexBarTests/MergedOverviewProviderLimitTests.swift new file mode 100644 index 0000000000..04e570d38b --- /dev/null +++ b/Tests/CodexBarTests/MergedOverviewProviderLimitTests.swift @@ -0,0 +1,30 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +@Suite(.serialized) +@MainActor +struct MergedOverviewProviderLimitTests { + @Test + func `resolved providers cap stale selection fallback`() throws { + let suite = "MergedOverviewProviderLimitTests-stale-selection-cap" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + let configStore = testConfigStore(suiteName: suite) + let store = SettingsStore( + userDefaults: defaults, + configStore: configStore, + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + + store.mergedOverviewSelectedProviders = [.grok] + let activeProviders: [UsageProvider] = [.codex, .claude, .cursor] + let resolved = store.resolvedMergedOverviewProviders( + activeProviders: activeProviders, + maxVisibleProviders: 2) + + #expect(resolved == [.codex, .claude]) + #expect(resolved.count <= 2) + } +} diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 0b6331be29..356825ad6e 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -962,7 +962,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "The memory-pressure debug fixture installs its synthetic entry in the Codex cache slot."), SuppressedProviderReference( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1136, + line: 1123, anchor: "controller.refreshOpenMenuIfStillVisible(menu, provider: .codex)", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), @@ -2492,7 +2492,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1169, + line: 1156, anchor: "return .provider((self.resolvedMenuProvider(enabledProviders: enabledProviders) ?? .codex).instanceID)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2500,7 +2500,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1182, + line: 1169, anchor: "return self.store.enabledFirstPartyProvidersForDisplay().first ?? .codex", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index d406922880..487e75d79a 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -490,28 +490,6 @@ struct SettingsStoreTests { #expect(resolved == [.codex, .cursor, .opencode]) } - @Test - func `resolved merged overview providers caps stale selection fallback`() throws { - let suite = "SettingsStoreTests-merged-overview-stale-selection-cap" - let defaults = try #require(UserDefaults(suiteName: suite)) - defaults.removePersistentDomain(forName: suite) - let configStore = testConfigStore(suiteName: suite) - let store = SettingsStore( - userDefaults: defaults, - configStore: configStore, - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - - store.mergedOverviewSelectedProviders = [.grok] - let activeProviders: [UsageProvider] = [.codex, .claude, .cursor] - let resolved = store.resolvedMergedOverviewProviders( - activeProviders: activeProviders, - maxVisibleProviders: 2) - - #expect(resolved == [.codex, .claude]) - #expect(resolved.count <= 2) - } - @Test func `reconcile merged overview selection removes unavailable without auto fill`() throws { let suite = "SettingsStoreTests-merged-overview-reconcile" From 6e7b6178a3cc2ed86e1e9490b7e551b1eed997a8 Mon Sep 17 00:00:00 2001 From: Alec Gutman Date: Mon, 10 Aug 2026 19:30:47 -0700 Subject: [PATCH 51/51] Fix spend dashboard history assertion --- .../SpendDashboardTokenActivityIntegrationTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/SpendDashboardTokenActivityIntegrationTests.swift b/Tests/CodexBarTests/SpendDashboardTokenActivityIntegrationTests.swift index c86e948408..a4360f113f 100644 --- a/Tests/CodexBarTests/SpendDashboardTokenActivityIntegrationTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTokenActivityIntegrationTests.swift @@ -139,7 +139,7 @@ struct SpendDashboardTokenActivityIntegrationTests { request, cacheRootResolver: { _ in cacheRoot }, codexSnapshotLoader: { context in - #expect(context.historyDays == SpendDashboardSource.scanDays) + #expect(context.historyDays == request.historyDays) #expect(context.cacheRoot == cacheRoot) return snapshot })