Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,9 @@ struct ProvidersPane: View {
}

func providerErrorDisplay(_ provider: UsageProvider) -> ProviderErrorDisplay? {
guard let full = self.store.error(for: provider), !full.isEmpty else { return nil }
guard let full = self.store.error(for: provider) ?? self.store.diagnostic(for: provider),
!full.isEmpty
else { return nil }
let preview = self.store.userFacingError(for: provider) ?? full
return ProviderErrorDisplay(
preview: self.truncated(preview, prefix: ""),
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/UsageStore+Accessors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,18 @@ extension UsageStore {
self.errors[provider]
}

func diagnostic(for provider: UsageProvider) -> String? {
self.diagnostics[provider]
}

func userFacingError(for provider: UsageProvider) -> String? {
if let raw = self.errors[provider] {
guard provider == .codex else { return raw }
return CodexUIErrorMapper.userFacingMessage(raw)
}
if let diagnostic = self.diagnostics[provider] {
return diagnostic
}
return self.unavailableMessage(for: provider)
}

Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/UsageStore+BackgroundRefresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ extension UsageStore {
self.snapshots.removeValue(forKey: provider)
self.lastKnownResetSnapshots.removeValue(forKey: provider)
self.errors[provider] = nil
self.diagnostics[provider] = nil
if provider == .gemini {
self.clearGeminiConsumerTierDeprecationObservation()
}
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/UsageStore+CodexResetCredits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ extension ProviderFetchOutcome {
sourceLabel: result.sourceLabel,
strategyID: result.strategyID,
strategyKind: result.strategyKind,
diagnostic: result.diagnostic,
claudeOAuthKeychainPersistentRefHash: result.claudeOAuthKeychainPersistentRefHash,
claudeOAuthHistoryOwnerIdentifier: result.claudeOAuthHistoryOwnerIdentifier,
claudeOAuthKeychainCredentialMismatch: result.claudeOAuthKeychainCredentialMismatch,
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/UsageStore+Refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ extension UsageStore {
}
}

self.diagnostics[provider] = nil
let claudeAuthStateBeforeFetch = provider == .claude
? await Self.captureClaudeRefreshAuthState(invalidateCredentialsFile: true)
: nil
Expand Down Expand Up @@ -554,6 +555,7 @@ extension UsageStore {
}
self.lastSourceLabels[provider] = result.sourceLabel
self.errors[provider] = nil
self.diagnostics[provider] = result.diagnostic
if let tokenAccount = currentTokenAccount {
self.cacheTokenAccountSnapshot(
provider: provider,
Expand Down Expand Up @@ -957,6 +959,7 @@ extension UsageStore {
let shouldNotifyPermissionPrompt = Self.isPermissionPromptWaiting(error)
await MainActor.run {
guard self.isCurrentProviderRefreshGeneration(provider, generation: context.generation) else { return }
self.diagnostics[provider] = nil
if provider == .gemini, Self.isGeminiConsumerTierDeprecationError(error) {
// This is a durable provider migration signal, not a transient fetch failure.
// Surface it immediately so a cached snapshot cannot hide the required handoff.
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extension UsageStore {
var menuObservationToken: Int {
_ = self.snapshots
_ = self.errors
_ = self.diagnostics
_ = self.knownLimitsAvailabilityByProvider
_ = self.lastSourceLabels
_ = self.lastFetchAttempts
Expand Down Expand Up @@ -44,6 +45,7 @@ extension UsageStore {
var iconObservationToken: Int {
_ = self.snapshots
_ = self.errors
_ = self.diagnostics
_ = self.knownLimitsAvailabilityByProvider
_ = self.credits
_ = self.lastCreditsError
Expand Down Expand Up @@ -153,6 +155,7 @@ final class UsageStore {

var snapshots: [UsageProvider: UsageSnapshot] = [:]
var errors: [UsageProvider: String] = [:]
var diagnostics: [UsageProvider: String] = [:]
var geminiObservedConsumerTierDeprecation = false
var knownLimitsAvailabilityByProvider: [UsageProvider: UsageLimitsAvailability] = [:]
var lastSourceLabels: [UsageProvider: String] = [:]
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBarCLI/CLIPayloads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct ProviderPayload: Encodable {
let credits: CreditsSnapshot?
let antigravityPlanInfo: AntigravityPlanInfoSummary?
let openaiDashboard: OpenAIDashboardSnapshot?
let diagnostic: String?
let error: ProviderErrorPayload?
let pace: ProviderPacePayload?

Expand All @@ -28,6 +29,7 @@ struct ProviderPayload: Encodable {
case credits
case antigravityPlanInfo
case openaiDashboard
case diagnostic
case error
case pace
}
Expand All @@ -44,6 +46,7 @@ struct ProviderPayload: Encodable {
antigravityPlanInfo: AntigravityPlanInfoSummary?,
openaiDashboard: OpenAIDashboardSnapshot?,
error: ProviderErrorPayload?,
diagnostic: String? = nil,
pace: ProviderPacePayload? = nil)
{
self.provider = provider.rawValue
Expand All @@ -56,6 +59,7 @@ struct ProviderPayload: Encodable {
self.credits = credits
self.antigravityPlanInfo = antigravityPlanInfo
self.openaiDashboard = openaiDashboard
self.diagnostic = diagnostic
self.error = error
self.pace = pace
}
Expand All @@ -72,6 +76,7 @@ struct ProviderPayload: Encodable {
antigravityPlanInfo: AntigravityPlanInfoSummary?,
openaiDashboard: OpenAIDashboardSnapshot?,
error: ProviderErrorPayload?,
diagnostic: String? = nil,
pace: ProviderPacePayload? = nil)
{
self.provider = providerID
Expand All @@ -84,6 +89,7 @@ struct ProviderPayload: Encodable {
self.credits = credits
self.antigravityPlanInfo = antigravityPlanInfo
self.openaiDashboard = openaiDashboard
self.diagnostic = diagnostic
self.error = error
self.pace = pace
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/CodexBarCLI/CLIUsageCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ private struct UsageSuccessRenderInput {
let dashboard: OpenAIDashboardSnapshot?
let effectiveSourceMode: ProviderSourceMode
let command: UsageCommandContext
let diagnostic: String?
let notes: [String]
}

Expand Down Expand Up @@ -287,6 +288,7 @@ extension CodexBarCLI {
credits: CreditsSnapshot?,
antigravityPlanInfo: AntigravityPlanInfoSummary?,
dashboard: OpenAIDashboardSnapshot?,
diagnostic: String?,
weeklyWorkDays: Int?) -> ProviderPayload
{
ProviderPayload(
Expand All @@ -301,6 +303,7 @@ extension CodexBarCLI {
antigravityPlanInfo: antigravityPlanInfo,
openaiDashboard: dashboard,
error: nil,
diagnostic: diagnostic,
pace: CLIRenderer.providerPacePayload(provider: provider, snapshot: usage, weeklyWorkDays: weeklyWorkDays))
}

Expand Down Expand Up @@ -354,6 +357,7 @@ extension CodexBarCLI {
credits: input.credits,
antigravityPlanInfo: input.antigravityPlanInfo,
dashboard: input.dashboard,
diagnostic: input.diagnostic,
weeklyWorkDays: input.command.weeklyWorkDays))
}
}
Expand Down Expand Up @@ -458,7 +462,7 @@ extension CodexBarCLI {
let notes = Self.usageTextNotes(
provider: provider,
sourceMode: effectiveSourceMode,
resolvedSourceLabel: source)
resolvedSourceLabel: source) + (result.diagnostic.map { [$0] } ?? [])

Self.appendSuccessRenderOutput(
UsageSuccessRenderInput(
Expand All @@ -474,6 +478,7 @@ extension CodexBarCLI {
dashboard: dashboard,
effectiveSourceMode: effectiveSourceMode,
command: command,
diagnostic: result.diagnostic,
notes: notes),
output: &output)
case let .failure(error):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ struct CodexOAuthFetchStrategy: ProviderFetchStrategy {
dashboard: oauthResult.dashboard,
sourceLabel: oauthResult.sourceLabel,
strategyID: oauthResult.strategyID,
strategyKind: oauthResult.strategyKind)
strategyKind: oauthResult.strategyKind,
diagnostic: oauthResult.diagnostic)
}

private static func fetchResetCreditsIfRequested(
Expand Down
15 changes: 14 additions & 1 deletion Sources/CodexBarCore/Providers/Grok/GrokAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public struct GrokCredentials: Sendable {
public let firstName: String?
public let lastName: String?
public let teamId: String?
/// The principal scope reported by Grok's cached OIDC credential, when available.
/// Keep this optional because older auth.json entries do not include it.
public let principalType: String?
public let oidcIssuer: String?
public let oidcClientId: String?
public let expiresAt: Date?
Expand All @@ -25,6 +28,7 @@ public struct GrokCredentials: Sendable {
firstName: String?,
lastName: String?,
teamId: String?,
principalType: String? = nil,
oidcIssuer: String?,
oidcClientId: String?,
expiresAt: Date?,
Expand All @@ -39,6 +43,7 @@ public struct GrokCredentials: Sendable {
self.firstName = firstName
self.lastName = lastName
self.teamId = teamId
self.principalType = principalType
self.oidcIssuer = oidcIssuer
self.oidcClientId = oidcClientId
self.expiresAt = expiresAt
Expand All @@ -56,6 +61,11 @@ public struct GrokCredentials: Sendable {
return Date() >= expiresAt
}

public var isTeamPrincipal: Bool {
self.principalType?.trimmingCharacters(in: .whitespacesAndNewlines)
.caseInsensitiveCompare("team") == .orderedSame
}

public var loginMethod: String? {
switch self.authMode?.lowercased() {
case "oidc": "SuperGrok"
Expand Down Expand Up @@ -147,6 +157,7 @@ public enum GrokCredentialsStore {
firstName: (entry["first_name"] as? String)?.nilIfEmpty,
lastName: (entry["last_name"] as? String)?.nilIfEmpty,
teamId: (entry["team_id"] as? String)?.nilIfEmpty,
principalType: (entry["principal_type"] as? String)?.nilIfEmpty,
oidcIssuer: (entry["oidc_issuer"] as? String)?.nilIfEmpty,
oidcClientId: (entry["oidc_client_id"] as? String)?.nilIfEmpty,
expiresAt: Self.parseDate(entry["expires_at"]),
Expand Down Expand Up @@ -175,7 +186,9 @@ public enum GrokCredentialsStore {
guard let value = raw as? String, !value.isEmpty else { return nil }
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: value) { return date }
if let date = formatter.date(from: value) {
return date
}
formatter.formatOptions = [.withInternetDateTime]
return formatter.date(from: value)
}
Expand Down
52 changes: 47 additions & 5 deletions Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ public enum GrokProviderDescriptor {
private static func primaryLabel(duration seconds: TimeInterval) -> String? {
guard seconds > 3600 else { return nil }
let days = Int((seconds / 86400).rounded(.toNearestOrAwayFromZero))
if (4...12).contains(days) { return "Weekly" }
if (20...45).contains(days) { return "Monthly" }
if (4...12).contains(days) {
return "Weekly"
}
if (20...45).contains(days) {
return "Monthly"
}
return nil
}
}
Expand All @@ -90,7 +94,8 @@ struct GrokCLIFetchStrategy: ProviderFetchStrategy {
let snap = try await probe.fetch(env: context.env)
return self.makeResult(
usage: snap.toUsageSnapshot(),
sourceLabel: "grok-cli")
sourceLabel: "grok-cli",
diagnostic: snap.diagnostic)
}

func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool {
Expand All @@ -101,6 +106,10 @@ struct GrokCLIFetchStrategy: ProviderFetchStrategy {
struct GrokWebFetchStrategy: ProviderFetchStrategy {
let id: String = "grok.web"
let kind: ProviderFetchKind = .web
typealias WebBillingFetch = @Sendable () async throws -> (
snapshot: GrokWebBillingSnapshot,
sourceLabel: String,
authenticatedByAuthFile: Bool)

static func canImportBrowserCookies(runtime: ProviderRuntime, env: [String: String]) -> Bool {
runtime == .app || env["CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT"] == "1"
Expand All @@ -118,7 +127,36 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let (webBilling, sourceLabel, authenticatedByAuthFile) = try await self.fetchWebBilling(context: context)
try await self.fetch(context, webBilling: { [self] in
try await self.fetchWebBilling(context: context)
})
}

func fetch(
_ context: ProviderFetchContext,
webBilling fetchWebBilling: @escaping WebBillingFetch) async throws -> ProviderFetchResult
{
let webBilling: GrokWebBillingSnapshot
let sourceLabel: String
let authenticatedByAuthFile: Bool
do {
(webBilling, sourceLabel, authenticatedByAuthFile) = try await fetchWebBilling()
} catch GrokWebBillingError.teamUsageUnsupported {
guard let authState = try? GrokCredentialsStore.load(env: context.env),
!authState.isExpired,
authState.isTeamPrincipal
else {
throw GrokWebBillingError.teamUsageUnsupported
}
let identitySnapshot = GrokStatusProbe.identityOnlySnapshot(
credentials: authState,
localSummary: GrokLocalSessionScanner.summarize(env: context.env),
cliVersion: GrokStatusProbe.detectVersion(env: context.env))
return self.makeResult(
usage: identitySnapshot.toUsageSnapshot(),
sourceLabel: "grok-web",
diagnostic: identitySnapshot.diagnostic)
}
let credentials = Self.credentialsForWebBillingSnapshot(
credentials: try? GrokCredentialsStore.load(env: context.env),
authenticatedByAuthFile: authenticatedByAuthFile)
Expand Down Expand Up @@ -198,17 +236,21 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
credentials: credentials)
}
var lastError: Error?
var teamUsageUnsupportedError: Error?
for session in sessions {
for authCredentials in Self.cookieAuthAttempts(credentials: credentials) {
do {
let snapshot = try await fetchSnapshot(session.cookieHeader, authCredentials)
return (snapshot, session.sourceLabel)
} catch {
if case GrokWebBillingError.teamUsageUnsupported = error {
teamUsageUnsupportedError = error
}
lastError = error
}
}
}
throw lastError ?? GrokWebBillingError.missingCredentials
throw teamUsageUnsupportedError ?? lastError ?? GrokWebBillingError.missingCredentials
}

static func cookieAuthAttempts(credentials: GrokCredentials?) -> [GrokCredentials?] {
Expand Down
Loading