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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Cost history chart: localize the Projects, Conversations, session, and standard/fast mode labels that previously stayed English for all 23 app languages (#2983). Thanks @Yuxin-Qiao!
### Added
- Usage & Spend: aggregate per-project spend into ranked, window-scoped rows and carry project/session breakdowns through the cached Codex prefill so the dashboard and the menu chart agree on project data (#2984). Thanks @Yuxin-Qiao!
- Grok: show SuperGrok Heavy from the CLI settings `subscription_tier_display` instead of labeling every OIDC login as SuperGrok (#2991). Thanks @olddonkey!

## 0.51.0 — 2026-08-16

Expand Down
62 changes: 62 additions & 0 deletions Sources/CodexBarCore/Providers/Grok/GrokCLISettingsFetcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

/// Reads the Grok CLI settings envelope. Plan names live here (`subscription_tier_display`),
/// not on `/v1/billing?format=credits`.
enum GrokCLISettingsFetcher {
static let defaultEndpoint = URL(string: "https://cli-chat-proxy.grok.com/v1/settings")!
static let requestTimeoutSeconds: TimeInterval = 2

static func endpoint(fromBilling billing: URL) -> URL {
var components = URLComponents(url: billing, resolvingAgainstBaseURL: false)
components?.path = "/v1/settings"
components?.query = nil
return components?.url ?? self.defaultEndpoint
}

static func subscriptionTierDisplay(
credentials: GrokCredentials,
session transport: any ProviderHTTPTransport,
endpoint: URL = Self.defaultEndpoint) async throws -> String?
{
guard !credentials.isExpired else { return nil }

var request = URLRequest(url: endpoint)
request.httpMethod = "GET"
request.timeoutInterval = Self.requestTimeoutSeconds
request.setValue("Bearer \(credentials.accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("xai-grok-cli", forHTTPHeaderField: "x-xai-token-auth")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("CodexBar", forHTTPHeaderField: "User-Agent")

let response: ProviderHTTPResponse
do {
response = try await transport.response(for: request)
} catch is CancellationError {
throw CancellationError()
} catch let error as URLError where error.code == .cancelled {
throw error
} catch {
return nil
}
guard response.statusCode == 200 else { return nil }
return self.parse(response.data)
}

static func parse(_ data: Data) -> String? {
struct SettingsResponse: Decodable {
let subscriptionTierDisplay: String?

enum CodingKeys: String, CodingKey {
case subscriptionTierDisplay = "subscription_tier_display"
}
}

guard let response = try? JSONDecoder().decode(SettingsResponse.self, from: data) else {
return nil
}
return GrokPlan.displayName(from: response.subscriptionTierDisplay)
}
}
21 changes: 16 additions & 5 deletions Sources/CodexBarCore/Providers/Grok/GrokCreditsProxyFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,36 @@ public enum GrokCreditsProxyFetcher {
throw GrokWebBillingError.parseFailed
}

let subscriptionTier = GrokPlan.displayName(from: config.subscriptionTier ?? response.subscriptionTier)
let resetsAt = config.currentPeriod?.end.flatMap(Self.parseISO8601)
?? config.billingPeriodEnd.flatMap(Self.parseISO8601)

if let percent = config.creditUsagePercent, percent.isFinite {
return GrokWebBillingSnapshot(
usedPercent: min(100, max(0, percent)),
resetsAt: resetsAt)
resetsAt: resetsAt,
subscriptionTier: subscriptionTier)
}

if let cap = config.onDemandCap?.val,
cap > 0,
let used = config.onDemandUsed?.val
{
let percent = min(100, max(0, used / cap * 100))
return GrokWebBillingSnapshot(usedPercent: percent, resetsAt: resetsAt)
return GrokWebBillingSnapshot(
usedPercent: percent,
resetsAt: resetsAt,
subscriptionTier: subscriptionTier)
}

guard resetsAt != nil else {
throw GrokWebBillingError.parseFailed
if resetsAt != nil {
return GrokWebBillingSnapshot(
usedPercent: 0,
resetsAt: resetsAt,
subscriptionTier: subscriptionTier)
}
return GrokWebBillingSnapshot(usedPercent: 0, resetsAt: resetsAt)

throw GrokWebBillingError.parseFailed
}

private static func parseISO8601(_ raw: String) -> Date? {
Expand All @@ -87,6 +96,7 @@ public enum GrokCreditsProxyFetcher {

private struct CreditsResponse: Decodable {
let config: CreditsConfig?
let subscriptionTier: String?
}

private struct CreditsConfig: Decodable {
Expand All @@ -95,6 +105,7 @@ public enum GrokCreditsProxyFetcher {
let billingPeriodEnd: String?
let onDemandCap: CreditsAmount?
let onDemandUsed: CreditsAmount?
let subscriptionTier: String?
}

private struct CurrentPeriod: Decodable {
Expand Down
28 changes: 28 additions & 0 deletions Sources/CodexBarCore/Providers/Grok/GrokPlan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

/// Grok consumer plan labels. OIDC only tells CodexBar that the user signed in as SuperGrok;
/// the billed tier (SuperGrok vs SuperGrok Heavy) comes from CLI settings
/// `subscription_tier_display`.
public enum GrokPlan: Sendable {
/// Prefer the billing `subscriptionTier`, then the OIDC login-method fallback.
public static func loginMethod(subscriptionTier: String?, credentials: GrokCredentials?) -> String? {
self.displayName(from: subscriptionTier) ?? credentials?.loginMethod
}

public static func displayName(from raw: String?) -> String? {
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return nil }
switch self.compactToken(trimmed) {
case "supergrokheavy", "heavy":
return "SuperGrok Heavy"
case "supergrok":
return "SuperGrok"
default:
return trimmed
}
}

private static func compactToken(_ raw: String) -> String {
raw.lowercased().filter(\.isLetter)
}
}
37 changes: 27 additions & 10 deletions Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
snapshot: GrokWebBillingSnapshot,
sourceLabel: String,
authenticatedByAuthFile: Bool)
typealias SettingsTierFetch = @Sendable (GrokCredentials?) async throws -> String?

/// Browser-cookie import must stay limited to surfaces where a person explicitly asked for it:
/// the menu-bar app runtime, a `userInitiated` interaction (set only by explicit refresh
Expand Down Expand Up @@ -190,42 +191,58 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {

func fetch(
_ context: ProviderFetchContext,
webBilling fetchWebBilling: @escaping WebBillingFetch) async throws -> ProviderFetchResult
webBilling fetchWebBilling: @escaping WebBillingFetch,
settingsTier loadSettingsTier: SettingsTierFetch? = nil) async throws -> ProviderFetchResult
{
let authCredentials = (try? GrokCredentialsStore.load(env: context.env)).flatMap { credentials in
credentials.isExpired ? nil : credentials
}
let resolveSettingsTier = loadSettingsTier ?? { credentials in
try await GrokStatusProbe.loadSettingsTier(credentials: credentials)
}

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 {
guard let authState = authCredentials, authState.isTeamPrincipal else {
throw GrokWebBillingError.teamUsageUnsupported
}
let subscriptionTier = try await resolveSettingsTier(authState)
let identitySnapshot = GrokStatusProbe.identityOnlySnapshot(
credentials: authState,
localSummary: GrokLocalSessionScanner.summarize(env: context.env),
cliVersion: GrokStatusProbe.detectVersion(env: context.env))
cliVersion: GrokStatusProbe.detectVersion(env: context.env),
subscriptionTier: subscriptionTier)
return self.makeResult(
usage: identitySnapshot.toUsageSnapshot(),
sourceLabel: "grok-web",
diagnostic: identitySnapshot.diagnostic)
}
let credentials = Self.credentialsForWebBillingSnapshot(
credentials: try? GrokCredentialsStore.load(env: context.env),
credentials: authCredentials,
authenticatedByAuthFile: authenticatedByAuthFile)
// Cookie/gRPC fallback is a different browser session. Never attach the
// auth.json account's settings tier onto that usage.
let subscriptionTier: String? = if authenticatedByAuthFile {
try await resolveSettingsTier(authCredentials)
} else {
nil
}
let enrichedBilling = webBilling.applying(subscriptionTier: subscriptionTier)
let snapshot = GrokUsageSnapshot(
billing: nil,
webBilling: webBilling,
webBilling: enrichedBilling,
credentials: GrokStatusProbe.credentialsForSnapshot(
credentials: credentials,
billing: nil,
webBilling: webBilling),
webBilling: enrichedBilling),
localSummary: GrokLocalSessionScanner.summarize(env: context.env),
cliVersion: GrokStatusProbe.detectVersion(env: context.env),
updatedAt: Date())
updatedAt: Date(),
subscriptionTier: subscriptionTier ?? enrichedBilling.subscriptionTier)
return self.makeResult(
usage: snapshot.toUsageSnapshot(),
sourceLabel: sourceLabel)
Expand Down
53 changes: 47 additions & 6 deletions Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public struct GrokUsageSnapshot: Sendable {
public let cliVersion: String?
public let diagnostic: String?
public let updatedAt: Date
public let subscriptionTier: String?

public init(
billing: GrokBillingResponse?,
Expand All @@ -16,7 +17,8 @@ public struct GrokUsageSnapshot: Sendable {
localSummary: GrokLocalSessionSummary?,
cliVersion: String?,
updatedAt: Date,
diagnostic: String? = nil)
diagnostic: String? = nil,
subscriptionTier: String? = nil)
{
self.billing = billing
self.webBilling = webBilling
Expand All @@ -25,6 +27,7 @@ public struct GrokUsageSnapshot: Sendable {
self.cliVersion = cliVersion
self.diagnostic = diagnostic
self.updatedAt = updatedAt
self.subscriptionTier = subscriptionTier
}

public func toUsageSnapshot() -> UsageSnapshot {
Expand Down Expand Up @@ -55,7 +58,9 @@ public struct GrokUsageSnapshot: Sendable {
providerID: .grok,
accountEmail: self.credentials?.email,
accountOrganization: self.credentials?.teamId,
loginMethod: self.credentials?.loginMethod)
loginMethod: GrokPlan.loginMethod(
subscriptionTier: self.subscriptionTier ?? self.webBilling?.subscriptionTier,
credentials: self.credentials))

return UsageSnapshot(
primary: primary,
Expand Down Expand Up @@ -123,16 +128,19 @@ public struct GrokStatusProbe: Sendable {
billingAttempted: billingAttempted,
error: rpcError)
{
let subscriptionTier = try await Self.loadSettingsTier(credentials: credentials)
return Self.identityOnlySnapshot(
credentials: credentials,
localSummary: localSummary,
cliVersion: cliVersion)
cliVersion: cliVersion,
subscriptionTier: subscriptionTier)
}

if billing == nil {
throw rpcError ?? GrokRPCError.notAuthenticated
}

let subscriptionTier = try await Self.loadSettingsTier(credentials: credentials)
return GrokUsageSnapshot(
billing: billing,
webBilling: nil,
Expand All @@ -142,14 +150,16 @@ public struct GrokStatusProbe: Sendable {
webBilling: nil),
localSummary: localSummary,
cliVersion: cliVersion,
updatedAt: Date())
updatedAt: Date(),
subscriptionTier: subscriptionTier)
}

static func identityOnlySnapshot(
credentials: GrokCredentials,
localSummary: GrokLocalSessionSummary?,
cliVersion: String?,
updatedAt: Date = .init()) -> GrokUsageSnapshot
updatedAt: Date = .init(),
subscriptionTier: String? = nil) -> GrokUsageSnapshot
{
GrokUsageSnapshot(
billing: nil,
Expand All @@ -158,7 +168,38 @@ public struct GrokStatusProbe: Sendable {
localSummary: localSummary,
cliVersion: cliVersion,
updatedAt: updatedAt,
diagnostic: GrokStatusProbe.teamUsageUnavailableMessage)
diagnostic: GrokStatusProbe.teamUsageUnavailableMessage,
subscriptionTier: subscriptionTier)
}

static let settingsJoinGrace = Duration.seconds(2)

static func loadSettingsTier(
credentials: GrokCredentials?,
session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> String?
{
guard let credentials, !credentials.isExpired else { return nil }
let sourceTask = Task<String?, Error> {
try await GrokCLISettingsFetcher.subscriptionTierDisplay(
credentials: credentials,
session: transport)
}
let outcome = await BoundedTaskJoin(sourceTask: sourceTask).value(joinGrace: Self.settingsJoinGrace)
try Task.checkCancellation()
switch outcome {
case let .value(tier):
return tier
case .timedOut:
return nil
case let .failure(error):
if error is CancellationError {
throw CancellationError()
}
if let urlError = error as? URLError, urlError.code == .cancelled {
throw urlError
}
return nil
}
}

static func isBillingMethodUnavailable(_ error: Error?) -> Bool {
Expand Down
12 changes: 11 additions & 1 deletion Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@ import FoundationNetworking
public struct GrokWebBillingSnapshot: Sendable, Equatable {
public let usedPercent: Double?
public let resetsAt: Date?
public let subscriptionTier: String?

public init(usedPercent: Double?, resetsAt: Date?) {
public init(usedPercent: Double?, resetsAt: Date?, subscriptionTier: String? = nil) {
self.usedPercent = usedPercent
self.resetsAt = resetsAt
self.subscriptionTier = subscriptionTier
}

/// Overlay the CLI settings plan name. Usage percent stays on the existing credits rules.
func applying(subscriptionTier raw: String?) -> GrokWebBillingSnapshot {
GrokWebBillingSnapshot(
usedPercent: self.usedPercent,
resetsAt: self.resetsAt,
subscriptionTier: GrokPlan.displayName(from: raw) ?? self.subscriptionTier)
}
}

Expand Down
Loading