Skip to content
Closed
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
25 changes: 25 additions & 0 deletions .github/pr-proof/grok-product-usage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Grok product usage breakdown proof

These images are the production `UsageMenuCardView`, rendered offscreen at
310 pt from a **live** multi-product Grok snapshot. The card model was built
with `UsageMenuCardView.Model.make` and `hidePersonalInfo: true`.

- **Source:** the `usage` object printed by this branch's
`CodexBarCLI usage --provider grok --source oauth --format json`
(2026-09-25 04:54 UTC). The run read only `~/.grok/auth.json`, with an
isolated `CFFIXED_USER_HOME` and `CODEXBAR_DISABLE_KEYCHAIN_ACCESS=1`.
- **Redaction:** the account email and organization were removed from that
JSON before rendering.
- **What the snapshot contains:** one primary window (Weekly, 6% used) and a
`Usage breakdown` section with `Grok Chat 4%` and `Grok Build 2%`. Those rows
come from the live credits payload
`creditUsagePercent 6 = GrokChat 4 + GrokBuild 2`.
- **Before and after:** `after.png` and `after-dark.png` render the snapshot
as-is. `before.png` renders the same snapshot with `details` cleared. That is
what released 0.65.0 shows for this payload, because its decoder drops
`productUsage`. This change touches no view code.

No app was launched, no window was shown, and nothing was captured from the
screen. The render harness was a temporary test and was not committed. It
decodes the JSON with an ISO 8601 `JSONDecoder`, builds the card model, then
renders through `NSHostingView` + `cacheDisplay` into a 2× bitmap.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-proof/grok-product-usage/after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-proof/grok-product-usage/before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 49 additions & 3 deletions Sources/CodexBarCore/Providers/Grok/GrokCreditsProxyFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum GrokCreditsProxyFetcher {
public static let defaultEndpoint = URL(
string: "https://cli-chat-proxy.grok.com/v1/billing?format=credits")!
private static let requestTimeoutSeconds: TimeInterval = 15
private static let productCompositionTolerancePercent = 1.0

public static func fetch(
credentials: GrokCredentials,
Expand Down Expand Up @@ -67,7 +68,8 @@ public enum GrokCreditsProxyFetcher {
usedPercent: min(100, max(0, percent)),
resetsAt: resetsAt,
windowMinutes: windowMinutes,
subscriptionTier: subscriptionTier)
subscriptionTier: subscriptionTier,
productUsage: Self.composingProducts(config.productUsage?.values ?? [], creditUsagePercent: percent))
}

if let cap = config.onDemandCap?.val,
Expand All @@ -79,20 +81,32 @@ public enum GrokCreditsProxyFetcher {
usedPercent: percent,
resetsAt: resetsAt,
windowMinutes: windowMinutes,
subscriptionTier: subscriptionTier)
subscriptionTier: subscriptionTier,
productUsage: [])
}

if resetsAt != nil {
return GrokWebBillingSnapshot(
usedPercent: nil,
resetsAt: resetsAt,
windowMinutes: windowMinutes,
subscriptionTier: subscriptionTier)
subscriptionTier: subscriptionTier,
productUsage: [])
}

throw GrokWebBillingError.parseFailed
}

private static func composingProducts(
_ products: [GrokProductUsage],
creditUsagePercent: Double) -> [GrokProductUsage]
{
// Shares must compose this payload's credit percentage; any malformed entry drops the breakdown.
guard !products.isEmpty else { return [] }
let sum = products.reduce(0) { $0 + $1.usedPercent }
return abs(sum - creditUsagePercent) <= Self.productCompositionTolerancePercent ? products : []
}

private static func windowMinutes(start: String?, end: Date?, now: Date) -> Int? {
guard let start = ISO8601DateParser.parse(start),
let end, end > start, start <= now,
Expand All @@ -115,6 +129,38 @@ public enum GrokCreditsProxyFetcher {
let onDemandCap: CreditsAmount?
let onDemandUsed: CreditsAmount?
let subscriptionTier: String?
let productUsage: LossyProductUsageArray?
}

private struct LossyProductUsageArray: Decodable {
let values: [GrokProductUsage]?

init(from decoder: Decoder) {
self.values = (try? decoder.singleValueContainer().decode([LossyProductUsage].self))?
.map(\.value)
}
}

private struct LossyProductUsage: Decodable {
let value: GrokProductUsage

private enum CodingKeys: String, CodingKey {
case product
case usagePercent
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let product = try container.decode(String.self, forKey: .product)
.trimmingCharacters(in: .whitespacesAndNewlines)
let percent = try container.decode(Double.self, forKey: .usagePercent)
guard !product.isEmpty, percent.isFinite, percent >= 0 else {
throw DecodingError.dataCorrupted(.init(
codingPath: decoder.codingPath,
debugDescription: "Invalid product usage"))
}
self.value = GrokProductUsage(product: product, usedPercent: percent)
}
}

private struct CurrentPeriod: Decodable {
Expand Down
30 changes: 18 additions & 12 deletions Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ public enum GrokProviderDescriptor {
manualCookieHeader: nil).sourceMode ?? base
})

fileprivate static func withResetCreditDetails(
usage: UsageSnapshot,
resetCredits: GrokRateLimitResetCreditsSnapshot?,
now: Date) -> UsageSnapshot
{
let enriched = usage.withGrokResetCredits(resetCredits)
let resetSections = GrokRemainingResetsFetcher.detailSections(snapshot: resetCredits, now: now)
return enriched.replacing(details: .value(resetSections + enriched.details))
}

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .grok,
Expand Down Expand Up @@ -201,12 +211,10 @@ struct GrokCLIFetchStrategy: ProviderFetchStrategy {
at: snapshot.updatedAt,
requiresCompleteness: context.requiresOptionalUsageCompleteness)
return self.makeResult(
usage: usage
.withGrokResetCredits(resetResolution.snapshot)
.replacing(details: .value(
GrokRemainingResetsFetcher.detailSections(
snapshot: resetResolution.snapshot,
now: snapshot.updatedAt))),
usage: GrokProviderDescriptor.withResetCreditDetails(
usage: usage,
resetCredits: resetResolution.snapshot,
now: snapshot.updatedAt),
sourceLabel: "grok-cli",
supplementalUsageTask: resetResolution.supplementalUsageTask,
diagnostic: snapshot.diagnostic)
Expand Down Expand Up @@ -530,12 +538,10 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
at: snapshot.updatedAt,
requiresCompleteness: context.requiresOptionalUsageCompleteness)
return self.makeResult(
usage: usage
.withGrokResetCredits(resetResolution.snapshot)
.replacing(details: .value(
GrokRemainingResetsFetcher.detailSections(
snapshot: resetResolution.snapshot,
now: snapshot.updatedAt))),
usage: GrokProviderDescriptor.withResetCreditDetails(
usage: usage,
resetCredits: resetResolution.snapshot,
now: snapshot.updatedAt),
sourceLabel: billingResult.sourceLabel,
supplementalUsageTask: resetResolution.supplementalUsageTask,
diagnostic: enrichedBilling.usedPercent == nil ? GrokStatusProbe.usageUnavailableMessage : nil)
Expand Down
31 changes: 31 additions & 0 deletions Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import Foundation

enum GrokProductUsageDetails {
static func sections(for products: [GrokProductUsage]) -> [ProviderDetailSection] {
let rows = products.enumerated()
.filter { $0.element.usedPercent.isFinite && $0.element.usedPercent > 0 }
.sorted {
$0.element.usedPercent == $1.element.usedPercent
? $0.offset < $1.offset
: $0.element.usedPercent > $1.element.usedPercent
}
.map { _, usage in
let product = usage.product.trimmingCharacters(in: .whitespacesAndNewlines)
let label = switch product {
case "GrokBuild": "Grok Build"
case "GrokChat": "Grok Chat"
case "GrokImagine": "Grok Imagine"
case "GrokAppBuilder": "Grok App Builder"
default: product
}
return ProviderDetailSection.makeRow(
id: "grok.product.\(product)",
label: label,
value: UsageFormatter.percentString(usage.usedPercent))
}
guard !rows.isEmpty else { return [] }
return [.makeSection(title: "Usage breakdown", rows: rows)]
}
}

public struct GrokUsageSnapshot: Sendable {
public let billing: GrokBillingResponse?
public let webBilling: GrokWebBillingSnapshot?
Expand Down Expand Up @@ -34,6 +62,7 @@ public struct GrokUsageSnapshot: Sendable {
// Primary window: credit usage (against included limit) from the CLI RPC,
// falling back to the web billing RPC used by grok.com when the agent surface lacks billing.
var primary: RateWindow?
var details: [ProviderDetailSection] = []
if let billing,
let percent = billing.monthlyUsedPercent
{
Expand All @@ -52,6 +81,7 @@ public struct GrokUsageSnapshot: Sendable {
windowMinutes: webBilling.windowMinutes,
resetsAt: webBilling.resetsAt,
resetDescription: nil)
details = GrokProductUsageDetails.sections(for: webBilling.productUsage)
}

let identity = ProviderIdentitySnapshot(
Expand All @@ -68,6 +98,7 @@ public struct GrokUsageSnapshot: Sendable {
tertiary: nil,
costUsage: self.localSummary?.toCostUsageTokenSnapshot(
historyDays: GrokLocalSessionScanner.defaultLookbackDays),
details: details,
updatedAt: self.updatedAt,
identity: identity)
}
Expand Down
25 changes: 21 additions & 4 deletions Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import Foundation
import FoundationNetworking
#endif

public struct GrokProductUsage: Sendable, Equatable {
public let product: String
public let usedPercent: Double

public init(product: String, usedPercent: Double) {
self.product = product
self.usedPercent = usedPercent
}
}

public struct GrokWebBillingSnapshot: Sendable, Equatable {
public let usedPercent: Double?
public let resetsAt: Date?
Expand All @@ -17,21 +27,25 @@ public struct GrokWebBillingSnapshot: Sendable, Equatable {
public let usedPercentIsWirePublished: Bool
/// The parser validated an active current period with an omitted proto3 usage scalar.
public let usedPercentIsImplicitZero: Bool
/// Shares compose this snapshot's credit `usedPercent` from the same payload before clamping; empty if unverified.
public let productUsage: [GrokProductUsage]

public init(
usedPercent: Double?,
resetsAt: Date?,
windowMinutes: Int? = nil,
subscriptionTier: String? = nil,
usedPercentIsWirePublished: Bool = true,
usedPercentIsImplicitZero: Bool = false)
usedPercentIsImplicitZero: Bool = false,
productUsage: [GrokProductUsage] = [])
{
self.usedPercent = usedPercent
self.resetsAt = resetsAt
self.windowMinutes = windowMinutes
self.subscriptionTier = subscriptionTier
self.usedPercentIsWirePublished = usedPercentIsWirePublished
self.usedPercentIsImplicitZero = usedPercentIsImplicitZero
self.productUsage = productUsage
}

/// Overlay the CLI settings plan name. Usage percent stays on the existing credits rules.
Expand All @@ -42,20 +56,23 @@ public struct GrokWebBillingSnapshot: Sendable, Equatable {
windowMinutes: self.windowMinutes,
subscriptionTier: GrokPlan.displayName(from: raw) ?? self.subscriptionTier,
usedPercentIsWirePublished: self.usedPercentIsWirePublished,
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero)
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero,
productUsage: self.productUsage)
}

/// Keep period and plan metadata a second billing surface did not publish. Usage percent
/// always stays with the surface that produced this snapshot, so an unknown percent is
/// never backfilled from another response.
/// never backfilled from another response. Product shares compose that same payload's
/// `usedPercent` and are never borrowed from another billing surface.
func completing(with other: GrokWebBillingSnapshot) -> GrokWebBillingSnapshot {
GrokWebBillingSnapshot(
usedPercent: self.usedPercent,
resetsAt: other.resetsAt ?? self.resetsAt,
windowMinutes: other.resetsAt == nil ? self.windowMinutes : other.windowMinutes,
subscriptionTier: self.subscriptionTier ?? other.subscriptionTier,
usedPercentIsWirePublished: self.usedPercentIsWirePublished,
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero)
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero,
productUsage: self.productUsage)
}
}

Expand Down
Loading
Loading