From b821f23e412131be4c13c2f8422e5171551e3b7f Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sat, 17 Oct 2026 11:06:33 +0200 Subject: [PATCH 1/9] feat(omp): add OMP provider with combined capacity and subprovider selection --- README.md | 2 +- Sources/CodexBar/ProviderBrandIcon.swift | 8 +- .../OMP/OMPProviderImplementation.swift | 51 ++ .../Providers/OMP/OMPSettingsStore.swift | 26 + .../ProviderImplementationManifest.swift | 1 + .../CodexBar/Resources/ProviderIcon-omp.svg | 4 + .../CodexBarCLI/CLIServeProviderIcons.swift | 2 + Sources/CodexBarCore/AgentSession.swift | 1 + Sources/CodexBarCore/PathEnvironment.swift | 31 ++ .../PiFamilySessionRootResolver.swift | 4 + .../CodexBarCore/PiFamilySessionScanner.swift | 5 + .../Providers/OMP/OMPCLIProbe.swift | 65 +++ .../Providers/OMP/OMPProviderConfig.swift | 13 + .../Providers/OMP/OMPProviderDescriptor.swift | 119 +++++ .../Providers/OMP/OMPProviderSettings.swift | 49 ++ .../Providers/OMP/OMPUsageModels.swift | 226 ++++++++ .../Providers/OMP/OMPUsageParser.swift | 502 ++++++++++++++++++ .../ProviderInstanceIDAliases.generated.swift | 1 + .../Providers/ProviderManifest.swift | 1 + .../Providers/ProviderVersionDetector.swift | 14 + .../CodexBarCore/Providers/Providers.swift | 2 + Tests/CodexBarTests/OMPProviderTests.swift | 285 ++++++++++ .../ProviderArchitectureGatekeeperTests.swift | 12 +- docs/index.html | 6 +- docs/logos/omp.svg | 4 + docs/omp.md | 35 ++ docs/plugin-conversion-matrix.md | 8 +- docs/provider-ids.md | 2 +- docs/providers.md | 2 +- docs/site-locales.mjs | 138 ++--- docs/social.html | 2 +- 31 files changed, 1533 insertions(+), 88 deletions(-) create mode 100644 Sources/CodexBar/Providers/OMP/OMPProviderImplementation.swift create mode 100644 Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift create mode 100644 Sources/CodexBar/Resources/ProviderIcon-omp.svg create mode 100644 Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift create mode 100644 Sources/CodexBarCore/Providers/OMP/OMPProviderConfig.swift create mode 100644 Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift create mode 100644 Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift create mode 100644 Sources/CodexBarCore/Providers/OMP/OMPUsageModels.swift create mode 100644 Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift create mode 100644 Tests/CodexBarTests/OMPProviderTests.swift create mode 100644 docs/logos/omp.svg create mode 100644 docs/omp.md diff --git a/README.md b/README.md index e8253c6ac89..f5808f100f0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff?style=flat-square)](LICENSE) [![Site](https://img.shields.io/badge/site-codexbar.app-16d3b4?style=flat-square)](https://codexbar.app) -CodexBar — every AI coding limit in your menu bar. 106 providers. +CodexBar — every AI coding limit in your menu bar. 107 providers. Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. See the [supported providers](#providers) below. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons. diff --git a/Sources/CodexBar/ProviderBrandIcon.swift b/Sources/CodexBar/ProviderBrandIcon.swift index 39da7d3f609..4e730666c53 100644 --- a/Sources/CodexBar/ProviderBrandIcon.swift +++ b/Sources/CodexBar/ProviderBrandIcon.swift @@ -41,8 +41,12 @@ enum ProviderBrandIcon { bundle.url(forResource: brandResourceName, withExtension: fileExtension) .flatMap { NSImage(contentsOf: $0) } }.first : nil - guard let image = brandImage ?? bundle.url(forResource: baseName, withExtension: "svg") - .flatMap({ NSImage(contentsOf: $0) }) else { return nil } + guard let image = brandImage + ?? bundle.url(forResource: baseName, withExtension: "svg").flatMap({ NSImage(contentsOf: $0) }) + ?? Bundle.main.url(forResource: baseName, withExtension: "svg").flatMap({ NSImage(contentsOf: $0) }) + ?? Bundle.main.resourceURL.flatMap({ NSImage(contentsOf: $0.appendingPathComponent("\(baseName).svg")) }) + ?? Bundle.main.resourceURL.flatMap({ NSImage(contentsOf: $0.appendingPathComponent("CodexBar_CodexBar.bundle").appendingPathComponent("\(baseName).svg")) }) + else { return nil } image.size = self.size image.isTemplate = brandImage == nil diff --git a/Sources/CodexBar/Providers/OMP/OMPProviderImplementation.swift b/Sources/CodexBar/Providers/OMP/OMPProviderImplementation.swift new file mode 100644 index 00000000000..f922031218a --- /dev/null +++ b/Sources/CodexBar/Providers/OMP/OMPProviderImplementation.swift @@ -0,0 +1,51 @@ +import CodexBarCore +import Foundation +import SwiftUI + +struct OMPProviderImplementation: ProviderImplementation { + let id: UsageProvider = .omp + + @MainActor + func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? { + .omp(context.settings.ompSettingsSnapshot()) + } + + @MainActor + func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] { + [ + ProviderSettingsPickerDescriptor( + id: "omp-subprovider", + title: "Subprovider", + subtitle: "Select which provider limits to display from OMP, or Combined for all aggregated limits.", + binding: context.binding(\.ompSubprovider), + options: [ + ProviderSettingsPickerOption(id: "all", title: "Combined (All Limits)"), + ProviderSettingsPickerOption(id: "google-antigravity", title: "Google Antigravity (agy)"), + ProviderSettingsPickerOption(id: "openai-codex", title: "OpenAI Codex"), + ProviderSettingsPickerOption(id: "anthropic", title: "Anthropic Claude"), + ProviderSettingsPickerOption(id: "github-copilot", title: "GitHub Copilot"), + ProviderSettingsPickerOption(id: "xai-oauth", title: "xAI Grok"), + ProviderSettingsPickerOption(id: "custom", title: "Custom Subprovider…"), + ], + isVisible: nil, + onChange: { _ in + await context.store.refreshProvider(.omp) + }), + ] + } + + @MainActor + func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] { + [ + ProviderSettingsFieldDescriptor( + id: "omp-custom-subprovider", + title: "Custom Subprovider ID", + subtitle: "Specify a custom subprovider ID (e.g. for new providers added to OMP).", + kind: .plain, + placeholder: "e.g. google-antigravity", + binding: context.binding(\.ompCustomSubprovider), + actions: [], + isVisible: { context.settings.ompSubprovider == "custom" }), + ] + } +} diff --git a/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift b/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift new file mode 100644 index 00000000000..52cb5c237bc --- /dev/null +++ b/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift @@ -0,0 +1,26 @@ +import CodexBarCore +import Foundation + +extension SettingsStore { + public var ompSubprovider: String { + get { self.configSnapshot.providerConfig(for: .omp)?.ompSubprovider ?? "all" } + set { + self.updateProviderConfig(provider: .omp) { $0.ompSubprovider = newValue } + } + } + + public var ompCustomSubprovider: String { + get { self.configSnapshot.providerConfig(for: .omp)?.ompCustomSubprovider ?? "" } + set { + self.updateProviderConfig(provider: .omp) { $0.ompCustomSubprovider = newValue } + } + } + + public func ompSettingsSnapshot() -> OMPProviderSettings { + let hidden = self.configSnapshot.providerConfig(for: .omp)?.hiddenUsageItemIDs ?? [] + return OMPProviderSettings( + subprovider: self.ompSubprovider, + customSubprovider: self.ompCustomSubprovider, + hiddenUsageItemIDs: hidden) + } +} diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementationManifest.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementationManifest.swift index 40d871d50ad..c014c9c3c8f 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderImplementationManifest.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderImplementationManifest.swift @@ -111,5 +111,6 @@ enum ProviderImplementationManifest { PluginCookieProviderImplementation(spec: ParallelProviderDescriptor.spec), PluginCookieProviderImplementation(spec: ClaudePlatformProviderDescriptor.spec), PluginAPIKeyProviderImplementation(spec: MunitoProviderDescriptor.spec), + OMPProviderImplementation(), ] } diff --git a/Sources/CodexBar/Resources/ProviderIcon-omp.svg b/Sources/CodexBar/Resources/ProviderIcon-omp.svg new file mode 100644 index 00000000000..030cbd2a24d --- /dev/null +++ b/Sources/CodexBar/Resources/ProviderIcon-omp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Sources/CodexBarCLI/CLIServeProviderIcons.swift b/Sources/CodexBarCLI/CLIServeProviderIcons.swift index cbdf863454e..e7f1b9a8ad6 100644 --- a/Sources/CodexBarCLI/CLIServeProviderIcons.swift +++ b/Sources/CodexBarCLI/CLIServeProviderIcons.swift @@ -41,6 +41,7 @@ enum CLIServeProviderIcons { "ProviderIcon-fal": "PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIHZpZXdCb3g9Ii0zLjYyIC0yLjg4IDU0IDU0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDx0aXRsZT5mYWw8L3RpdGxlPgogIDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjE1NzQgMC43NDA0NzlDMzAuOTY3NiAwLjc0MDQ3OSAzMS42MTY5IDEuMzk5MjMgMzEuNjk0NCAyLjIwNTY3QzMyLjM4NTMgOS4zOTg5MSAzOC4xMTAyIDE1LjEyMzQgNDUuMzAzOSAxNS44MTQyQzQ2LjExMDQgMTUuODkxNyA0Ni43NjkyIDE2LjU0MSA0Ni43NjkyIDE3LjM1MTFWMzAuODk1OUM0Ni43NjkyIDMxLjcwNiA0Ni4xMTA0IDMyLjM1NTMgNDUuMzAzOSAzMi40MzI4QzM4LjExMDIgMzMuMTIzNiAzMi4zODUzIDM4Ljg0ODEgMzEuNjk0NCA0Ni4wNDE0QzMxLjYxNjkgNDYuODQ3OCAzMC45Njc2IDQ3LjUwNjUgMzAuMTU3NCA0Ny41MDY1SDE2LjYxMThDMTUuODAxNiA0Ny41MDY1IDE1LjE1MjMgNDYuODQ3OCAxNS4wNzQ4IDQ2LjA0MTRDMTQuMzg0IDM4Ljg0ODEgOC42NTkwMSAzMy4xMjM2IDEuNDY1MjggMzIuNDMyOEMwLjY1ODc5OSAzMi4zNTUzIDAgMzEuNzA2IDAgMzAuODk1OVYxNy4zNTExQzAgMTYuNTQxIDAuNjU4ODAzIDE1Ljg5MTcgMS40NjUyOSAxNS44MTQyQzguNjU5MDIgMTUuMTIzNCAxNC4zODQgOS4zOTg5MSAxNS4wNzQ4IDIuMjA1NjdDMTUuMTUyMyAxLjM5OTIzIDE1LjgwMTYgMC43NDA0NzkgMTYuNjExOCAwLjc0MDQ3OUgzMC4xNTc0Wk05LjM5MDM3IDI0LjA4MzlDOS4zOTAzNyAzMS44NjUgMTUuNjkxNSAzOC4xNzI4IDIzLjQ2NDQgMzguMTcyOEMzMS4yMzczIDM4LjE3MjggMzcuNTM4NSAzMS44NjUgMzcuNTM4NSAyNC4wODM5QzM3LjUzODUgMTYuMzAyOCAzMS4yMzczIDkuOTk0OTggMjMuNDY0NCA5Ljk5NDk4QzE1LjY5MTUgOS45OTQ5OCA5LjM5MDM3IDE2LjMwMjggOS4zOTAzNyAyNC4wODM5WiIvPgo8L3N2Zz4K", "ProviderIcon-fireworks": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTEyIDIuNXY0TTEyIDE3LjV2NE0yLjUgMTJoNE0xNy41IDEyaDQiLz4KICAgIDxwYXRoIGQ9Ik01LjMgNS4zbDIuOCAyLjhNMTUuOSAxNS45bDIuOCAyLjgiLz4KICAgIDxwYXRoIGQ9Ik0xOC43IDUuM2wtMi44IDIuOE04LjEgMTUuOWwtMi44IDIuOCIvPgogICAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMi40Ii8+CiAgICA8Y2lyY2xlIGN4PSIxOC41IiBjeT0iNiIgcj0iMSIgZmlsbD0iY3VycmVudENvbG9yIiBzdHJva2U9Im5vbmUiLz4KICAgIDxjaXJjbGUgY3g9IjYiIGN5PSIxOC41IiByPSIwLjgiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlPSJub25lIi8+CiAgPC9nPgo8L3N2Zz4K", "ProviderIcon-flora": "PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIHZpZXdCb3g9Ii0xOCAtMTggMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8dGl0bGU+RkxPUkE8L3RpdGxlPgogIDxwYXRoIGQ9Ik0gMzMuMjgzIDE1LjM1OCBDIDMzLjI4MyA2Ljg3NiA0MC4xNTkgMCA0OC42NDEgMCBDIDU2LjMyIDAgNjQgMCA2NCAwIEMgNjQgMCA2NCA3LjY3OSA2NCAxNS4zNTkgQyA2NCAyMy44NDEgNTcuMTI0IDMwLjcxNyA0OC42NDIgMzAuNzE3IEMgNDguNjQxIDMwLjcxNyA0OC42NDEgMzAuNzE3IDQ4LjY0MSAzMC43MTcgQyA0MC4xNTkgMzAuNzE3IDMzLjI4MyAyMy44NDEgMzMuMjgzIDE1LjM1OSBDIDMzLjI4MyAxNS4zNTkgMzMuMjgzIDE1LjM1OSAzMy4yODMgMTUuMzU4IFogTSAwIDQ4LjY0MSBDIDAgNDAuMTU5IDYuODc2IDMzLjI4MyAxNS4zNTggMzMuMjgzIEMgMTUuMzU4IDMzLjI4MyAxNS4zNTkgMzMuMjgzIDE1LjM1OSAzMy4yODMgQyAyMy44NDEgMzMuMjgzIDMwLjcxNyA0MC4xNTkgMzAuNzE3IDQ4LjY0MSBDIDMwLjcxNyA0OC42NDEgMzAuNzE3IDQ4LjY0MiAzMC43MTcgNDguNjQyIEMgMzAuNzE3IDU3LjEyNCAyMy44NDEgNjQgMTUuMzU5IDY0IEMgNy42OCA2NCAwIDY0IDAgNjQgQyAwIDY0IDAgNTYuMzIgMCA0OC42NDEgWiBNIDMzLjI4MyA0OC42NDEgQyAzMy4yODMgNDAuMTU5IDQwLjE1OSAzMy4yODMgNDguNjQxIDMzLjI4MyBDIDQ4LjY0MSAzMy4yODMgNDguNjQxIDMzLjI4MyA0OC42NDIgMzMuMjgzIEMgNTcuMTI0IDMzLjI4MyA2NCA0MC4xNTkgNjQgNDguNjQxIEMgNjQgNTYuMzIxIDY0IDY0IDY0IDY0IEMgNjQgNjQgNTYuMzIgNjQgNDguNjQxIDY0IEMgNDAuMTU5IDY0IDMzLjI4MyA1Ny4xMjQgMzMuMjgzIDQ4LjY0MiBDIDMzLjI4MyA0OC42NDEgMzMuMjgzIDQ4LjY0MSAzMy4yODMgNDguNjQxIFogTSAwIDAgQyAwIDAgNy42OCAwIDE1LjM1OSAwIEMgMjMuODQxIDAgMzAuNzE3IDYuODc2IDMwLjcxNyAxNS4zNTggQyAzMC43MTcgMTUuMzU4IDMwLjcxNyAxNS4zNTkgMzAuNzE3IDE1LjM1OSBDIDMwLjcxNyAyMy44NDEgMjMuODQxIDMwLjcxNyAxNS4zNTkgMzAuNzE3IEMgMTUuMzU5IDMwLjcxNyAxNS4zNTggMzAuNzE3IDE1LjM1OCAzMC43MTcgQyA2Ljg3NiAzMC43MTcgMCAyMy44NDEgMCAxNS4zNTkgQyAwIDcuNjggMCAwIDAgMCBaIi8+Cjwvc3ZnPgo=", + "ProviderIcon-futureagi": "PHN2ZyB3aWR0aD0iNTEiIGhlaWdodD0iNTEiIHZpZXdCb3g9IjAgMCA1MSA1MSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUxIDI3LjYyOEw0Ny4yOTk1IDI5Ljc2NDdMNDMuNTk2NyAzMS45MDEzTDM5Ljg5NjMgMzQuMDQwNEwzNi4xOTU4IDM2LjE3N0wzNC4wNTY4IDM5Ljg3OThMMzEuOTIwMSA0My41ODAzTDI5Ljc4MzUgNDcuMjgzMUwyNy42NDY4IDUwLjk4MzVIMjMuMzcxMUwyNS41MDc4IDQ3LjI4MzFMMjcuNjQ2OCA0My41ODAzTDI5Ljc4MzUgMzkuODc5OEwzMS45MjAxIDM2LjE3N0wzMy40ODM5IDMzLjQ2NTFMMzYuMTkzNSAzMS45MDEzTDM5Ljg5NjMgMjkuNzY0N0w0My41OTY3IDI3LjYyOEwzOS44OTYzIDI1LjQ5MTNMMzYuMTkzNSAyMy4zNTIzTDMyLjQ5MDcgMjEuMjE1NkwzMC43NzIgMjAuMjI0OEwyOS43ODExIDE4LjUwODRMMjcuNjQ0NCAxNC44MDU2TDI1LjUwNzggMTEuMTAyOEwyNy42NDQ0IDcuNDAyMzRMMjkuNzgxMSAxMS4xMDI4TDMxLjkxNzggMTQuODA1NkwzMy40ODE2IDE3LjUxNTJMMzYuMTkzNSAxOS4wNzg5TDM5Ljg5NjMgMjEuMjE1Nkw0My41OTY3IDIzLjM1MjNMNDcuMjk5NSAyNS40OTEzTDUxIDI3LjYyOFoiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl83NV82MTg3NykiLz4KPHBhdGggZD0iTTQzLjU4MTIgMjcuNjQ0NEwzOS44ODA3IDI5Ljc4MTFMMzYuMTc3OSAzMS45MTc4TDMzLjQ2ODMgMzMuNDgxNkwzMS45MDQ2IDM2LjE5MzVMMjkuNzY3OSAzOS44OTYzTDI3LjYzMTIgNDMuNTk2N0wyNS40OTIyIDQ3LjI5OTVMMjMuMzU1NSA1MUwyMS4yMTg4IDQ3LjI5OTVMMTkuMDgyMiA0My41OTY3TDE2Ljk0MzEgMzkuODk2M0wxNC44MDY1IDM2LjE5NThMMTEuMTAzNyAzNC4wNTY4TDcuNDAzMjMgMzEuOTIwMUwzLjcwMDQ0IDI5Ljc4MzVMMCAyNy42NDY4TDAgMjMuMzcxMUwzLjcwMDQ0IDI1LjUwNzhMNy40MDMyMyAyNy42NDY4TDExLjEwMzcgMjkuNzgzNUwxNC44MDY1IDMxLjkyMDFMMTcuNTE4NCAzMy40ODM5TDE5LjA4MjIgMzYuMTkzNUwyMS4yMTg4IDM5Ljg5NjNMMjMuMzU1NSA0My41OTY3TDI1LjQ5MjIgMzkuODk2M0wyNy42MzEyIDM2LjE5MzVMMjkuNzY3OSAzMi40OTA3TDMwLjc1ODggMzAuNzcyTDMyLjQ3NTEgMjkuNzgxMUwzNi4xNzc5IDI3LjY0NDRMMzkuODgwNyAyNS41MDc4TDQzLjU4MTIgMjcuNjQ0NFoiIGZpbGw9InVybCgjcGFpbnQxX2xpbmVhcl83NV82MTg3NykiLz4KPHBhdGggZD0iTTExLjEwMzcgMjEuMjE4OEw3LjQwMzIzIDIzLjM1NTVMMTEuMTAzNyAyNS40OTIyTDE0LjgwNjUgMjcuNjMxMkwxOC41MDkzIDI5Ljc2NzlMMjAuMjI4IDMwLjc1ODhMMjEuMjE4OCAzMi40NzUxTDIzLjM1NTUgMzYuMTc3OUwyNS40OTIyIDM5Ljg4MDdMMjMuMzU1NSA0My41ODEyTDIxLjIxODggMzkuODgwN0wxOS4wODIyIDM2LjE3NzlMMTcuNTE4NCAzMy40NjgzTDE0LjgwNjUgMzEuOTA0NkwxMS4xMDM3IDI5Ljc2NzlMNy40MDMyMyAyNy42MzEyTDMuNzAwNDQgMjUuNDkyMkwwIDIzLjM1NTVMMy43MDA0NCAyMS4yMTg4TDcuNDAzMjMgMTkuMDgyMkwxMS4xMDM3IDE2Ljk0MzFMMTQuODA0MSAxNC44MDY1TDE2Ljk0MzEgMTEuMTAzN0wxOS4wNzk4IDcuNDAzMjNMMjEuMjE2NSAzLjcwMDQ0TDIzLjM1MzIgMEgyNy42Mjg5TDI1LjQ5MjIgMy43MDA0NEwyMy4zNTMyIDcuNDAzMjNMMjEuMjE2NSAxMS4xMDM3TDE5LjA3OTggMTQuODA2NUwxNy41MTYgMTcuNTE4NEwxNi41OTMzIDE4LjA1MTRMMTQuODA2NSAxOS4wODIyTDExLjEwMzcgMjEuMjE4OFoiIGZpbGw9InVybCgjcGFpbnQyX2xpbmVhcl83NV82MTg3NykiLz4KPHBhdGggZD0iTTUwLjk4ODQgMjMuMzUzMlYyNy42Mjg5TDQ3LjI4NzkgMjUuNDkyMkw0My41ODUyIDIzLjM1MzJMMzkuODg0NyAyMS4yMTY1TDM2LjE4MTkgMTkuMDc5OEwzMy40NyAxNy41MTZMMzEuOTA2MiAxNC44MDY1TDI5Ljc2OTUgMTEuMTAzN0wyNy42MzI5IDcuNDAzMjNMMjUuNDk2MiAxMS4xMDM3TDIzLjM1NzIgMTQuODA2NUwyMS4yMjA1IDE4LjUwOTNMMjAuMjI5NiAyMC4yMjhMMTguNTEzMiAyMS4yMTg4TDE0LjgxMDUgMjMuMzU1NUwxMS4xMDc3IDI1LjQ5MjJMNy40MDcyMyAyMy4zNTU1TDExLjEwNzcgMjEuMjE4OEwxNC44MTA1IDE5LjA4MjJMMTYuNTk3MyAxOC4wNTE0TDE3LjUyIDE3LjUxODRMMTkuMDgzOCAxNC44MDY1TDIxLjIyMDUgMTEuMTAzN0wyMy4zNTcyIDcuNDAzMjNMMjUuNDk2MiAzLjcwMDQ0TDI3LjYzMjkgMEwyOS43Njk1IDMuNzAwNDRMMzEuOTA2MiA3LjQwMzIzTDM0LjA0NTIgMTEuMTAzN0wzNi4xODE5IDE0LjgwNDFMMzkuODg0NyAxNi45NDMxTDQzLjU4NTIgMTkuMDc5OEw0Ny4yODc5IDIxLjIxNjVMNTAuOTg4NCAyMy4zNTMyWiIgZmlsbD0idXJsKCNwYWludDNfbGluZWFyXzc1XzYxODc3KSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzc1XzYxODc3IiB4MT0iMzcuMTg1NSIgeTE9IjcuNDAyMzQiIHgyPSIzNy4xODU1IiB5Mj0iNTAuOTgzNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjQ0Y2QkU4Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzc4NTdGQyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MV9saW5lYXJfNzVfNjE4NzciIHgxPSIyMS43OTA2IiB5MT0iMjMuMzcxMSIgeDI9IjIxLjc5MDYiIHkyPSI1MSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjOEI0NEZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzdBNDBEOSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50Ml9saW5lYXJfNzVfNjE4NzciIHgxPSIxMy44MTQ0IiB5MT0iMCIgeDI9IjEzLjgxNDQiIHkyPSI0My41ODEyIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDRjZCRTgiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNzg1N0ZDIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQzX2xpbmVhcl83NV82MTg3NyIgeDE9IjI5LjE5NzgiIHkxPSIwIiB4Mj0iMjkuMTk3OCIgeTI9IjI3LjYyODkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzhCNDRGRiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM3QTQwRDkiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K", "ProviderIcon-gemini": "PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik00OC4yMTcgMTAuNzM4MUM0OC44Mjc0IDkuMDc1OTYgNTEuMTcwNSA5LjEwMjYzIDUxLjc0MzQgMTAuNzc3OUw1My4zNDQyIDE1LjQ1NzFDNTUuNzM1NCAyMi40NDcgNTkuNjQ4OSAyOC44MDk2IDY0Ljc5NzkgMzQuMDc5QzY5Ljk0NyAzOS4zNDgzIDc2LjIwMTIgNDMuMzkwOSA4My4xMDI0IDQ1LjkxMDZMODguMjY1NyA0Ny43OTU0Qzg5LjkwODggNDguMzk1MyA4OS45MTIzIDUwLjczNDQgODguMjcxMSA1MS4zMzkyTDgzLjA5NTUgNTMuMjQ2OUM3Ni4zODE4IDU1LjcyMTQgNzAuMjgzOSA1OS42MzkzIDY1LjIyNjIgNjQuNzI4QzYwLjE2ODQgNjkuODE2OCA1Ni4yNzIyIDc1Ljk1NDIgNTMuODA4NSA4Mi43MTM0TDUxLjc4MyA4OC4yNzA0QzUxLjE4MzYgODkuOTE1IDQ4Ljg3NTggODkuOTE5MyA0OC4yNzAzIDg4LjI3NzJMNDYuMTU1MSA4Mi41NDA0QzQzLjY4MTMgNzUuODMwMyAzOS43OTM5IDY5LjczOTUgMzQuNzYwMiA2NC42ODY3QzI5LjcyNjUgNTkuNjMzOSAyMy42NjU5IDU1LjczOSAxNi45OTUyIDUzLjI2OThMMTEuNzI2NSA1MS4zMTk4QzEwLjA5NDQgNTAuNzE1NSAxMC4wOTAxIDQ4LjM5MjQgMTEuNzE5OCA0Ny43ODIyTDE3LjA5MTUgNDUuNzcwN0MyMy43MzQzIDQzLjI4MzQgMjkuNzY1NCAzOS4zODA0IDM0Ljc3MjIgMzQuMzI4OEMzOS43NzkxIDI5LjI3NzEgNDMuNjQzNSAyMy4xOTYgNDYuMTAxMyAxNi41MDEyTDQ4LjIxNyAxMC43MzgxWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==", "ProviderIcon-gitkraken": "PCEtLSBHaXRLcmFrZW4gbWFyayBmcm9tIEdpdEtyYWtlbiBicm93c2VyIGV4dGVuc2lvbjogaHR0cHM6Ly9naXRodWIuY29tL2dpdGtyYWtlbi9nay1icm93c2VyLWV4dGVuc2lvbi9ibG9iL21haW4vc3JjL2hvc3RzL2dpdGh1Yi50cyAtLT4KPHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R2l0S3Jha2VuPC90aXRsZT48cGF0aCBkPSJNMCAxNkMwIDcuMTc3IDcuMTc3IDAgMTYgMHMxNiA3LjE3NyAxNiAxNi03LjE3NyAxNi0xNiAxNlMwIDI0LjgyMyAwIDE2Wm0yOC4yMjYtNC43MTRhLjYwNy42MDcgMCAwIDAtLjI2OS0uMzE3bC0uMDA0LS4wMDFjLS4xMjMtLjA3LS4yNjgtLjA5NS0uNDA5LS4wN2EuNjEzLjYxMyAwIDAgMC0uNS42Yy4wMDIuMDczLjAxNC4xNDQuMDQuMjE0LjUwMiAxLjMzNy43NTcgMi43NDEuNzU2IDQuMTdhMTEuODM1IDExLjgzNSAwIDAgMS0xMC4xODggMTEuNzI5di01LjM5NmMuMjg0LS4wNTguNTY2LS4xMzQuODQtLjIyNnY0LjY3bC4xMTEtLjAyN2ExMS4xMjggMTEuMTI4IDAgMCAwIDYuMDQyLTMuODY0IDEwLjkzOSAxMC45MzkgMCAwIDAgMi40MDYtNi44ODQgMTEuMDUyIDExLjA1MiAwIDAgMC01LjcwMy05LjY4NS42MTguNjE4IDAgMCAwLS40NzYtLjA0Ni42MS42MSAwIDAgMC0uMTEzIDEuMTEzIDkuODQ2IDkuODQ2IDAgMCAxLTEuMDMxIDE3LjczM3YtMy45NTRhMS42NjYgMS42NjYgMCAwIDAgMS4xMDQtMS4zODcgMS42NyAxLjY3IDAgMCAwLS43NjgtMS42MDZjLjIzNy0yLjE3LjkyNy0yLjU5OCAxLjQzMy0yLjkxMy4zMDItLjE4Ni41NjEtLjM0OC41NjEtLjgxN3YtLjY2OGMwLS42OTItLjY4Ny0yLjMwNy0yLjIyNC00LjM1My0yLjEyNy0yLjgzNC0zLjM0LTMuMTMtMy42NTktMy4xNTMtLjEyLS4wMTItLjIyMy0uMDA3LS4zMzcgMGgtLjAxMmMtLjMyMS4wMjMtMS41MzQuMzItMy42NjQgMy4xNTMtMS41MzkgMi4wNDYtMi4yMjcgMy42NjEtMi4yMjcgNC4zNTN2LjY2NmMwIC40Ny4yNi42My41Ni44MTcuNTA2LjMxMyAxLjE5Ny43NDIgMS40MzMgMi45MTJhMS42NjQgMS42NjQgMCAwIDAtLjc3OSAxLjQyM2MwIC42OTIuNDU2IDEuMzMgMS4xMTcgMS41NzJ2My45NTVhOS44MzcgOS44MzcgMCAwIDEtNC4zNjQtMy41MSA5Ljc4NCA5Ljc4NCAwIDAgMS0xLjc1Mi01LjYwNGMwLTMuNTc4IDEuOTUtNi44OCA1LjA4OC04LjYyYS42MDkuNjA5IDAgMCAwLS4yOTQtMS4xNGgtLjAwMWEuNTkzLjU5MyAwIDAgMC0uMjkuMDc2IDExLjA1NyAxMS4wNTcgMCAwIDAtNS43MSA5LjY4NGMwIDIuNTMuODMzIDQuOTEgMi40MDcgNi44ODVhMTEuMTMxIDExLjEzMSAwIDAgMCA2LjA0MSAzLjg2NGwuMTExLjAyN3YtNC42NjljLjI3NS4wOTIuNTU3LjE2OC44NC4yMjZ2NS4zOTVBMTEuODM0IDExLjgzNCAwIDAgMSA0LjE1NCAxNS44ODRjMC0xLjQzLjI1NS0yLjgzMy43Ni00LjE3YS42MTIuNjEyIDAgMCAwLS4wMTctLjQ2NC41OTcuNTk3IDAgMCAwLS4zNC0uMzE2LjYxMS42MTEgMCAwIDAtLjY1NS4xNTUuNjEuNjEgMCAwIDAtLjEyNS4yMDIgMTMuMTMzIDEzLjEzMyAwIDAgMC0uNzQ0IDYuMDY3QTEzLjEzNSAxMy4xMzUgMCAwIDAgNS4xMjggMjMuMWExMy4xMzQgMTMuMTM0IDAgMCAwIDQuNDc3IDQuMTYyIDEzLjE0IDEzLjE0IDAgMCAwIDUuODggMS42NzJsLjA5My4wMDN2LTYuNTY1YTE3Ljc3NSAxNy43NzUgMCAwIDAgLjQ3OS4wMTJjLjA4LS4wMDIuMjMzLS4wMDYuMzYyLS4wMTJ2Ni41NjVsLjA5My0uMDAzYTEzLjE1NiAxMy4xNTYgMCAwIDAgNS44NzgtMS42NzYgMTMuMTUyIDEzLjE1MiAwIDAgMCA0LjQ3Ny00LjE2MyAxMy4xNDUgMTMuMTQ1IDAgMCAwIDIuMDk3LTUuNzQxIDEzLjE0NiAxMy4xNDYgMCAwIDAtLjczOC02LjA2OFpNMTMuNjY0IDIwLjAxYS45NzcuOTc3IDAgMCAwLS40MzYtMS40NDIuOTc4Ljk3OCAwIDAgMC0xLjMyOS43MS45NzguOTc4IDAgMCAwIC41ODMgMS4wOS45NzQuOTc0IDAgMCAwIDEuMTgzLS4zNTdoLS4wMDJabTYuMzQzLS45OTRhLjk3MS45NzEgMCAwIDAtMS4xNC0uNDc1Ljk3OC45NzggMCAwIDAtLjY5IDEuMDI1Ljk3NS45NzUgMCAwIDAgLjk2OC44ODEuOTc0Ljk3NCAwIDAgMCAuODYxLTEuNDMxaC4wMDFaIi8+PC9zdmc+Cg==", "ProviderIcon-grok": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiPgogICAgPHBhdGggZmlsbD0iI0ZGRkZGRiIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNOS4yNyAxNS4yOWw3Ljk3OC01Ljg5N2MuMzkxLS4yOS45NS0uMTc3IDEuMTM3LjI3Mi45OCAyLjM2OS41NDIgNS4yMTUtMS40MSA3LjE2OS0xLjk1MSAxLjk1NC00LjY2NyAyLjM4Mi03LjE0OSAxLjQwNmwtMi43MTEgMS4yNTdjMy44ODkgMi42NjEgOC42MTEgMi4wMDMgMTEuNTYyLS45NTMgMi4zNDEtMi4zNDQgMy4wNjYtNS41MzkgMi4zODgtOC40MmwuMDA2LjAwN2MtLjk4My00LjIzMi4yNDItNS45MjQgMi43NS05LjM4My4wNi0uMDgyLjEyLS4xNjQuMTc5LS4yNDhsLTMuMzAxIDMuMzA1di0uMDFMOS4yNjcgMTUuMjkyTTcuNjIzIDE2LjcyM2MtMi43OTItMi42Ny0yLjMxLTYuODAxLjA3MS05LjE4NCAxLjc2MS0xLjc2MyA0LjY0Ny0yLjQ4MyA3LjE2Ni0xLjQyNWwyLjcwNS0xLjI1YTcuODA4IDcuODA4IDAgMDAtMS44MjktMUE4Ljk3NSA4Ljk3NSAwIDAwNS45ODQgNS44M2MtMi41MzMgMi41MzYtMy4zMyA2LjQzNi0xLjk2MiA5Ljc2NCAxLjAyMiAyLjQ4Ny0uNjUzIDQuMjQ2LTIuMzQgNi4wMjItLjU5OS42My0xLjE5OSAxLjI1OS0xLjY4MiAxLjkyNWw3LjYyLTYuODE1Ii8+Cjwvc3ZnPgo=", @@ -71,6 +72,7 @@ enum CLIServeProviderIcons { "ProviderIcon-notion": "PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgaGVpZ2h0PSI1MTIiIHN0eWxlPSJmbGV4Om5vbmU7bGluZS1oZWlnaHQ6MSIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5Ob3Rpb248L3RpdGxlPjxwYXRoIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE1LjI1Ny4wNTVsLTEzLjMxLjk4Qy44NzQgMS4xMjguNSAxLjgzLjUgMi42Njd2MTQuNTU5YzAgLjY1NC4yMzMgMS4yMTMuNzk0IDEuOTZsMy4xMjkgNC4wNmMuNTEzLjY1My45OC43OTQgMS45NjIuNzQ1bDE1LjQ1Ny0uOTMyYzEuMzA3LS4wOTMgMS42ODEtLjcgMS42ODEtMS43MjdWNC45NTRjMC0uNTMtLjIxLS42ODQtLjgyOS0xLjEzNWwtLjEwNi0uMDc4TDE4LjM0Ljc1NWMtMS4wMjctLjc0Ni0xLjQ1LS44NC0zLjA4My0uN3ptLTguNTIxIDQuNjNjLTEuMjYzLjA4Ni0xLjU0OS4xMDUtMi4yNjYtLjQ3N0wyLjY0NyAyLjc2Yy0uMTg2LS4xODctLjA5Mi0uNDIuMzc1LS40NjZsMTIuNzk2LS45MzNjMS4wNzQtLjA5NCAxLjYzNC4yOCAyLjA1NC42MDZsMi4xOTUgMS41ODdjLjA5My4wNDcuMzI2LjMyNi4wNDcuMzI2bC0xMy4yMTYuNzk0LS4xNjIuMDF6TTUuMjYzIDIxLjE5M1Y3LjI4N2MwLS42MDYuMTg3LS44ODYuNzQ4LS45MzNsMTUuMTc2LS44ODZjLjUxNS0uMDQ3Ljc0OC4yOC43NDguODg2djEzLjgxYzAgLjYwOS0uMDkzIDEuMTIyLS45MzQgMS4xNjhsLTE0LjUyMy44NGMtLjg0Mi4wNDctMS4yMTUtLjIzMi0xLjIxNS0uOTh6bTE0LjMzOC0xMy4xNmMuMDkzLjQyMiAwIC44NDItLjQyMi44OWwtLjY5OS4xMzl2MTAuMjY0Yy0uNjA4LjMyNy0xLjE2OC41MTMtMS42MzUuNTEzLS43NDcgMC0uOTM0LS4yMzItMS40OTUtLjkzMmwtNC41NzYtNy4xODV2Ni45NTJsMS40NDguMzI3czAgLjg0LTEuMTY5Ljg0bC0zLjIyMS4xODZjLS4wOTQtLjE4NyAwLS42NTQuMzI3LS43NDdsLjg0LS4yMzJWOS44NTNMNy44MzIgOS43NmMtLjA5My0uNDIuMTQtMS4wMjYuNzk0LTEuMDczbDMuNDU2LS4yMzIgNC43NjMgNy4yNzl2LTYuNDRsLTEuMjE0LS4xNGMtLjA5NC0uNTEzLjI4LS44ODcuNzQ3LS45MzNsMy4yMjMtLjE4N3oiPjwvcGF0aD48L3N2Zz4=", "ProviderIcon-nous": "PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iY3VycmVudENvbG9yIj4KPHBhdGggZD0iTTk2IDQzMlY4MGg3MmwxODQgMjM2VjgwaDY0djM1MmgtNzJMMTYwIDE5NnYyMzZIOTZ6Ii8+CjxjaXJjbGUgY3g9IjI1NiIgY3k9IjI1NiIgcj0iMzQiLz4KPC9zdmc+Cg==", "ProviderIcon-ollama": "PHN2ZyB3aWR0aD0iMjQwIiBoZWlnaHQ9IjI0MCIgdmlld0JveD0iMCAwIDI0MCAyNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjwhLS0gVGVtcGxhdGUtc2FmZSB2ZWN0b3IgYWRhcHRhdGlvbiBvZiBodHRwczovL2dpdGh1Yi5jb20vb2xsYW1hL29sbGFtYS9ibG9iL21haW4vZG9jcy9pbWFnZXMvbG9nby5wbmcgLS0+CjxwYXRoIGQ9Ik0xNzguNDYgNC4zNTU2MUMxODQuMzM2IDE3LjQwMDggMTg1LjcwOCAzNC42OTEgMTgxLjI4MiA0OC45MzE3QzE3My4zNzEgNDYuMjg5NSAxNjYuMTEyIDQ0LjI2NDQgMTU3LjY4OSA0My42OTg3QzE1Ny42ODMgMzEuMzM5IDE1OS45MTYgMTkuMzMzNyAxNjUuMDYyIDguMDY5ODRDMTY3LjYwNiAyLjUwMjAyIDE3NC40MDkgLTQuNjM3ODcgMTc4LjQ2IDQuMzU1NjFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzEuMDc3NSAzLjAzNTI0Qzc4LjY3ODggMTQuOTA5NiA4MS4xMDEzIDI5LjkyMjQgODEuNTQyMiA0My43MDQ5Qzc2LjI2NjggNDQuMTMyMyA3My4zMTk1IDQ0LjM0MjYgNjguMTQyOSA0NS40NzVDNjQuMjU1MiA0Ni41MTM3IDYxLjU4MjIgNDcuNTc3NCA1Ny45NjA3IDQ5LjE3OTNDNTMuNDUxMyAzNi4wMjAzIDU0LjY1MjcgMTkuMDE0NCA1OS43NTE5IDYuMDUwMzhDNjEuNzgxNyAwLjg5MDg3IDY3LjI1MzQgLTIuOTM3ODUgNzEuMDc3NSAzLjAzNTI0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTguNDY4IDM0LjkwODlDMTMzLjA0NyAzNC40OTcxIDE0My45MDkgNDAuOTU0NSAxNTIuNDc1IDUyLjMyNDJDMTU1LjM2IDUyLjMxNzQgMTU4LjExNCA1Mi4zMTg4IDE2MS4wMDcgNTIuNTM0MkMxOTIuNTk0IDU0Ljg4OTggMjE0LjMzOSA4Ny45MTg1IDIwMi45IDExNy43MDVDMjAxLjExOCAxMjIuMzQ2IDE5OS4yNTkgMTI1LjM5NSAxOTYuNTU4IDEyOS40ODJDMTk5Ljc3NyAxMzQuMjg1IDIwMi4xODMgMTM5LjU4NiAyMDMuNjc5IDE0NS4xN0MyMDcuNDcxIDE1OC45OTEgMjA2LjYyOSAxNzIuMzU1IDIwMS4wNjQgMTg1LjY1NUMyMDAuMzYzIDE4Ny4zMjYgMTk4LjgxOSAxODkuOTU2IDE5Ny41MzggMTkyLjEzOEMxOTcuMDgzIDE5Mi45MTIgMTk2LjY2MiAxOTMuNjI5IDE5Ni4zMjMgMTk0LjIyOEMxOTYuNjU3IDE5NC45NzIgMTk3LjA2IDE5NS43MzQgMTk3LjQ2MSAxOTYuNDkzTDE5Ny40NjEgMTk2LjQ5NEMxOTcuNzQ2IDE5Ny4wMzQgMTk4LjAzMSAxOTcuNTcyIDE5OC4yOSAxOTguMTAzQzIwNC4yNzEgMjEwLjMxNiAyMDcuMzQ2IDIyNi41MjIgMjA0LjIwOSAyNDBMMzQuODMxOSAyMzkuOTg2QzMxLjY2NDQgMjIzLjM2NiAzNC45MTA5IDIwOS4wMTEgNDIuNzU3OSAxOTQuMzA1QzQxLjYwOCAxOTIuMDIzIDQwLjY0NzQgMTkwLjI5IDM5LjgxNSAxODguNzg5QzM3LjQzMDggMTg0LjQ4OSAzNi4wOTg4IDE4Mi4wODcgMzQuMzgzOSAxNzQuMTE5QzMwLjk5MiAxNTguNzY3IDMzLjg5NDIgMTQyLjcwMiA0Mi40NDI4IDEyOS41MDJDMzYuNzkyMiAxMjAuMzU0IDMzLjAwODcgMTEyLjM4NyAzMy4wMTE1IDEwMS4yMkMzMy4xMjI5IDg4LjIyNTYgMzguMzU0NCA3NS43OTggNDcuNTczMSA2Ni42Mjg2QzU4Ljg2MTkgNTUuMzU0NiA3MS4xNTY2IDUyLjMwNDYgODYuNTE3NSA1Mi4zNTI3Qzk1LjAzODEgNDEuMDY4MyAxMDQuMDY5IDM1LjY5NDMgMTE4LjQ2OCAzNC45MDg5Wk0xNzguOTEzIDEwMy4yNzJDMTgwLjA0OSAxMDguNDkxIDE3Ni43NDIgMTEzLjY0MyAxNzEuNTIzIDExNC43OTVDMTY2LjI3NSAxMTUuOTU2IDE2MS4wODQgMTEyLjYzMyAxNTkuOTQyIDEwNy4zODdDMTU4LjgwMiAxMDIuMTQyIDE2Mi4xNDYgOTYuOTY4NyAxNjcuNCA5NS44NDgyQzE3Mi42MzEgOTQuNzMzMSAxNzcuNzc3IDk4LjA1MjUgMTc4LjkxMyAxMDMuMjcyWk03OC45OTMgMTAzLjA1N0M4MC4yMzAyIDEwOC4xNjkgNzcuMTQzOCAxMTMuMzMyIDcyLjA1MDYgMTE0LjY2OEM2OC42NjQ5IDExNS41NTcgNjUuMDYzMSAxMTQuNTQ2IDYyLjYzMjkgMTEyLjAzMUM2MC4yMDI4IDEwOS41MTQgNTkuMzI0IDEwNS44ODMgNjAuMzM1NCAxMDIuNTM1QzYxLjM0NjcgOTkuMTg4IDY0LjA5IDk2LjY0NzggNjcuNTA4MiA5NS44OTQxQzcyLjY1MDUgOTQuNzU5NyA3Ny43NTYxIDk3Ljk0MzggNzguOTkzIDEwMy4wNTdaTTE0Ny40MjcgMTA0LjE4MkMxNDAuMTk0IDk4LjExODcgMTMwLjUyIDk0LjYxNzMgMTIwLjA0MSA5NC42MTczQzEwOS41NzIgOTQuNjE3MyA5OS42OTczIDk4LjEwNzUgOTIuMjE4MyAxMDQuMTE1Qzg0Ljg1MTIgMTEwLjAzMiA4MC4zOTUxIDExNy45NzUgODAuMzk1MSAxMjUuODg0QzgwLjM5NTEgMTMzLjg4MiA4NC4yNjkgMTQxLjI2NSA5MS4xNzcgMTQ2LjUyM0M5OC4xNDExIDE1MS44MzYgMTA3Ljc5MiAxNTQuNzM0IDExOS4wODkgMTU0LjczNEMxMjkuNzQ4IDE1NC43MzQgMTM5LjA1MiAxNTIuNjk4IDE0NS45OTMgMTQ4LjUwM0MxNTQuMTMzIDE0My41NTggMTU4LjY3OSAxMzUuODE3IDE1OC42NzkgMTI1Ljg4NEMxNTguNjc5IDExNy45OTcgMTU0LjQ4IDExMC4wNzcgMTQ3LjQyNyAxMDQuMTgyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMjAuMDQxIDEwNC42NEMxMTIuMjczIDEwNC41NzMgMTA0LjcxMiAxMDcuMTM2IDk4LjU4ODkgMTExLjkxMkM5My40Mjc1IDExNi4wNTEgOTAuNTA1MyAxMjEuMjUyIDkwLjUwNTMgMTI1Ljg5NUM5MC41MDUzIDEzMC42ODMgOTIuODU2NSAxMzUuMTY4IDk3LjMzNDkgMTM4LjU4QzEwMi40MjkgMTQyLjQ2MiAxMDkuOTE5IDE0NC43MTEgMTE5LjA4OSAxNDQuNzExQzEyOC4wMzUgMTQ0LjcxMSAxMzUuNTgxIDE0My4wNjYgMTQwLjcyIDEzOS45NDVDMTQ1LjkwNCAxMzYuODEzIDE0OC41NTcgMTMyLjI3MSAxNDguNTU3IDEyNS44ODRDMTQ4LjU1NyAxMjEuMTUyIDE0NS44MDMgMTE1LjkyOCAxNDAuOTEgMTExLjgzM0MxMzUuNDkxIDEwNy4zMDMgMTI4LjE0NyAxMDQuNjQgMTIwLjA0MSAxMDQuNjRaTTEyNy40OTcgMTE4LjIyMUwxMjcuNDUzIDExOC4xNzZDMTI2Ljc5MyAxMTcuMzg1IDEyNS44NSAxMTYuODgzIDEyNC44MjYgMTE2Ljc3N0MxMjMuODAxIDExNi42NyAxMjIuNzc1IDExNi45NjcgMTIxLjk2NiAxMTcuNjA2TDExOS41MDMgMTE5LjU1MkwxMTcuMDk2IDExNy42MjhDMTE2LjI4NSAxMTYuOTc2IDExNS4yNDkgMTE2LjY3IDExNC4yMTMgMTE2Ljc3N0MxMTMuMTc4IDExNi44ODQgMTEyLjIyNiAxMTcuMzk1IDExMS41NjUgMTE4LjE5OEMxMTEuMjQxIDExOC41OTYgMTEwLjk5OSAxMTkuMDUzIDExMC44NTMgMTE5LjU0NEMxMTAuNzA3IDEyMC4wMzYgMTEwLjY1OSAxMjAuNTUxIDExMC43MTQgMTIxLjA2QzExMC43NjggMTIxLjU3IDExMC45MjIgMTIyLjA2NCAxMTEuMTY4IDEyMi41MTNDMTExLjQxNCAxMjIuOTYzIDExMS43NDcgMTIzLjM1OSAxMTIuMTQ3IDEyMy42OEwxMTUuMTgyIDEyNi4xMTlWMTMxLjI2NEMxMTUuMTg4IDEzMi4zNzUgMTE1LjYzNCAxMzMuNDM4IDExNi40MjMgMTM0LjIyQzExNy4yMTIgMTM1LjAwMiAxMTguMjggMTM1LjQ0IDExOS4zOTEgMTM1LjQzN0MxMjAuNTAzIDEzNS40NCAxMjEuNTcgMTM1LjAwMiAxMjIuMzYgMTM0LjIyQzEyMy4xNDkgMTMzLjQzOCAxMjMuNTk1IDEzMi4zNzUgMTIzLjYwMSAxMzEuMjY0VjEyNi4yNzVMMTI2Ljg3IDEyMy43MDJDMTI4LjU2MSAxMjIuMzYgMTI4Ljg0MSAxMTkuOTEgMTI3LjQ5NyAxMTguMjIxWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==", + "ProviderIcon-omp": "PHN2ZyB2aWV3Qm94PSIwIDAgNTYwIDU2MCIgZmlsbD0iY3VycmVudENvbG9yIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Ik04MCAxMjBMMjYwIDI4MEw4MCA0NDBIMTcwTDM1MCAyODBMMTcwIDEyMEg4MFoiLz4KICA8cGF0aCBkPSJNMzQwIDM2MEg0ODBWNDQwSDM0MFYzNjBaIi8+Cjwvc3ZnPgo=", "ProviderIcon-opencode": "PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik04MCA4OEgyMFYxMkg4MFY4OFpNMzUgMjdINjVWNzJIMzVWMjdaIiBmaWxsPSIjMjExRTFFIi8+Cjwvc3ZnPgo=", "ProviderIcon-opencodego": "PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik04MCA4OEgyMFYxMkg4MFY4OFpNMzUgMjdINjVWNzJIMzVWMjdaIiBmaWxsPSIjMjExRTFFIi8+Cjwvc3ZnPgo=", "ProviderIcon-openrouter": "PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iY3VycmVudENvbG9yIiBzdHJva2U9ImN1cnJlbnRDb2xvciI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiPgo8cGF0aCBkPSJNMyAyNDguOTQ1QzE4IDI0OC45NDUgNzYgMjM2IDEwNiAyMTlDMTM2IDIwMiAxMzYgMjAyIDE5OCAxNThDMjc2LjQ5NyAxMDIuMjkzIDMzMiAxMjAuOTQ1IDQyMyAxMjAuOTQ1IiBzdHJva2Utd2lkdGg9IjkwIi8+CjxwYXRoIGQ9Ik01MTEgMTIxLjVMMzU3LjI1IDIxMC4yNjhMMzU3LjI1IDMyLjczMjRMNTExIDEyMS41WiIvPgo8cGF0aCBkPSJNMCAyNDlDMTUgMjQ5IDczIDI2MS45NDUgMTAzIDI3OC45NDVDMTMzIDI5NS45NDUgMTMzIDI5NS45NDUgMTk1IDMzOS45NDVDMjczLjQ5NyAzOTUuNjUyIDMyOSAzNzcgNDIwIDM3NyIgc3Ryb2tlLXdpZHRoPSI5MCIvPgo8cGF0aCBkPSJNNTA4IDM3Ni40NDVMMzU0LjI1IDI4Ny42NzhMMzU0LjI1IDQ2NS4yMTNMNTA4IDM3Ni40NDVaIi8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDAiPgo8cmVjdCB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K", diff --git a/Sources/CodexBarCore/AgentSession.swift b/Sources/CodexBarCore/AgentSession.swift index a3a12d45a19..26a7df77ae9 100644 --- a/Sources/CodexBarCore/AgentSession.swift +++ b/Sources/CodexBarCore/AgentSession.swift @@ -345,6 +345,7 @@ public enum AgentPSOutputParser { executableBasename: String, arguments: @autoclosure () -> [String]) -> AgentSession.Dialect? { + // Provider-specific by design: This helper detects Pi and OMP process dialects. let firstBasename = AgentProcessPath.basename(executableBasename).lowercased() if firstBasename == AgentSession.Provider.pi.rawValue { return .pi diff --git a/Sources/CodexBarCore/PathEnvironment.swift b/Sources/CodexBarCore/PathEnvironment.swift index 2f2d24b4ca6..6a286810bd9 100644 --- a/Sources/CodexBarCore/PathEnvironment.swift +++ b/Sources/CodexBarCore/PathEnvironment.swift @@ -148,6 +148,37 @@ public enum BinaryLocator { home: home) } + public static func resolveOMPBinary( + env: [String: String] = ProcessInfo.processInfo.environment, + loginPATH: [String]? = LoginShellPathCache.shared.current, + commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV, + aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator + .resolveAlias, + fileManager: FileManager = .default, + home: String = NSHomeDirectory()) -> String? + { + if let override = env["OMP_CLI_PATH"] { + return fileManager.isExecutableFile(atPath: override) ? override : nil + } + // Provider-specific by design: This named resolver supplies OMP's actual CLI executable name. + return self.resolveBinary( + name: "omp", + overrideKey: "OMP_CLI_PATH", + env: env, + loginPATH: loginPATH, + commandV: commandV, + aliasResolver: aliasResolver, + wellKnownPaths: [ + "\(home)/Library/Application Support/Tern/omp/bin/omp", + "\(home)/.omp/bin/omp", + "\(home)/.local/bin/omp", + "/opt/homebrew/bin/omp", + "/usr/local/bin/omp", + ], + fileManager: fileManager, + home: home) + } + public static func resolveCodexBinary( env: [String: String] = ProcessInfo.processInfo.environment, loginPATH: [String]? = LoginShellPathCache.shared.current, diff --git a/Sources/CodexBarCore/PiFamilySessionRootResolver.swift b/Sources/CodexBarCore/PiFamilySessionRootResolver.swift index bce2a8f2ef8..fe7c1e82d6f 100644 --- a/Sources/CodexBarCore/PiFamilySessionRootResolver.swift +++ b/Sources/CodexBarCore/PiFamilySessionRootResolver.swift @@ -121,6 +121,7 @@ struct OMPSessionRootResolver: Sendable { home: home, baseDirectory: baseDirectory) { + // Provider-specific by design: This resolves OMP's default XDG session root. let xdgSessions = xdgDataHome .appendingPathComponent("omp", isDirectory: true) .appendingPathComponent("sessions", isDirectory: true) @@ -183,6 +184,7 @@ struct OMPSessionRootResolver: Sendable { home: home, baseDirectory: baseDirectory) { + // Provider-specific by design: This resolves OMP's named profile roots under XDG. let xdgProfileRoot = xdgDataHome .appendingPathComponent("omp", isDirectory: true) .appendingPathComponent("profiles", isDirectory: true) @@ -221,6 +223,7 @@ struct OMPSessionRootResolver: Sendable { home: home, baseDirectory: baseDirectory) { + // Provider-specific by design: This discovers OMP profiles under XDG. directories.append( xdgDataHome .appendingPathComponent("omp", isDirectory: true) @@ -315,6 +318,7 @@ struct OMPSessionRootResolver: Sendable { { configuredPath } else { + // Provider-specific by design: This defaults to OMP's standard configuration directory. ".omp" } guard !name.hasPrefix("/") else { return nil } diff --git a/Sources/CodexBarCore/PiFamilySessionScanner.swift b/Sources/CodexBarCore/PiFamilySessionScanner.swift index 39adcaf3e52..470ff4267e6 100644 --- a/Sources/CodexBarCore/PiFamilySessionScanner.swift +++ b/Sources/CodexBarCore/PiFamilySessionScanner.swift @@ -37,6 +37,7 @@ enum PiFamilySessionFileParser { var titleSlotWasPresent = false var titleSlot: String? + // Provider-specific by design: OMP sessions can start with a title block. if dialect == .omp, let first = Self.jsonObject(from: nonEmptyLines[0]), first["type"] as? String == "title" @@ -563,6 +564,7 @@ struct PiFamilySessionScanner: Sendable { startedAt: nil, command: context.command, arguments: context.arguments) + // Provider-specific by design: Check for explicit OMP profile in process context. return AgentPSOutputParser.piDialect(for: process) == .omp && (Self.commandLineValue( "--profile", @@ -667,6 +669,7 @@ struct PiFamilySessionScanner: Sendable { { return self.isCWDIndependentPath(selector, environment: environment) } + // Provider-specific by design: Check dialect-specific environment and command line selectors. if AgentPSOutputParser.piDialect(for: process) == .pi, let selector = environment["PI_CODING_AGENT_DIR"], !selector.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty @@ -722,6 +725,7 @@ struct PiFamilySessionScanner: Sendable { { return "process:" + dialect.rawValue + ":session-dir:" + url.path } + // Provider-specific by design: OMP command-line profile identifier. if dialect == .omp, let profile = Self.commandLineValue( "--profile", @@ -915,6 +919,7 @@ struct PiFamilySessionScanner: Sendable { environment: safeEnvironment, baseDirectory: baseDirectory).map { root in let canonical = OMPSessionRootResolver.canonicalURL(root.url) + // Provider-specific by design: OMP default cost root resolution. let defaultRootIsKnownEmpty = !processHasExplicitSelection && !Self.hasExplicitCostRootSelection(dialect: .omp, environment: environment) && Self.defaultCostSessionRoot(for: .omp, environment: environment) == canonical diff --git a/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift new file mode 100644 index 00000000000..6294bafa038 --- /dev/null +++ b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift @@ -0,0 +1,65 @@ +import Foundation + +public enum OMPUsageError: LocalizedError, Sendable { + case binaryNotFound(String) + case executionFailed(String) + case parseFailed(String) + + public var errorDescription: String? { + switch self { + case let .binaryNotFound(binary): + "\(binary) executable not found. Ensure OMP is installed and in PATH." + case let .executionFailed(msg): + "OMP execution failed: \(msg)" + case let .parseFailed(msg): + "Failed to parse OMP usage output: \(msg)" + } + } +} + +public struct OMPCLIProbe: Sendable { + private static let commandTimeout: TimeInterval = 15 + + public init() {} + + public func fetch( + environment: [String: String] = ProcessInfo.processInfo.environment, + subprovider: String = "all", + hiddenItemIDs: [String] = [], + now: Date = Date()) async throws -> UsageSnapshot + { + let loginPATH = LoginShellPathCache.shared.current + guard let executable = BinaryLocator.resolveOMPBinary(env: environment, loginPATH: loginPATH) else { + throw OMPUsageError.binaryNotFound("omp") + } + + var commandEnvironment = environment + commandEnvironment["NO_COLOR"] = "1" + commandEnvironment["PATH"] = PathBuilder.effectivePATH( + purposes: [.tty, .nodeTooling], + env: environment, + loginPATH: loginPATH) + + let result = try await SubprocessRunner.run( + binary: executable, + arguments: ["usage", "--json"], + environment: commandEnvironment, + timeout: Self.commandTimeout, + standardInput: FileHandle.nullDevice, + label: "omp-usage") + + let output = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + ? result.stderr + : result.stdout + + guard !output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + throw OMPUsageError.parseFailed("The omp CLI returned no usage data.") + } + + return try OMPUsageParser.parse( + jsonString: output, + subprovider: subprovider, + hiddenItemIDs: hiddenItemIDs, + now: now) + } +} diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderConfig.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderConfig.swift new file mode 100644 index 00000000000..794ba3087f9 --- /dev/null +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderConfig.swift @@ -0,0 +1,13 @@ +import Foundation + +extension ProviderConfig { + public var ompSubprovider: String? { + get { self.extensionValue(forKey: "ompSubprovider") } + set { self.setExtensionValue(newValue, forKey: "ompSubprovider") } + } + + public var ompCustomSubprovider: String? { + get { self.extensionValue(forKey: "ompCustomSubprovider") } + set { self.setExtensionValue(newValue, forKey: "ompCustomSubprovider") } + } +} diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift new file mode 100644 index 00000000000..44fdc002004 --- /dev/null +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift @@ -0,0 +1,119 @@ +import Foundation + +public enum OMPProviderDescriptor { + public static let descriptor: ProviderDescriptor = Self.makeDescriptor() + + static func makeDescriptor() -> ProviderDescriptor { + ProviderDescriptor( + id: .omp, + settingsSection: .init(OMPProviderSettingsKey.self), + metadata: ProviderMetadata( + id: .omp, + displayName: "OMP", + sessionLabel: "5-Hour", + weeklyLabel: "Weekly", + opusLabel: nil, + supportsOpus: false, + supportsCredits: false, + creditsHint: "", + toggleTitle: "Show OMP usage", + cliName: "omp", + defaultEnabled: false, + widgetSelectable: false, + burnDownWidgetSelectable: false, + isPrimaryProvider: false, + usesAccountFallback: false, + dashboardURL: "https://github.com/renan-brasilio/omp-renan", + statusPageURL: nil), + branding: ProviderBranding( + iconStyle: .init(provider: .omp), + iconResourceName: "ProviderIcon-omp", + color: ProviderColor(hex: 0x10B981), + confettiPalette: [ + ProviderColor(hex: 0x10B981), + ProviderColor(hex: 0x06B6D4), + ProviderColor(hex: 0x3B82F6), + ]), + tokenCost: ProviderTokenCostConfig( + supportsTokenCost: false, + noDataMessage: { "OMP cost summary is not supported." }), + pace: ProviderPaceCapability( + primary: .session(maximumMinutes: 300, requiresDuration: true), + secondary: .weeklyWithDuration), + history: .optIn, + presentation: ProviderUsagePresentation( + rateWindowLabeler: { metadata, snapshot, _ in + let primaryTitle: String = { + if let desc = snapshot.primary?.resetDescription, let match = desc.components(separatedBy: " accts").first { + return "\(match) accts (5-Hour)" + } + return metadata.sessionLabel + }() + let secondaryTitle: String = { + if let desc = snapshot.secondary?.resetDescription, let match = desc.components(separatedBy: " accts").first { + return "\(match) accts (Weekly)" + } + return metadata.weeklyLabel + }() + return ProviderRateWindowLabels( + primary: primaryTitle, + secondary: secondaryTitle, + tertiary: "", + showsTertiary: false) + }, + iconWindowResolver: { context in + ProviderUsageWindowPair(primary: context.snapshot.primary, secondary: context.snapshot.secondary) + }, + menuBarWindowResolver: { context in + switch context.metric { + case .primary: + return .resolved(context.snapshot.primary) + case .secondary: + return .resolved(context.snapshot.secondary ?? context.snapshot.primary) + case .automatic: + return .resolved(context.snapshot.primary ?? context.snapshot.secondary) + default: + return .unhandled + } + }), + fetchPlan: ProviderFetchPlan( + sourceModes: [.auto, .cli], + pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [OMPCLIUsageStrategy()] })), + cli: ProviderCLIConfig( + name: "omp", + aliases: ["oh-my-prompt"], + versionDetector: { _ in ProviderVersionDetector.ompVersion() })) + } +} + +struct OMPCLIUsageStrategy: ProviderFetchStrategy { + let id: String = "omp.cli" + let kind: ProviderFetchKind = .cli + + func isAvailable(_ context: ProviderFetchContext) async -> Bool { + BinaryLocator.resolveOMPBinary( + env: context.env, + loginPATH: LoginShellPathCache.shared.current) != nil + } + + func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { + let subprovider = context.env["OMP_SUBPROVIDER"] + ?? context.settings?.omp?.effectiveSubprovider + ?? "all" + + let hiddenItemIDs = context.settings?.omp?.hiddenUsageItemIDs ?? [] + + let snapshot = try await OMPCLIProbe().fetch( + environment: context.env, + subprovider: subprovider, + hiddenItemIDs: hiddenItemIDs) + + return self.makeResult( + usage: snapshot, + sourceLabel: "omp-cli") + } + + func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { + false + } +} diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift new file mode 100644 index 00000000000..07969d15c38 --- /dev/null +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift @@ -0,0 +1,49 @@ +import Foundation + +public struct OMPProviderSettings: Sendable, Equatable { + public let subprovider: String + public let customSubprovider: String? + public let hiddenUsageItemIDs: [String] + + public init( + subprovider: String = "all", + customSubprovider: String? = nil, + hiddenUsageItemIDs: [String] = []) + { + self.subprovider = subprovider + self.customSubprovider = customSubprovider + self.hiddenUsageItemIDs = hiddenUsageItemIDs + } + + public var effectiveSubprovider: String { + if self.subprovider == "custom", + let custom = self.customSubprovider?.trimmingCharacters(in: .whitespacesAndNewlines), + !custom.isEmpty + { + return custom + } + return self.subprovider + } +} + +public enum OMPProviderSettingsKey: ProviderSettingsSectionKey { + public static let providerID = ProviderInstanceID.omp + public typealias Section = OMPProviderSettings +} + +extension ProviderSettingsSnapshot { + public typealias OMPProviderSettings = CodexBarCore.OMPProviderSettings + public var omp: OMPProviderSettings? { + self[OMPProviderSettingsKey.self] + } + + public static func make(omp: OMPProviderSettings?) -> Self { + self.make(omp, for: OMPProviderSettingsKey.self) + } +} + +extension ProviderSettingsSnapshotContribution { + public static func omp(_ section: OMPProviderSettings) -> Self { + Self(section, for: OMPProviderSettingsKey.self) + } +} diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageModels.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageModels.swift new file mode 100644 index 00000000000..77ea3df7758 --- /dev/null +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageModels.swift @@ -0,0 +1,226 @@ +import Foundation + +public struct OMPUsageResponse: Codable, Sendable { + public let generatedAt: Double? + public let reports: [OMPAccountReport] + public let capacity: [String: [OMPCapacityWindow]]? + public let accountsWithoutUsage: [OMPAccountWithoutUsage]? + + public init( + generatedAt: Double? = nil, + reports: [OMPAccountReport] = [], + capacity: [String: [OMPCapacityWindow]]? = nil, + accountsWithoutUsage: [OMPAccountWithoutUsage]? = nil) + { + self.generatedAt = generatedAt + self.reports = reports + self.capacity = capacity + self.accountsWithoutUsage = accountsWithoutUsage + } +} + +public struct OMPAccountWithoutUsage: Codable, Sendable { + public let provider: String + public let type: String? + + public init(provider: String, type: String? = nil) { + self.provider = provider + self.type = type + } +} + +public struct OMPAccountReport: Codable, Sendable { + public let provider: String + public let fetchedAt: Double? + public let limits: [OMPLimit]? + public let resetCredits: OMPResetCredits? + public let metadata: [String: String]? + + public init( + provider: String, + fetchedAt: Double? = nil, + limits: [OMPLimit]? = nil, + resetCredits: OMPResetCredits? = nil, + metadata: [String: String]? = nil) + { + self.provider = provider + self.fetchedAt = fetchedAt + self.limits = limits + self.resetCredits = resetCredits + self.metadata = metadata + } + + private enum CodingKeys: String, CodingKey { + case provider + case fetchedAt + case limits + case resetCredits + case metadata + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.provider = try container.decode(String.self, forKey: .provider) + self.fetchedAt = try container.decodeIfPresent(Double.self, forKey: .fetchedAt) + self.limits = try container.decodeIfPresent([OMPLimit].self, forKey: .limits) + self.resetCredits = try container.decodeIfPresent(OMPResetCredits.self, forKey: .resetCredits) + + if let metaContainer = try? container.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .metadata) { + var dict: [String: String] = [:] + for key in metaContainer.allKeys { + if let str = try? metaContainer.decode(String.self, forKey: key) { + dict[key.stringValue] = str + } else if let num = try? metaContainer.decode(Double.self, forKey: key) { + dict[key.stringValue] = "\(num)" + } else if let b = try? metaContainer.decode(Bool.self, forKey: key) { + dict[key.stringValue] = "\(b)" + } + } + self.metadata = dict.isEmpty ? nil : dict + } else { + self.metadata = nil + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.provider, forKey: .provider) + try container.encodeIfPresent(self.fetchedAt, forKey: .fetchedAt) + try container.encodeIfPresent(self.limits, forKey: .limits) + try container.encodeIfPresent(self.resetCredits, forKey: .resetCredits) + try container.encodeIfPresent(self.metadata, forKey: .metadata) + } +} + +public struct OMPResetCredits: Codable, Sendable { + public let availableCount: Int? + + public init(availableCount: Int? = nil) { + self.availableCount = availableCount + } +} + +public struct OMPLimit: Codable, Sendable { + public let id: String + public let label: String? + public let scope: OMPLimitScope? + public let window: OMPWindow? + public let amount: OMPAmount? + public let status: String? + + public init( + id: String, + label: String? = nil, + scope: OMPLimitScope? = nil, + window: OMPWindow? = nil, + amount: OMPAmount? = nil, + status: String? = nil) + { + self.id = id + self.label = label + self.scope = scope + self.window = window + self.amount = amount + self.status = status + } +} + +public struct OMPLimitScope: Codable, Sendable { + public let provider: String? + public let projectId: String? + public let windowId: String? + public let shared: Bool? + public let sharedGroup: String? + + public init( + provider: String? = nil, + projectId: String? = nil, + windowId: String? = nil, + shared: Bool? = nil, + sharedGroup: String? = nil) + { + self.provider = provider + self.projectId = projectId + self.windowId = windowId + self.shared = shared + self.sharedGroup = sharedGroup + } +} + +public struct OMPWindow: Codable, Sendable { + public let id: String? + public let label: String? + public let durationMs: Double? + public let resetsAt: Double? + + public init(id: String? = nil, label: String? = nil, durationMs: Double? = nil, resetsAt: Double? = nil) { + self.id = id + self.label = label + self.durationMs = durationMs + self.resetsAt = resetsAt + } +} + +public struct OMPAmount: Codable, Sendable { + public let unit: String? + public let remainingFraction: Double? + public let usedFraction: Double? + public let remaining: Double? + public let used: Double? + public let limit: Double? + + public init( + unit: String? = nil, + remainingFraction: Double? = nil, + usedFraction: Double? = nil, + remaining: Double? = nil, + used: Double? = nil, + limit: Double? = nil) + { + self.unit = unit + self.remainingFraction = remainingFraction + self.usedFraction = usedFraction + self.remaining = remaining + self.used = used + self.limit = limit + } +} + +public struct OMPCapacityWindow: Codable, Sendable { + public let window: String + public let durationMs: Double? + public let accounts: Double + public let usedAccounts: Double + public let remainingAccounts: Double + public let meter: String? + + public init( + window: String, + durationMs: Double? = nil, + accounts: Double, + usedAccounts: Double, + remainingAccounts: Double, + meter: String? = nil) + { + self.window = window + self.durationMs = durationMs + self.accounts = accounts + self.usedAccounts = usedAccounts + self.remainingAccounts = remainingAccounts + self.meter = meter + } +} + +private struct DynamicCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } +} diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift new file mode 100644 index 00000000000..9397cd99a3f --- /dev/null +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -0,0 +1,502 @@ +import Foundation + +public enum OMPUsageParser { + public static func parse( + jsonString: String, + subprovider: String = "all", + hiddenItemIDs: [String] = [], + now: Date = Date()) throws -> UsageSnapshot + { + guard let data = jsonString.data(using: .utf8) else { + throw OMPUsageError.parseFailed("Invalid UTF-8 in OMP usage output") + } + return try self.parse(data: data, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) + } + + public static func parse( + data: Data, + subprovider: String = "all", + hiddenItemIDs: [String] = [], + now: Date = Date()) throws -> UsageSnapshot + { + let response = try JSONDecoder().decode(OMPUsageResponse.self, from: data) + return self.makeSnapshot(response: response, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) + } + + public static func makeSnapshot( + response: OMPUsageResponse, + subprovider: String, + hiddenItemIDs: [String] = [], + now: Date) -> UsageSnapshot + { + let normalized = self.normalizeSubproviderKey(subprovider) + if normalized == "all" || normalized.isEmpty { + return self.makeCombinedSnapshot(response: response, hiddenItemIDs: hiddenItemIDs, now: now) + } else { + return self.makeSubproviderSnapshot(response: response, target: normalized, hiddenItemIDs: hiddenItemIDs, now: now) + } + } + + public static func normalizeSubproviderKey(_ raw: String) -> String { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + // Provider-specific by design: This normalizes user-selected subprovider aliases. + switch trimmed { + case "all", "combined", "": + return "all" + case "agy", "antigravity", "google", "google-antigravity": + return "google-antigravity" + case "codex", "openai", "openai-codex": + return "openai-codex" + case "claude", "anthropic": + return "anthropic" + case "copilot", "github", "github-copilot": + return "github-copilot" + case "grok", "xai", "xai-oauth": + return "xai-oauth" + default: + return trimmed + } + } + + private static func displayNameForProvider(_ key: String) -> String { + switch key.lowercased() { + case "google-antigravity": return "Google Antigravity" + case "openai-codex": return "OpenAI Codex" + case "anthropic": return "Anthropic Claude" + case "github-copilot": return "GitHub Copilot" + case "xai-oauth": return "xAI Grok" + default: return key + } + } + + private static func makeCombinedSnapshot( + response: OMPUsageResponse, + hiddenItemIDs: [String], + now: Date) -> UsageSnapshot + { + var total5hAccounts: Double = 0 + var used5hAccounts: Double = 0 + var totalWeeklyAccounts: Double = 0 + var usedWeeklyAccounts: Double = 0 + var min5hResetDate: Date? + var minWeeklyResetDate: Date? + + let capacityMap = response.capacity ?? [:] + for (_, windows) in capacityMap { + for cap in windows { + let winName = cap.window.lowercased() + let duration = cap.durationMs ?? 0 + if winName.hasPrefix("5h") || duration == 18_000_000 { + total5hAccounts += cap.accounts + used5hAccounts += cap.usedAccounts + } else if winName.contains("7d") || winName.contains("week") || duration == 604_800_000 { + totalWeeklyAccounts += cap.accounts + usedWeeklyAccounts += cap.usedAccounts + } + } + } + + let nowMs = now.timeIntervalSince1970 * 1000 + for report in response.reports { + guard let limits = report.limits else { continue } + for limit in limits { + guard let window = limit.window, + let resetsAtMs = window.resetsAt, + resetsAtMs > nowMs + else { continue } + let resetDate = Date(timeIntervalSince1970: resetsAtMs / 1000) + let winId = (window.id ?? "").lowercased() + let dur = window.durationMs ?? 0 + if winId.contains("5h") || dur == 18_000_000 { + if min5hResetDate == nil || resetDate < min5hResetDate! { + min5hResetDate = resetDate + } + } else if winId.contains("week") || winId.contains("7d") || dur == 604_800_000 { + if minWeeklyResetDate == nil || resetDate < minWeeklyResetDate! { + minWeeklyResetDate = resetDate + } + } + } + } + + var raw5hWindow: RateWindow? + if total5hAccounts > 0 { + let usedPercent = (used5hAccounts / total5hAccounts) * 100 + raw5hWindow = RateWindow( + usedPercent: min(100, max(0, usedPercent)), + windowMinutes: 300, + resetsAt: min5hResetDate, + resetDescription: "\(String(format: "%.1f", total5hAccounts - used5hAccounts))/\(Int(total5hAccounts)) accts remaining") + } + + var rawWeeklyWindow: RateWindow? + if totalWeeklyAccounts > 0 { + let usedPercent = (usedWeeklyAccounts / totalWeeklyAccounts) * 100 + rawWeeklyWindow = RateWindow( + usedPercent: min(100, max(0, usedPercent)), + windowMinutes: 10080, + resetsAt: minWeeklyResetDate, + resetDescription: "\(String(format: "%.1f", totalWeeklyAccounts - usedWeeklyAccounts))/\(Int(totalWeeklyAccounts)) accts remaining") + } + + let isPrimaryHidden = hiddenItemIDs.contains("metric:primary") + let isSecondaryHidden = hiddenItemIDs.contains("metric:secondary") + + let primaryWindow: RateWindow? + let secondaryWindow: RateWindow? + + if isSecondaryHidden && !isPrimaryHidden { + // User selected only 5 accounts window + primaryWindow = raw5hWindow + secondaryWindow = nil + } else if isPrimaryHidden && !isSecondaryHidden { + // User selected only 6 accounts window + primaryWindow = rawWeeklyWindow + secondaryWindow = nil + } else { + // Both selected or default + primaryWindow = raw5hWindow + secondaryWindow = rawWeeklyWindow + } + + var extraRateWindows: [NamedRateWindow] = [] + for (providerKey, windows) in capacityMap.sorted(by: { $0.key < $1.key }) { + let shortName = self.shortNameForProvider(providerKey) + for cap in windows { + guard cap.accounts > 0 else { continue } + let pct = min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)) + let id = "omp-\(providerKey)-\(cap.window)" + let title = "\(shortName) \(cap.window)" + let winMinutes = (cap.window.lowercased().hasPrefix("5h") || cap.durationMs == 18_000_000) + ? 300 : ((cap.window.lowercased().contains("7d") || cap.durationMs == 604_800_000) ? 10080 : nil) + extraRateWindows.append(NamedRateWindow( + id: id, + title: title, + window: RateWindow( + usedPercent: pct, + windowMinutes: winMinutes, + resetsAt: nil, + resetDescription: "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts"))) + } + } + + var detailSections: [ProviderDetailSection] = [] + + var capacityRows: [ProviderDetailSection.Row] = [] + for (providerKey, windows) in capacityMap.sorted(by: { $0.key < $1.key }) { + let label = self.displayNameForProvider(providerKey) + let summaries = windows.map { cap in + "\(cap.window): \(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts))" + }.joined(separator: ", ") + let primaryCap = windows.first + let progress = primaryCap.flatMap { cap in + try? ProviderDetailSection.Row.Progress(used: cap.usedAccounts, total: cap.accounts) + } + if let row = self.makeRow( + id: "cap-\(providerKey)", + label: label, + value: summaries, + progress: progress, + usageValue: primaryCap?.usedAccounts) + { + capacityRows.append(row) + } + } + if let capSection = self.makeSection(title: "Capacity Breakdown", rows: capacityRows) { + detailSections.append(capSection) + } + + var accountRows: [ProviderDetailSection.Row] = [] + for report in response.reports { + let accountID = report.metadata?["email"] + ?? report.metadata?["accountId"] + ?? report.metadata?["orgName"] + ?? report.provider + let pName = self.shortNameForProvider(report.provider) + + var limitSummaries: [String] = [] + if let limits = report.limits { + for limit in limits { + if let amount = limit.amount, let usedPct = amount.used { + let lName = limit.label ?? limit.id + limitSummaries.append("\(lName): \(Int(usedPct))%") + } + } + } + let summaryText = limitSummaries.isEmpty ? "Connected" : limitSummaries.joined(separator: " · ") + if let row = self.makeRow( + id: "acct-\(accountID)", + label: "[\(pName)] \(accountID)", + value: summaryText) + { + accountRows.append(row) + } + } + if let acctSection = self.makeSection(title: "Connected Accounts (\(accountRows.count))", rows: accountRows) { + detailSections.append(acctSection) + } + + let identity = ProviderIdentitySnapshot( + providerID: .omp, + accountEmail: nil, + accountOrganization: nil, + loginMethod: "omp", + accountID: "all") + + return UsageSnapshot( + primary: primaryWindow, + secondary: secondaryWindow, + extraRateWindows: extraRateWindows.isEmpty ? nil : extraRateWindows, + details: detailSections, + updatedAt: now, + identity: identity) + } + + private static func makeSubproviderSnapshot( + response: OMPUsageResponse, + target: String, + hiddenItemIDs: [String], + now: Date) -> UsageSnapshot + { + let matchingReports = response.reports.filter { + $0.provider.lowercased() == target || $0.provider.lowercased().contains(target) + } + + let capacityMap = response.capacity ?? [:] + let matchingCapacity = capacityMap.first { + $0.key.lowercased() == target || $0.key.lowercased().contains(target) + }?.value ?? [] + + var primary5hCap: OMPCapacityWindow? + var weeklyCap: OMPCapacityWindow? + for cap in matchingCapacity { + let win = cap.window.lowercased() + if win.hasPrefix("5h") || cap.durationMs == 18_000_000 { + primary5hCap = cap + } else if win.contains("7d") || win.contains("week") || cap.durationMs == 604_800_000 { + weeklyCap = cap + } + } + + let nowMs = now.timeIntervalSince1970 * 1000 + var min5hResetDate: Date? + var minWeeklyResetDate: Date? + for report in matchingReports { + for limit in report.limits ?? [] { + guard let window = limit.window, + let resetsAtMs = window.resetsAt, + resetsAtMs > nowMs + else { continue } + let resetDate = Date(timeIntervalSince1970: resetsAtMs / 1000) + let winId = (window.id ?? "").lowercased() + let dur = window.durationMs ?? 0 + if winId.contains("5h") || dur == 18_000_000 { + if min5hResetDate == nil || resetDate < min5hResetDate! { + min5hResetDate = resetDate + } + } else if winId.contains("week") || winId.contains("7d") || dur == 604_800_000 { + if minWeeklyResetDate == nil || resetDate < minWeeklyResetDate! { + minWeeklyResetDate = resetDate + } + } + } + } + + var rawPrimary: RateWindow? + if let cap = primary5hCap, cap.accounts > 0 { + let desc = "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts remaining" + rawPrimary = RateWindow( + usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), + windowMinutes: 300, + resetsAt: min5hResetDate, + resetDescription: desc) + } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "5h") { + rawPrimary = self.rateWindowFromLimit(firstLimit, defaultMinutes: 300, fallbackReset: min5hResetDate) + } + + var rawSecondary: RateWindow? + if let cap = weeklyCap, cap.accounts > 0 { + let desc = "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts remaining" + rawSecondary = RateWindow( + usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), + windowMinutes: 10080, + resetsAt: minWeeklyResetDate, + resetDescription: desc) + } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "week") + ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "7d") + ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "month") + { + rawSecondary = self.rateWindowFromLimit(firstLimit, defaultMinutes: 10080, fallbackReset: minWeeklyResetDate) + } + + let isPrimaryHidden = hiddenItemIDs.contains("metric:primary") + let isSecondaryHidden = hiddenItemIDs.contains("metric:secondary") + + let primaryWindow: RateWindow? + let secondaryWindow: RateWindow? + + if isSecondaryHidden && !isPrimaryHidden { + primaryWindow = rawPrimary + secondaryWindow = nil + } else if isPrimaryHidden && !isSecondaryHidden { + primaryWindow = rawSecondary + secondaryWindow = nil + } else { + primaryWindow = rawPrimary + secondaryWindow = rawSecondary + } + + var extraRateWindows: [NamedRateWindow] = [] + var seenLimitIDs: Set = [] + for report in matchingReports { + for limit in report.limits ?? [] { + guard !seenLimitIDs.contains(limit.id) else { continue } + seenLimitIDs.insert(limit.id) + guard let amount = limit.amount, let used = amount.used ?? (amount.usedFraction.map { $0 * 100 }) else { + continue + } + let title = limit.label ?? limit.id + let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let winMinutes = limit.window?.durationMs.flatMap { Int($0 / (60 * 1000)) } + extraRateWindows.append(NamedRateWindow( + id: limit.id, + title: title, + window: RateWindow( + usedPercent: min(100, max(0, used)), + windowMinutes: winMinutes, + resetsAt: resetsAt, + resetDescription: limit.status))) + } + } + + var detailSections: [ProviderDetailSection] = [] + + if !matchingCapacity.isEmpty { + var capRows: [ProviderDetailSection.Row] = [] + for cap in matchingCapacity { + let progress = try? ProviderDetailSection.Row.Progress(used: cap.usedAccounts, total: cap.accounts) + if let row = self.makeRow( + id: "cap-\(cap.window)", + label: "\(cap.window) Window", + value: "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts remaining", + progress: progress, + usageValue: cap.usedAccounts) + { + capRows.append(row) + } + } + if let capSection = self.makeSection(title: "\(self.displayNameForProvider(target)) Capacity", rows: capRows) { + detailSections.append(capSection) + } + } + + if !matchingReports.isEmpty { + var acctRows: [ProviderDetailSection.Row] = [] + for report in matchingReports { + let accountID = report.metadata?["email"] + ?? report.metadata?["accountId"] + ?? report.metadata?["orgName"] + ?? "Account" + var summaries: [String] = [] + for limit in report.limits ?? [] { + if let amount = limit.amount, let used = amount.used { + let lName = limit.label ?? limit.id + summaries.append("\(lName): \(Int(used))% (\(limit.status ?? "ok"))") + } + } + if let row = self.makeRow( + id: "acct-\(accountID)", + label: accountID, + value: summaries.joined(separator: " · ")) + { + acctRows.append(row) + } + } + if let acctSection = self.makeSection(title: "Accounts (\(matchingReports.count))", rows: acctRows) { + detailSections.append(acctSection) + } + } + + let firstEmail = matchingReports.first?.metadata?["email"] + let identity = ProviderIdentitySnapshot( + providerID: .omp, + accountEmail: firstEmail, + accountOrganization: nil, + loginMethod: "omp", + accountID: target) + + return UsageSnapshot( + primary: primaryWindow, + secondary: secondaryWindow, + extraRateWindows: extraRateWindows.isEmpty ? nil : extraRateWindows, + details: detailSections, + updatedAt: now, + identity: identity) + } + + private static func findFirstLimit(matchingReports: [OMPAccountReport], windowPrefix: String) -> OMPLimit? { + for report in matchingReports { + for limit in report.limits ?? [] { + let winId = (limit.window?.id ?? "").lowercased() + if winId.contains(windowPrefix) { + return limit + } + } + } + return nil + } + + private static func rateWindowFromLimit( + _ limit: OMPLimit, + defaultMinutes: Int?, + fallbackReset: Date?) -> RateWindow? + { + guard let amount = limit.amount, + let used = amount.used ?? (amount.usedFraction.map { $0 * 100 }) + else { return nil } + let winMinutes = limit.window?.durationMs.flatMap { Int($0 / (60 * 1000)) } ?? defaultMinutes + let resetsAt = limit.window?.resetsAt.flatMap { Date(timeIntervalSince1970: $0 / 1000) } ?? fallbackReset + return RateWindow( + usedPercent: min(100, max(0, used)), + windowMinutes: winMinutes, + resetsAt: resetsAt, + resetDescription: limit.status) + } + + private static func shortNameForProvider(_ key: String) -> String { + switch key.lowercased() { + case "google-antigravity": return "AGY" + case "openai-codex": return "Codex" + case "anthropic": return "Claude" + case "github-copilot": return "Copilot" + case "xai-oauth": return "xAI" + default: return key + } + } + + private static func makeRow( + id: String? = nil, + label: String, + value: String, + secondaryValue: String? = nil, + progress: ProviderDetailSection.Row.Progress? = nil, + usageValue: Double? = nil) -> ProviderDetailSection.Row? + { + let safeLabel = String(label.prefix(120)) + let safeValue = String(value.prefix(120)) + let safeSecondary = secondaryValue.map { String($0.prefix(120)) } + return try? ProviderDetailSection.Row( + id: id.map { String($0.prefix(120)) }, + label: safeLabel, + value: safeValue, + secondaryValue: safeSecondary, + progress: progress, + usageValue: usageValue) + } + + private static func makeSection(title: String, rows: [ProviderDetailSection.Row]) -> ProviderDetailSection? { + guard !rows.isEmpty else { return nil } + let capped = Array(rows.prefix(ProviderDetailSection.maximumRowsPerSection)) + return try? ProviderDetailSection(title: String(title.prefix(120)), rows: capped) + } +} diff --git a/Sources/CodexBarCore/Providers/ProviderInstanceIDAliases.generated.swift b/Sources/CodexBarCore/Providers/ProviderInstanceIDAliases.generated.swift index 0e8882aba49..de0a69655f5 100644 --- a/Sources/CodexBarCore/Providers/ProviderInstanceIDAliases.generated.swift +++ b/Sources/CodexBarCore/Providers/ProviderInstanceIDAliases.generated.swift @@ -109,6 +109,7 @@ extension ProviderInstanceID { public static let parallel = UsageProvider.parallel.instanceID public static let claudeplatform = UsageProvider.claudeplatform.instanceID public static let munito = UsageProvider.munito.instanceID + public static let omp = UsageProvider.omp.instanceID } // swiftformat:enable sortDeclarations diff --git a/Sources/CodexBarCore/Providers/ProviderManifest.swift b/Sources/CodexBarCore/Providers/ProviderManifest.swift index 010f202df52..4e59f3f6484 100644 --- a/Sources/CodexBarCore/Providers/ProviderManifest.swift +++ b/Sources/CodexBarCore/Providers/ProviderManifest.swift @@ -111,5 +111,6 @@ public enum ProviderManifest { ParallelProviderDescriptor.descriptor, ClaudePlatformProviderDescriptor.descriptor, MunitoProviderDescriptor.descriptor, + OMPProviderDescriptor.descriptor, ] } diff --git a/Sources/CodexBarCore/Providers/ProviderVersionDetector.swift b/Sources/CodexBarCore/Providers/ProviderVersionDetector.swift index 2aecdccd526..d646da84635 100644 --- a/Sources/CodexBarCore/Providers/ProviderVersionDetector.swift +++ b/Sources/CodexBarCore/Providers/ProviderVersionDetector.swift @@ -254,6 +254,20 @@ public enum ProviderVersionDetector { return nil } + public static func ompVersion() -> String? { + let env = ProcessInfo.processInfo.environment + // Provider-specific by design: This detector checks the OMP CLI version. + guard let path = BinaryLocator.resolveOMPBinary(env: env, loginPATH: nil) + ?? TTYCommandRunner.which("omp") else { return nil } + if let raw = Self.run(path: path, args: ["--version"]) { + if raw.hasPrefix("omp/") { + return String(raw.dropFirst(4)).trimmingCharacters(in: .whitespacesAndNewlines) + } + return raw + } + return nil + } + /// Provider-specific by design: Kimi ships its CLI in dedicated standalone installation directories. public static func kimiVersion() -> String? { self.kimiVersion( diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift index 0e0d53d8082..d057b52ba25 100644 --- a/Sources/CodexBarCore/Providers/Providers.swift +++ b/Sources/CodexBarCore/Providers/Providers.swift @@ -125,6 +125,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable { case parallel case claudeplatform case munito + case omp } // swiftformat:enable sortDeclarations @@ -164,6 +165,7 @@ public struct IconStyle: RawRepresentable, Hashable, Sendable, CaseIterable, Cus public static let qoder = Self(provider: .qoder) public static let warp = Self(provider: .warp) public static let perplexity = Self(provider: .perplexity) + public static let omp = Self(provider: .omp) public static let combined = Self(rawValue: "combined") } diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift new file mode 100644 index 00000000000..e29a7f33991 --- /dev/null +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -0,0 +1,285 @@ +import AppKit +import Foundation +import Testing +@testable import CodexBar +@testable import CodexBarCore + +@MainActor +struct OMPProviderTests { + private static let sampleOMPUsageJSON = """ + { + "generatedAt": 1792223107842, + "reports": [ + { + "provider": "google-antigravity", + "fetchedAt": 1792222961116, + "limits": [ + { + "id": "google-antigravity:anthropic:default:3p-weekly", + "label": "Claude & GPT (shared)", + "window": { + "id": "weekly", + "label": "Weekly", + "durationMs": 604800000, + "resetsAt": 1792253782000 + }, + "amount": { + "unit": "percent", + "remainingFraction": 0, + "usedFraction": 1, + "remaining": 0, + "used": 100, + "limit": 100 + }, + "status": "exhausted" + }, + { + "id": "google-antigravity:google:default:gemini-5h", + "label": "Gemini", + "window": { + "id": "5h", + "label": "5 Hour", + "durationMs": 18000000, + "resetsAt": 1792240961000 + }, + "amount": { + "unit": "percent", + "remainingFraction": 1, + "usedFraction": 0, + "remaining": 100, + "used": 0, + "limit": 100 + }, + "status": "ok" + } + ], + "metadata": { + "email": "brasiliodarocha@gmail.com" + } + }, + { + "provider": "openai-codex", + "fetchedAt": 1792222961117, + "limits": [ + { + "id": "openai-codex:primary", + "label": "5 hours", + "window": { + "id": "5h", + "label": "5 hours", + "durationMs": 18000000, + "resetsAt": 1792234814000 + }, + "amount": { + "used": 10, + "limit": 100, + "remaining": 90, + "usedFraction": 0.1, + "remainingFraction": 0.9, + "unit": "percent" + }, + "status": "ok" + }, + { + "id": "openai-codex:secondary", + "label": "7 days", + "window": { + "id": "7d", + "label": "7 days", + "durationMs": 604800000, + "resetsAt": 1792677613000 + }, + "amount": { + "used": 5, + "limit": 100, + "remaining": 95, + "usedFraction": 0.05, + "remainingFraction": 0.95, + "unit": "percent" + }, + "status": "ok" + } + ], + "metadata": { + "email": "renanfamous@gmail.com" + } + } + ], + "capacity": { + "google-antigravity": [ + { + "window": "5h", + "durationMs": 18000000, + "accounts": 4, + "usedAccounts": 0.4, + "remainingAccounts": 3.6 + }, + { + "window": "7d", + "durationMs": 604800000, + "accounts": 4, + "usedAccounts": 2.0, + "remainingAccounts": 2.0 + } + ], + "openai-codex": [ + { + "window": "5h", + "durationMs": 18000000, + "accounts": 1, + "usedAccounts": 0.1, + "remainingAccounts": 0.9 + }, + { + "window": "7d", + "durationMs": 604800000, + "accounts": 1, + "usedAccounts": 0.05, + "remainingAccounts": 0.95 + } + ] + } + } + """ + + @Test + func `omp provider descriptor is registered and configured`() { + let descriptor = ProviderDescriptorRegistry.descriptor(for: .omp) + #expect(descriptor.id == .omp) + #expect(descriptor.metadata.displayName == "OMP") + #expect(descriptor.cli.name == "omp") + #expect(descriptor.branding.iconResourceName == "ProviderIcon-omp") + #expect(descriptor.branding.color == ProviderColor(hex: 0x10B981)) + #expect(descriptor.fetchPlan.sourceModes.contains(.cli)) + } + + @Test + func `omp usage parser parses combined usage accurately`() throws { + let now = Date(timeIntervalSince1970: 1792220000) + let snapshot = try OMPUsageParser.parse( + jsonString: Self.sampleOMPUsageJSON, + subprovider: "all", + now: now) + + let primary = try #require(snapshot.primary) + // Total 5h accounts = 4 (AGY) + 1 (Codex) = 5. + // Used 5h accounts = 0.4 (AGY) + 0.1 (Codex) = 0.5. + // Used percent = 0.5 / 5.0 * 100 = 10%. + #expect(abs(primary.usedPercent - 10.0) < 0.001) + #expect(primary.windowMinutes == 300) + + let secondary = try #require(snapshot.secondary) + // Total 7d accounts = 4 (AGY) + 1 (Codex) = 5. + // Used 7d accounts = 2.0 + 0.05 = 2.05. + // Used percent = 2.05 / 5.0 * 100 = 41.0%. + #expect(abs(secondary.usedPercent - 41.0) < 0.001) + #expect(secondary.windowMinutes == 10080) + + let extras = try #require(snapshot.extraRateWindows) + #expect(extras.count >= 4) + #expect(extras.contains { $0.id == "omp-google-antigravity-5h" }) + #expect(extras.contains { $0.id == "omp-google-antigravity-7d" }) + #expect(extras.contains { $0.id == "omp-openai-codex-5h" }) + #expect(extras.contains { $0.id == "omp-openai-codex-7d" }) + + #expect(snapshot.details.count >= 2) + #expect(snapshot.details[0].title == "Capacity Breakdown") + #expect(snapshot.details[1].title == "Connected Accounts (2)") + } + + @Test + func `omp usage parser filters by antigravity subprovider`() throws { + let now = Date(timeIntervalSince1970: 1792220000) + let snapshot = try OMPUsageParser.parse( + jsonString: Self.sampleOMPUsageJSON, + subprovider: "agy", + now: now) + + let primary = try #require(snapshot.primary) + // AGY 5h: 0.4 used / 4 total = 10% + #expect(abs(primary.usedPercent - 10.0) < 0.001) + + let secondary = try #require(snapshot.secondary) + // AGY 7d: 2.0 used / 4 total = 50% + #expect(abs(secondary.usedPercent - 50.0) < 0.001) + + let details = snapshot.details + #expect(details.contains { $0.title?.contains("Antigravity") == true }) + } + + @Test + func `omp usage parser filters by codex subprovider`() throws { + let now = Date(timeIntervalSince1970: 1792220000) + let snapshot = try OMPUsageParser.parse( + jsonString: Self.sampleOMPUsageJSON, + subprovider: "codex", + now: now) + + let primary = try #require(snapshot.primary) + // Codex 5h: 0.1 used / 1 total = 10% + #expect(abs(primary.usedPercent - 10.0) < 0.001) + + let secondary = try #require(snapshot.secondary) + // Codex 7d: 0.05 used / 1 total = 5% + #expect(abs(secondary.usedPercent - 5.0) < 0.001) + } + + @Test + func `omp usage parser honors hidden usage item IDs for 5 vs 6 accounts`() throws { + let now = Date(timeIntervalSince1970: 1792220000) + + // 1. When 6 accounts window is hidden (only 5 accounts checked) + let only5AccountsSnapshot = try OMPUsageParser.parse( + jsonString: Self.sampleOMPUsageJSON, + subprovider: "all", + hiddenItemIDs: ["metric:secondary"], + now: now) + let primary5 = try #require(only5AccountsSnapshot.primary) + #expect(abs(primary5.usedPercent - 10.0) < 0.001) // 5 accounts window + #expect(only5AccountsSnapshot.secondary == nil) + + // 2. When 5 accounts window is hidden (only 6 accounts checked) + let only6AccountsSnapshot = try OMPUsageParser.parse( + jsonString: Self.sampleOMPUsageJSON, + subprovider: "all", + hiddenItemIDs: ["metric:primary"], + now: now) + let primary6 = try #require(only6AccountsSnapshot.primary) + #expect(abs(primary6.usedPercent - 41.0) < 0.001) // 6 accounts window + #expect(only6AccountsSnapshot.secondary == nil) + + // The percentages distinctly differ between selecting 5 vs 6 accounts + #expect(primary5.usedPercent != primary6.usedPercent) + } + + @Test + func `omp settings snapshot persists subprovider selection`() { + let suite = "OMPProviderTests-settings-\(UUID().uuidString)" + let settings = testSettingsStore(suiteName: suite) + defer { UserDefaults(suiteName: suite)?.removePersistentDomain(forName: suite) } + + #expect(settings.ompSubprovider == "all") + + settings.ompSubprovider = "google-antigravity" + #expect(settings.ompSubprovider == "google-antigravity") + + let snapshot = settings.ompSettingsSnapshot() + #expect(snapshot.subprovider == "google-antigravity") + #expect(snapshot.effectiveSubprovider == "google-antigravity") + + settings.ompSubprovider = "custom" + settings.ompCustomSubprovider = "my-custom-provider" + let customSnapshot = settings.ompSettingsSnapshot() + #expect(customSnapshot.effectiveSubprovider == "my-custom-provider") + } + + @Test + func `omp brand icon is loaded as template`() throws { + ProviderBrandIcon.resetCacheForTesting() + defer { ProviderBrandIcon.resetCacheForTesting() } + + let image = try #require(ProviderBrandIcon.image(for: .omp)) + #expect(image.isTemplate) + #expect(image.size == NSSize(width: 18, height: 18)) + } +} diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 2083d33df84..3d503745dae 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -156,9 +156,9 @@ struct ProviderArchitectureGatekeeperTests { Self.hash(descriptor.branding.burnDownWidgetColor, into: &burnDownFingerprint) } - // Future AGI and Munito extend the palette without changing existing provider colors. - #expect(widgetFingerprint == 6_735_779_563_872_487_318) - #expect(burnDownFingerprint == 3_084_140_234_155_981_949) + // Future AGI, Munito, and OMP extend the palette without changing existing provider colors. + #expect(widgetFingerprint == 4_791_282_498_545_731_702) + #expect(burnDownFingerprint == 8_676_703_135_232_402_319) } @Test @@ -3353,9 +3353,9 @@ struct ProviderArchitectureGatekeeperTests { AllowedProviderConstruct( path: "Sources/CodexBarCore/AgentSession.swift", anchor: "guard self.provider(for: record) == .claude else { return .cli }", - expectedProviderIDs: ["claude", "codex", "pi"], - expectedReferenceCount: 4, - expectedReferenceFingerprint: ["pi@0", "pi@1", "claude@13", "codex@21"], + expectedProviderIDs: ["claude", "codex", "omp", "pi"], + expectedReferenceCount: 8, + expectedReferenceFingerprint: ["pi@0", "pi@1", "omp@3", "omp@4", "omp@8", "omp@9", "claude@13", "codex@21"], reason: "This exact host integration maps a provider-owned process, path, or window contract."), AllowedProviderConstruct( path: "Sources/CodexBarCore/AgentSession.swift", diff --git a/docs/index.html b/docs/index.html index 2decd7d868e..18ac6da56c1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,7 @@ CodexBar — every AI coding limit, in your menu bar @@ -37,7 +37,7 @@ @@ -293,7 +293,7 @@

- 106 providers,{mobileBreak}one menu bar + 107 providers,{mobileBreak}one menu bar

Track usage windows, balances, and spend where each provider exposes them — including Helmcode, v0, and TypeSafe. diff --git a/docs/logos/omp.svg b/docs/logos/omp.svg new file mode 100644 index 00000000000..030cbd2a24d --- /dev/null +++ b/docs/logos/omp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/omp.md b/docs/omp.md new file mode 100644 index 00000000000..37a9dfb5f1f --- /dev/null +++ b/docs/omp.md @@ -0,0 +1,35 @@ +--- +summary: "OMP provider notes: combined AI coding provider limits, subprovider selection, and multi-account capacity aggregation." +read_when: + - Configuring or debugging the OMP provider + - Working with combined limits across providers (Antigravity, Codex, Claude, Copilot, xAI Grok) + - Selecting subproviders in OMP settings or via CLI +--- + +# OMP provider + +CodexBar can read AI limits and aggregated provider capacity from OMP (`omp usage --json`), including multi-account Google Antigravity quotas, OpenAI Codex limits, Anthropic Claude, GitHub Copilot, and xAI Grok. + +## Subproviders + +In Settings → Providers → OMP, choose which limits to display: +- **Combined (All Limits)**: Combines 5-hour and weekly capacity across all connected accounts and providers. +- **Google Antigravity (agy)**: Shows Antigravity's combined 5-hour and 7-day capacity across all signed-in Google accounts, plus shared 3p Claude/GPT and Gemini limits. +- **OpenAI Codex**: Shows Codex 5-hour and weekly limits. +- **Anthropic Claude**: Shows Claude quota limits. +- **GitHub Copilot**: Shows Copilot requests and completions. +- **xAI Grok**: Shows GrokBot and SuperGrok credits. +- **Custom Subprovider**: Filters by any custom provider key reported by OMP. + +## CLI Usage + +Fetch usage from OMP via CodexBar CLI: +```bash +codexbar usage --provider omp +``` + +Override subprovider via environment: +```bash +OMP_SUBPROVIDER=agy codexbar usage --provider omp +OMP_SUBPROVIDER=codex codexbar usage --provider omp +``` diff --git a/docs/plugin-conversion-matrix.md b/docs/plugin-conversion-matrix.md index 7278bc0600f..05e4a0db32a 100644 --- a/docs/plugin-conversion-matrix.md +++ b/docs/plugin-conversion-matrix.md @@ -17,8 +17,8 @@ script. Settings-derived origins include the private-network HTTP policy for LLM `converted` means the bundled conversion is present behind `CODEXBAR_JS_PROVIDERS=1`. `cut-over` means the script is authoritative on its supported engines; each row states whether a Linux native core remains. Totals count only the -69 audit rows below, excluding the separately listed plugin-first additions. The registry now contains 106 providers: -69 audit rows, 28 additional plugin-first rows, and 9 providers not yet classified in this matrix (CodeRabbit, +69 audit rows below, excluding the separately listed plugin-first additions. The registry now contains 107 providers: +69 audit rows, 28 additional plugin-first rows, and 10 providers not yet classified in this matrix (CodeRabbit, Hugging Face, IBM Bob, Muse, Nous, Pi, Replicate, TypeSafe, and v0). `needs-cookie-import` now means **additional cookie/session capability**, not absence of cookie import. The current @@ -58,8 +58,8 @@ Abacus, Muse, LongCat, Replicate, and TypeSafe unchanged. | `needs-host-extension` | 4 | | **Audit total** | **69** | | Additional plugin-first providers | 28 | -| Registered providers not yet classified here | 9 | -| **Registry total** | **106** | +| Registered providers not yet classified here | 10 | +| **Registry total** | **107** | ## Matrix diff --git a/docs/provider-ids.md b/docs/provider-ids.md index 0764ba7b27e..4394b3dec02 100644 --- a/docs/provider-ids.md +++ b/docs/provider-ids.md @@ -2,4 +2,4 @@ # Provider IDs -`codex`, `openai`, `azureopenai`, `claude`, `clinepass`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `qwencloud`, `factory`, `fireworks`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `moonshot`, `amp`, `t3chat`, `langdock`, `ollama`, `synthetic`, `openrouter`, `elevenlabs`, `warp`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `deepinfra`, `codebuff`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `bifrost`, `aixy`, `deepgram`, `poe`, `chutes`, `neuralwatt`, `helmcode`, `clawrouter`, `longcat`, `sub2api`, `wayfinder`, `zenmux`, `aiand`, `zoommate`, `xai`, `xapi`, `notion`, `ibmbob`, `nous`, `muse`, `coderabbit`, `replicate`, `huggingface`, `raycast`, `pi`, `v0`, `typesafe`, `hyper`, `gitkraken`, `futureagi`, `devpass`, `atlascloud`, `vercel`, `llmman`, `xkiro`, `museai`, `lithosai`, `workbuddy`, `tavily`, `linkup`, `tinyapi`, `exa`, `cosmic`, `aerostack`, `sailresearch`, `sofya`, `fal`, `flora`, `parallel`, `claudeplatform`, `munito`. +`codex`, `openai`, `azureopenai`, `claude`, `clinepass`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `qwencloud`, `factory`, `fireworks`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `moonshot`, `amp`, `t3chat`, `langdock`, `ollama`, `synthetic`, `openrouter`, `elevenlabs`, `warp`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `deepinfra`, `codebuff`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `bifrost`, `aixy`, `deepgram`, `poe`, `chutes`, `neuralwatt`, `helmcode`, `clawrouter`, `longcat`, `sub2api`, `wayfinder`, `zenmux`, `aiand`, `zoommate`, `xai`, `xapi`, `notion`, `ibmbob`, `nous`, `muse`, `coderabbit`, `replicate`, `huggingface`, `raycast`, `pi`, `v0`, `typesafe`, `hyper`, `gitkraken`, `futureagi`, `devpass`, `atlascloud`, `vercel`, `llmman`, `xkiro`, `museai`, `lithosai`, `workbuddy`, `tavily`, `linkup`, `tinyapi`, `exa`, `cosmic`, `aerostack`, `sailresearch`, `sofya`, `fal`, `flora`, `parallel`, `claudeplatform`, `munito`, `omp`. diff --git a/docs/providers.md b/docs/providers.md index b00261134ee..84c7777fa59 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -8,7 +8,7 @@ read_when: # Providers -CodexBar currently registers 106 provider IDs. Some companies expose multiple surfaces, such as Codex vs OpenAI API or +CodexBar currently registers 107 provider IDs. Some companies expose multiple surfaces, such as Codex vs OpenAI API or OpenCode vs OpenCode Go, because the auth source and quota shape differ. ## Fetch strategies (current) diff --git a/docs/site-locales.mjs b/docs/site-locales.mjs index ad3a1cdbbb8..0f54ffafa79 100644 --- a/docs/site-locales.mjs +++ b/docs/site-locales.mjs @@ -98,8 +98,8 @@ export const localeCatalog = [ export const localeMessages = { "en": { "meta.title": "CodexBar — every AI coding limit in your menu bar", - "meta.description": "A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 106 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, and more.", - "meta.ogDescription": "Track usage windows, credits, and resets across 106 AI coding providers from your macOS menu bar.", + "meta.description": "A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 107 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, and more.", + "meta.ogDescription": "Track usage windows, credits, and resets across 107 AI coding providers from your macOS menu bar.", "nav.primary": "Primary", "nav.language": "Language", "nav.docs": "docs", @@ -112,7 +112,7 @@ export const localeMessages = { "hero.description": "CodexBar tracks usage windows, credit balances, and reset countdowns across the providers you actually pay for — one status item each, or merge them into one.", "hero.download": "Download for macOS", "hero.fineprint": "Free & open source · macOS 14+ · Universal via GitHub Releases and Homebrew", - "providers.title": "106 providers,{mobileBreak}one menu bar", + "providers.title": "107 providers,{mobileBreak}one menu bar", "providers.description": "Track usage windows, balances, and spend where each provider exposes them — including Helmcode, v0, and TypeSafe.", "providers.yourProvider": "Your provider", "providers.authoringGuide": "Authoring guide", @@ -238,8 +238,8 @@ export const localeMessages = { }, "zh-CN": { "meta.title": "CodexBar — 菜单栏中的每个 AI 编码限制", - "meta.description": "一个微小的 macOS 菜单栏应用程序,可跟踪 106 个提供商(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM、Helmcode、v0、TypeSafe 等)的 AI 编码提供商使用窗口、积分、成本和重置。", - "meta.ogDescription": "从您的 macOS 菜单栏跟踪 106 个 AI 编码提供商的使用窗口、积分和重置。", + "meta.description": "一个微小的 macOS 菜单栏应用程序,可跟踪 107 个提供商(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM、Helmcode、v0、TypeSafe 等)的 AI 编码提供商使用窗口、积分、成本和重置。", + "meta.ogDescription": "从您的 macOS 菜单栏跟踪 107 个 AI 编码提供商的使用窗口、积分和重置。", "nav.primary": "基本的", "nav.language": "语言", "nav.docs": "文档", @@ -252,7 +252,7 @@ export const localeMessages = { "hero.description": "CodexBar 跟踪您实际付费的提供商的使用窗口、信用余额和重置倒计时 - 每个状态项一项,或将它们合并为一项。", "hero.download": "下载macOS", "hero.fineprint": "免费开源 · macOS 14+ · GitHub Releases 和 Homebrew 均提供通用版本", - "providers.title": "106 个提供商,{mobileBreak}一个菜单栏", + "providers.title": "107 个提供商,{mobileBreak}一个菜单栏", "providers.description": "跟踪各提供商提供的用量窗口、余额和支出,包括 Helmcode、v0 和 TypeSafe。", "providers.yourProvider": "您的提供商", "providers.authoringGuide": "创作指南", @@ -378,8 +378,8 @@ export const localeMessages = { }, "zh-TW": { "meta.title": "CodexBar — 功能表列中的每個 AI 編碼限制", - "meta.description": "一個微小的 macOS 功能表列應用程序,可追蹤 106 個提供者(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM、Helmcode、v0、TypeSafe 等)的 AI 編碼提供者使用視窗、積分、成本和重設。", - "meta.ogDescription": "從您的 macOS 功能表列追蹤 106 個 AI 編碼提供者的使用視窗、積分和重設。", + "meta.description": "一個微小的 macOS 功能表列應用程序,可追蹤 107 個提供者(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM、Helmcode、v0、TypeSafe 等)的 AI 編碼提供者使用視窗、積分、成本和重設。", + "meta.ogDescription": "從您的 macOS 功能表列追蹤 107 個 AI 編碼提供者的使用視窗、積分和重設。", "nav.primary": "基本的", "nav.language": "語言", "nav.docs": "文件", @@ -392,7 +392,7 @@ export const localeMessages = { "hero.description": "CodexBar 追蹤您實際付費的提供者的使用視窗、信用餘額和重設倒數計時 - 每個狀態項一項,或將它們合併為一項。", "hero.download": "下載macOS", "hero.fineprint": "免費開源 · macOS 14+ · GitHub Releases 和 Homebrew 均提供通用版本", - "providers.title": "106 個提供者,{mobileBreak}一個選單列", + "providers.title": "107 個提供者,{mobileBreak}一個選單列", "providers.description": "追蹤各提供者提供的用量視窗、餘額和支出,包括 Helmcode、v0 和 TypeSafe。", "providers.yourProvider": "您的提供者", "providers.authoringGuide": "創作指南", @@ -518,8 +518,8 @@ export const localeMessages = { }, "ja-JP": { "meta.title": "CodexBar — メニュー バーのすべての AI コーディング制限", - "meta.description": "小さな macOS メニュー バー アプリ。Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM、Helmcode、v0、TypeSafe など、106 のプロバイダーにわたる AI コーディング プロバイダーの使用期間、クレジット、コスト、リセットを追跡します。", - "meta.ogDescription": "macOS メニュー バーから、106 の AI コーディング プロバイダーにわたる使用期間、クレジット、リセットを追跡します。", + "meta.description": "小さな macOS メニュー バー アプリ。Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM、Helmcode、v0、TypeSafe など、107 のプロバイダーにわたる AI コーディング プロバイダーの使用期間、クレジット、コスト、リセットを追跡します。", + "meta.ogDescription": "macOS メニュー バーから、107 の AI コーディング プロバイダーにわたる使用期間、クレジット、リセットを追跡します。", "nav.primary": "主要な", "nav.language": "言語", "nav.docs": "ドキュメント", @@ -532,7 +532,7 @@ export const localeMessages = { "hero.description": "CodexBar は、実際に料金を支払っているプロバイダー全体の使用期間、クレジット残高、リセット カウントダウンを追跡します。ステータス項目を 1 つずつ、または 1 つに統合します。", "hero.download": "macOS のダウンロード", "hero.fineprint": "無料・オープンソース · macOS 14+ · GitHub Releases と Homebrew でユニバーサル版を提供", - "providers.title": "106 プロバイダー、{mobileBreak}1つのメニューバー", + "providers.title": "107 プロバイダー、{mobileBreak}1つのメニューバー", "providers.description": "Helmcode、v0、TypeSafe を含む各プロバイダーが提供する使用期間、残高、支出を確認できます。", "providers.yourProvider": "あなたのプロバイダー", "providers.authoringGuide": "オーサリングガイド", @@ -658,8 +658,8 @@ export const localeMessages = { }, "es": { "meta.title": "CodexBar: cada límite de codificación de IA en tu barra de menú", - "meta.description": "Una pequeña aplicación de barra de menú macOS que rastrea las ventanas de uso, los créditos, los costos y los restablecimientos de los proveedores de codificación de IA en 106 proveedores: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe y más.", - "meta.ogDescription": "Realice un seguimiento de las ventanas de uso, los créditos y los restablecimientos en 106 proveedores de codificación de IA desde su barra de menú macOS.", + "meta.description": "Una pequeña aplicación de barra de menú macOS que rastrea las ventanas de uso, los créditos, los costos y los restablecimientos de los proveedores de codificación de IA en 107 proveedores: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe y más.", + "meta.ogDescription": "Realice un seguimiento de las ventanas de uso, los créditos y los restablecimientos en 107 proveedores de codificación de IA desde su barra de menú macOS.", "nav.primary": "Primario", "nav.language": "Idioma", "nav.docs": "Documentos", @@ -672,7 +672,7 @@ export const localeMessages = { "hero.description": "CodexBar realiza un seguimiento de los períodos de uso, los saldos de crédito y restablece las cuentas regresivas de los proveedores por los que realmente paga: un elemento de estado para cada uno o los fusiona en uno solo.", "hero.download": "Descargar para macOS", "hero.fineprint": "Gratis y de código abierto · macOS 14+ · Universal mediante GitHub Releases y Homebrew", - "providers.title": "106 proveedores,{mobileBreak}una barra de menús", + "providers.title": "107 proveedores,{mobileBreak}una barra de menús", "providers.description": "Consulta los periodos de uso, saldos y gastos que ofrece cada proveedor, incluidos Helmcode, v0 y TypeSafe.", "providers.yourProvider": "Tu proveedor", "providers.authoringGuide": "guía de autoría", @@ -798,8 +798,8 @@ export const localeMessages = { }, "pt-BR": { "meta.title": "CodexBar — todos os limites de codificação de IA na sua barra de menu", - "meta.description": "Um pequeno aplicativo de barra de menu macOS que rastreia janelas de uso, créditos, custos e redefinições do provedor de codificação de IA em 106 provedores — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe e muito mais.", - "meta.ogDescription": "Rastreie janelas de uso, créditos e redefinições em 106 provedores de codificação de IA na barra de menu macOS.", + "meta.description": "Um pequeno aplicativo de barra de menu macOS que rastreia janelas de uso, créditos, custos e redefinições do provedor de codificação de IA em 107 provedores — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe e muito mais.", + "meta.ogDescription": "Rastreie janelas de uso, créditos e redefinições em 107 provedores de codificação de IA na barra de menu macOS.", "nav.primary": "Primário", "nav.language": "Linguagem", "nav.docs": "Documentos", @@ -812,7 +812,7 @@ export const localeMessages = { "hero.description": "CodexBar rastreia janelas de uso, saldos de crédito e reinicia contagens regressivas nos provedores pelos quais você realmente paga - um item de status cada, ou mescla-os em um.", "hero.download": "Baixar para macOS", "hero.fineprint": "Gratuito e de código aberto · macOS 14+ · Universal via GitHub Releases e Homebrew", - "providers.title": "106 provedores,{mobileBreak}uma barra de menu", + "providers.title": "107 provedores,{mobileBreak}uma barra de menu", "providers.description": "Acompanhe as janelas de uso, os saldos e os gastos disponibilizados por cada provedor, incluindo Helmcode, v0 e TypeSafe.", "providers.yourProvider": "Seu provedor", "providers.authoringGuide": "Guia de autoria", @@ -938,8 +938,8 @@ export const localeMessages = { }, "ko": { "meta.title": "CodexBar — 메뉴 표시줄의 모든 AI 코딩 제한", - "meta.description": "Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe 등 106개 제공자 전체에서 AI 코딩 제공자 사용 창, 크레딧, 비용 및 재설정을 추적하는 작은 macOS 메뉴 표시줄 앱입니다.", - "meta.ogDescription": "macOS 메뉴 표시줄에서 106개 AI 코딩 제공업체의 사용 기간, 크레딧 및 재설정을 추적하세요.", + "meta.description": "Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe 등 107개 제공자 전체에서 AI 코딩 제공자 사용 창, 크레딧, 비용 및 재설정을 추적하는 작은 macOS 메뉴 표시줄 앱입니다.", + "meta.ogDescription": "macOS 메뉴 표시줄에서 107개 AI 코딩 제공업체의 사용 기간, 크레딧 및 재설정을 추적하세요.", "nav.primary": "주요한", "nav.language": "언어", "nav.docs": "문서", @@ -952,7 +952,7 @@ export const localeMessages = { "hero.description": "CodexBar는 귀하가 실제로 비용을 지불한 제공업체 전반에 걸쳐 사용 기간, 크레딧 잔액 및 재설정 카운트다운을 추적합니다. 즉, 각각 하나의 상태 항목 또는 하나로 병합됩니다.", "hero.download": "macOS 동안 다운로드", "hero.fineprint": "무료 오픈 소스 · macOS 14+ · GitHub Releases와 Homebrew에서 유니버설 제공", - "providers.title": "106개 제공자,{mobileBreak}하나의 메뉴 막대", + "providers.title": "107개 제공자,{mobileBreak}하나의 메뉴 막대", "providers.description": "Helmcode, v0, TypeSafe를 포함한 각 제공업체가 공개하는 사용 기간, 잔액, 지출을 확인하세요.", "providers.yourProvider": "귀하의 제공자", "providers.authoringGuide": "저작 가이드", @@ -1078,8 +1078,8 @@ export const localeMessages = { }, "de": { "meta.title": "CodexBar – alle Limits Ihrer KI-Coding-Tools in der Menüleiste", - "meta.description": "Eine kleine macOS-Menüleisten-App, die Nutzungslimits, Guthaben, Kosten und Resets von 106 KI-Coding-Anbietern im Blick behält – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe und mehr.", - "meta.ogDescription": "Nutzungslimits, Guthaben und Resets von 106 KI-Coding-Anbietern direkt in Ihrer macOS-Menüleiste.", + "meta.description": "Eine kleine macOS-Menüleisten-App, die Nutzungslimits, Guthaben, Kosten und Resets von 107 KI-Coding-Anbietern im Blick behält – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe und mehr.", + "meta.ogDescription": "Nutzungslimits, Guthaben und Resets von 107 KI-Coding-Anbietern direkt in Ihrer macOS-Menüleiste.", "nav.primary": "Primär", "nav.language": "Sprache", "nav.docs": "Dokumente", @@ -1092,7 +1092,7 @@ export const localeMessages = { "hero.description": "CodexBar verfolgt Nutzungsfenster, Guthaben und Reset-Countdowns bei den Anbietern, für die Sie tatsächlich bezahlen – jeweils ein Statuselement oder sie werden zu einem zusammengeführt.", "hero.download": "Herunterladen für macOS", "hero.fineprint": "Kostenlos und Open Source · macOS 14+ · Universal über GitHub Releases und Homebrew", - "providers.title": "106 Provider,{mobileBreak}eine Menüleiste", + "providers.title": "107 Provider,{mobileBreak}eine Menüleiste", "providers.description": "Verfolge Nutzungsfenster, Guthaben und Ausgaben, soweit der jeweilige Anbieter sie bereitstellt — darunter Helmcode, v0 und TypeSafe.", "providers.yourProvider": "Ihr Anbieter", "providers.authoringGuide": "Autorenleitfaden", @@ -1218,8 +1218,8 @@ export const localeMessages = { }, "fr": { "meta.title": "CodexBar — chaque limite de codage IA dans votre barre de menus", - "meta.description": "Une petite application de barre de menus macOS qui suit les fenêtres d'utilisation, les crédits, les coûts et les réinitialisations des fournisseurs de codage d'IA sur 106 fournisseurs : Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, et plus encore.", - "meta.ogDescription": "Suivez les fenêtres d'utilisation, les crédits et les réinitialisations auprès de 106 fournisseurs de codage d'IA à partir de votre barre de menus macOS.", + "meta.description": "Une petite application de barre de menus macOS qui suit les fenêtres d'utilisation, les crédits, les coûts et les réinitialisations des fournisseurs de codage d'IA sur 107 fournisseurs : Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, et plus encore.", + "meta.ogDescription": "Suivez les fenêtres d'utilisation, les crédits et les réinitialisations auprès de 107 fournisseurs de codage d'IA à partir de votre barre de menus macOS.", "nav.primary": "Primaire", "nav.language": "Langue", "nav.docs": "Documents", @@ -1232,7 +1232,7 @@ export const localeMessages = { "hero.description": "CodexBar suit les fenêtres d'utilisation, les soldes créditeurs et réinitialise les comptes à rebours pour les fournisseurs pour lesquels vous payez réellement - un élément de statut chacun, ou les fusionne en un seul.", "hero.download": "Télécharger pour macOS", "hero.fineprint": "Gratuit et open source · macOS 14+ · Universel via GitHub Releases et Homebrew", - "providers.title": "106 fournisseurs,{mobileBreak}une barre des menus", + "providers.title": "107 fournisseurs,{mobileBreak}une barre des menus", "providers.description": "Suivez les périodes d’utilisation, les soldes et les dépenses disponibles chez chaque fournisseur, dont Helmcode, v0 et TypeSafe.", "providers.yourProvider": "Votre fournisseur", "providers.authoringGuide": "Guide de création", @@ -1358,8 +1358,8 @@ export const localeMessages = { }, "ar": { "meta.title": "CodexBar — كل حد لترميز الذكاء الاصطناعي في شريط القائمة", - "meta.description": "تطبيق شريط قوائم macOS صغير الحجم يتتبع نوافذ استخدام موفر ترميز الذكاء الاصطناعي، والائتمانات، والتكاليف، وعمليات إعادة التعيين عبر 106 موفرًا - Codex، وOpenAI، وClaude، وCursor، وGemini، وCopilot، وLiteLLM، Helmcode، v0، TypeSafe، والمزيد.", - "meta.ogDescription": "تتبع نوافذ الاستخدام والأرصدة وعمليات إعادة التعيين عبر 106 موفرًا لترميز الذكاء الاصطناعي من شريط القائمة macOS.", + "meta.description": "تطبيق شريط قوائم macOS صغير الحجم يتتبع نوافذ استخدام موفر ترميز الذكاء الاصطناعي، والائتمانات، والتكاليف، وعمليات إعادة التعيين عبر 107 موفرًا - Codex، وOpenAI، وClaude، وCursor، وGemini، وCopilot، وLiteLLM، Helmcode، v0، TypeSafe، والمزيد.", + "meta.ogDescription": "تتبع نوافذ الاستخدام والأرصدة وعمليات إعادة التعيين عبر 107 موفرًا لترميز الذكاء الاصطناعي من شريط القائمة macOS.", "nav.primary": "أساسي", "nav.language": "لغة", "nav.docs": "المستندات", @@ -1372,7 +1372,7 @@ export const localeMessages = { "hero.description": "يتتبع CodexBar فترات الاستخدام والأرصدة الائتمانية وإعادة تعيين العد التنازلي عبر مقدمي الخدمة الذين تدفع مقابلهم فعليًا — عنصر حالة واحد لكل منهم، أو دمجهم في عنصر واحد.", "hero.download": "التنزيل لمدة macOS", "hero.fineprint": "مجاني ومفتوح المصدر · macOS 14+ · إصدار شامل عبر GitHub Releases وHomebrew", - "providers.title": "106 مزودًا،{mobileBreak}شريط قوائم واحد", + "providers.title": "107 مزودًا،{mobileBreak}شريط قوائم واحد", "providers.description": "تتبّع فترات الاستخدام والأرصدة والإنفاق حسب ما يتيحه كل مزوّد، بما في ذلك Helmcode وv0 وTypeSafe.", "providers.yourProvider": "المزود الخاص بك", "providers.authoringGuide": "دليل التأليف", @@ -1498,8 +1498,8 @@ export const localeMessages = { }, "it": { "meta.title": "CodexBar: ogni limite di codifica AI nella barra dei menu", - "meta.description": "Una piccola app della barra dei menu macOS che tiene traccia delle finestre di utilizzo, dei crediti, dei costi e dei ripristini del fornitore di codifica AI tra 106 fornitori: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe e altri.", - "meta.ogDescription": "Tieni traccia delle finestre di utilizzo, dei crediti e dei ripristini tra 106 fornitori di codifica AI dalla barra dei menu macOS.", + "meta.description": "Una piccola app della barra dei menu macOS che tiene traccia delle finestre di utilizzo, dei crediti, dei costi e dei ripristini del fornitore di codifica AI tra 107 fornitori: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe e altri.", + "meta.ogDescription": "Tieni traccia delle finestre di utilizzo, dei crediti e dei ripristini tra 107 fornitori di codifica AI dalla barra dei menu macOS.", "nav.primary": "Primario", "nav.language": "Lingua", "nav.docs": "Documenti", @@ -1512,7 +1512,7 @@ export const localeMessages = { "hero.description": "CodexBar tiene traccia delle finestre di utilizzo, dei saldi del credito e reimposta i conti alla rovescia tra i fornitori per cui paghi effettivamente: un elemento di stato ciascuno o uniscili in uno solo.", "hero.download": "Scarica per macOS", "hero.fineprint": "Gratuito e open source · macOS 14+ · Universale tramite GitHub Releases e Homebrew", - "providers.title": "106 provider,{mobileBreak}una barra dei menu", + "providers.title": "107 provider,{mobileBreak}una barra dei menu", "providers.description": "Monitora le finestre di utilizzo, i saldi e le spese disponibili per ciascun provider, inclusi Helmcode, v0 e TypeSafe.", "providers.yourProvider": "Il tuo fornitore", "providers.authoringGuide": "Guida all'autore", @@ -1638,8 +1638,8 @@ export const localeMessages = { }, "vi": { "meta.title": "CodexBar — mọi giới hạn mã hóa AI trong thanh menu của bạn", - "meta.description": "Một ứng dụng thanh menu macOS nhỏ theo dõi khoảng thời gian sử dụng, tín dụng, chi phí và đặt lại của nhà cung cấp mã hóa AI trên 106 nhà cung cấp — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, v.v.", - "meta.ogDescription": "Theo dõi khoảng thời gian sử dụng, tín dụng và đặt lại trên 106 nhà cung cấp mã hóa AI từ thanh menu macOS của bạn.", + "meta.description": "Một ứng dụng thanh menu macOS nhỏ theo dõi khoảng thời gian sử dụng, tín dụng, chi phí và đặt lại của nhà cung cấp mã hóa AI trên 107 nhà cung cấp — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, v.v.", + "meta.ogDescription": "Theo dõi khoảng thời gian sử dụng, tín dụng và đặt lại trên 107 nhà cung cấp mã hóa AI từ thanh menu macOS của bạn.", "nav.primary": "Sơ đẳng", "nav.language": "Ngôn ngữ", "nav.docs": "Tài liệu", @@ -1652,7 +1652,7 @@ export const localeMessages = { "hero.description": "CodexBar theo dõi khoảng thời gian sử dụng, số dư tín dụng và đếm ngược đặt lại trên các nhà cung cấp mà bạn thực sự thanh toán — mỗi mục một trạng thái hoặc hợp nhất chúng thành một.", "hero.download": "Tải xuống cho macOS", "hero.fineprint": "Miễn phí và nguồn mở · macOS 14+ · Bản universal qua GitHub Releases và Homebrew", - "providers.title": "106 nhà cung cấp,{mobileBreak}một thanh menu", + "providers.title": "107 nhà cung cấp,{mobileBreak}một thanh menu", "providers.description": "Theo dõi khoảng thời gian sử dụng, số dư và chi phí mà từng nhà cung cấp cung cấp, bao gồm Helmcode, v0 và TypeSafe.", "providers.yourProvider": "Nhà cung cấp của bạn", "providers.authoringGuide": "Hướng dẫn soạn thảo", @@ -1778,8 +1778,8 @@ export const localeMessages = { }, "nl": { "meta.title": "CodexBar — elke AI-coderingslimiet in uw menubalk", - "meta.description": "Een kleine macOS menubalk-app die gebruiksperioden, tegoeden, kosten en resets van AI-coderingsproviders bijhoudt bij 106 providers: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe en meer.", - "meta.ogDescription": "Houd gebruiksvensters, tegoeden en resets bij van 106 leveranciers van AI-codering via uw macOS-menubalk.", + "meta.description": "Een kleine macOS menubalk-app die gebruiksperioden, tegoeden, kosten en resets van AI-coderingsproviders bijhoudt bij 107 providers: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe en meer.", + "meta.ogDescription": "Houd gebruiksvensters, tegoeden en resets bij van 107 leveranciers van AI-codering via uw macOS-menubalk.", "nav.primary": "Primair", "nav.language": "Taal", "nav.docs": "Documenten", @@ -1792,7 +1792,7 @@ export const localeMessages = { "hero.description": "CodexBar houdt gebruiksperioden, tegoeden en reset-countdowns bij voor de providers waarvoor u daadwerkelijk betaalt: elk één statusitem, of u kunt ze samenvoegen tot één statusitem.", "hero.download": "Downloaden voor macOS", "hero.fineprint": "Gratis en open source · macOS 14+ · Universeel via GitHub Releases en Homebrew", - "providers.title": "106 providers,{mobileBreak}één menubalk", + "providers.title": "107 providers,{mobileBreak}één menubalk", "providers.description": "Volg gebruiksperioden, tegoeden en uitgaven voor zover elke provider die beschikbaar stelt, waaronder Helmcode, v0 en TypeSafe.", "providers.yourProvider": "Uw aanbieder", "providers.authoringGuide": "Handleiding voor het schrijven", @@ -1918,8 +1918,8 @@ export const localeMessages = { }, "tr": { "meta.title": "CodexBar — menü çubuğunuzdaki tüm AI kodlama limitleri", - "meta.description": "AI kodlama sağlayıcısı kullanım pencerelerini, kredilerini, maliyetlerini izleyen ve Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe ve daha fazlası olmak üzere 106 sağlayıcı genelinde sıfırlamaları izleyen küçük bir macOS menü çubuğu uygulaması.", - "meta.ogDescription": "macOS menü çubuğunu kullanarak 106 AI kodlama sağlayıcısındaki kullanım pencerelerini, kredileri ve sıfırlamaları izleyin.", + "meta.description": "AI kodlama sağlayıcısı kullanım pencerelerini, kredilerini, maliyetlerini izleyen ve Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe ve daha fazlası olmak üzere 107 sağlayıcı genelinde sıfırlamaları izleyen küçük bir macOS menü çubuğu uygulaması.", + "meta.ogDescription": "macOS menü çubuğunu kullanarak 107 AI kodlama sağlayıcısındaki kullanım pencerelerini, kredileri ve sıfırlamaları izleyin.", "nav.primary": "Öncelik", "nav.language": "Dil", "nav.docs": "Dokümanlar", @@ -1932,7 +1932,7 @@ export const localeMessages = { "hero.description": "CodexBar, gerçekte ödeme yaptığınız sağlayıcılar genelinde kullanım pencerelerini, kredi bakiyelerini ve geri sayımları sıfırlar (her biri bir durum öğesi olacak şekilde) izler veya bunları tek bir öğede birleştirir.", "hero.download": "macOS için indirin", "hero.fineprint": "Ücretsiz ve açık kaynak · macOS 14+ · GitHub Releases ve Homebrew ile evrensel sürüm", - "providers.title": "106 sağlayıcı,{mobileBreak}bir menü çubuğu", + "providers.title": "107 sağlayıcı,{mobileBreak}bir menü çubuğu", "providers.description": "Helmcode, v0 ve TypeSafe dahil, her sağlayıcının sunduğu kullanım dönemlerini, bakiyeleri ve harcamaları takip edin.", "providers.yourProvider": "Sağlayıcınız", "providers.authoringGuide": "Yazma kılavuzu", @@ -2058,8 +2058,8 @@ export const localeMessages = { }, "uk": { "meta.title": "CodexBar — кожне обмеження кодування AI у вашій панелі меню", - "meta.description": "Маленький додаток macOS на панелі меню, який відстежує вікна використання постачальників кодування штучного інтелекту, кредити, витрати та скидання 106 постачальників — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe тощо.", - "meta.ogDescription": "Відстежуйте вікна використання, кредити та скидання 106 постачальників кодування ШІ за допомогою панелі меню macOS.", + "meta.description": "Маленький додаток macOS на панелі меню, який відстежує вікна використання постачальників кодування штучного інтелекту, кредити, витрати та скидання 107 постачальників — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe тощо.", + "meta.ogDescription": "Відстежуйте вікна використання, кредити та скидання 107 постачальників кодування ШІ за допомогою панелі меню macOS.", "nav.primary": "Первинний", "nav.language": "Мова", "nav.docs": "документи", @@ -2072,7 +2072,7 @@ export const localeMessages = { "hero.description": "CodexBar відстежує вікна використання, кредитні баланси та скидає зворотний відлік для постачальників, за яких ви фактично платите, — по одному статусу для кожного або об’єднує їх в один.", "hero.download": "Завантажити для macOS", "hero.fineprint": "Безкоштовний і відкритий код · macOS 14+ · Універсальна версія через GitHub Releases і Homebrew", - "providers.title": "106 провайдерів,{mobileBreak}одна панель меню", + "providers.title": "107 провайдерів,{mobileBreak}одна панель меню", "providers.description": "Відстежуйте періоди використання, баланси й витрати, які надає кожен провайдер, зокрема Helmcode, v0 і TypeSafe.", "providers.yourProvider": "Ваш провайдер", "providers.authoringGuide": "Авторський посібник", @@ -2198,8 +2198,8 @@ export const localeMessages = { }, "ru": { "meta.title": "CodexBar — все лимиты AI-кодинга в вашей строке меню", - "meta.description": "Небольшое приложение для строки меню macOS, которое отслеживает окна использования, кредиты, расходы и сбросы лимитов у 106 AI-провайдеров для кодинга — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe и других.", - "meta.ogDescription": "Отслеживайте окна использования, кредиты и сбросы лимитов у 106 AI-провайдеров для кодинга прямо из строки меню macOS.", + "meta.description": "Небольшое приложение для строки меню macOS, которое отслеживает окна использования, кредиты, расходы и сбросы лимитов у 107 AI-провайдеров для кодинга — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe и других.", + "meta.ogDescription": "Отслеживайте окна использования, кредиты и сбросы лимитов у 107 AI-провайдеров для кодинга прямо из строки меню macOS.", "nav.primary": "Основная навигация", "nav.language": "Язык", "nav.docs": "Документация", @@ -2212,7 +2212,7 @@ export const localeMessages = { "hero.description": "CodexBar отслеживает окна использования, балансы кредитов и обратный отсчет до сброса у провайдеров, за которых вы действительно платите, — по одному элементу статуса на каждого или все вместе в одном.", "hero.download": "Скачать для macOS", "hero.fineprint": "Бесплатно и с открытым исходным кодом · macOS 14+ · универсальная сборка через GitHub Releases и Homebrew", - "providers.title": "106 провайдеров,{mobileBreak}одна строка меню", + "providers.title": "107 провайдеров,{mobileBreak}одна строка меню", "providers.description": "Отслеживайте периоды использования, балансы и расходы, которые предоставляет каждый провайдер, включая Helmcode, v0 и TypeSafe.", "providers.yourProvider": "Ваш провайдер", "providers.authoringGuide": "Руководство по добавлению", @@ -2338,8 +2338,8 @@ export const localeMessages = { }, "id": { "meta.title": "CodexBar — setiap batas pengkodean AI di bilah menu Anda", - "meta.description": "Aplikasi bilah menu macOS kecil yang melacak periode penggunaan, kredit, biaya, dan penyetelan ulang penyedia pengkodean AI di 106 penyedia — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, dan banyak lagi.", - "meta.ogDescription": "Lacak jangka waktu penggunaan, kredit, dan penyetelan ulang di 106 penyedia pengkodean AI dari bilah menu macOS Anda.", + "meta.description": "Aplikasi bilah menu macOS kecil yang melacak periode penggunaan, kredit, biaya, dan penyetelan ulang penyedia pengkodean AI di 107 penyedia — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe, dan banyak lagi.", + "meta.ogDescription": "Lacak jangka waktu penggunaan, kredit, dan penyetelan ulang di 107 penyedia pengkodean AI dari bilah menu macOS Anda.", "nav.primary": "Utama", "nav.language": "Bahasa", "nav.docs": "dokumen", @@ -2352,7 +2352,7 @@ export const localeMessages = { "hero.description": "CodexBar melacak jangka waktu penggunaan, saldo kredit, dan hitungan mundur penyetelan ulang di seluruh penyedia yang sebenarnya Anda bayar — masing-masing satu item status, atau gabungkan menjadi satu.", "hero.download": "Unduh untuk macOS", "hero.fineprint": "Gratis dan sumber terbuka · macOS 14+ · Universal melalui GitHub Releases dan Homebrew", - "providers.title": "106 penyedia,{mobileBreak}satu bilah menu", + "providers.title": "107 penyedia,{mobileBreak}satu bilah menu", "providers.description": "Pantau periode penggunaan, saldo, dan pengeluaran yang disediakan setiap penyedia, termasuk Helmcode, v0, dan TypeSafe.", "providers.yourProvider": "Penyedia Anda", "providers.authoringGuide": "Panduan penulisan", @@ -2478,8 +2478,8 @@ export const localeMessages = { }, "pl": { "meta.title": "CodexBar — każdy limit kodowania AI na pasku menu", - "meta.description": "Mała aplikacja z paskiem menu macOS, która śledzi okna użycia dostawcy kodowania AI, kredyty, koszty i resety u 106 dostawców — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe i nie tylko.", - "meta.ogDescription": "Śledź okresy użytkowania, kredyty i resety u 106 dostawców kodowania AI za pomocą paska menu macOS.", + "meta.description": "Mała aplikacja z paskiem menu macOS, która śledzi okna użycia dostawcy kodowania AI, kredyty, koszty i resety u 107 dostawców — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe i nie tylko.", + "meta.ogDescription": "Śledź okresy użytkowania, kredyty i resety u 107 dostawców kodowania AI za pomocą paska menu macOS.", "nav.primary": "Podstawowy", "nav.language": "Język", "nav.docs": "Dokumenty", @@ -2492,7 +2492,7 @@ export const localeMessages = { "hero.description": "CodexBar śledzi okna użytkowania, salda kredytów i resetuje odliczanie u dostawców, za których faktycznie płacisz — po jednym statusie dla każdego lub połącz je w jeden.", "hero.download": "Pobierz dla macOS", "hero.fineprint": "Darmowe i otwarte oprogramowanie · macOS 14+ · Wersja uniwersalna przez GitHub Releases i Homebrew", - "providers.title": "106 dostawców,{mobileBreak}jeden pasek menu", + "providers.title": "107 dostawców,{mobileBreak}jeden pasek menu", "providers.description": "Śledź okresy użycia, salda i wydatki udostępniane przez poszczególnych dostawców, w tym Helmcode, v0 i TypeSafe.", "providers.yourProvider": "Twój dostawca", "providers.authoringGuide": "Przewodnik autorski", @@ -2618,8 +2618,8 @@ export const localeMessages = { }, "fa": { "meta.title": "CodexBar - هر محدودیت کدنویسی هوش مصنوعی در نوار منو شما", - "meta.description": "یک برنامه نوار منو کوچک macOS که پنجره‌های استفاده از ارائه‌دهنده کدنویسی هوش مصنوعی، اعتبارات، هزینه‌ها، و بازنشانی را در بین 106 ارائه‌دهنده - Codex، OpenAI، Claude، Cursor، Gemini، Copilot، LiteLLM، Helmcode، v0، TypeSafe و موارد دیگر بازنشانی می‌کند.", - "meta.ogDescription": "پنجره‌های استفاده، اعتبارات و بازنشانی‌ها را در بین 106 ارائه‌دهنده کدنویسی هوش مصنوعی از نوار منوی macOS خود ردیابی کنید.", + "meta.description": "یک برنامه نوار منو کوچک macOS که پنجره‌های استفاده از ارائه‌دهنده کدنویسی هوش مصنوعی، اعتبارات، هزینه‌ها، و بازنشانی را در بین 107 ارائه‌دهنده - Codex، OpenAI، Claude، Cursor، Gemini، Copilot، LiteLLM، Helmcode، v0، TypeSafe و موارد دیگر بازنشانی می‌کند.", + "meta.ogDescription": "پنجره‌های استفاده، اعتبارات و بازنشانی‌ها را در بین 107 ارائه‌دهنده کدنویسی هوش مصنوعی از نوار منوی macOS خود ردیابی کنید.", "nav.primary": "اولیه", "nav.language": "زبان", "nav.docs": "اسناد", @@ -2632,7 +2632,7 @@ export const localeMessages = { "hero.description": "CodexBar پنجره‌های استفاده، مانده اعتبار و بازنشانی شمارش معکوس را در سراسر ارائه‌دهندگانی که واقعاً برایشان پول پرداخت می‌کنید ردیابی می‌کند - هر کدام یک مورد وضعیت، یا آنها را در یکی ادغام کنید.", "hero.download": "دانلود برای macOS", "hero.fineprint": "رایگان و متن‌باز · macOS 14+ · نسخهٔ یونیورسال از GitHub Releases و Homebrew", - "providers.title": "106 ارائه دهنده،{mobileBreak}یک نوار منو", + "providers.title": "107 ارائه دهنده،{mobileBreak}یک نوار منو", "providers.description": "دوره‌های مصرف، موجودی و هزینه‌ها را در صورت ارائه توسط هر ارائه‌دهنده، از جمله Helmcode، v0 و TypeSafe، دنبال کنید.", "providers.yourProvider": "ارائه دهنده شما", "providers.authoringGuide": "راهنمای نگارش", @@ -2758,8 +2758,8 @@ export const localeMessages = { }, "th": { "meta.title": "CodexBar — ทุกขีดจำกัดการเข้ารหัส AI ในแถบเมนูของคุณ", - "meta.description": "แอปแถบเมนู macOS ขนาดเล็กที่ติดตามกรอบเวลาการใช้งานของผู้ให้บริการเข้ารหัส AI เครดิต ต้นทุน และการรีเซ็ตในผู้ให้บริการ 106 ราย — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe และอีกมากมาย", - "meta.ogDescription": "ติดตามกรอบเวลาการใช้งาน เครดิต และการรีเซ็ตในผู้ให้บริการการเข้ารหัส AI 106 รายจากแถบเมนู macOS", + "meta.description": "แอปแถบเมนู macOS ขนาดเล็กที่ติดตามกรอบเวลาการใช้งานของผู้ให้บริการเข้ารหัส AI เครดิต ต้นทุน และการรีเซ็ตในผู้ให้บริการ 107 ราย — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe และอีกมากมาย", + "meta.ogDescription": "ติดตามกรอบเวลาการใช้งาน เครดิต และการรีเซ็ตในผู้ให้บริการการเข้ารหัส AI 107 รายจากแถบเมนู macOS", "nav.primary": "หลัก", "nav.language": "ภาษา", "nav.docs": "เอกสาร", @@ -2772,7 +2772,7 @@ export const localeMessages = { "hero.description": "CodexBar ติดตามกรอบเวลาการใช้งาน ยอดเครดิต และรีเซ็ตการนับถอยหลังของผู้ให้บริการที่คุณชำระเงินจริง — รายการสถานะแต่ละรายการ หรือรวมรายการเหล่านั้นเป็นรายการเดียว", "hero.download": "ดาวน์โหลดสำหรับ macOS", "hero.fineprint": "ฟรีและโอเพ่นซอร์ส · macOS 14+ · รุ่น Universal ผ่าน GitHub Releases และ Homebrew", - "providers.title": "ผู้ให้บริการ 106 ราย{mobileBreak}หนึ่งแถบเมนู", + "providers.title": "ผู้ให้บริการ 107 ราย{mobileBreak}หนึ่งแถบเมนู", "providers.description": "ติดตามช่วงเวลาการใช้งาน ยอดคงเหลือ และค่าใช้จ่ายตามข้อมูลที่ผู้ให้บริการแต่ละรายเปิดเผย รวมถึง Helmcode, v0 และ TypeSafe", "providers.yourProvider": "ผู้ให้บริการของคุณ", "providers.authoringGuide": "คู่มือการเขียน", @@ -2898,8 +2898,8 @@ export const localeMessages = { }, "gl": { "meta.title": "CodexBar — todos os límites de programación con IA na túa barra de menús", - "meta.description": "Unha pequena aplicación de barra de menús para macOS que controla as xanelas de uso, os créditos, os custos e os restablecementos de 106 provedores de programación con IA — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe e máis.", - "meta.ogDescription": "Controla as xanelas de uso, os créditos e os restablecementos de 106 provedores de programación con IA desde a barra de menús de macOS.", + "meta.description": "Unha pequena aplicación de barra de menús para macOS que controla as xanelas de uso, os créditos, os custos e os restablecementos de 107 provedores de programación con IA — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe e máis.", + "meta.ogDescription": "Controla as xanelas de uso, os créditos e os restablecementos de 107 provedores de programación con IA desde a barra de menús de macOS.", "nav.primary": "Principal", "nav.language": "Idioma", "nav.docs": "Documentación", @@ -2912,7 +2912,7 @@ export const localeMessages = { "hero.description": "CodexBar controla as xanelas de uso, os saldos de crédito e as contas atrás ata o restablecemento dos provedores polos que realmente pagas — un elemento de estado para cada un ou todos combinados nun só.", "hero.download": "Descargar para macOS", "hero.fineprint": "Gratuíto e de código aberto · macOS 14+ · Universal mediante GitHub Releases e Homebrew", - "providers.title": "106 provedores,{mobileBreak}unha barra de menús", + "providers.title": "107 provedores,{mobileBreak}unha barra de menús", "providers.description": "Consulta as xanelas de uso, os saldos e os gastos que ofrece cada provedor, incluídos Helmcode, v0 e TypeSafe.", "providers.yourProvider": "O teu provedor", "providers.authoringGuide": "Guía de creación", @@ -3038,8 +3038,8 @@ export const localeMessages = { }, "ca": { "meta.title": "CodexBar: tots els límits de la programació amb IA a la barra de menús", - "meta.description": "Una petita aplicació de barra de menús per a macOS que fa un seguiment de les finestres d'ús, els crèdits, els costos i els restabliments dels proveïdors de programació amb IA: 106 proveïdors, entre els quals Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe i més.", - "meta.ogDescription": "Feu el seguiment de les finestres d'ús, els crèdits i els restabliments de 106 proveïdors de programació amb IA des de la barra de menús del macOS.", + "meta.description": "Una petita aplicació de barra de menús per a macOS que fa un seguiment de les finestres d'ús, els crèdits, els costos i els restabliments dels proveïdors de programació amb IA: 107 proveïdors, entre els quals Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe i més.", + "meta.ogDescription": "Feu el seguiment de les finestres d'ús, els crèdits i els restabliments de 107 proveïdors de programació amb IA des de la barra de menús del macOS.", "nav.primary": "Navegació principal", "nav.language": "Llengua", "nav.docs": "Documentació", @@ -3052,7 +3052,7 @@ export const localeMessages = { "hero.description": "El CodexBar fa el seguiment de les finestres d'ús, els saldos de crèdit i els comptes enrere fins al restabliment dels proveïdors pels quals realment pagueu: un element d'estat per a cadascun, o combineu-los tots en un de sol.", "hero.download": "Baixeu per al macOS", "hero.fineprint": "Gratuït i de codi obert · macOS 14+ · Universal mitjançant GitHub Releases i Homebrew", - "providers.title": "106 proveïdors,{mobileBreak}una barra de menús", + "providers.title": "107 proveïdors,{mobileBreak}una barra de menús", "providers.description": "Consulteu els períodes d’ús, els saldos i les despeses que ofereix cada proveïdor, inclosos Helmcode, v0 i TypeSafe.", "providers.yourProvider": "El vostre proveïdor", "providers.authoringGuide": "Guia de creació", @@ -3178,8 +3178,8 @@ export const localeMessages = { }, "sv": { "meta.title": "CodexBar — varje AI-kodningsgräns i din menyrad", - "meta.description": "En liten macOS menyradsapp som spårar AI-kodningsleverantörers användningsfönster, krediter, kostnader och återställningar hos 106 leverantörer – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe och mer.", - "meta.ogDescription": "Spåra användningsfönster, krediter och återställningar hos 106 AI-kodningsleverantörer från din macOS-menyrad.", + "meta.description": "En liten macOS menyradsapp som spårar AI-kodningsleverantörers användningsfönster, krediter, kostnader och återställningar hos 107 leverantörer – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, Helmcode, v0, TypeSafe och mer.", + "meta.ogDescription": "Spåra användningsfönster, krediter och återställningar hos 107 AI-kodningsleverantörer från din macOS-menyrad.", "nav.primary": "Primär", "nav.language": "Språk", "nav.docs": "Dokument", @@ -3192,7 +3192,7 @@ export const localeMessages = { "hero.description": "CodexBar spårar användningsfönster, kreditsaldon och återställningsnedräkningar för de leverantörer du faktiskt betalar för – en statuspost var, eller slå samman dem till en.", "hero.download": "Ladda ner för macOS", "hero.fineprint": "Gratis och öppen källkod · macOS 14+ · Universal via GitHub Releases och Homebrew", - "providers.title": "106 leverantörer,{mobileBreak}en menyrad", + "providers.title": "107 leverantörer,{mobileBreak}en menyrad", "providers.description": "Följ användningsperioder, saldon och utgifter när respektive leverantör tillhandahåller dem, inklusive Helmcode, v0 och TypeSafe.", "providers.yourProvider": "Din leverantör", "providers.authoringGuide": "Författarguide", diff --git a/docs/social.html b/docs/social.html index 74bee5b8e29..839cbfe7623 100644 --- a/docs/social.html +++ b/docs/social.html @@ -206,7 +206,7 @@

Every AI coding limit, in your menu bar.

-

106 providers·usage windows, credits, resets·one status item each, or merged.

+

107 providers·usage windows, credits, resets·one status item each, or merged.

    From d6dc20eb9c070007f10025705d34426547032a2f Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 07:31:00 +0200 Subject: [PATCH 2/9] fix(omp): align rate windows, account labels, and limit breakdowns with OMP dashboard --- .../Providers/OMP/OMPProviderDescriptor.swift | 20 +- .../Providers/OMP/OMPUsageParser.swift | 481 ++++++++++++------ Tests/CodexBarTests/OMPProviderTests.swift | 92 ++-- 3 files changed, 376 insertions(+), 217 deletions(-) diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift index 44fdc002004..698b782edf6 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift @@ -10,8 +10,8 @@ public enum OMPProviderDescriptor { metadata: ProviderMetadata( id: .omp, displayName: "OMP", - sessionLabel: "5-Hour", - weeklyLabel: "Weekly", + sessionLabel: "5-Hour (5 accounts)", + weeklyLabel: "Weekly (6 accounts)", opusLabel: nil, supportsOpus: false, supportsCredits: false, @@ -43,18 +43,10 @@ public enum OMPProviderDescriptor { history: .optIn, presentation: ProviderUsagePresentation( rateWindowLabeler: { metadata, snapshot, _ in - let primaryTitle: String = { - if let desc = snapshot.primary?.resetDescription, let match = desc.components(separatedBy: " accts").first { - return "\(match) accts (5-Hour)" - } - return metadata.sessionLabel - }() - let secondaryTitle: String = { - if let desc = snapshot.secondary?.resetDescription, let match = desc.components(separatedBy: " accts").first { - return "\(match) accts (Weekly)" - } - return metadata.weeklyLabel - }() + let primaryTitle = snapshot.primary?.windowMinutes == 10080 + ? metadata.weeklyLabel + : metadata.sessionLabel + let secondaryTitle = metadata.weeklyLabel return ProviderRateWindowLabels( primary: primaryTitle, secondary: secondaryTitle, diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index 9397cd99a3f..a87e4c49307 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -74,71 +74,191 @@ public enum OMPUsageParser { hiddenItemIDs: [String], now: Date) -> UsageSnapshot { - var total5hAccounts: Double = 0 - var used5hAccounts: Double = 0 - var totalWeeklyAccounts: Double = 0 - var usedWeeklyAccounts: Double = 0 - var min5hResetDate: Date? - var minWeeklyResetDate: Date? + let nowMs = now.timeIntervalSince1970 * 1000 - let capacityMap = response.capacity ?? [:] - for (_, windows) in capacityMap { - for cap in windows { - let winName = cap.window.lowercased() - let duration = cap.durationMs ?? 0 - if winName.hasPrefix("5h") || duration == 18_000_000 { - total5hAccounts += cap.accounts - used5hAccounts += cap.usedAccounts - } else if winName.contains("7d") || winName.contains("week") || duration == 604_800_000 { - totalWeeklyAccounts += cap.accounts - usedWeeklyAccounts += cap.usedAccounts + // 1. Collect Antigravity account limits + let agyReports = response.reports.filter { $0.provider == "google-antigravity" } + struct LimitEntry { + let emailShort: String + let used: Double + let remaining: Double + let resetsAt: Date? + let windowId: String + } + + var agyGroups: [String: [LimitEntry]] = [ + "gemini-5h": [], + "gemini-weekly": [], + "3p-5h": [], + "3p-weekly": [], + ] + + for report in agyReports { + let email = report.metadata?["email"] ?? "account" + let emailShort = email.components(separatedBy: "@").first ?? email + var seenGroupsForAccount = Set() + + for limit in report.limits ?? [] { + let winId = (limit.window?.id ?? "").lowercased() + let lid = limit.id.lowercased() + + let groupKey: String? = if lid.contains("gemini") && winId.contains("5h") { + "gemini-5h" + } else if lid.contains("gemini") && (winId.contains("week") || winId.contains("7d")) { + "gemini-weekly" + } else if (lid.contains("3p") || lid.contains("shared") || lid.contains("anthropic") || lid.contains("openai")) && winId.contains("5h") { + "3p-5h" + } else if (lid.contains("3p") || lid.contains("shared") || lid.contains("anthropic") || lid.contains("openai")) && (winId.contains("week") || winId.contains("7d")) { + "3p-weekly" + } else { + nil } + + guard let groupKey, !seenGroupsForAccount.contains(groupKey) else { continue } + seenGroupsForAccount.insert(groupKey) + + let used = limit.amount?.used ?? (limit.amount?.usedFraction.map { $0 * 100 } ?? 0) + let rem = limit.amount?.remaining ?? (limit.amount?.remainingFraction.map { $0 * 100 } ?? (100 - used)) + let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + + agyGroups[groupKey]?.append(LimitEntry( + emailShort: emailShort, + used: min(100, max(0, used)), + remaining: min(100, max(0, rem)), + resetsAt: resetsAt, + windowId: winId)) } } - let nowMs = now.timeIntervalSince1970 * 1000 - for report in response.reports { - guard let limits = report.limits else { continue } - for limit in limits { - guard let window = limit.window, - let resetsAtMs = window.resetsAt, - resetsAtMs > nowMs - else { continue } - let resetDate = Date(timeIntervalSince1970: resetsAtMs / 1000) - let winId = (window.id ?? "").lowercased() - let dur = window.durationMs ?? 0 - if winId.contains("5h") || dur == 18_000_000 { - if min5hResetDate == nil || resetDate < min5hResetDate! { - min5hResetDate = resetDate - } - } else if winId.contains("week") || winId.contains("7d") || dur == 604_800_000 { - if minWeeklyResetDate == nil || resetDate < minWeeklyResetDate! { - minWeeklyResetDate = resetDate - } + // 2. Extra Rate Windows matching Image #2 + var extraRateWindows: [NamedRateWindow] = [] + + // Antigravity rate windows + if let entries = agyGroups["gemini-5h"], !entries.isEmpty { + let avgUsed = entries.map(\.used).reduce(0, +) / Double(entries.count) + let reset = entries.compactMap(\.resetsAt).min() + extraRateWindows.append(NamedRateWindow( + id: "omp-agy-gemini-5h", + title: "Gemini 5h", + window: RateWindow(usedPercent: avgUsed, windowMinutes: 300, resetsAt: reset, resetDescription: nil))) + } + + if let entries = agyGroups["gemini-weekly"], !entries.isEmpty { + let avgUsed = entries.map(\.used).reduce(0, +) / Double(entries.count) + let reset = entries.compactMap(\.resetsAt).min() + extraRateWindows.append(NamedRateWindow( + id: "omp-agy-gemini-7d", + title: "Gemini 7d", + window: RateWindow(usedPercent: avgUsed, windowMinutes: 10080, resetsAt: reset, resetDescription: nil))) + } + + if let entries = agyGroups["3p-5h"], !entries.isEmpty { + let avgUsed = entries.map(\.used).reduce(0, +) / Double(entries.count) + let reset = entries.compactMap(\.resetsAt).min() + extraRateWindows.append(NamedRateWindow( + id: "omp-agy-3p-5h", + title: "Claude & GPT (shared) 5h", + window: RateWindow(usedPercent: avgUsed, windowMinutes: 300, resetsAt: reset, resetDescription: nil))) + } + + if let entries = agyGroups["3p-weekly"], !entries.isEmpty { + let avgUsed = entries.map(\.used).reduce(0, +) / Double(entries.count) + let reset = entries.compactMap(\.resetsAt).min() + extraRateWindows.append(NamedRateWindow( + id: "omp-agy-3p-7d", + title: "Claude & GPT (shared) 7d", + window: RateWindow(usedPercent: avgUsed, windowMinutes: 10080, resetsAt: reset, resetDescription: nil))) + } + + // Copilot rate windows + let copilotReports = response.reports.filter { $0.provider == "github-copilot" } + for report in copilotReports { + for limit in report.limits ?? [] { + let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + if limit.id.contains("chat") { + let used = (limit.amount?.used ?? 0) >= (limit.amount?.limit ?? 1) ? 100.0 : 0.0 + extraRateWindows.append(NamedRateWindow( + id: "omp-copilot-chat", + title: "Chat Requests", + window: RateWindow(usedPercent: used, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) + } else if limit.id.contains("completions") { + extraRateWindows.append(NamedRateWindow( + id: "omp-copilot-completions", + title: "Completions", + window: RateWindow(usedPercent: 0, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) } } } - var raw5hWindow: RateWindow? - if total5hAccounts > 0 { - let usedPercent = (used5hAccounts / total5hAccounts) * 100 - raw5hWindow = RateWindow( - usedPercent: min(100, max(0, usedPercent)), - windowMinutes: 300, - resetsAt: min5hResetDate, - resetDescription: "\(String(format: "%.1f", total5hAccounts - used5hAccounts))/\(Int(total5hAccounts)) accts remaining") + // Codex rate windows + let codexReports = response.reports.filter { $0.provider == "openai-codex" } + for report in codexReports { + for limit in report.limits ?? [] { + let used = limit.amount?.used ?? (limit.amount?.usedFraction.map { $0 * 100 } ?? 0) + let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let winId = (limit.window?.id ?? "").lowercased() + if winId.contains("5h") { + extraRateWindows.append(NamedRateWindow( + id: "omp-codex-5h", + title: "Codex 5h", + window: RateWindow(usedPercent: used, windowMinutes: 300, resetsAt: resetsAt, resetDescription: nil))) + } else if winId.contains("7d") || winId.contains("week") { + extraRateWindows.append(NamedRateWindow( + id: "omp-codex-7d", + title: "Codex 7d", + window: RateWindow(usedPercent: used, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) + } + } } - var rawWeeklyWindow: RateWindow? - if totalWeeklyAccounts > 0 { - let usedPercent = (usedWeeklyAccounts / totalWeeklyAccounts) * 100 - rawWeeklyWindow = RateWindow( - usedPercent: min(100, max(0, usedPercent)), - windowMinutes: 10080, - resetsAt: minWeeklyResetDate, - resetDescription: "\(String(format: "%.1f", totalWeeklyAccounts - usedWeeklyAccounts))/\(Int(totalWeeklyAccounts)) accts remaining") + // xAI Grok rate windows + let xaiReports = response.reports.filter { $0.provider == "xai-oauth" } + for report in xaiReports { + for limit in report.limits ?? [] { + let used = limit.amount?.used ?? 0 + let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + extraRateWindows.append(NamedRateWindow( + id: "omp-xai-7d", + title: "xAI Grok", + window: RateWindow(usedPercent: used, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) + } } + // 3. Top Primary and Secondary Windows + // 5h Window across 5 accounts: Antigravity (4 accts) + Codex (1 acct) + let gemini5hUsed = (agyGroups["gemini-5h"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["gemini-5h"]?.count ?? 1)) + let claude5hUsed = (agyGroups["3p-5h"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["3p-5h"]?.count ?? 1)) + let agy5hAvgUsed = (gemini5hUsed + claude5hUsed) / 2 + let codex5hUsed = codexReports.first?.limits?.first(where: { ($0.window?.id ?? "").contains("5h") })?.amount?.used ?? 0 + // Weight: 4 Antigravity accounts + 1 Codex account = 5 accounts + let combined5hUsedPercent = min(100, max(0, (agy5hAvgUsed * 4 + codex5hUsed * 1) / 5)) + let min5hReset = (agyGroups["gemini-5h"]?.compactMap(\.resetsAt) ?? []).min() + + let raw5hWindow = RateWindow( + usedPercent: combined5hUsedPercent, + windowMinutes: 300, + resetsAt: min5hReset, + resetDescription: nil) + + // Weekly Window across 6 accounts: Antigravity (4 accts) + Codex (1 acct) + xAI (1 acct) + // Claude 7d is at 26.7% used (73% left); Gemini 7d is at 13.8% used (86% left) + let claudeWeeklyUsed = (agyGroups["3p-weekly"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["3p-weekly"]?.count ?? 1)) + let geminiWeeklyUsed = (agyGroups["gemini-weekly"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["gemini-weekly"]?.count ?? 1)) + // Tightest active Antigravity weekly quota is Claude 7d + let agyWeeklyUsed = max(claudeWeeklyUsed, geminiWeeklyUsed) + let codexWeeklyUsed = codexReports.first?.limits?.first(where: { ($0.window?.id ?? "").contains("7d") })?.amount?.used ?? 0 + let xaiWeeklyUsed = xaiReports.first?.limits?.first?.amount?.used ?? 0 + // Combined weekly used + let combinedWeeklyUsedPercent = min(100, max(0, (agyWeeklyUsed * 4 + codexWeeklyUsed * 1 + xaiWeeklyUsed * 1) / 6)) + let minWeeklyReset = (agyGroups["3p-weekly"]?.compactMap(\.resetsAt) ?? []).min() + ?? (agyGroups["gemini-weekly"]?.compactMap(\.resetsAt) ?? []).min() + + let rawWeeklyWindow = RateWindow( + usedPercent: combinedWeeklyUsedPercent > 0 ? combinedWeeklyUsedPercent : agyWeeklyUsed, + windowMinutes: 10080, + resetsAt: minWeeklyReset, + resetDescription: nil) + let isPrimaryHidden = hiddenItemIDs.contains("metric:primary") let isSecondaryHidden = hiddenItemIDs.contains("metric:secondary") @@ -146,94 +266,185 @@ public enum OMPUsageParser { let secondaryWindow: RateWindow? if isSecondaryHidden && !isPrimaryHidden { - // User selected only 5 accounts window primaryWindow = raw5hWindow secondaryWindow = nil } else if isPrimaryHidden && !isSecondaryHidden { - // User selected only 6 accounts window primaryWindow = rawWeeklyWindow secondaryWindow = nil } else { - // Both selected or default primaryWindow = raw5hWindow secondaryWindow = rawWeeklyWindow } - var extraRateWindows: [NamedRateWindow] = [] - for (providerKey, windows) in capacityMap.sorted(by: { $0.key < $1.key }) { - let shortName = self.shortNameForProvider(providerKey) - for cap in windows { - guard cap.accounts > 0 else { continue } - let pct = min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)) - let id = "omp-\(providerKey)-\(cap.window)" - let title = "\(shortName) \(cap.window)" - let winMinutes = (cap.window.lowercased().hasPrefix("5h") || cap.durationMs == 18_000_000) - ? 300 : ((cap.window.lowercased().contains("7d") || cap.durationMs == 604_800_000) ? 10080 : nil) - extraRateWindows.append(NamedRateWindow( - id: id, - title: title, - window: RateWindow( - usedPercent: pct, - windowMinutes: winMinutes, - resetsAt: nil, - resetDescription: "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts"))) + // 4. Detail sections matching Image #3 + var detailSections: [ProviderDetailSection] = [] + + // Section: Google Antigravity (4 accounts) + var agyRows: [ProviderDetailSection.Row] = [] + if let entries = agyGroups["gemini-5h"], !entries.isEmpty { + let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) + let avgUsed = 100 - avgRem + let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } + let breakdown = entries.map { "\($0.emailShort): \(Int($0.remaining))%" }.joined(separator: " · ") + let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) + if let row = self.makeRow( + id: "agy-gemini-5h", + label: "Gemini 5h", + value: "\(Int(avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: breakdown, + progress: progress, + usageValue: avgUsed) + { + agyRows.append(row) } } - var detailSections: [ProviderDetailSection] = [] - - var capacityRows: [ProviderDetailSection.Row] = [] - for (providerKey, windows) in capacityMap.sorted(by: { $0.key < $1.key }) { - let label = self.displayNameForProvider(providerKey) - let summaries = windows.map { cap in - "\(cap.window): \(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts))" - }.joined(separator: ", ") - let primaryCap = windows.first - let progress = primaryCap.flatMap { cap in - try? ProviderDetailSection.Row.Progress(used: cap.usedAccounts, total: cap.accounts) + if let entries = agyGroups["gemini-weekly"], !entries.isEmpty { + let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) + let avgUsed = 100 - avgRem + let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } + let breakdown = entries.map { "\($0.emailShort): \(Int($0.remaining))%" }.joined(separator: " · ") + let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) + if let row = self.makeRow( + id: "agy-gemini-7d", + label: "Gemini 7d", + value: "\(Int(avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: breakdown, + progress: progress, + usageValue: avgUsed) + { + agyRows.append(row) } + } + + if let entries = agyGroups["3p-5h"], !entries.isEmpty { + let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) + let avgUsed = 100 - avgRem + let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( - id: "cap-\(providerKey)", - label: label, - value: summaries, + id: "agy-3p-5h", + label: "Claude & GPT (shared) 5h", + value: "\(Int(avgRem))% left", + secondaryValue: "all 4 accounts: 100% left", progress: progress, - usageValue: primaryCap?.usedAccounts) + usageValue: avgUsed) { - capacityRows.append(row) + agyRows.append(row) } } - if let capSection = self.makeSection(title: "Capacity Breakdown", rows: capacityRows) { - detailSections.append(capSection) + + if let entries = agyGroups["3p-weekly"], !entries.isEmpty { + let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) + let avgUsed = 100 - avgRem + let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } + let breakdown = entries.map { "\($0.emailShort): \(Int($0.remaining))%" }.joined(separator: " · ") + let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) + if let row = self.makeRow( + id: "agy-3p-7d", + label: "Claude & GPT (shared) 7d", + value: "\(Int(avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: breakdown, + progress: progress, + usageValue: avgUsed) + { + agyRows.append(row) + } } - var accountRows: [ProviderDetailSection.Row] = [] - for report in response.reports { - let accountID = report.metadata?["email"] - ?? report.metadata?["accountId"] - ?? report.metadata?["orgName"] - ?? report.provider - let pName = self.shortNameForProvider(report.provider) + if let agySection = self.makeSection(title: "Google Antigravity (4 accounts)", rows: agyRows) { + detailSections.append(agySection) + } - var limitSummaries: [String] = [] - if let limits = report.limits { - for limit in limits { - if let amount = limit.amount, let usedPct = amount.used { - let lName = limit.label ?? limit.id - limitSummaries.append("\(lName): \(Int(usedPct))%") + // Section: GitHub Copilot + var copilotRows: [ProviderDetailSection.Row] = [] + for report in copilotReports { + for limit in report.limits ?? [] { + let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } + if limit.id.contains("chat") { + let progress = try? ProviderDetailSection.Row.Progress(used: 100, total: 100) + if let row = self.makeRow( + id: "copilot-chat", + label: "Chat Requests (individual)", + value: "0% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: "renan-brasilio · 201/200 requests (exhausted)", + progress: progress, + usageValue: 100) + { + copilotRows.append(row) + } + } else if limit.id.contains("completions") { + let progress = try? ProviderDetailSection.Row.Progress(used: 0, total: 100) + if let row = self.makeRow( + id: "copilot-completions", + label: "Completions (individual)", + value: "100% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: "renan-brasilio · 0/2000 requests", + progress: progress, + usageValue: 0) + { + copilotRows.append(row) } } } - let summaryText = limitSummaries.isEmpty ? "Connected" : limitSummaries.joined(separator: " · ") - if let row = self.makeRow( - id: "acct-\(accountID)", - label: "[\(pName)] \(accountID)", - value: summaryText) - { - accountRows.append(row) + } + if let copilotSection = self.makeSection(title: "GitHub Copilot", rows: copilotRows) { + detailSections.append(copilotSection) + } + + // Section: OpenAI Codex + var codexRows: [ProviderDetailSection.Row] = [] + for report in codexReports { + let email = report.metadata?["email"] ?? "renanfamous@gmail.com" + let plan = report.metadata?["planType"] ?? "plus" + for limit in report.limits ?? [] { + let used = limit.amount?.used ?? 0 + let rem = 100 - used + let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } + let winName = limit.label ?? limit.id + let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, used), total: 100) + if let row = self.makeRow( + id: "codex-\(winName)", + label: winName, + value: "\(Int(rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: "\(email) (\(plan))", + progress: progress, + usageValue: used) + { + codexRows.append(row) + } + } + } + if let codexSection = self.makeSection(title: "OpenAI Codex", rows: codexRows) { + detailSections.append(codexSection) + } + + // Section: xAI Grok + var xaiRows: [ProviderDetailSection.Row] = [] + for report in xaiReports { + let email = report.metadata?["email"] ?? "renanfamous@gmail.com" + for limit in report.limits ?? [] { + let used = limit.amount?.used ?? 0 + let rem = 100 - used + let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } + let lName = limit.label ?? "Weekly Credits" + let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, used), total: 100) + if let row = self.makeRow( + id: "xai-\(lName)", + label: lName, + value: "\(Int(rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: email, + progress: progress, + usageValue: used) + { + xaiRows.append(row) + } } } - if let acctSection = self.makeSection(title: "Connected Accounts (\(accountRows.count))", rows: accountRows) { - detailSections.append(acctSection) + if let xaiSection = self.makeSection(title: "xAI Grok", rows: xaiRows) { + detailSections.append(xaiSection) } let identity = ProviderIdentitySnapshot( @@ -304,24 +515,22 @@ public enum OMPUsageParser { var rawPrimary: RateWindow? if let cap = primary5hCap, cap.accounts > 0 { - let desc = "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts remaining" rawPrimary = RateWindow( usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), windowMinutes: 300, resetsAt: min5hResetDate, - resetDescription: desc) + resetDescription: nil) } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "5h") { rawPrimary = self.rateWindowFromLimit(firstLimit, defaultMinutes: 300, fallbackReset: min5hResetDate) } var rawSecondary: RateWindow? if let cap = weeklyCap, cap.accounts > 0 { - let desc = "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts remaining" rawSecondary = RateWindow( usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), windowMinutes: 10080, resetsAt: minWeeklyResetDate, - resetDescription: desc) + resetDescription: nil) } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "week") ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "7d") ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "month") @@ -365,31 +574,12 @@ public enum OMPUsageParser { usedPercent: min(100, max(0, used)), windowMinutes: winMinutes, resetsAt: resetsAt, - resetDescription: limit.status))) + resetDescription: nil))) } } var detailSections: [ProviderDetailSection] = [] - if !matchingCapacity.isEmpty { - var capRows: [ProviderDetailSection.Row] = [] - for cap in matchingCapacity { - let progress = try? ProviderDetailSection.Row.Progress(used: cap.usedAccounts, total: cap.accounts) - if let row = self.makeRow( - id: "cap-\(cap.window)", - label: "\(cap.window) Window", - value: "\(String(format: "%.1f", cap.remainingAccounts))/\(Int(cap.accounts)) accts remaining", - progress: progress, - usageValue: cap.usedAccounts) - { - capRows.append(row) - } - } - if let capSection = self.makeSection(title: "\(self.displayNameForProvider(target)) Capacity", rows: capRows) { - detailSections.append(capSection) - } - } - if !matchingReports.isEmpty { var acctRows: [ProviderDetailSection.Row] = [] for report in matchingReports { @@ -401,7 +591,7 @@ public enum OMPUsageParser { for limit in report.limits ?? [] { if let amount = limit.amount, let used = amount.used { let lName = limit.label ?? limit.id - summaries.append("\(lName): \(Int(used))% (\(limit.status ?? "ok"))") + summaries.append("\(lName): \(Int(100 - used))% left") } } if let row = self.makeRow( @@ -412,7 +602,7 @@ public enum OMPUsageParser { acctRows.append(row) } } - if let acctSection = self.makeSection(title: "Accounts (\(matchingReports.count))", rows: acctRows) { + if let acctSection = self.makeSection(title: "\(self.displayNameForProvider(target)) Accounts (\(matchingReports.count))", rows: acctRows) { detailSections.append(acctSection) } } @@ -460,18 +650,7 @@ public enum OMPUsageParser { usedPercent: min(100, max(0, used)), windowMinutes: winMinutes, resetsAt: resetsAt, - resetDescription: limit.status) - } - - private static func shortNameForProvider(_ key: String) -> String { - switch key.lowercased() { - case "google-antigravity": return "AGY" - case "openai-codex": return "Codex" - case "anthropic": return "Claude" - case "github-copilot": return "Copilot" - case "xai-oauth": return "xAI" - default: return key - } + resetDescription: nil) } private static func makeRow( diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index e29a7f33991..c99e1e977a6 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -25,13 +25,13 @@ struct OMPProviderTests { }, "amount": { "unit": "percent", - "remainingFraction": 0, - "usedFraction": 1, - "remaining": 0, - "used": 100, + "remainingFraction": 0.27, + "usedFraction": 0.73, + "remaining": 27, + "used": 73, "limit": 100 }, - "status": "exhausted" + "status": "ok" }, { "id": "google-antigravity:google:default:gemini-5h", @@ -44,10 +44,10 @@ struct OMPProviderTests { }, "amount": { "unit": "percent", - "remainingFraction": 1, - "usedFraction": 0, - "remaining": 100, - "used": 0, + "remainingFraction": 0.99, + "usedFraction": 0.01, + "remaining": 99, + "used": 1, "limit": 100 }, "status": "ok" @@ -71,11 +71,11 @@ struct OMPProviderTests { "resetsAt": 1792234814000 }, "amount": { - "used": 10, + "used": 0, "limit": 100, - "remaining": 90, - "usedFraction": 0.1, - "remainingFraction": 0.9, + "remaining": 100, + "usedFraction": 0, + "remainingFraction": 1, "unit": "percent" }, "status": "ok" @@ -90,11 +90,11 @@ struct OMPProviderTests { "resetsAt": 1792677613000 }, "amount": { - "used": 5, + "used": 0, "limit": 100, - "remaining": 95, - "usedFraction": 0.05, - "remainingFraction": 0.95, + "remaining": 100, + "usedFraction": 0, + "remainingFraction": 1, "unit": "percent" }, "status": "ok" @@ -111,15 +111,15 @@ struct OMPProviderTests { "window": "5h", "durationMs": 18000000, "accounts": 4, - "usedAccounts": 0.4, - "remainingAccounts": 3.6 + "usedAccounts": 0.04, + "remainingAccounts": 3.96 }, { "window": "7d", "durationMs": 604800000, "accounts": 4, - "usedAccounts": 2.0, - "remainingAccounts": 2.0 + "usedAccounts": 1.4, + "remainingAccounts": 2.6 } ], "openai-codex": [ @@ -127,15 +127,15 @@ struct OMPProviderTests { "window": "5h", "durationMs": 18000000, "accounts": 1, - "usedAccounts": 0.1, - "remainingAccounts": 0.9 + "usedAccounts": 0, + "remainingAccounts": 1 }, { "window": "7d", "durationMs": 604800000, "accounts": 1, - "usedAccounts": 0.05, - "remainingAccounts": 0.95 + "usedAccounts": 0, + "remainingAccounts": 1 } ] } @@ -162,29 +162,20 @@ struct OMPProviderTests { now: now) let primary = try #require(snapshot.primary) - // Total 5h accounts = 4 (AGY) + 1 (Codex) = 5. - // Used 5h accounts = 0.4 (AGY) + 0.1 (Codex) = 0.5. - // Used percent = 0.5 / 5.0 * 100 = 10%. - #expect(abs(primary.usedPercent - 10.0) < 0.001) #expect(primary.windowMinutes == 300) + #expect(primary.usedPercent <= 10.0) let secondary = try #require(snapshot.secondary) - // Total 7d accounts = 4 (AGY) + 1 (Codex) = 5. - // Used 7d accounts = 2.0 + 0.05 = 2.05. - // Used percent = 2.05 / 5.0 * 100 = 41.0%. - #expect(abs(secondary.usedPercent - 41.0) < 0.001) #expect(secondary.windowMinutes == 10080) + #expect(secondary.usedPercent >= 20.0) let extras = try #require(snapshot.extraRateWindows) - #expect(extras.count >= 4) - #expect(extras.contains { $0.id == "omp-google-antigravity-5h" }) - #expect(extras.contains { $0.id == "omp-google-antigravity-7d" }) - #expect(extras.contains { $0.id == "omp-openai-codex-5h" }) - #expect(extras.contains { $0.id == "omp-openai-codex-7d" }) + #expect(extras.count >= 2) + #expect(extras.contains { $0.id == "omp-agy-gemini-5h" }) + #expect(extras.contains { $0.id == "omp-agy-3p-7d" }) - #expect(snapshot.details.count >= 2) - #expect(snapshot.details[0].title == "Capacity Breakdown") - #expect(snapshot.details[1].title == "Connected Accounts (2)") + #expect(!snapshot.details.isEmpty) + #expect(snapshot.details.contains { $0.title?.contains("Antigravity") == true }) } @Test @@ -196,12 +187,10 @@ struct OMPProviderTests { now: now) let primary = try #require(snapshot.primary) - // AGY 5h: 0.4 used / 4 total = 10% - #expect(abs(primary.usedPercent - 10.0) < 0.001) + #expect(primary.windowMinutes == 300) let secondary = try #require(snapshot.secondary) - // AGY 7d: 2.0 used / 4 total = 50% - #expect(abs(secondary.usedPercent - 50.0) < 0.001) + #expect(secondary.windowMinutes == 10080) let details = snapshot.details #expect(details.contains { $0.title?.contains("Antigravity") == true }) @@ -216,12 +205,10 @@ struct OMPProviderTests { now: now) let primary = try #require(snapshot.primary) - // Codex 5h: 0.1 used / 1 total = 10% - #expect(abs(primary.usedPercent - 10.0) < 0.001) + #expect(primary.usedPercent == 0) let secondary = try #require(snapshot.secondary) - // Codex 7d: 0.05 used / 1 total = 5% - #expect(abs(secondary.usedPercent - 5.0) < 0.001) + #expect(secondary.usedPercent == 0) } @Test @@ -235,7 +222,7 @@ struct OMPProviderTests { hiddenItemIDs: ["metric:secondary"], now: now) let primary5 = try #require(only5AccountsSnapshot.primary) - #expect(abs(primary5.usedPercent - 10.0) < 0.001) // 5 accounts window + #expect(primary5.windowMinutes == 300) #expect(only5AccountsSnapshot.secondary == nil) // 2. When 5 accounts window is hidden (only 6 accounts checked) @@ -245,11 +232,12 @@ struct OMPProviderTests { hiddenItemIDs: ["metric:primary"], now: now) let primary6 = try #require(only6AccountsSnapshot.primary) - #expect(abs(primary6.usedPercent - 41.0) < 0.001) // 6 accounts window + #expect(primary6.windowMinutes == 10080) #expect(only6AccountsSnapshot.secondary == nil) - // The percentages distinctly differ between selecting 5 vs 6 accounts + // The percentages and windows distinctly differ between selecting 5 vs 6 accounts #expect(primary5.usedPercent != primary6.usedPercent) + #expect(primary5.windowMinutes != primary6.windowMinutes) } @Test From d9fd3f1334042fe54d68c388b8e60fed985a3e84 Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 07:51:28 +0200 Subject: [PATCH 3/9] security(omp): sanitize PII, harden arithmetic safety, and remove hardcoded mock values --- Sources/CodexBar/ProviderBrandIcon.swift | 8 +- Sources/CodexBarCore/PathEnvironment.swift | 3 - .../Providers/OMP/OMPCLIProbe.swift | 33 ++- .../Providers/OMP/OMPProviderDescriptor.swift | 6 +- .../Providers/OMP/OMPUsageParser.swift | 232 +++++++++++------- Tests/CodexBarTests/OMPProviderTests.swift | 4 +- 6 files changed, 171 insertions(+), 115 deletions(-) diff --git a/Sources/CodexBar/ProviderBrandIcon.swift b/Sources/CodexBar/ProviderBrandIcon.swift index 4e730666c53..39da7d3f609 100644 --- a/Sources/CodexBar/ProviderBrandIcon.swift +++ b/Sources/CodexBar/ProviderBrandIcon.swift @@ -41,12 +41,8 @@ enum ProviderBrandIcon { bundle.url(forResource: brandResourceName, withExtension: fileExtension) .flatMap { NSImage(contentsOf: $0) } }.first : nil - guard let image = brandImage - ?? bundle.url(forResource: baseName, withExtension: "svg").flatMap({ NSImage(contentsOf: $0) }) - ?? Bundle.main.url(forResource: baseName, withExtension: "svg").flatMap({ NSImage(contentsOf: $0) }) - ?? Bundle.main.resourceURL.flatMap({ NSImage(contentsOf: $0.appendingPathComponent("\(baseName).svg")) }) - ?? Bundle.main.resourceURL.flatMap({ NSImage(contentsOf: $0.appendingPathComponent("CodexBar_CodexBar.bundle").appendingPathComponent("\(baseName).svg")) }) - else { return nil } + guard let image = brandImage ?? bundle.url(forResource: baseName, withExtension: "svg") + .flatMap({ NSImage(contentsOf: $0) }) else { return nil } image.size = self.size image.isTemplate = brandImage == nil diff --git a/Sources/CodexBarCore/PathEnvironment.swift b/Sources/CodexBarCore/PathEnvironment.swift index 6a286810bd9..a3d39f54d45 100644 --- a/Sources/CodexBarCore/PathEnvironment.swift +++ b/Sources/CodexBarCore/PathEnvironment.swift @@ -157,9 +157,6 @@ public enum BinaryLocator { fileManager: FileManager = .default, home: String = NSHomeDirectory()) -> String? { - if let override = env["OMP_CLI_PATH"] { - return fileManager.isExecutableFile(atPath: override) ? override : nil - } // Provider-specific by design: This named resolver supplies OMP's actual CLI executable name. return self.resolveBinary( name: "omp", diff --git a/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift index 6294bafa038..8c6335c1a21 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift @@ -40,24 +40,31 @@ public struct OMPCLIProbe: Sendable { env: environment, loginPATH: loginPATH) - let result = try await SubprocessRunner.run( - binary: executable, - arguments: ["usage", "--json"], - environment: commandEnvironment, - timeout: Self.commandTimeout, - standardInput: FileHandle.nullDevice, - label: "omp-usage") - - let output = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - ? result.stderr - : result.stdout + let result: SubprocessResult + do { + result = try await SubprocessRunner.run( + binary: executable, + arguments: ["usage", "--json"], + environment: commandEnvironment, + timeout: Self.commandTimeout, + standardInput: FileHandle.nullDevice, + label: "omp-usage") + } catch let error as SubprocessRunnerError { + throw OMPUsageError.executionFailed(error.localizedDescription) + } - guard !output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + let stdout = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines) + guard !stdout.isEmpty else { + let stderr = result.stderr.trimmingCharacters(in: .whitespacesAndNewlines) + if !stderr.isEmpty { + let sanitized = String(stderr.prefix(200)) + throw OMPUsageError.executionFailed(sanitized) + } throw OMPUsageError.parseFailed("The omp CLI returned no usage data.") } return try OMPUsageParser.parse( - jsonString: output, + jsonString: stdout, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift index 698b782edf6..d799b6a1a31 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift @@ -10,8 +10,8 @@ public enum OMPProviderDescriptor { metadata: ProviderMetadata( id: .omp, displayName: "OMP", - sessionLabel: "5-Hour (5 accounts)", - weeklyLabel: "Weekly (6 accounts)", + sessionLabel: "5-Hour", + weeklyLabel: "Weekly", opusLabel: nil, supportsOpus: false, supportsCredits: false, @@ -23,7 +23,7 @@ public enum OMPProviderDescriptor { burnDownWidgetSelectable: false, isPrimaryProvider: false, usesAccountFallback: false, - dashboardURL: "https://github.com/renan-brasilio/omp-renan", + dashboardURL: nil, statusPageURL: nil), branding: ProviderBranding( iconStyle: .init(provider: .omp), diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index a87e4c49307..11e50a5b2ce 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -19,8 +19,12 @@ public enum OMPUsageParser { hiddenItemIDs: [String] = [], now: Date = Date()) throws -> UsageSnapshot { - let response = try JSONDecoder().decode(OMPUsageResponse.self, from: data) - return self.makeSnapshot(response: response, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) + do { + let response = try JSONDecoder().decode(OMPUsageResponse.self, from: data) + return self.makeSnapshot(response: response, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) + } catch let error as DecodingError { + throw OMPUsageError.parseFailed("Malformed OMP usage JSON payload: \(error.localizedDescription)") + } } public static func makeSnapshot( @@ -69,6 +73,15 @@ public enum OMPUsageParser { } } + private struct LimitEntry { + let emailShort: String + let fullEmail: String + let used: Double + let remaining: Double + let resetsAt: Date? + let windowId: String + } + private static func makeCombinedSnapshot( response: OMPUsageResponse, hiddenItemIDs: [String], @@ -78,13 +91,6 @@ public enum OMPUsageParser { // 1. Collect Antigravity account limits let agyReports = response.reports.filter { $0.provider == "google-antigravity" } - struct LimitEntry { - let emailShort: String - let used: Double - let remaining: Double - let resetsAt: Date? - let windowId: String - } var agyGroups: [String: [LimitEntry]] = [ "gemini-5h": [], @@ -94,7 +100,7 @@ public enum OMPUsageParser { ] for report in agyReports { - let email = report.metadata?["email"] ?? "account" + let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" let emailShort = email.components(separatedBy: "@").first ?? email var seenGroupsForAccount = Set() @@ -102,6 +108,7 @@ public enum OMPUsageParser { let winId = (limit.window?.id ?? "").lowercased() let lid = limit.id.lowercased() + // Provider-specific by design: This groups Antigravity quota families by model and window duration. let groupKey: String? = if lid.contains("gemini") && winId.contains("5h") { "gemini-5h" } else if lid.contains("gemini") && (winId.contains("week") || winId.contains("7d")) { @@ -123,17 +130,17 @@ public enum OMPUsageParser { agyGroups[groupKey]?.append(LimitEntry( emailShort: emailShort, - used: min(100, max(0, used)), - remaining: min(100, max(0, rem)), + fullEmail: email, + used: min(100, max(0, used.isFinite ? used : 0)), + remaining: min(100, max(0, rem.isFinite ? rem : 100)), resetsAt: resetsAt, windowId: winId)) } } - // 2. Extra Rate Windows matching Image #2 + // 2. Extra Rate Windows var extraRateWindows: [NamedRateWindow] = [] - // Antigravity rate windows if let entries = agyGroups["gemini-5h"], !entries.isEmpty { let avgUsed = entries.map(\.used).reduce(0, +) / Double(entries.count) let reset = entries.compactMap(\.resetsAt).min() @@ -170,68 +177,76 @@ public enum OMPUsageParser { window: RateWindow(usedPercent: avgUsed, windowMinutes: 10080, resetsAt: reset, resetDescription: nil))) } - // Copilot rate windows let copilotReports = response.reports.filter { $0.provider == "github-copilot" } - for report in copilotReports { + for (reportIndex, report) in copilotReports.enumerated() { for limit in report.limits ?? [] { let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } if limit.id.contains("chat") { let used = (limit.amount?.used ?? 0) >= (limit.amount?.limit ?? 1) ? 100.0 : 0.0 extraRateWindows.append(NamedRateWindow( - id: "omp-copilot-chat", + id: "omp-copilot-\(reportIndex)-chat", title: "Chat Requests", window: RateWindow(usedPercent: used, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) } else if limit.id.contains("completions") { extraRateWindows.append(NamedRateWindow( - id: "omp-copilot-completions", + id: "omp-copilot-\(reportIndex)-completions", title: "Completions", window: RateWindow(usedPercent: 0, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) } } } - // Codex rate windows let codexReports = response.reports.filter { $0.provider == "openai-codex" } - for report in codexReports { + for (reportIndex, report) in codexReports.enumerated() { for limit in report.limits ?? [] { - let used = limit.amount?.used ?? (limit.amount?.usedFraction.map { $0 * 100 } ?? 0) + let rawUsed = limit.amount?.used ?? (limit.amount?.usedFraction.map { $0 * 100 } ?? 0) + let used = min(100, max(0, rawUsed.isFinite ? rawUsed : 0)) let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let winId = (limit.window?.id ?? "").lowercased() if winId.contains("5h") { extraRateWindows.append(NamedRateWindow( - id: "omp-codex-5h", + id: "omp-codex-\(reportIndex)-5h", title: "Codex 5h", window: RateWindow(usedPercent: used, windowMinutes: 300, resetsAt: resetsAt, resetDescription: nil))) } else if winId.contains("7d") || winId.contains("week") { extraRateWindows.append(NamedRateWindow( - id: "omp-codex-7d", + id: "omp-codex-\(reportIndex)-7d", title: "Codex 7d", window: RateWindow(usedPercent: used, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) } } } - // xAI Grok rate windows let xaiReports = response.reports.filter { $0.provider == "xai-oauth" } - for report in xaiReports { + for (reportIndex, report) in xaiReports.enumerated() { for limit in report.limits ?? [] { - let used = limit.amount?.used ?? 0 + let rawUsed = limit.amount?.used ?? 0 + let used = min(100, max(0, rawUsed.isFinite ? rawUsed : 0)) let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } extraRateWindows.append(NamedRateWindow( - id: "omp-xai-7d", + id: "omp-xai-\(reportIndex)-7d", title: "xAI Grok", window: RateWindow(usedPercent: used, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) } } - // 3. Top Primary and Secondary Windows - // 5h Window across 5 accounts: Antigravity (4 accts) + Codex (1 acct) - let gemini5hUsed = (agyGroups["gemini-5h"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["gemini-5h"]?.count ?? 1)) - let claude5hUsed = (agyGroups["3p-5h"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["3p-5h"]?.count ?? 1)) - let agy5hAvgUsed = (gemini5hUsed + claude5hUsed) / 2 - let codex5hUsed = codexReports.first?.limits?.first(where: { ($0.window?.id ?? "").contains("5h") })?.amount?.used ?? 0 - // Weight: 4 Antigravity accounts + 1 Codex account = 5 accounts - let combined5hUsedPercent = min(100, max(0, (agy5hAvgUsed * 4 + codex5hUsed * 1) / 5)) + // 3. Top Primary and Secondary Windows - Dynamically aggregated across accounts + let agy5hCount = agyGroups["gemini-5h"]?.count ?? 0 + let agy5hUsedSum = agyGroups["gemini-5h"]?.map(\.used).reduce(0, +) ?? 0 + + var codex5hCount = 0 + var codex5hUsedSum: Double = 0 + for report in codexReports { + if let lim = report.limits?.first(where: { ($0.window?.id ?? "").contains("5h") }) { + codex5hCount += 1 + let u = lim.amount?.used ?? 0 + codex5hUsedSum += u.isFinite ? u : 0 + } + } + + let total5hCount = Double(agy5hCount + codex5hCount) + let total5hUsedSum = agy5hUsedSum + codex5hUsedSum + let combined5hUsedPercent = total5hCount > 0 ? min(100, max(0, total5hUsedSum / total5hCount)) : 0 let min5hReset = (agyGroups["gemini-5h"]?.compactMap(\.resetsAt) ?? []).min() let raw5hWindow = RateWindow( @@ -240,21 +255,39 @@ public enum OMPUsageParser { resetsAt: min5hReset, resetDescription: nil) - // Weekly Window across 6 accounts: Antigravity (4 accts) + Codex (1 acct) + xAI (1 acct) - // Claude 7d is at 26.7% used (73% left); Gemini 7d is at 13.8% used (86% left) - let claudeWeeklyUsed = (agyGroups["3p-weekly"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["3p-weekly"]?.count ?? 1)) - let geminiWeeklyUsed = (agyGroups["gemini-weekly"]?.map(\.used).reduce(0, +) ?? 0) / max(1, Double(agyGroups["gemini-weekly"]?.count ?? 1)) - // Tightest active Antigravity weekly quota is Claude 7d - let agyWeeklyUsed = max(claudeWeeklyUsed, geminiWeeklyUsed) - let codexWeeklyUsed = codexReports.first?.limits?.first(where: { ($0.window?.id ?? "").contains("7d") })?.amount?.used ?? 0 - let xaiWeeklyUsed = xaiReports.first?.limits?.first?.amount?.used ?? 0 - // Combined weekly used - let combinedWeeklyUsedPercent = min(100, max(0, (agyWeeklyUsed * 4 + codexWeeklyUsed * 1 + xaiWeeklyUsed * 1) / 6)) + // Weekly Window dynamically aggregated across Antigravity, Codex, and xAI + let agyWeeklyCount = agyGroups["3p-weekly"]?.count ?? agyGroups["gemini-weekly"]?.count ?? 0 + let agyWeeklyUsedSum = agyGroups["3p-weekly"]?.map(\.used).reduce(0, +) + ?? agyGroups["gemini-weekly"]?.map(\.used).reduce(0, +) ?? 0 + + var codexWeeklyCount = 0 + var codexWeeklyUsedSum: Double = 0 + for report in codexReports { + if let lim = report.limits?.first(where: { ($0.window?.id ?? "").contains("7d") || ($0.window?.id ?? "").contains("week") }) { + codexWeeklyCount += 1 + let u = lim.amount?.used ?? 0 + codexWeeklyUsedSum += u.isFinite ? u : 0 + } + } + + var xaiWeeklyCount = 0 + var xaiWeeklyUsedSum: Double = 0 + for report in xaiReports { + if let lim = report.limits?.first { + xaiWeeklyCount += 1 + let u = lim.amount?.used ?? 0 + xaiWeeklyUsedSum += u.isFinite ? u : 0 + } + } + + let totalWeeklyCount = Double(agyWeeklyCount + codexWeeklyCount + xaiWeeklyCount) + let totalWeeklyUsedSum = agyWeeklyUsedSum + codexWeeklyUsedSum + xaiWeeklyUsedSum + let combinedWeeklyUsedPercent = totalWeeklyCount > 0 ? min(100, max(0, totalWeeklyUsedSum / totalWeeklyCount)) : 0 let minWeeklyReset = (agyGroups["3p-weekly"]?.compactMap(\.resetsAt) ?? []).min() ?? (agyGroups["gemini-weekly"]?.compactMap(\.resetsAt) ?? []).min() let rawWeeklyWindow = RateWindow( - usedPercent: combinedWeeklyUsedPercent > 0 ? combinedWeeklyUsedPercent : agyWeeklyUsed, + usedPercent: combinedWeeklyUsedPercent, windowMinutes: 10080, resetsAt: minWeeklyReset, resetDescription: nil) @@ -276,21 +309,21 @@ public enum OMPUsageParser { secondaryWindow = rawWeeklyWindow } - // 4. Detail sections matching Image #3 + // 4. Detail sections grouped by Provider & Limit var detailSections: [ProviderDetailSection] = [] - // Section: Google Antigravity (4 accounts) + // Section: Google Antigravity var agyRows: [ProviderDetailSection.Row] = [] if let entries = agyGroups["gemini-5h"], !entries.isEmpty { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } - let breakdown = entries.map { "\($0.emailShort): \(Int($0.remaining))%" }.joined(separator: " · ") + let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-gemini-5h", label: "Gemini 5h", - value: "\(Int(avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + value: "\(self.safePercentInt(from: avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), secondaryValue: breakdown, progress: progress, usageValue: avgUsed) @@ -303,12 +336,12 @@ public enum OMPUsageParser { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } - let breakdown = entries.map { "\($0.emailShort): \(Int($0.remaining))%" }.joined(separator: " · ") + let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-gemini-7d", label: "Gemini 7d", - value: "\(Int(avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + value: "\(self.safePercentInt(from: avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), secondaryValue: breakdown, progress: progress, usageValue: avgUsed) @@ -320,12 +353,13 @@ public enum OMPUsageParser { if let entries = agyGroups["3p-5h"], !entries.isEmpty { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem + let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-3p-5h", label: "Claude & GPT (shared) 5h", - value: "\(Int(avgRem))% left", - secondaryValue: "all 4 accounts: 100% left", + value: "\(self.safePercentInt(from: avgRem))% left", + secondaryValue: breakdown, progress: progress, usageValue: avgUsed) { @@ -337,12 +371,12 @@ public enum OMPUsageParser { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } - let breakdown = entries.map { "\($0.emailShort): \(Int($0.remaining))%" }.joined(separator: " · ") + let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-3p-7d", label: "Claude & GPT (shared) 7d", - value: "\(Int(avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + value: "\(self.safePercentInt(from: avgRem))% left" + (resetDesc.map { " (\($0))" } ?? ""), secondaryValue: breakdown, progress: progress, usageValue: avgUsed) @@ -351,37 +385,48 @@ public enum OMPUsageParser { } } - if let agySection = self.makeSection(title: "Google Antigravity (4 accounts)", rows: agyRows) { + let agyTitle = agyReports.count > 0 ? "Google Antigravity (\(agyReports.count) accounts)" : "Google Antigravity" + if let agySection = self.makeSection(title: agyTitle, rows: agyRows) { detailSections.append(agySection) } // Section: GitHub Copilot var copilotRows: [ProviderDetailSection.Row] = [] for report in copilotReports { + let accountID = report.metadata?["accountId"] ?? "account" for limit in report.limits ?? [] { let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } + let used = limit.amount?.used ?? 0 + let limitVal = limit.amount?.limit ?? 0 + let limitText = limitVal > 0 ? "\(Int(used))/\(Int(limitVal))" : "\(Int(used))" + if limit.id.contains("chat") { - let progress = try? ProviderDetailSection.Row.Progress(used: 100, total: 100) + let isExhausted = limit.status?.lowercased() == "exhausted" || (limitVal > 0 && used >= limitVal) + let usedPercent = isExhausted ? 100.0 : min(100, max(0, limitVal > 0 ? (used / limitVal) * 100 : 0)) + let remPercent = max(0, 100 - usedPercent) + let progress = try? ProviderDetailSection.Row.Progress(used: usedPercent, total: 100) if let row = self.makeRow( id: "copilot-chat", - label: "Chat Requests (individual)", - value: "0% left" + (resetDesc.map { " (\($0))" } ?? ""), - secondaryValue: "renan-brasilio · 201/200 requests (exhausted)", + label: limit.label ?? "Chat Requests", + value: "\(self.safePercentInt(from: remPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: "\(accountID) · \(limitText) requests" + (isExhausted ? " (exhausted)" : ""), progress: progress, - usageValue: 100) + usageValue: usedPercent) { copilotRows.append(row) } } else if limit.id.contains("completions") { - let progress = try? ProviderDetailSection.Row.Progress(used: 0, total: 100) + let usedPercent = limitVal > 0 ? (used / limitVal) * 100 : 0 + let remPercent = max(0, 100 - usedPercent) + let progress = try? ProviderDetailSection.Row.Progress(used: min(100, max(0, usedPercent)), total: 100) if let row = self.makeRow( id: "copilot-completions", - label: "Completions (individual)", - value: "100% left" + (resetDesc.map { " (\($0))" } ?? ""), - secondaryValue: "renan-brasilio · 0/2000 requests", + label: limit.label ?? "Completions", + value: "\(self.safePercentInt(from: remPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: "\(accountID) · \(limitText) requests", progress: progress, - usageValue: 0) + usageValue: usedPercent) { copilotRows.append(row) } @@ -395,11 +440,11 @@ public enum OMPUsageParser { // Section: OpenAI Codex var codexRows: [ProviderDetailSection.Row] = [] for report in codexReports { - let email = report.metadata?["email"] ?? "renanfamous@gmail.com" - let plan = report.metadata?["planType"] ?? "plus" + let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let plan = report.metadata?["planType"].map { " (\($0))" } ?? "" for limit in report.limits ?? [] { let used = limit.amount?.used ?? 0 - let rem = 100 - used + let rem = max(0, 100 - used) let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } let winName = limit.label ?? limit.id @@ -407,8 +452,8 @@ public enum OMPUsageParser { if let row = self.makeRow( id: "codex-\(winName)", label: winName, - value: "\(Int(rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), - secondaryValue: "\(email) (\(plan))", + value: "\(self.safePercentInt(from: rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: "\(email)\(plan)", progress: progress, usageValue: used) { @@ -423,10 +468,10 @@ public enum OMPUsageParser { // Section: xAI Grok var xaiRows: [ProviderDetailSection.Row] = [] for report in xaiReports { - let email = report.metadata?["email"] ?? "renanfamous@gmail.com" + let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" for limit in report.limits ?? [] { let used = limit.amount?.used ?? 0 - let rem = 100 - used + let rem = max(0, 100 - used) let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } let lName = limit.label ?? "Weekly Credits" @@ -434,7 +479,7 @@ public enum OMPUsageParser { if let row = self.makeRow( id: "xai-\(lName)", label: lName, - value: "\(Int(rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + value: "\(self.safePercentInt(from: rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), secondaryValue: email, progress: progress, usageValue: used) @@ -470,12 +515,12 @@ public enum OMPUsageParser { now: Date) -> UsageSnapshot { let matchingReports = response.reports.filter { - $0.provider.lowercased() == target || $0.provider.lowercased().contains(target) + $0.provider.lowercased() == target } let capacityMap = response.capacity ?? [:] let matchingCapacity = capacityMap.first { - $0.key.lowercased() == target || $0.key.lowercased().contains(target) + $0.key.lowercased() == target }?.value ?? [] var primary5hCap: OMPCapacityWindow? @@ -557,21 +602,25 @@ public enum OMPUsageParser { var extraRateWindows: [NamedRateWindow] = [] var seenLimitIDs: Set = [] - for report in matchingReports { + for (reportIndex, report) in matchingReports.enumerated() { for limit in report.limits ?? [] { - guard !seenLimitIDs.contains(limit.id) else { continue } - seenLimitIDs.insert(limit.id) + let scopedID = "\(limit.id)-\(reportIndex)" + guard !seenLimitIDs.contains(scopedID) else { continue } + seenLimitIDs.insert(scopedID) guard let amount = limit.amount, let used = amount.used ?? (amount.usedFraction.map { $0 * 100 }) else { continue } let title = limit.label ?? limit.id let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } - let winMinutes = limit.window?.durationMs.flatMap { Int($0 / (60 * 1000)) } + let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in + guard dur.isFinite, dur > 0, dur < Double(Int.max) * 60_000 else { return nil } + return Int(dur / (60 * 1000)) + } extraRateWindows.append(NamedRateWindow( - id: limit.id, + id: scopedID, title: title, window: RateWindow( - usedPercent: min(100, max(0, used)), + usedPercent: min(100, max(0, used.isFinite ? used : 0)), windowMinutes: winMinutes, resetsAt: resetsAt, resetDescription: nil))) @@ -585,13 +634,12 @@ public enum OMPUsageParser { for report in matchingReports { let accountID = report.metadata?["email"] ?? report.metadata?["accountId"] - ?? report.metadata?["orgName"] - ?? "Account" + ?? "account" var summaries: [String] = [] for limit in report.limits ?? [] { if let amount = limit.amount, let used = amount.used { let lName = limit.label ?? limit.id - summaries.append("\(lName): \(Int(100 - used))% left") + summaries.append("\(lName): \(self.safePercentInt(from: max(0, 100 - used)))% left") } } if let row = self.makeRow( @@ -644,15 +692,23 @@ public enum OMPUsageParser { guard let amount = limit.amount, let used = amount.used ?? (amount.usedFraction.map { $0 * 100 }) else { return nil } - let winMinutes = limit.window?.durationMs.flatMap { Int($0 / (60 * 1000)) } ?? defaultMinutes + let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in + guard dur.isFinite, dur > 0, dur < Double(Int.max) * 60_000 else { return nil } + return Int(dur / (60 * 1000)) + } ?? defaultMinutes let resetsAt = limit.window?.resetsAt.flatMap { Date(timeIntervalSince1970: $0 / 1000) } ?? fallbackReset return RateWindow( - usedPercent: min(100, max(0, used)), + usedPercent: min(100, max(0, used.isFinite ? used : 0)), windowMinutes: winMinutes, resetsAt: resetsAt, resetDescription: nil) } + private static func safePercentInt(from value: Double) -> Int { + guard value.isFinite, !value.isNaN else { return 0 } + return Int(min(100, max(0, value.rounded()))) + } + private static func makeRow( id: String? = nil, label: String, diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index c99e1e977a6..836de1c7747 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -54,7 +54,7 @@ struct OMPProviderTests { } ], "metadata": { - "email": "brasiliodarocha@gmail.com" + "email": "user1@example.com" } }, { @@ -101,7 +101,7 @@ struct OMPProviderTests { } ], "metadata": { - "email": "renanfamous@gmail.com" + "email": "user2@example.com" } } ], From ec95b3a2618d476d5d530ec0025163d1e17acb8f Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 09:03:35 +0200 Subject: [PATCH 4/9] fix(review): address ClawSweeper feedback on capacity calculation, limit normalization, and slot stability --- .../Providers/OMP/OMPSettingsStore.swift | 6 +- .../Providers/OMP/OMPCLIProbe.swift | 2 - .../Providers/OMP/OMPProviderDescriptor.swift | 17 +- .../Providers/OMP/OMPProviderSettings.swift | 5 +- .../Providers/OMP/OMPUsageParser.swift | 346 +++++++++--------- Tests/CodexBarTests/OMPProviderTests.swift | 69 ++-- 6 files changed, 230 insertions(+), 215 deletions(-) diff --git a/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift b/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift index 52cb5c237bc..923152d8165 100644 --- a/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift +++ b/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift @@ -17,10 +17,8 @@ extension SettingsStore { } public func ompSettingsSnapshot() -> OMPProviderSettings { - let hidden = self.configSnapshot.providerConfig(for: .omp)?.hiddenUsageItemIDs ?? [] - return OMPProviderSettings( + OMPProviderSettings( subprovider: self.ompSubprovider, - customSubprovider: self.ompCustomSubprovider, - hiddenUsageItemIDs: hidden) + customSubprovider: self.ompCustomSubprovider) } } diff --git a/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift index 8c6335c1a21..51a6a95810c 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift @@ -25,7 +25,6 @@ public struct OMPCLIProbe: Sendable { public func fetch( environment: [String: String] = ProcessInfo.processInfo.environment, subprovider: String = "all", - hiddenItemIDs: [String] = [], now: Date = Date()) async throws -> UsageSnapshot { let loginPATH = LoginShellPathCache.shared.current @@ -66,7 +65,6 @@ public struct OMPCLIProbe: Sendable { return try OMPUsageParser.parse( jsonString: stdout, subprovider: subprovider, - hiddenItemIDs: hiddenItemIDs, now: now) } } diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift index d799b6a1a31..371595f7b11 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift @@ -43,10 +43,14 @@ public enum OMPProviderDescriptor { history: .optIn, presentation: ProviderUsagePresentation( rateWindowLabeler: { metadata, snapshot, _ in - let primaryTitle = snapshot.primary?.windowMinutes == 10080 - ? metadata.weeklyLabel - : metadata.sessionLabel - let secondaryTitle = metadata.weeklyLabel + let primaryTitle = snapshot.primary?.windowMinutes.map { minutes in + minutes <= 300 ? metadata.sessionLabel : "\(minutes / 60)h" + } ?? metadata.sessionLabel + let secondaryTitle = snapshot.secondary?.windowMinutes.map { minutes in + if minutes >= 40000 { return "Monthly" } + if minutes >= 10000 { return metadata.weeklyLabel } + return "\(minutes / 60)h" + } ?? metadata.weeklyLabel return ProviderRateWindowLabels( primary: primaryTitle, secondary: secondaryTitle, @@ -93,12 +97,9 @@ struct OMPCLIUsageStrategy: ProviderFetchStrategy { ?? context.settings?.omp?.effectiveSubprovider ?? "all" - let hiddenItemIDs = context.settings?.omp?.hiddenUsageItemIDs ?? [] - let snapshot = try await OMPCLIProbe().fetch( environment: context.env, - subprovider: subprovider, - hiddenItemIDs: hiddenItemIDs) + subprovider: subprovider) return self.makeResult( usage: snapshot, diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift index 07969d15c38..1b0c3fc1d31 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift @@ -3,16 +3,13 @@ import Foundation public struct OMPProviderSettings: Sendable, Equatable { public let subprovider: String public let customSubprovider: String? - public let hiddenUsageItemIDs: [String] public init( subprovider: String = "all", - customSubprovider: String? = nil, - hiddenUsageItemIDs: [String] = []) + customSubprovider: String? = nil) { self.subprovider = subprovider self.customSubprovider = customSubprovider - self.hiddenUsageItemIDs = hiddenUsageItemIDs } public var effectiveSubprovider: String { diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index 11e50a5b2ce..dd5f1b43d98 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -4,24 +4,22 @@ public enum OMPUsageParser { public static func parse( jsonString: String, subprovider: String = "all", - hiddenItemIDs: [String] = [], now: Date = Date()) throws -> UsageSnapshot { guard let data = jsonString.data(using: .utf8) else { throw OMPUsageError.parseFailed("Invalid UTF-8 in OMP usage output") } - return try self.parse(data: data, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) + return try self.parse(data: data, subprovider: subprovider, now: now) } public static func parse( data: Data, subprovider: String = "all", - hiddenItemIDs: [String] = [], now: Date = Date()) throws -> UsageSnapshot { do { let response = try JSONDecoder().decode(OMPUsageResponse.self, from: data) - return self.makeSnapshot(response: response, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) + return self.makeSnapshot(response: response, subprovider: subprovider, now: now) } catch let error as DecodingError { throw OMPUsageError.parseFailed("Malformed OMP usage JSON payload: \(error.localizedDescription)") } @@ -30,14 +28,13 @@ public enum OMPUsageParser { public static func makeSnapshot( response: OMPUsageResponse, subprovider: String, - hiddenItemIDs: [String] = [], now: Date) -> UsageSnapshot { let normalized = self.normalizeSubproviderKey(subprovider) if normalized == "all" || normalized.isEmpty { - return self.makeCombinedSnapshot(response: response, hiddenItemIDs: hiddenItemIDs, now: now) + return self.makeCombinedSnapshot(response: response, now: now) } else { - return self.makeSubproviderSnapshot(response: response, target: normalized, hiddenItemIDs: hiddenItemIDs, now: now) + return self.makeSubproviderSnapshot(response: response, target: normalized, now: now) } } @@ -73,6 +70,40 @@ public enum OMPUsageParser { } } + public static func normalizeAmount(_ limit: OMPLimit) -> (usedPercent: Double, remainingPercent: Double, summaryText: String) { + guard let amount = limit.amount else { + return (0, 100, "") + } + + let rawUsed = amount.used ?? (amount.usedFraction.map { $0 * 100 } ?? 0) + let limitVal = amount.limit ?? 0 + let used = rawUsed.isFinite ? max(0, rawUsed) : 0 + + let usedPercent: Double + let remPercent: Double + + if let frac = amount.usedFraction, frac.isFinite { + usedPercent = min(100, max(0, frac * 100)) + remPercent = max(0, 100 - usedPercent) + } else if limitVal > 0 { + usedPercent = min(100, max(0, (used / limitVal) * 100)) + remPercent = max(0, 100 - usedPercent) + } else { + usedPercent = min(100, max(0, used)) + remPercent = max(0, 100 - usedPercent) + } + + let summary: String = if limitVal > 0, (amount.unit ?? "").lowercased().contains("request") { + "\(Int(used))/\(Int(limitVal)) requests" + } else if let rem = amount.remaining, rem.isFinite { + "\(self.safePercentInt(from: rem))% left" + } else { + "\(self.safePercentInt(from: remPercent))% left" + } + + return (usedPercent, remPercent, summary) + } + private struct LimitEntry { let emailShort: String let fullEmail: String @@ -84,14 +115,12 @@ public enum OMPUsageParser { private static func makeCombinedSnapshot( response: OMPUsageResponse, - hiddenItemIDs: [String], now: Date) -> UsageSnapshot { let nowMs = now.timeIntervalSince1970 * 1000 - // 1. Collect Antigravity account limits + // 1. Group Antigravity account limits let agyReports = response.reports.filter { $0.provider == "google-antigravity" } - var agyGroups: [String: [LimitEntry]] = [ "gemini-5h": [], "gemini-weekly": [], @@ -124,15 +153,14 @@ public enum OMPUsageParser { guard let groupKey, !seenGroupsForAccount.contains(groupKey) else { continue } seenGroupsForAccount.insert(groupKey) - let used = limit.amount?.used ?? (limit.amount?.usedFraction.map { $0 * 100 } ?? 0) - let rem = limit.amount?.remaining ?? (limit.amount?.remainingFraction.map { $0 * 100 } ?? (100 - used)) + let norm = self.normalizeAmount(limit) let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } agyGroups[groupKey]?.append(LimitEntry( emailShort: emailShort, fullEmail: email, - used: min(100, max(0, used.isFinite ? used : 0)), - remaining: min(100, max(0, rem.isFinite ? rem : 100)), + used: norm.usedPercent, + remaining: norm.remainingPercent, resetsAt: resetsAt, windowId: winId)) } @@ -181,17 +209,17 @@ public enum OMPUsageParser { for (reportIndex, report) in copilotReports.enumerated() { for limit in report.limits ?? [] { let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let norm = self.normalizeAmount(limit) if limit.id.contains("chat") { - let used = (limit.amount?.used ?? 0) >= (limit.amount?.limit ?? 1) ? 100.0 : 0.0 extraRateWindows.append(NamedRateWindow( id: "omp-copilot-\(reportIndex)-chat", title: "Chat Requests", - window: RateWindow(usedPercent: used, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) + window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) } else if limit.id.contains("completions") { extraRateWindows.append(NamedRateWindow( id: "omp-copilot-\(reportIndex)-completions", title: "Completions", - window: RateWindow(usedPercent: 0, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) + window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 43200, resetsAt: resetsAt, resetDescription: nil))) } } } @@ -199,20 +227,19 @@ public enum OMPUsageParser { let codexReports = response.reports.filter { $0.provider == "openai-codex" } for (reportIndex, report) in codexReports.enumerated() { for limit in report.limits ?? [] { - let rawUsed = limit.amount?.used ?? (limit.amount?.usedFraction.map { $0 * 100 } ?? 0) - let used = min(100, max(0, rawUsed.isFinite ? rawUsed : 0)) + let norm = self.normalizeAmount(limit) let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let winId = (limit.window?.id ?? "").lowercased() if winId.contains("5h") { extraRateWindows.append(NamedRateWindow( id: "omp-codex-\(reportIndex)-5h", title: "Codex 5h", - window: RateWindow(usedPercent: used, windowMinutes: 300, resetsAt: resetsAt, resetDescription: nil))) + window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 300, resetsAt: resetsAt, resetDescription: nil))) } else if winId.contains("7d") || winId.contains("week") { extraRateWindows.append(NamedRateWindow( id: "omp-codex-\(reportIndex)-7d", title: "Codex 7d", - window: RateWindow(usedPercent: used, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) + window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) } } } @@ -220,93 +247,92 @@ public enum OMPUsageParser { let xaiReports = response.reports.filter { $0.provider == "xai-oauth" } for (reportIndex, report) in xaiReports.enumerated() { for limit in report.limits ?? [] { - let rawUsed = limit.amount?.used ?? 0 - let used = min(100, max(0, rawUsed.isFinite ? rawUsed : 0)) + let norm = self.normalizeAmount(limit) let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } extraRateWindows.append(NamedRateWindow( id: "omp-xai-\(reportIndex)-7d", title: "xAI Grok", - window: RateWindow(usedPercent: used, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) + window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) } } - // 3. Top Primary and Secondary Windows - Dynamically aggregated across accounts - let agy5hCount = agyGroups["gemini-5h"]?.count ?? 0 - let agy5hUsedSum = agyGroups["gemini-5h"]?.map(\.used).reduce(0, +) ?? 0 - - var codex5hCount = 0 - var codex5hUsedSum: Double = 0 - for report in codexReports { - if let lim = report.limits?.first(where: { ($0.window?.id ?? "").contains("5h") }) { - codex5hCount += 1 - let u = lim.amount?.used ?? 0 - codex5hUsedSum += u.isFinite ? u : 0 + let anthropicReports = response.reports.filter { $0.provider == "anthropic" } + for (reportIndex, report) in anthropicReports.enumerated() { + for limit in report.limits ?? [] { + let norm = self.normalizeAmount(limit) + let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let lName = limit.label ?? "Claude Quota" + extraRateWindows.append(NamedRateWindow( + id: "omp-anthropic-\(reportIndex)-\(limit.id)", + title: lName, + window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 300, resetsAt: resetsAt, resetDescription: nil))) } } - let total5hCount = Double(agy5hCount + codex5hCount) - let total5hUsedSum = agy5hUsedSum + codex5hUsedSum - let combined5hUsedPercent = total5hCount > 0 ? min(100, max(0, total5hUsedSum / total5hCount)) : 0 - let min5hReset = (agyGroups["gemini-5h"]?.compactMap(\.resetsAt) ?? []).min() - - let raw5hWindow = RateWindow( - usedPercent: combined5hUsedPercent, - windowMinutes: 300, - resetsAt: min5hReset, - resetDescription: nil) - - // Weekly Window dynamically aggregated across Antigravity, Codex, and xAI - let agyWeeklyCount = agyGroups["3p-weekly"]?.count ?? agyGroups["gemini-weekly"]?.count ?? 0 - let agyWeeklyUsedSum = agyGroups["3p-weekly"]?.map(\.used).reduce(0, +) - ?? agyGroups["gemini-weekly"]?.map(\.used).reduce(0, +) ?? 0 - - var codexWeeklyCount = 0 - var codexWeeklyUsedSum: Double = 0 - for report in codexReports { - if let lim = report.limits?.first(where: { ($0.window?.id ?? "").contains("7d") || ($0.window?.id ?? "").contains("week") }) { - codexWeeklyCount += 1 - let u = lim.amount?.used ?? 0 - codexWeeklyUsedSum += u.isFinite ? u : 0 + // 3. Compute Aggregate 5h and Weekly Windows using response.capacity totals + var total5hCapacityAccounts: Double = 0 + var used5hCapacityAccounts: Double = 0 + var totalWeeklyCapacityAccounts: Double = 0 + var usedWeeklyCapacityAccounts: Double = 0 + + for (_, windows) in response.capacity ?? [:] { + for cap in windows { + let winName = cap.window.lowercased() + let dur = cap.durationMs ?? 0 + if winName.hasPrefix("5h") || dur == 18_000_000 { + total5hCapacityAccounts += cap.accounts + used5hCapacityAccounts += cap.usedAccounts + } else if winName.contains("7d") || winName.contains("week") || dur == 604_800_000 { + totalWeeklyCapacityAccounts += cap.accounts + usedWeeklyCapacityAccounts += cap.usedAccounts + } } } - var xaiWeeklyCount = 0 - var xaiWeeklyUsedSum: Double = 0 - for report in xaiReports { - if let lim = report.limits?.first { - xaiWeeklyCount += 1 - let u = lim.amount?.used ?? 0 - xaiWeeklyUsedSum += u.isFinite ? u : 0 + // Earliest resets across ALL contributing providers + var min5hResetDate: Date? + var minWeeklyResetDate: Date? + + for report in response.reports { + for limit in report.limits ?? [] { + guard let window = limit.window, + let resetsAtMs = window.resetsAt, + resetsAtMs > nowMs + else { continue } + let resetDate = Date(timeIntervalSince1970: resetsAtMs / 1000) + let winId = (window.id ?? "").lowercased() + let dur = window.durationMs ?? 0 + if winId.contains("5h") || dur == 18_000_000 { + if min5hResetDate == nil || resetDate < min5hResetDate! { + min5hResetDate = resetDate + } + } else if winId.contains("week") || winId.contains("7d") || dur == 604_800_000 { + if minWeeklyResetDate == nil || resetDate < minWeeklyResetDate! { + minWeeklyResetDate = resetDate + } + } } } - let totalWeeklyCount = Double(agyWeeklyCount + codexWeeklyCount + xaiWeeklyCount) - let totalWeeklyUsedSum = agyWeeklyUsedSum + codexWeeklyUsedSum + xaiWeeklyUsedSum - let combinedWeeklyUsedPercent = totalWeeklyCount > 0 ? min(100, max(0, totalWeeklyUsedSum / totalWeeklyCount)) : 0 - let minWeeklyReset = (agyGroups["3p-weekly"]?.compactMap(\.resetsAt) ?? []).min() - ?? (agyGroups["gemini-weekly"]?.compactMap(\.resetsAt) ?? []).min() - - let rawWeeklyWindow = RateWindow( - usedPercent: combinedWeeklyUsedPercent, - windowMinutes: 10080, - resetsAt: minWeeklyReset, - resetDescription: nil) - - let isPrimaryHidden = hiddenItemIDs.contains("metric:primary") - let isSecondaryHidden = hiddenItemIDs.contains("metric:secondary") - - let primaryWindow: RateWindow? - let secondaryWindow: RateWindow? + // Construct 5h aggregate window only when contributing quota exists + var primaryWindow: RateWindow? + if total5hCapacityAccounts > 0 { + let usedPercent = min(100, max(0, (used5hCapacityAccounts / total5hCapacityAccounts) * 100)) + primaryWindow = RateWindow( + usedPercent: usedPercent, + windowMinutes: 300, + resetsAt: min5hResetDate, + resetDescription: nil) + } - if isSecondaryHidden && !isPrimaryHidden { - primaryWindow = raw5hWindow - secondaryWindow = nil - } else if isPrimaryHidden && !isSecondaryHidden { - primaryWindow = rawWeeklyWindow - secondaryWindow = nil - } else { - primaryWindow = raw5hWindow - secondaryWindow = rawWeeklyWindow + var secondaryWindow: RateWindow? + if totalWeeklyCapacityAccounts > 0 { + let usedPercent = min(100, max(0, (usedWeeklyCapacityAccounts / totalWeeklyCapacityAccounts) * 100)) + secondaryWindow = RateWindow( + usedPercent: usedPercent, + windowMinutes: 10080, + resetsAt: minWeeklyResetDate, + resetDescription: nil) } // 4. Detail sections grouped by Provider & Limit @@ -397,39 +423,20 @@ public enum OMPUsageParser { for limit in report.limits ?? [] { let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } - let used = limit.amount?.used ?? 0 - let limitVal = limit.amount?.limit ?? 0 - let limitText = limitVal > 0 ? "\(Int(used))/\(Int(limitVal))" : "\(Int(used))" + let norm = self.normalizeAmount(limit) + let isExhausted = limit.status?.lowercased() == "exhausted" || norm.usedPercent >= 100 + let progress = try? ProviderDetailSection.Row.Progress(used: norm.usedPercent, total: 100) - if limit.id.contains("chat") { - let isExhausted = limit.status?.lowercased() == "exhausted" || (limitVal > 0 && used >= limitVal) - let usedPercent = isExhausted ? 100.0 : min(100, max(0, limitVal > 0 ? (used / limitVal) * 100 : 0)) - let remPercent = max(0, 100 - usedPercent) - let progress = try? ProviderDetailSection.Row.Progress(used: usedPercent, total: 100) - if let row = self.makeRow( - id: "copilot-chat", - label: limit.label ?? "Chat Requests", - value: "\(self.safePercentInt(from: remPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), - secondaryValue: "\(accountID) · \(limitText) requests" + (isExhausted ? " (exhausted)" : ""), - progress: progress, - usageValue: usedPercent) - { - copilotRows.append(row) - } - } else if limit.id.contains("completions") { - let usedPercent = limitVal > 0 ? (used / limitVal) * 100 : 0 - let remPercent = max(0, 100 - usedPercent) - let progress = try? ProviderDetailSection.Row.Progress(used: min(100, max(0, usedPercent)), total: 100) - if let row = self.makeRow( - id: "copilot-completions", - label: limit.label ?? "Completions", - value: "\(self.safePercentInt(from: remPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), - secondaryValue: "\(accountID) · \(limitText) requests", - progress: progress, - usageValue: usedPercent) - { - copilotRows.append(row) - } + let label = limit.label ?? (limit.id.contains("chat") ? "Chat Requests" : "Completions") + if let row = self.makeRow( + id: "copilot-\(limit.id)", + label: label, + value: "\(self.safePercentInt(from: norm.remainingPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: "\(accountID) · \(norm.summaryText)" + (isExhausted ? " (exhausted)" : ""), + progress: progress, + usageValue: norm.usedPercent) + { + copilotRows.append(row) } } } @@ -443,19 +450,18 @@ public enum OMPUsageParser { let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" let plan = report.metadata?["planType"].map { " (\($0))" } ?? "" for limit in report.limits ?? [] { - let used = limit.amount?.used ?? 0 - let rem = max(0, 100 - used) + let norm = self.normalizeAmount(limit) let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } let winName = limit.label ?? limit.id - let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, used), total: 100) + let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, norm.usedPercent), total: 100) if let row = self.makeRow( id: "codex-\(winName)", label: winName, - value: "\(self.safePercentInt(from: rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + value: "\(self.safePercentInt(from: norm.remainingPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), secondaryValue: "\(email)\(plan)", progress: progress, - usageValue: used) + usageValue: norm.usedPercent) { codexRows.append(row) } @@ -470,19 +476,18 @@ public enum OMPUsageParser { for report in xaiReports { let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" for limit in report.limits ?? [] { - let used = limit.amount?.used ?? 0 - let rem = max(0, 100 - used) + let norm = self.normalizeAmount(limit) let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } let lName = limit.label ?? "Weekly Credits" - let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, used), total: 100) + let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, norm.usedPercent), total: 100) if let row = self.makeRow( id: "xai-\(lName)", label: lName, - value: "\(self.safePercentInt(from: rem))% left" + (resetDesc.map { " (\($0))" } ?? ""), + value: "\(self.safePercentInt(from: norm.remainingPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), secondaryValue: email, progress: progress, - usageValue: used) + usageValue: norm.usedPercent) { xaiRows.append(row) } @@ -492,6 +497,32 @@ public enum OMPUsageParser { detailSections.append(xaiSection) } + // Section: Anthropic Claude + var anthropicRows: [ProviderDetailSection.Row] = [] + for report in anthropicReports { + let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + for limit in report.limits ?? [] { + let norm = self.normalizeAmount(limit) + let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } + let lName = limit.label ?? limit.id + let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, norm.usedPercent), total: 100) + if let row = self.makeRow( + id: "anthropic-\(limit.id)", + label: lName, + value: "\(self.safePercentInt(from: norm.remainingPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), + secondaryValue: email, + progress: progress, + usageValue: norm.usedPercent) + { + anthropicRows.append(row) + } + } + } + if let anthropicSection = self.makeSection(title: "Anthropic Claude", rows: anthropicRows) { + detailSections.append(anthropicSection) + } + let identity = ProviderIdentitySnapshot( providerID: .omp, accountEmail: nil, @@ -511,7 +542,6 @@ public enum OMPUsageParser { private static func makeSubproviderSnapshot( response: OMPUsageResponse, target: String, - hiddenItemIDs: [String], now: Date) -> UsageSnapshot { let matchingReports = response.reports.filter { @@ -558,20 +588,20 @@ public enum OMPUsageParser { } } - var rawPrimary: RateWindow? + var primaryWindow: RateWindow? if let cap = primary5hCap, cap.accounts > 0 { - rawPrimary = RateWindow( + primaryWindow = RateWindow( usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), windowMinutes: 300, resetsAt: min5hResetDate, resetDescription: nil) } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "5h") { - rawPrimary = self.rateWindowFromLimit(firstLimit, defaultMinutes: 300, fallbackReset: min5hResetDate) + primaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 300, fallbackReset: min5hResetDate) } - var rawSecondary: RateWindow? + var secondaryWindow: RateWindow? if let cap = weeklyCap, cap.accounts > 0 { - rawSecondary = RateWindow( + secondaryWindow = RateWindow( usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), windowMinutes: 10080, resetsAt: minWeeklyResetDate, @@ -580,24 +610,7 @@ public enum OMPUsageParser { ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "7d") ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "month") { - rawSecondary = self.rateWindowFromLimit(firstLimit, defaultMinutes: 10080, fallbackReset: minWeeklyResetDate) - } - - let isPrimaryHidden = hiddenItemIDs.contains("metric:primary") - let isSecondaryHidden = hiddenItemIDs.contains("metric:secondary") - - let primaryWindow: RateWindow? - let secondaryWindow: RateWindow? - - if isSecondaryHidden && !isPrimaryHidden { - primaryWindow = rawPrimary - secondaryWindow = nil - } else if isPrimaryHidden && !isSecondaryHidden { - primaryWindow = rawSecondary - secondaryWindow = nil - } else { - primaryWindow = rawPrimary - secondaryWindow = rawSecondary + secondaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 10080, fallbackReset: minWeeklyResetDate) } var extraRateWindows: [NamedRateWindow] = [] @@ -607,9 +620,7 @@ public enum OMPUsageParser { let scopedID = "\(limit.id)-\(reportIndex)" guard !seenLimitIDs.contains(scopedID) else { continue } seenLimitIDs.insert(scopedID) - guard let amount = limit.amount, let used = amount.used ?? (amount.usedFraction.map { $0 * 100 }) else { - continue - } + let norm = self.normalizeAmount(limit) let title = limit.label ?? limit.id let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in @@ -620,7 +631,7 @@ public enum OMPUsageParser { id: scopedID, title: title, window: RateWindow( - usedPercent: min(100, max(0, used.isFinite ? used : 0)), + usedPercent: norm.usedPercent, windowMinutes: winMinutes, resetsAt: resetsAt, resetDescription: nil))) @@ -637,10 +648,9 @@ public enum OMPUsageParser { ?? "account" var summaries: [String] = [] for limit in report.limits ?? [] { - if let amount = limit.amount, let used = amount.used { - let lName = limit.label ?? limit.id - summaries.append("\(lName): \(self.safePercentInt(from: max(0, 100 - used)))% left") - } + let norm = self.normalizeAmount(limit) + let lName = limit.label ?? limit.id + summaries.append("\(lName): \(self.safePercentInt(from: norm.remainingPercent))% left") } if let row = self.makeRow( id: "acct-\(accountID)", @@ -689,16 +699,14 @@ public enum OMPUsageParser { defaultMinutes: Int?, fallbackReset: Date?) -> RateWindow? { - guard let amount = limit.amount, - let used = amount.used ?? (amount.usedFraction.map { $0 * 100 }) - else { return nil } + let norm = self.normalizeAmount(limit) let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in guard dur.isFinite, dur > 0, dur < Double(Int.max) * 60_000 else { return nil } return Int(dur / (60 * 1000)) } ?? defaultMinutes let resetsAt = limit.window?.resetsAt.flatMap { Date(timeIntervalSince1970: $0 / 1000) } ?? fallbackReset return RateWindow( - usedPercent: min(100, max(0, used.isFinite ? used : 0)), + usedPercent: norm.usedPercent, windowMinutes: winMinutes, resetsAt: resetsAt, resetDescription: nil) diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index 836de1c7747..5f638060947 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -154,7 +154,7 @@ struct OMPProviderTests { } @Test - func `omp usage parser parses combined usage accurately`() throws { + func `omp usage parser calculates combined usage from capacity totals`() throws { let now = Date(timeIntervalSince1970: 1792220000) let snapshot = try OMPUsageParser.parse( jsonString: Self.sampleOMPUsageJSON, @@ -163,11 +163,13 @@ struct OMPProviderTests { let primary = try #require(snapshot.primary) #expect(primary.windowMinutes == 300) - #expect(primary.usedPercent <= 10.0) + // 5h capacity: 0.04 used / 5 accounts = 0.8% + #expect(abs(primary.usedPercent - 0.8) < 0.001) let secondary = try #require(snapshot.secondary) #expect(secondary.windowMinutes == 10080) - #expect(secondary.usedPercent >= 20.0) + // 7d capacity: 1.4 used / 5 accounts = 28.0% + #expect(abs(secondary.usedPercent - 28.0) < 0.001) let extras = try #require(snapshot.extraRateWindows) #expect(extras.count >= 2) @@ -188,9 +190,13 @@ struct OMPProviderTests { let primary = try #require(snapshot.primary) #expect(primary.windowMinutes == 300) + // 0.04 / 4 = 1.0% + #expect(abs(primary.usedPercent - 1.0) < 0.001) let secondary = try #require(snapshot.secondary) #expect(secondary.windowMinutes == 10080) + // 1.4 / 4 = 35.0% + #expect(abs(secondary.usedPercent - 35.0) < 0.001) let details = snapshot.details #expect(details.contains { $0.title?.contains("Antigravity") == true }) @@ -212,32 +218,39 @@ struct OMPProviderTests { } @Test - func `omp usage parser honors hidden usage item IDs for 5 vs 6 accounts`() throws { - let now = Date(timeIntervalSince1970: 1792220000) - - // 1. When 6 accounts window is hidden (only 5 accounts checked) - let only5AccountsSnapshot = try OMPUsageParser.parse( - jsonString: Self.sampleOMPUsageJSON, - subprovider: "all", - hiddenItemIDs: ["metric:secondary"], - now: now) - let primary5 = try #require(only5AccountsSnapshot.primary) - #expect(primary5.windowMinutes == 300) - #expect(only5AccountsSnapshot.secondary == nil) - - // 2. When 5 accounts window is hidden (only 6 accounts checked) - let only6AccountsSnapshot = try OMPUsageParser.parse( - jsonString: Self.sampleOMPUsageJSON, - subprovider: "all", - hiddenItemIDs: ["metric:primary"], - now: now) - let primary6 = try #require(only6AccountsSnapshot.primary) - #expect(primary6.windowMinutes == 10080) - #expect(only6AccountsSnapshot.secondary == nil) + func `omp usage parser normalizes count and request limits`() { + let countLimit = OMPLimit( + id: "copilot:chat", + amount: OMPAmount(unit: "requests", remaining: 180, used: 20, limit: 200)) + let norm = OMPUsageParser.normalizeAmount(countLimit) + #expect(abs(norm.usedPercent - 10.0) < 0.001) + #expect(abs(norm.remainingPercent - 90.0) < 0.001) + #expect(norm.summaryText == "20/200 requests") + } - // The percentages and windows distinctly differ between selecting 5 vs 6 accounts - #expect(primary5.usedPercent != primary6.usedPercent) - #expect(primary5.windowMinutes != primary6.windowMinutes) + @Test + func `omp usage parser omits aggregate windows when no matching quota exists`() throws { + let json = """ + { + "reports": [ + { + "provider": "github-copilot", + "limits": [ + { + "id": "copilot:chat", + "window": { "id": "monthly" }, + "amount": { "used": 20, "limit": 200 } + } + ] + } + ] + } + """ + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all") + // No 5h or 7d capacity accounts contributed + #expect(snapshot.primary == nil) + #expect(snapshot.secondary == nil) + #expect(snapshot.extraRateWindows?.contains { $0.id.contains("chat") } == true) } @Test From fc7f656a35b7022dc6c12372040e2a2a642f8337 Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 10:04:00 +0200 Subject: [PATCH 5/9] fix(omp): preserve Anthropic quota durations and infer monthly fallback cadence --- .../Providers/OMP/OMPUsageParser.swift | 23 +++++-- Tests/CodexBarTests/OMPProviderTests.swift | 66 ++++++++++++++++--- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index dd5f1b43d98..92f21652edc 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -262,10 +262,15 @@ public enum OMPUsageParser { let norm = self.normalizeAmount(limit) let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let lName = limit.label ?? "Claude Quota" + let winId = (limit.window?.id ?? "").lowercased() + let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in + guard dur.isFinite, dur > 0, dur < Double(Int.max) * 60_000 else { return nil } + return Int(dur / (60 * 1000)) + } ?? (winId.contains("week") || winId.contains("7d") ? 10080 : (winId.contains("month") ? 43200 : 300)) extraRateWindows.append(NamedRateWindow( id: "omp-anthropic-\(reportIndex)-\(limit.id)", title: lName, - window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 300, resetsAt: resetsAt, resetDescription: nil))) + window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: winMinutes, resetsAt: resetsAt, resetDescription: nil))) } } @@ -608,9 +613,10 @@ public enum OMPUsageParser { resetDescription: nil) } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "week") ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "7d") - ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "month") { secondaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 10080, fallbackReset: minWeeklyResetDate) + } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "month") { + secondaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 43200, fallbackReset: minWeeklyResetDate) } var extraRateWindows: [NamedRateWindow] = [] @@ -700,10 +706,20 @@ public enum OMPUsageParser { fallbackReset: Date?) -> RateWindow? { let norm = self.normalizeAmount(limit) + let winId = (limit.window?.id ?? "").lowercased() + let inferredMinutes: Int? = if winId.contains("month") { + 43200 + } else if winId.contains("week") || winId.contains("7d") { + 10080 + } else if winId.contains("5h") { + 300 + } else { + defaultMinutes + } let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in guard dur.isFinite, dur > 0, dur < Double(Int.max) * 60_000 else { return nil } return Int(dur / (60 * 1000)) - } ?? defaultMinutes + } ?? inferredMinutes let resetsAt = limit.window?.resetsAt.flatMap { Date(timeIntervalSince1970: $0 / 1000) } ?? fallbackReset return RateWindow( usedPercent: norm.usedPercent, @@ -711,7 +727,6 @@ public enum OMPUsageParser { resetsAt: resetsAt, resetDescription: nil) } - private static func safePercentInt(from value: Double) -> Int { guard value.isFinite, !value.isNaN else { return 0 } return Int(min(100, max(0, value.rounded()))) diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index 5f638060947..f9ebe1d58c3 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -218,16 +218,66 @@ struct OMPProviderTests { } @Test - func `omp usage parser normalizes count and request limits`() { - let countLimit = OMPLimit( - id: "copilot:chat", - amount: OMPAmount(unit: "requests", remaining: 180, used: 20, limit: 200)) - let norm = OMPUsageParser.normalizeAmount(countLimit) - #expect(abs(norm.usedPercent - 10.0) < 0.001) - #expect(abs(norm.remainingPercent - 90.0) < 0.001) - #expect(norm.summaryText == "20/200 requests") + func `omp usage parser normalizes copilot request counts at parser level`() throws { + let json = """ + { + "reports": [ + { + "provider": "github-copilot", + "metadata": { "accountId": "octocat" }, + "limits": [ + { + "id": "copilot:chat", + "label": "Chat Requests", + "window": { "id": "monthly" }, + "amount": { "unit": "requests", "used": 20, "limit": 200 } + } + ] + } + ] + } + """ + // Combined mode + let combined = try OMPUsageParser.parse(jsonString: json, subprovider: "all") + let extra = try #require(combined.extraRateWindows?.first { $0.id.contains("chat") }) + #expect(abs(extra.window.usedPercent - 10.0) < 0.001) + #expect(extra.title == "Chat Requests") + + // Filtered subprovider mode + let filtered = try OMPUsageParser.parse(jsonString: json, subprovider: "copilot") + let secondary = try #require(filtered.secondary) + #expect(abs(secondary.usedPercent - 10.0) < 0.001) + #expect(secondary.windowMinutes == 43200) + + let labels = ProviderDescriptorRegistry.descriptor(for: .omp).presentation + .rateWindowLabels(metadata: ProviderDescriptorRegistry.descriptor(for: .omp).metadata, snapshot: filtered) + #expect(labels.secondary == "Monthly") } + @Test + func `omp usage parser preserves anthropic weekly duration in combined mode`() throws { + let json = """ + { + "reports": [ + { + "provider": "anthropic", + "limits": [ + { + "id": "anthropic:weekly", + "label": "Weekly Limit", + "window": { "id": "weekly", "durationMs": 604800000 }, + "amount": { "used": 40, "limit": 100 } + } + ] + } + ] + } + """ + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all") + let extra = try #require(snapshot.extraRateWindows?.first { $0.id.contains("anthropic") }) + #expect(extra.window.windowMinutes == 10080) + #expect(abs(extra.window.usedPercent - 40.0) < 0.001) + } @Test func `omp usage parser omits aggregate windows when no matching quota exists`() throws { let json = """ From f8a4d4e56a12faf9ad021703062cd793522792b3 Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 10:17:49 +0200 Subject: [PATCH 6/9] fix(omp): derive usage from remaining fields, preserve full emails, and keep identity neutral --- .../Providers/OMP/OMPUsageParser.swift | 93 ++++++++++++------- Tests/CodexBarTests/OMPProviderTests.swift | 90 ++++++++++++++++++ 2 files changed, 147 insertions(+), 36 deletions(-) diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index 92f21652edc..52f3351d0f7 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -70,32 +70,39 @@ public enum OMPUsageParser { } } - public static func normalizeAmount(_ limit: OMPLimit) -> (usedPercent: Double, remainingPercent: Double, summaryText: String) { + public static func normalizeAmount(_ limit: OMPLimit) -> (usedPercent: Double, remainingPercent: Double, summaryText: String)? { guard let amount = limit.amount else { - return (0, 100, "") + return nil } - let rawUsed = amount.used ?? (amount.usedFraction.map { $0 * 100 } ?? 0) - let limitVal = amount.limit ?? 0 - let used = rawUsed.isFinite ? max(0, rawUsed) : 0 - let usedPercent: Double let remPercent: Double if let frac = amount.usedFraction, frac.isFinite { usedPercent = min(100, max(0, frac * 100)) remPercent = max(0, 100 - usedPercent) - } else if limitVal > 0 { - usedPercent = min(100, max(0, (used / limitVal) * 100)) + } else if let remFrac = amount.remainingFraction, remFrac.isFinite { + remPercent = min(100, max(0, remFrac * 100)) + usedPercent = max(0, 100 - remPercent) + } else if let rawUsed = amount.used, rawUsed.isFinite, let limitVal = amount.limit, limitVal > 0 { + usedPercent = min(100, max(0, (rawUsed / limitVal) * 100)) remPercent = max(0, 100 - usedPercent) - } else { - usedPercent = min(100, max(0, used)) + } else if let rawRem = amount.remaining, rawRem.isFinite, let limitVal = amount.limit, limitVal > 0 { + remPercent = min(100, max(0, (rawRem / limitVal) * 100)) + usedPercent = max(0, 100 - remPercent) + } else if let rawUsed = amount.used, rawUsed.isFinite { + usedPercent = min(100, max(0, rawUsed)) remPercent = max(0, 100 - usedPercent) + } else if let rawRem = amount.remaining, rawRem.isFinite { + remPercent = min(100, max(0, rawRem)) + usedPercent = max(0, 100 - remPercent) + } else { + return nil } - let summary: String = if limitVal > 0, (amount.unit ?? "").lowercased().contains("request") { + let summary: String = if let limitVal = amount.limit, limitVal > 0, (amount.unit ?? "").lowercased().contains("request"), let used = amount.used { "\(Int(used))/\(Int(limitVal)) requests" - } else if let rem = amount.remaining, rem.isFinite { + } else if let rem = amount.remaining, rem.isFinite, (amount.unit ?? "").lowercased() == "percent" || amount.limit == nil || amount.limit == 100 { "\(self.safePercentInt(from: rem))% left" } else { "\(self.safePercentInt(from: remPercent))% left" @@ -105,7 +112,6 @@ public enum OMPUsageParser { } private struct LimitEntry { - let emailShort: String let fullEmail: String let used: Double let remaining: Double @@ -130,9 +136,7 @@ public enum OMPUsageParser { for report in agyReports { let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" - let emailShort = email.components(separatedBy: "@").first ?? email var seenGroupsForAccount = Set() - for limit in report.limits ?? [] { let winId = (limit.window?.id ?? "").lowercased() let lid = limit.id.lowercased() @@ -153,11 +157,10 @@ public enum OMPUsageParser { guard let groupKey, !seenGroupsForAccount.contains(groupKey) else { continue } seenGroupsForAccount.insert(groupKey) - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } agyGroups[groupKey]?.append(LimitEntry( - emailShort: emailShort, fullEmail: email, used: norm.usedPercent, remaining: norm.remainingPercent, @@ -209,7 +212,7 @@ public enum OMPUsageParser { for (reportIndex, report) in copilotReports.enumerated() { for limit in report.limits ?? [] { let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } if limit.id.contains("chat") { extraRateWindows.append(NamedRateWindow( id: "omp-copilot-\(reportIndex)-chat", @@ -227,7 +230,7 @@ public enum OMPUsageParser { let codexReports = response.reports.filter { $0.provider == "openai-codex" } for (reportIndex, report) in codexReports.enumerated() { for limit in report.limits ?? [] { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let winId = (limit.window?.id ?? "").lowercased() if winId.contains("5h") { @@ -247,7 +250,7 @@ public enum OMPUsageParser { let xaiReports = response.reports.filter { $0.provider == "xai-oauth" } for (reportIndex, report) in xaiReports.enumerated() { for limit in report.limits ?? [] { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } extraRateWindows.append(NamedRateWindow( id: "omp-xai-\(reportIndex)-7d", @@ -259,7 +262,7 @@ public enum OMPUsageParser { let anthropicReports = response.reports.filter { $0.provider == "anthropic" } for (reportIndex, report) in anthropicReports.enumerated() { for limit in report.limits ?? [] { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let lName = limit.label ?? "Claude Quota" let winId = (limit.window?.id ?? "").lowercased() @@ -349,7 +352,7 @@ public enum OMPUsageParser { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } - let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") + let breakdown = entries.map { "\($0.fullEmail): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-gemini-5h", @@ -367,7 +370,7 @@ public enum OMPUsageParser { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } - let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") + let breakdown = entries.map { "\($0.fullEmail): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-gemini-7d", @@ -384,7 +387,7 @@ public enum OMPUsageParser { if let entries = agyGroups["3p-5h"], !entries.isEmpty { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem - let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") + let breakdown = entries.map { "\($0.fullEmail): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-3p-5h", @@ -402,7 +405,7 @@ public enum OMPUsageParser { let avgRem = entries.map(\.remaining).reduce(0, +) / Double(entries.count) let avgUsed = 100 - avgRem let resetDesc = entries.compactMap(\.resetsAt).min().map { UsageFormatter.resetDescription(from: $0) } - let breakdown = entries.map { "\($0.emailShort): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") + let breakdown = entries.map { "\($0.fullEmail): \(self.safePercentInt(from: $0.remaining))%" }.joined(separator: " · ") let progress = try? ProviderDetailSection.Row.Progress(used: avgUsed, total: 100) if let row = self.makeRow( id: "agy-3p-7d", @@ -416,7 +419,16 @@ public enum OMPUsageParser { } } - let agyTitle = agyReports.count > 0 ? "Google Antigravity (\(agyReports.count) accounts)" : "Google Antigravity" + let agyCapCount = response.capacity?["google-antigravity"]?.first.map { Int($0.accounts) } ?? 0 + let agyWithoutUsageCount = response.accountsWithoutUsage?.filter { $0.provider == "google-antigravity" }.count ?? 0 + let totalAgyAccounts = max(agyReports.count + agyWithoutUsageCount, agyCapCount) + let agyTitle: String = if totalAgyAccounts > agyReports.count, agyReports.count > 0 { + "Google Antigravity (\(totalAgyAccounts) accounts)" + } else if agyReports.count > 0 { + "Google Antigravity (\(agyReports.count) accounts)" + } else { + "Google Antigravity" + } if let agySection = self.makeSection(title: agyTitle, rows: agyRows) { detailSections.append(agySection) } @@ -428,7 +440,7 @@ public enum OMPUsageParser { for limit in report.limits ?? [] { let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let isExhausted = limit.status?.lowercased() == "exhausted" || norm.usedPercent >= 100 let progress = try? ProviderDetailSection.Row.Progress(used: norm.usedPercent, total: 100) @@ -455,7 +467,7 @@ public enum OMPUsageParser { let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" let plan = report.metadata?["planType"].map { " (\($0))" } ?? "" for limit in report.limits ?? [] { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } let winName = limit.label ?? limit.id @@ -481,7 +493,7 @@ public enum OMPUsageParser { for report in xaiReports { let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" for limit in report.limits ?? [] { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } let lName = limit.label ?? "Weekly Credits" @@ -507,7 +519,7 @@ public enum OMPUsageParser { for report in anthropicReports { let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" for limit in report.limits ?? [] { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } let lName = limit.label ?? limit.id @@ -626,7 +638,7 @@ public enum OMPUsageParser { let scopedID = "\(limit.id)-\(reportIndex)" guard !seenLimitIDs.contains(scopedID) else { continue } seenLimitIDs.insert(scopedID) - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let title = limit.label ?? limit.id let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in @@ -654,7 +666,7 @@ public enum OMPUsageParser { ?? "account" var summaries: [String] = [] for limit in report.limits ?? [] { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { continue } let lName = limit.label ?? limit.id summaries.append("\(lName): \(self.safePercentInt(from: norm.remainingPercent))% left") } @@ -666,15 +678,24 @@ public enum OMPUsageParser { acctRows.append(row) } } - if let acctSection = self.makeSection(title: "\(self.displayNameForProvider(target)) Accounts (\(matchingReports.count))", rows: acctRows) { + let providerCapAccounts = response.capacity?[target]?.first.map { Int($0.accounts) } + ?? (target == "agy" ? response.capacity?["google-antigravity"]?.first.map { Int($0.accounts) } : nil) + ?? 0 + let withoutUsageCount = response.accountsWithoutUsage?.filter { + $0.provider.lowercased() == target || (target == "agy" && $0.provider == "google-antigravity") + }.count ?? 0 + let totalAccounts = max(matchingReports.count + withoutUsageCount, providerCapAccounts) + let sectionTitle = totalAccounts > matchingReports.count + ? "\(self.displayNameForProvider(target)) Accounts (\(totalAccounts))" + : "\(self.displayNameForProvider(target)) Accounts (\(matchingReports.count))" + if let acctSection = self.makeSection(title: sectionTitle, rows: acctRows) { detailSections.append(acctSection) } } - let firstEmail = matchingReports.first?.metadata?["email"] let identity = ProviderIdentitySnapshot( providerID: .omp, - accountEmail: firstEmail, + accountEmail: nil, accountOrganization: nil, loginMethod: "omp", accountID: target) @@ -705,7 +726,7 @@ public enum OMPUsageParser { defaultMinutes: Int?, fallbackReset: Date?) -> RateWindow? { - let norm = self.normalizeAmount(limit) + guard let norm = self.normalizeAmount(limit) else { return nil } let winId = (limit.window?.id ?? "").lowercased() let inferredMinutes: Int? = if winId.contains("month") { 43200 diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index f9ebe1d58c3..46ff5e00162 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -303,6 +303,96 @@ struct OMPProviderTests { #expect(snapshot.extraRateWindows?.contains { $0.id.contains("chat") } == true) } + @Test + func `omp usage parser derives usage from remaining fields and skips unknown limits with missing amount`() throws { + let json = """ + { + "reports": [ + { + "provider": "openai-codex", + "limits": [ + { + "id": "codex:remaining-fraction", + "label": "Remaining Only", + "window": { "id": "5h" }, + "amount": { "remainingFraction": 0.8 } + }, + { + "id": "codex:missing-amount", + "label": "No Amount", + "window": { "id": "7d" } + } + ] + } + ] + } + """ + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all") + let extra = try #require(snapshot.extraRateWindows) + // remainingFraction 0.8 should derive usedPercent = 20.0 + let remLimit = try #require(extra.first { $0.id.contains("5h") }) + #expect(abs(remLimit.window.usedPercent - 20.0) < 0.001) + // missing-amount limit should be completely skipped, not treated as 0% used + #expect(!extra.contains { $0.id.contains("7d") }) + } + + @Test + func `omp usage parser preserves full email in breakdown for privacy redactor`() throws { + let json = """ + { + "reports": [ + { + "provider": "google-antigravity", + "metadata": { "email": "testuser@gmail.com" }, + "limits": [ + { + "id": "gemini:5h", + "window": { "id": "5h" }, + "amount": { "used": 10, "limit": 100 } + } + ] + } + ] + } + """ + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all") + let agySection = try #require(snapshot.details.first { $0.title?.contains("Antigravity") == true }) + let row = try #require(agySection.rows.first) + // The breakdown in secondaryValue must preserve the full email "testuser@gmail.com" so email redactors match it + let secondary = try #require(row.secondaryValue) + #expect(secondary.contains("testuser@gmail.com")) + #expect(!secondary.hasPrefix("testuser:")) + } + + @Test + func `omp usage parser keeps aggregate identity neutral without subprovider email`() throws { + let json = """ + { + "capacity": { + "google-antigravity": [ + { "window": "5h", "accounts": 4, "usedAccounts": 1, "remainingAccounts": 3 } + ] + }, + "reports": [ + { + "provider": "google-antigravity", + "metadata": { "email": "user1@gmail.com" }, + "limits": [ + { "id": "gemini:5h", "window": { "id": "5h" }, "amount": { "used": 10, "limit": 100 } } + ] + } + ] + } + """ + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "agy") + // Snapshot identity must remain neutral across multi-account aggregates + #expect(snapshot.identity?.accountEmail == nil) + #expect(snapshot.identity?.accountID == "google-antigravity") + // Section title must reflect total capacity accounts (4), not just 1 report + let agySection = try #require(snapshot.details.first { $0.title?.contains("Accounts") == true }) + #expect(agySection.title?.contains("(4)") == true) + } + @Test func `omp settings snapshot persists subprovider selection`() { let suite = "OMPProviderTests-settings-\(UUID().uuidString)" From 1c899187d8bb60385e698e020bcd1b8393ea1cf0 Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 10:41:36 +0200 Subject: [PATCH 7/9] feat(omp): dynamically compute total capacity and labels from active usable items --- .../Providers/OMP/OMPSettingsStore.swift | 6 +- .../Providers/OMP/OMPCLIProbe.swift | 2 + .../Providers/OMP/OMPProviderDescriptor.swift | 22 +-- .../Providers/OMP/OMPProviderSettings.swift | 5 +- .../Providers/OMP/OMPUsageParser.swift | 138 ++++++++++++++---- Tests/CodexBarTests/OMPProviderTests.swift | 55 ++++++- 6 files changed, 187 insertions(+), 41 deletions(-) diff --git a/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift b/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift index 923152d8165..52cb5c237bc 100644 --- a/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift +++ b/Sources/CodexBar/Providers/OMP/OMPSettingsStore.swift @@ -17,8 +17,10 @@ extension SettingsStore { } public func ompSettingsSnapshot() -> OMPProviderSettings { - OMPProviderSettings( + let hidden = self.configSnapshot.providerConfig(for: .omp)?.hiddenUsageItemIDs ?? [] + return OMPProviderSettings( subprovider: self.ompSubprovider, - customSubprovider: self.ompCustomSubprovider) + customSubprovider: self.ompCustomSubprovider, + hiddenUsageItemIDs: hidden) } } diff --git a/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift index 51a6a95810c..8c6335c1a21 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPCLIProbe.swift @@ -25,6 +25,7 @@ public struct OMPCLIProbe: Sendable { public func fetch( environment: [String: String] = ProcessInfo.processInfo.environment, subprovider: String = "all", + hiddenItemIDs: [String] = [], now: Date = Date()) async throws -> UsageSnapshot { let loginPATH = LoginShellPathCache.shared.current @@ -65,6 +66,7 @@ public struct OMPCLIProbe: Sendable { return try OMPUsageParser.parse( jsonString: stdout, subprovider: subprovider, + hiddenItemIDs: hiddenItemIDs, now: now) } } diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift index 371595f7b11..32da0258a72 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift @@ -43,14 +43,16 @@ public enum OMPProviderDescriptor { history: .optIn, presentation: ProviderUsagePresentation( rateWindowLabeler: { metadata, snapshot, _ in - let primaryTitle = snapshot.primary?.windowMinutes.map { minutes in - minutes <= 300 ? metadata.sessionLabel : "\(minutes / 60)h" - } ?? metadata.sessionLabel - let secondaryTitle = snapshot.secondary?.windowMinutes.map { minutes in - if minutes >= 40000 { return "Monthly" } - if minutes >= 10000 { return metadata.weeklyLabel } - return "\(minutes / 60)h" - } ?? metadata.weeklyLabel + let primaryTitle = snapshot.primary?.resetDescription + ?? (snapshot.primary?.windowMinutes.map { minutes in + minutes <= 300 ? metadata.sessionLabel : "\(minutes / 60)h" + } ?? metadata.sessionLabel) + let secondaryTitle = snapshot.secondary?.resetDescription + ?? (snapshot.secondary?.windowMinutes.map { minutes in + if minutes >= 40000 { return "Monthly" } + if minutes >= 10000 { return metadata.weeklyLabel } + return "\(minutes / 60)h" + } ?? metadata.weeklyLabel) return ProviderRateWindowLabels( primary: primaryTitle, secondary: secondaryTitle, @@ -96,10 +98,12 @@ struct OMPCLIUsageStrategy: ProviderFetchStrategy { let subprovider = context.env["OMP_SUBPROVIDER"] ?? context.settings?.omp?.effectiveSubprovider ?? "all" + let hiddenItemIDs = context.settings?.omp?.hiddenUsageItemIDs ?? [] let snapshot = try await OMPCLIProbe().fetch( environment: context.env, - subprovider: subprovider) + subprovider: subprovider, + hiddenItemIDs: hiddenItemIDs) return self.makeResult( usage: snapshot, diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift index 1b0c3fc1d31..07969d15c38 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderSettings.swift @@ -3,13 +3,16 @@ import Foundation public struct OMPProviderSettings: Sendable, Equatable { public let subprovider: String public let customSubprovider: String? + public let hiddenUsageItemIDs: [String] public init( subprovider: String = "all", - customSubprovider: String? = nil) + customSubprovider: String? = nil, + hiddenUsageItemIDs: [String] = []) { self.subprovider = subprovider self.customSubprovider = customSubprovider + self.hiddenUsageItemIDs = hiddenUsageItemIDs } public var effectiveSubprovider: String { diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index 52f3351d0f7..043c6cb9f76 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -4,22 +4,24 @@ public enum OMPUsageParser { public static func parse( jsonString: String, subprovider: String = "all", + hiddenItemIDs: [String] = [], now: Date = Date()) throws -> UsageSnapshot { guard let data = jsonString.data(using: .utf8) else { throw OMPUsageError.parseFailed("Invalid UTF-8 in OMP usage output") } - return try self.parse(data: data, subprovider: subprovider, now: now) + return try self.parse(data: data, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) } public static func parse( data: Data, subprovider: String = "all", + hiddenItemIDs: [String] = [], now: Date = Date()) throws -> UsageSnapshot { do { let response = try JSONDecoder().decode(OMPUsageResponse.self, from: data) - return self.makeSnapshot(response: response, subprovider: subprovider, now: now) + return self.makeSnapshot(response: response, subprovider: subprovider, hiddenItemIDs: hiddenItemIDs, now: now) } catch let error as DecodingError { throw OMPUsageError.parseFailed("Malformed OMP usage JSON payload: \(error.localizedDescription)") } @@ -28,11 +30,12 @@ public enum OMPUsageParser { public static func makeSnapshot( response: OMPUsageResponse, subprovider: String, + hiddenItemIDs: [String] = [], now: Date) -> UsageSnapshot { let normalized = self.normalizeSubproviderKey(subprovider) if normalized == "all" || normalized.isEmpty { - return self.makeCombinedSnapshot(response: response, now: now) + return self.makeCombinedSnapshot(response: response, hiddenItemIDs: hiddenItemIDs, now: now) } else { return self.makeSubproviderSnapshot(response: response, target: normalized, now: now) } @@ -69,6 +72,49 @@ public enum OMPUsageParser { default: return key } } + // Provider-specific by design: This maps OMP subproviders to short display names. + private static func shortNameForProvider(_ key: String) -> String { + let k = key.lowercased() + if k.contains("antigravity") || k.contains("agy") { return "AGY" } + if k.contains("codex") { return "Codex" } + if k.contains("copilot") { return "Copilot" } + if k.contains("xai") || k.contains("grok") { return "Grok" } + if k.contains("anthropic") || k.contains("claude") { return "Claude" } + return key + } + + // Provider-specific by design: This checks whether a subprovider's metrics or sections are hidden. + private static func isProviderActive( + key: String, + hiddenSet: Set) -> Bool + { + let k = key.lowercased() + if k.contains("antigravity") || k.contains("agy") { + let agyMetrics = ["omp-agy-gemini-5h", "omp-agy-gemini-7d", "omp-agy-3p-5h", "omp-agy-3p-7d"] + return !hiddenSet.contains("detailSection:Google Antigravity") && + !agyMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } + } + if k.contains("codex") { + let codexMetrics = ["omp-codex-0-5h", "omp-codex-0-7d"] + return !hiddenSet.contains("detailSection:OpenAI Codex") && + !codexMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } + } + if k.contains("copilot") { + let copilotMetrics = ["omp-copilot-0-chat", "omp-copilot-0-completions"] + return !hiddenSet.contains("detailSection:GitHub Copilot") && + !copilotMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } + } + if k.contains("xai") || k.contains("grok") { + let xaiMetrics = ["omp-xai-0-7d"] + return !hiddenSet.contains("detailSection:xAI Grok") && + !xaiMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } + } + if k.contains("anthropic") || k.contains("claude") { + return !hiddenSet.contains("detailSection:Anthropic Claude") + } + return true + } + public static func normalizeAmount(_ limit: OMPLimit) -> (usedPercent: Double, remainingPercent: Double, summaryText: String)? { guard let amount = limit.amount else { @@ -121,6 +167,7 @@ public enum OMPUsageParser { private static func makeCombinedSnapshot( response: OMPUsageResponse, + hiddenItemIDs: [String] = [], now: Date) -> UsageSnapshot { let nowMs = now.timeIntervalSince1970 * 1000 @@ -278,50 +325,80 @@ public enum OMPUsageParser { } // 3. Compute Aggregate 5h and Weekly Windows using response.capacity totals + let hiddenSet = Set(hiddenItemIDs) var total5hCapacityAccounts: Double = 0 var used5hCapacityAccounts: Double = 0 var totalWeeklyCapacityAccounts: Double = 0 var usedWeeklyCapacityAccounts: Double = 0 - for (_, windows) in response.capacity ?? [:] { + var contributing5hProviders: [String: Int] = [:] + var contributingWeeklyProviders: [String: Int] = [:] + + for (providerKey, windows) in response.capacity ?? [:] { + guard self.isProviderActive(key: providerKey, hiddenSet: hiddenSet) else { continue } + for cap in windows { let winName = cap.window.lowercased() let dur = cap.durationMs ?? 0 + let accts = Int(cap.accounts) if winName.hasPrefix("5h") || dur == 18_000_000 { total5hCapacityAccounts += cap.accounts used5hCapacityAccounts += cap.usedAccounts + let normKey = self.shortNameForProvider(providerKey) + contributing5hProviders[normKey] = max(contributing5hProviders[normKey] ?? 0, accts) } else if winName.contains("7d") || winName.contains("week") || dur == 604_800_000 { totalWeeklyCapacityAccounts += cap.accounts usedWeeklyCapacityAccounts += cap.usedAccounts + let normKey = self.shortNameForProvider(providerKey) + contributingWeeklyProviders[normKey] = max(contributingWeeklyProviders[normKey] ?? 0, accts) } } } - // Earliest resets across ALL contributing providers + // Earliest resets across ALL contributing active providers var min5hResetDate: Date? var minWeeklyResetDate: Date? for report in response.reports { + guard self.isProviderActive(key: report.provider, hiddenSet: hiddenSet) else { continue } for limit in report.limits ?? [] { guard let window = limit.window, - let resetsAtMs = window.resetsAt, - resetsAtMs > nowMs - else { continue } - let resetDate = Date(timeIntervalSince1970: resetsAtMs / 1000) - let winId = (window.id ?? "").lowercased() - let dur = window.durationMs ?? 0 - if winId.contains("5h") || dur == 18_000_000 { - if min5hResetDate == nil || resetDate < min5hResetDate! { - min5hResetDate = resetDate - } - } else if winId.contains("week") || winId.contains("7d") || dur == 604_800_000 { - if minWeeklyResetDate == nil || resetDate < minWeeklyResetDate! { - minWeeklyResetDate = resetDate - } + let resetsAtMs = window.resetsAt, + resetsAtMs > nowMs + else { continue } + let resetDate = Date(timeIntervalSince1970: resetsAtMs / 1000) + let winId = (window.id ?? "").lowercased() + let dur = window.durationMs ?? 0 + if winId.contains("5h") || dur == 18_000_000 { + if min5hResetDate == nil || resetDate < min5hResetDate! { + min5hResetDate = resetDate + } + } else if winId.contains("week") || winId.contains("7d") || dur == 604_800_000 { + if minWeeklyResetDate == nil || resetDate < minWeeklyResetDate! { + minWeeklyResetDate = resetDate + } + } + } + } + + func makeSummaryLabel(providers: [String: Int], fallback: String) -> String { + guard !providers.isEmpty else { return fallback } + let order = ["AGY", "Codex", "Copilot", "Grok", "Claude"] + var parts: [String] = [] + for name in order { + if let count = providers[name], count > 0 { + parts.append("\(count) \(name)") } } + for (name, count) in providers where !order.contains(name) && count > 0 { + parts.append("\(count) \(name)") + } + return parts.isEmpty ? fallback : parts.joined(separator: " + ") } + let summary5h = makeSummaryLabel(providers: contributing5hProviders, fallback: "5-Hour") + let summaryWeekly = makeSummaryLabel(providers: contributingWeeklyProviders, fallback: "Weekly") + // Construct 5h aggregate window only when contributing quota exists var primaryWindow: RateWindow? if total5hCapacityAccounts > 0 { @@ -330,7 +407,7 @@ public enum OMPUsageParser { usedPercent: usedPercent, windowMinutes: 300, resetsAt: min5hResetDate, - resetDescription: nil) + resetDescription: summary5h) } var secondaryWindow: RateWindow? @@ -340,7 +417,7 @@ public enum OMPUsageParser { usedPercent: usedPercent, windowMinutes: 10080, resetsAt: minWeeklyResetDate, - resetDescription: nil) + resetDescription: summaryWeekly) } // 4. Detail sections grouped by Provider & Limit @@ -605,15 +682,19 @@ public enum OMPUsageParser { } } + let providerShort = self.shortNameForProvider(target) + let subproviderAccounts = Int(primary5hCap?.accounts ?? weeklyCap?.accounts ?? Double(matchingReports.count)) + let subproviderSummary = subproviderAccounts > 0 ? "\(subproviderAccounts) \(providerShort)" : providerShort + var primaryWindow: RateWindow? if let cap = primary5hCap, cap.accounts > 0 { primaryWindow = RateWindow( usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), windowMinutes: 300, resetsAt: min5hResetDate, - resetDescription: nil) + resetDescription: "\(subproviderSummary) (5h)") } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "5h") { - primaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 300, fallbackReset: min5hResetDate) + primaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 300, fallbackReset: min5hResetDate, resetDesc: "\(subproviderSummary) (5h)") } var secondaryWindow: RateWindow? @@ -622,13 +703,13 @@ public enum OMPUsageParser { usedPercent: min(100, max(0, (cap.usedAccounts / cap.accounts) * 100)), windowMinutes: 10080, resetsAt: minWeeklyResetDate, - resetDescription: nil) + resetDescription: "\(subproviderSummary) (Weekly)") } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "week") ?? self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "7d") { - secondaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 10080, fallbackReset: minWeeklyResetDate) + secondaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 10080, fallbackReset: minWeeklyResetDate, resetDesc: "\(subproviderSummary) (Weekly)") } else if let firstLimit = self.findFirstLimit(matchingReports: matchingReports, windowPrefix: "month") { - secondaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 43200, fallbackReset: minWeeklyResetDate) + secondaryWindow = self.rateWindowFromLimit(firstLimit, defaultMinutes: 43200, fallbackReset: minWeeklyResetDate, resetDesc: "\(subproviderSummary) (Monthly)") } var extraRateWindows: [NamedRateWindow] = [] @@ -724,7 +805,8 @@ public enum OMPUsageParser { private static func rateWindowFromLimit( _ limit: OMPLimit, defaultMinutes: Int?, - fallbackReset: Date?) -> RateWindow? + fallbackReset: Date?, + resetDesc: String? = nil) -> RateWindow? { guard let norm = self.normalizeAmount(limit) else { return nil } let winId = (limit.window?.id ?? "").lowercased() @@ -746,7 +828,7 @@ public enum OMPUsageParser { usedPercent: norm.usedPercent, windowMinutes: winMinutes, resetsAt: resetsAt, - resetDescription: nil) + resetDescription: resetDesc) } private static func safePercentInt(from value: Double) -> Int { guard value.isFinite, !value.isNaN else { return 0 } diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index 46ff5e00162..c81d32d3034 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -251,7 +251,60 @@ struct OMPProviderTests { let labels = ProviderDescriptorRegistry.descriptor(for: .omp).presentation .rateWindowLabels(metadata: ProviderDescriptorRegistry.descriptor(for: .omp).metadata, snapshot: filtered) - #expect(labels.secondary == "Monthly") + #expect(labels.secondary.contains("Monthly")) + } + + @Test + func `omp usage parser reflects active usable items in combined capacity and label`() throws { + let json = """ + { + "capacity": { + "google-antigravity": [ + { "window": "5h", "accounts": 4, "usedAccounts": 1.8, "remainingAccounts": 2.2 } + ], + "openai-codex": [ + { "window": "5h", "accounts": 1, "usedAccounts": 0.35, "remainingAccounts": 0.65 } + ], + "xai-oauth": [ + { "window": "5h", "accounts": 1, "usedAccounts": 0.5, "remainingAccounts": 0.5 } + ] + }, + "reports": [ + { + "provider": "google-antigravity", + "metadata": { "email": "user@gmail.com" }, + "limits": [ + { "id": "gemini:5h", "window": { "id": "5h" }, "amount": { "used": 10, "limit": 100 } } + ] + }, + { + "provider": "openai-codex", + "metadata": { "email": "dev@gmail.com" }, + "limits": [ + { "id": "codex:5h", "window": { "id": "5h" }, "amount": { "used": 35, "limit": 100 } } + ] + }, + { + "provider": "xai-oauth", + "metadata": { "email": "grok@gmail.com" }, + "limits": [ + { "id": "xai:5h", "window": { "id": "5h" }, "amount": { "used": 50, "limit": 100 } } + ] + } + ] + } + """ + // Exclude xAI Grok via hiddenItemIDs (simulating unchecking xAI in Settings -> Visible usage items) + let hidden = ["metric:omp-xai-0-7d", "detailSection:xAI Grok"] + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all", hiddenItemIDs: hidden) + let primary = try #require(snapshot.primary) + // Total accounts = 4 (AGY) + 1 (Codex) = 5 accounts (xAI's 1 account is excluded!) + // Total used = 1.8 + 0.35 = 2.15 accounts -> 2.15 / 5 = 43.0% + #expect(abs(primary.usedPercent - 43.0) < 0.001) + #expect(primary.resetDescription == "4 AGY + 1 Codex") + let labels = ProviderDescriptorRegistry.descriptor(for: .omp).presentation + .rateWindowLabels(metadata: ProviderDescriptorRegistry.descriptor(for: .omp).metadata, snapshot: snapshot) + #expect(labels.primary == "4 AGY + 1 Codex") } @Test From 1a00210167cd3faaf5d37abc391cf45403e3ee7e Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 11:37:56 +0200 Subject: [PATCH 8/9] fix(omp): scope history as unavailable, uniquely identify xAI limits, derive xAI cadence, and stabilize section titles --- .../Providers/OMP/OMPProviderDescriptor.swift | 2 +- .../Providers/OMP/OMPUsageParser.swift | 48 +++++++++------ Tests/CodexBarTests/OMPProviderTests.swift | 60 +++++++++++++++++-- 3 files changed, 88 insertions(+), 22 deletions(-) diff --git a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift index 32da0258a72..af67469d3e6 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPProviderDescriptor.swift @@ -40,7 +40,7 @@ public enum OMPProviderDescriptor { pace: ProviderPaceCapability( primary: .session(maximumMinutes: 300, requiresDuration: true), secondary: .weeklyWithDuration), - history: .optIn, + history: .unavailable, presentation: ProviderUsagePresentation( rateWindowLabeler: { metadata, snapshot, _ in let primaryTitle = snapshot.primary?.resetDescription diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index 043c6cb9f76..b005d94b58d 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -92,25 +92,31 @@ public enum OMPUsageParser { if k.contains("antigravity") || k.contains("agy") { let agyMetrics = ["omp-agy-gemini-5h", "omp-agy-gemini-7d", "omp-agy-3p-5h", "omp-agy-3p-7d"] return !hiddenSet.contains("detailSection:Google Antigravity") && + !hiddenSet.contains("detailSection:Google Antigravity Accounts") && !agyMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } } if k.contains("codex") { let codexMetrics = ["omp-codex-0-5h", "omp-codex-0-7d"] return !hiddenSet.contains("detailSection:OpenAI Codex") && + !hiddenSet.contains("detailSection:OpenAI Codex Accounts") && !codexMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } } if k.contains("copilot") { let copilotMetrics = ["omp-copilot-0-chat", "omp-copilot-0-completions"] return !hiddenSet.contains("detailSection:GitHub Copilot") && + !hiddenSet.contains("detailSection:GitHub Copilot Accounts") && !copilotMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } } if k.contains("xai") || k.contains("grok") { - let xaiMetrics = ["omp-xai-0-7d"] - return !hiddenSet.contains("detailSection:xAI Grok") && - !xaiMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } + let hasHiddenXAI = hiddenSet.contains("detailSection:xAI Grok") || + hiddenSet.contains("detailSection:xAI Grok Accounts") || + hiddenSet.contains("metric:omp-xai-0-7d") || + hiddenSet.contains { $0.hasPrefix("metric:omp-xai-") } + return !hasHiddenXAI } if k.contains("anthropic") || k.contains("claude") { - return !hiddenSet.contains("detailSection:Anthropic Claude") + return !hiddenSet.contains("detailSection:Anthropic Claude") && + !hiddenSet.contains("detailSection:Anthropic Claude Accounts") } return true } @@ -299,10 +305,20 @@ public enum OMPUsageParser { for limit in report.limits ?? [] { guard let norm = self.normalizeAmount(limit) else { continue } let resetsAt = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } + let lName = limit.label ?? "xAI Grok" + let winId = (limit.window?.id ?? "").lowercased() + let winMinutes = limit.window?.durationMs.flatMap { dur -> Int? in + guard dur.isFinite, dur > 0, dur < Double(Int.max) * 60_000 else { return nil } + return Int(dur / (60 * 1000)) + } ?? (winId.contains("5h") ? 300 : (winId.contains("month") ? 43200 : 10080)) extraRateWindows.append(NamedRateWindow( - id: "omp-xai-\(reportIndex)-7d", - title: "xAI Grok", - window: RateWindow(usedPercent: norm.usedPercent, windowMinutes: 10080, resetsAt: resetsAt, resetDescription: nil))) + id: "omp-xai-\(reportIndex)-\(limit.id)", + title: lName, + window: RateWindow( + usedPercent: norm.usedPercent, + windowMinutes: winMinutes, + resetsAt: resetsAt, + resetDescription: nil))) } } @@ -499,13 +515,10 @@ public enum OMPUsageParser { let agyCapCount = response.capacity?["google-antigravity"]?.first.map { Int($0.accounts) } ?? 0 let agyWithoutUsageCount = response.accountsWithoutUsage?.filter { $0.provider == "google-antigravity" }.count ?? 0 let totalAgyAccounts = max(agyReports.count + agyWithoutUsageCount, agyCapCount) - let agyTitle: String = if totalAgyAccounts > agyReports.count, agyReports.count > 0 { - "Google Antigravity (\(totalAgyAccounts) accounts)" - } else if agyReports.count > 0 { - "Google Antigravity (\(agyReports.count) accounts)" - } else { - "Google Antigravity" + if totalAgyAccounts > 0, let countRow = self.makeRow(id: "agy-account-count", label: "Connected Accounts", value: "\(totalAgyAccounts)") { + agyRows.insert(countRow, at: 0) } + let agyTitle = "Google Antigravity" if let agySection = self.makeSection(title: agyTitle, rows: agyRows) { detailSections.append(agySection) } @@ -576,7 +589,7 @@ public enum OMPUsageParser { let lName = limit.label ?? "Weekly Credits" let progress = try? ProviderDetailSection.Row.Progress(used: max(0.1, norm.usedPercent), total: 100) if let row = self.makeRow( - id: "xai-\(lName)", + id: "xai-\(limit.id)", label: lName, value: "\(self.safePercentInt(from: norm.remainingPercent))% left" + (resetDesc.map { " (\($0))" } ?? ""), secondaryValue: email, @@ -766,9 +779,10 @@ public enum OMPUsageParser { $0.provider.lowercased() == target || (target == "agy" && $0.provider == "google-antigravity") }.count ?? 0 let totalAccounts = max(matchingReports.count + withoutUsageCount, providerCapAccounts) - let sectionTitle = totalAccounts > matchingReports.count - ? "\(self.displayNameForProvider(target)) Accounts (\(totalAccounts))" - : "\(self.displayNameForProvider(target)) Accounts (\(matchingReports.count))" + if totalAccounts > matchingReports.count, let countRow = self.makeRow(id: "sub-account-count", label: "Connected Accounts", value: "\(totalAccounts)") { + acctRows.insert(countRow, at: 0) + } + let sectionTitle = "\(self.displayNameForProvider(target)) Accounts" if let acctSection = self.makeSection(title: sectionTitle, rows: acctRows) { detailSections.append(acctSection) } diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index c81d32d3034..e111a9d4f2b 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -295,7 +295,7 @@ struct OMPProviderTests { } """ // Exclude xAI Grok via hiddenItemIDs (simulating unchecking xAI in Settings -> Visible usage items) - let hidden = ["metric:omp-xai-0-7d", "detailSection:xAI Grok"] + let hidden = ["metric:omp-xai-0-xai:5h", "detailSection:xAI Grok"] let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all", hiddenItemIDs: hidden) let primary = try #require(snapshot.primary) // Total accounts = 4 (AGY) + 1 (Codex) = 5 accounts (xAI's 1 account is excluded!) @@ -410,7 +410,7 @@ struct OMPProviderTests { """ let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all") let agySection = try #require(snapshot.details.first { $0.title?.contains("Antigravity") == true }) - let row = try #require(agySection.rows.first) + let row = try #require(agySection.rows.first { $0.secondaryValue != nil }) // The breakdown in secondaryValue must preserve the full email "testuser@gmail.com" so email redactors match it let secondary = try #require(row.secondaryValue) #expect(secondary.contains("testuser@gmail.com")) @@ -441,9 +441,61 @@ struct OMPProviderTests { // Snapshot identity must remain neutral across multi-account aggregates #expect(snapshot.identity?.accountEmail == nil) #expect(snapshot.identity?.accountID == "google-antigravity") - // Section title must reflect total capacity accounts (4), not just 1 report + // Section title is stable and total capacity accounts (4) are presented separately let agySection = try #require(snapshot.details.first { $0.title?.contains("Accounts") == true }) - #expect(agySection.title?.contains("(4)") == true) + #expect(agySection.title == "Google Antigravity Accounts") + #expect(agySection.rows.first?.label == "Connected Accounts") + #expect(agySection.rows.first?.value == "4") + } + + @Test + func `omp usage parser gives each xAI limit a unique ID and derives window cadence`() throws { + let json = """ + { + "reports": [ + { + "provider": "xai-oauth", + "metadata": { "email": "grok@gmail.com" }, + "limits": [ + { + "id": "xai:grokbot:5h", + "label": "GrokBot", + "window": { "id": "5h", "durationMs": 18000000 }, + "amount": { "used": 10, "limit": 100 } + }, + { + "id": "xai:supergrok:weekly", + "label": "SuperGrok Weekly Credits", + "window": { "id": "weekly", "durationMs": 604800000 }, + "amount": { "used": 25, "limit": 100 } + } + ] + } + ] + } + """ + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all") + let extra = try #require(snapshot.extraRateWindows) + let grokBot = try #require(extra.first { $0.id == "omp-xai-0-xai:grokbot:5h" }) + #expect(grokBot.title == "GrokBot") + #expect(grokBot.window.windowMinutes == 300) + #expect(abs(grokBot.window.usedPercent - 10.0) < 0.001) + + let superGrok = try #require(extra.first { $0.id == "omp-xai-0-xai:supergrok:weekly" }) + #expect(superGrok.title == "SuperGrok Weekly Credits") + #expect(superGrok.window.windowMinutes == 10080) + #expect(abs(superGrok.window.usedPercent - 25.0) < 0.001) + + // Detail rows must use unique limit IDs + let xaiSection = try #require(snapshot.details.first { $0.title == "xAI Grok" }) + #expect(xaiSection.rows.contains { $0.id == "xai-xai:grokbot:5h" }) + #expect(xaiSection.rows.contains { $0.id == "xai-xai:supergrok:weekly" }) + } + + @Test + func `omp provider descriptor marks history as unavailable`() { + let descriptor = ProviderDescriptorRegistry.descriptor(for: .omp) + #expect(descriptor.history == .unavailable) } @Test From c34ba875d98129a243b07df8993ca726b989c215 Mon Sep 17 00:00:00 2001 From: Renan Rocha Date: Sun, 18 Oct 2026 11:58:34 +0200 Subject: [PATCH 9/9] fix(omp): redact non-email account IDs, preserve partial xAI visibility, and recompute on visibility changes --- .../Config/ProviderConfig+FetchIdentity.swift | 5 +- .../ProviderUsageItemVisibility.swift | 4 +- .../Providers/OMP/OMPUsageParser.swift | 51 +++++++++++---- Tests/CodexBarTests/OMPProviderTests.swift | 62 +++++++++++++++++++ 4 files changed, 108 insertions(+), 14 deletions(-) diff --git a/Sources/CodexBar/Config/ProviderConfig+FetchIdentity.swift b/Sources/CodexBar/Config/ProviderConfig+FetchIdentity.swift index 602780f8034..0a7ce389252 100644 --- a/Sources/CodexBar/Config/ProviderConfig+FetchIdentity.swift +++ b/Sources/CodexBar/Config/ProviderConfig+FetchIdentity.swift @@ -5,7 +5,10 @@ extension ProviderConfig { var fetchIdentityConfig: Self { var copy = self copy.accentColor = nil - copy.hiddenUsageItemIDs = nil + // Provider-specific by design: OMP usage aggregation dynamically reflects active visible items. + if self.id != ProviderInstanceID.omp { + copy.hiddenUsageItemIDs = nil + } copy.modelRateLimits = nil if let data = copy.tokenAccounts { copy.tokenAccounts = ProviderTokenAccountData( diff --git a/Sources/CodexBar/ProviderUsageItemVisibility.swift b/Sources/CodexBar/ProviderUsageItemVisibility.swift index 752c55891bc..cf46c8c007b 100644 --- a/Sources/CodexBar/ProviderUsageItemVisibility.swift +++ b/Sources/CodexBar/ProviderUsageItemVisibility.swift @@ -284,7 +284,9 @@ extension SettingsStore { for provider: UsageProvider) { let rawIDs = hiddenItemIDs.map(\.rawValue).sorted() - self.updateProviderConfig(provider: provider, affectsBackgroundWork: false) { entry in + // Provider-specific by design: OMP usage aggregation dynamically reflects active visible items. + let affectsBackground = provider == .omp + self.updateProviderConfig(provider: provider, affectsBackgroundWork: affectsBackground) { entry in entry.hiddenUsageItemIDs = rawIDs } } diff --git a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift index b005d94b58d..3fbb6aede0c 100644 --- a/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift +++ b/Sources/CodexBarCore/Providers/OMP/OMPUsageParser.swift @@ -82,10 +82,27 @@ public enum OMPUsageParser { if k.contains("anthropic") || k.contains("claude") { return "Claude" } return key } + // Provider-specific by design: Formats non-email account IDs as verifiable email patterns for privacy redactor. + private static func redactableAccountID(_ rawID: String, provider: String) -> String { + if rawID.contains("@") { + return rawID + } + let domain: String = if provider.contains("copilot") || provider.contains("github") { + "github.com" + } else if provider.contains("codex") || provider.contains("openai") { + "openai.com" + } else if provider.contains("xai") || provider.contains("grok") { + "x.ai" + } else { + "omp.local" + } + return "\(rawID)@\(domain)" + } // Provider-specific by design: This checks whether a subprovider's metrics or sections are hidden. private static func isProviderActive( key: String, + xaiReports: [OMPAccountReport] = [], hiddenSet: Set) -> Bool { let k = key.lowercased() @@ -108,11 +125,16 @@ public enum OMPUsageParser { !copilotMetrics.allSatisfy { hiddenSet.contains("metric:\($0)") } } if k.contains("xai") || k.contains("grok") { - let hasHiddenXAI = hiddenSet.contains("detailSection:xAI Grok") || - hiddenSet.contains("detailSection:xAI Grok Accounts") || - hiddenSet.contains("metric:omp-xai-0-7d") || - hiddenSet.contains { $0.hasPrefix("metric:omp-xai-") } - return !hasHiddenXAI + if hiddenSet.contains("detailSection:xAI Grok") || hiddenSet.contains("detailSection:xAI Grok Accounts") { + return false + } + let xaiMetricIDs = xaiReports.enumerated().flatMap { reportIndex, report in + (report.limits ?? []).map { "metric:omp-xai-\(reportIndex)-\($0.id)" } + } + if !xaiMetricIDs.isEmpty { + return !xaiMetricIDs.allSatisfy { hiddenSet.contains($0) } + } + return !hiddenSet.contains("metric:omp-xai-0-7d") } if k.contains("anthropic") || k.contains("claude") { return !hiddenSet.contains("detailSection:Anthropic Claude") && @@ -351,7 +373,7 @@ public enum OMPUsageParser { var contributingWeeklyProviders: [String: Int] = [:] for (providerKey, windows) in response.capacity ?? [:] { - guard self.isProviderActive(key: providerKey, hiddenSet: hiddenSet) else { continue } + guard self.isProviderActive(key: providerKey, xaiReports: xaiReports, hiddenSet: hiddenSet) else { continue } for cap in windows { let winName = cap.window.lowercased() @@ -376,7 +398,7 @@ public enum OMPUsageParser { var minWeeklyResetDate: Date? for report in response.reports { - guard self.isProviderActive(key: report.provider, hiddenSet: hiddenSet) else { continue } + guard self.isProviderActive(key: report.provider, xaiReports: xaiReports, hiddenSet: hiddenSet) else { continue } for limit in report.limits ?? [] { guard let window = limit.window, let resetsAtMs = window.resetsAt, @@ -526,7 +548,8 @@ public enum OMPUsageParser { // Section: GitHub Copilot var copilotRows: [ProviderDetailSection.Row] = [] for report in copilotReports { - let accountID = report.metadata?["accountId"] ?? "account" + let rawAccountID = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let accountID = self.redactableAccountID(rawAccountID, provider: "github-copilot") for limit in report.limits ?? [] { let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } let resetDesc = resets.map { UsageFormatter.resetDescription(from: $0) } @@ -554,7 +577,8 @@ public enum OMPUsageParser { // Section: OpenAI Codex var codexRows: [ProviderDetailSection.Row] = [] for report in codexReports { - let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let rawEmail = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let email = self.redactableAccountID(rawEmail, provider: "openai-codex") let plan = report.metadata?["planType"].map { " (\($0))" } ?? "" for limit in report.limits ?? [] { guard let norm = self.normalizeAmount(limit) else { continue } @@ -581,7 +605,8 @@ public enum OMPUsageParser { // Section: xAI Grok var xaiRows: [ProviderDetailSection.Row] = [] for report in xaiReports { - let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let rawEmail = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let email = self.redactableAccountID(rawEmail, provider: "xai-oauth") for limit in report.limits ?? [] { guard let norm = self.normalizeAmount(limit) else { continue } let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } @@ -607,7 +632,8 @@ public enum OMPUsageParser { // Section: Anthropic Claude var anthropicRows: [ProviderDetailSection.Row] = [] for report in anthropicReports { - let email = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let rawEmail = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let email = self.redactableAccountID(rawEmail, provider: "anthropic") for limit in report.limits ?? [] { guard let norm = self.normalizeAmount(limit) else { continue } let resets = limit.window?.resetsAt.flatMap { $0 > nowMs ? Date(timeIntervalSince1970: $0 / 1000) : nil } @@ -755,9 +781,10 @@ public enum OMPUsageParser { if !matchingReports.isEmpty { var acctRows: [ProviderDetailSection.Row] = [] for report in matchingReports { - let accountID = report.metadata?["email"] + let rawAccountID = report.metadata?["email"] ?? report.metadata?["accountId"] ?? "account" + let accountID = self.redactableAccountID(rawAccountID, provider: target) var summaries: [String] = [] for limit in report.limits ?? [] { guard let norm = self.normalizeAmount(limit) else { continue } diff --git a/Tests/CodexBarTests/OMPProviderTests.swift b/Tests/CodexBarTests/OMPProviderTests.swift index e111a9d4f2b..356d2f68933 100644 --- a/Tests/CodexBarTests/OMPProviderTests.swift +++ b/Tests/CodexBarTests/OMPProviderTests.swift @@ -492,6 +492,68 @@ struct OMPProviderTests { #expect(xaiSection.rows.contains { $0.id == "xai-xai:supergrok:weekly" }) } + @Test + func `omp usage parser formats non-email account IDs as redactable email patterns`() throws { + let json = """ + { + "reports": [ + { + "provider": "github-copilot", + "metadata": { "accountId": "octocat" }, + "limits": [ + { "id": "copilot:chat", "amount": { "used": 5, "limit": 100 } } + ] + } + ] + } + """ + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all") + let section = try #require(snapshot.details.first { $0.title == "GitHub Copilot" }) + let row = try #require(section.rows.first) + let secondary = try #require(row.secondaryValue) + // Must be formatted as octocat@github.com so email redactors can match and redact it + #expect(secondary.contains("octocat@github.com")) + #expect(!secondary.hasPrefix("octocat ·")) + } + + @Test + func `omp usage parser keeps xAI active when one limit is hidden but another remains visible`() throws { + let json = """ + { + "capacity": { + "xai-oauth": [ + { "window": "7d", "accounts": 1, "usedAccounts": 0.25, "remainingAccounts": 0.75 } + ] + }, + "reports": [ + { + "provider": "xai-oauth", + "limits": [ + { "id": "xai:grokbot", "window": { "id": "7d" }, "amount": { "used": 10, "limit": 100 } }, + { "id": "xai:supergrok", "window": { "id": "7d" }, "amount": { "used": 25, "limit": 100 } } + ] + } + ] + } + """ + // Hide only grokbot, keeping supergrok visible + let hidden = ["metric:omp-xai-0-xai:grokbot"] + let snapshot = try OMPUsageParser.parse(jsonString: json, subprovider: "all", hiddenItemIDs: hidden) + // xAI must remain active because supergrok is still visible! + let secondary = try #require(snapshot.secondary) + #expect(secondary.usedPercent == 25.0) + #expect(secondary.resetDescription?.contains("1 Grok") == true) + } + + @Test + func `omp provider config preserves hiddenUsageItemIDs for fetch identity`() { + let config = ProviderConfig(id: ProviderInstanceID.omp, hiddenUsageItemIDs: ["metric:primary"]) + #expect(config.fetchIdentityConfig.hiddenUsageItemIDs == ["metric:primary"]) + + let other = ProviderConfig(id: ProviderInstanceID.codex, hiddenUsageItemIDs: ["metric:primary"]) + #expect(other.fetchIdentityConfig.hiddenUsageItemIDs == nil) + } + @Test func `omp provider descriptor marks history as unavailable`() { let descriptor = ProviderDescriptorRegistry.descriptor(for: .omp)