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

These images are the production `UsageMenuCardView`, rendered offscreen at
310 pt with `hidePersonalInfo: true`. The data comes from a **live**
grok.com `GetGrokCreditsConfig` gRPC-web response (2026-09-26 21:58 UTC),
fetched and parsed by this branch's `GrokWebBillingFetcher`.

- **Credentials:** the run read the bearer from `~/.grok/auth.json`. No
browser cookies and no Keychain were involved.
- **Parsed result:** `usedPercent 6.0` (wire-published) and
`productUsage [GrokChat 4.0, GrokBuild 2.0]`. The same response is checked
in, verbatim, as the fixture in `GrokWebBillingProductUsageTests`.
- **after.png:** that snapshot rendered as-is: one weekly bar plus
`Grok Chat 4%` / `Grok Build 2%`.
- **before.png:** the same snapshot with `details` cleared. That is what main
shows on this path, because its gRPC parser never pairs the `[1, 7]` ids with
their percentages.

The fetch-and-render harness was a temporary test and was not committed. It
calls `GrokCredentialsStore.load`, then `GrokWebBillingFetcher.fetch`, then
`GrokUsageSnapshot.toUsageSnapshot`, and renders through `NSHostingView` +
`cacheDisplay`.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Token history: show observed model names when per-model totals are unavailable, including Grok local sessions (#4056). Fixes #4054. Thanks @BUKOWSKIREAL!
- TypeSafe: show the credit balance in the menu bar and layout preview instead of missing-value dashes, using shared balance-label metadata (#4050). Thanks @lg!
- Grok: retain the product usage breakdown on the grok.com billing fallback, with bounded shared protobuf decoding (#4041). Thanks @olddonkey!
### Changed

- Menu bar: align the persistent Refresh row with other menu actions by removing its decorative icon, preserving the shortcut and accessibility action (#4057). Thanks @elijahfriedman!
Expand Down
14 changes: 2 additions & 12 deletions Sources/CodexBarCore/Providers/Grok/GrokCreditsProxyFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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 @@ -69,7 +68,8 @@ public enum GrokCreditsProxyFetcher {
resetsAt: resetsAt,
windowMinutes: windowMinutes,
subscriptionTier: subscriptionTier,
productUsage: Self.composingProducts(config.productUsage?.values ?? [], creditUsagePercent: percent))
productUsage: GrokProductUsage.composing(
config.productUsage?.values ?? [], creditUsagePercent: percent))
}

if let cap = config.onDemandCap?.val,
Expand All @@ -95,16 +95,6 @@ public enum GrokCreditsProxyFetcher {
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 Down
62 changes: 62 additions & 0 deletions Sources/CodexBarCore/Providers/Grok/GrokProtobufField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation

/// Bounded protobuf wire reader shared by Grok billing and reset-coupon messages.
struct GrokProtobufField {
var number: UInt64
var varint: UInt64?
var fixed32: Float?
var message: [UInt8]?

static func fields(in bytes: [UInt8]) -> [Self]? {
var fields: [Self] = []
var index = 0
while index < bytes.count {
guard let field = self.read(bytes, index: &index) else { return nil }
fields.append(field)
}
return fields
}

static func read(_ bytes: [UInt8], index: inout Int) -> Self? {
guard let key = self.readVarint(bytes, index: &index),
key >> 3 > 0, key >> 3 <= 536_870_911 else { return nil }
var field = Self(number: key >> 3)
switch key & 0x07 {
case 0:
guard let value = self.readVarint(bytes, index: &index) else { return nil }
field.varint = value
case 1:
guard bytes.count - index >= 8 else { return nil }
index += 8
case 2:
guard let length = self.readVarint(bytes, index: &index),
length <= UInt64(bytes.count - index) else { return nil }
let end = index + Int(length)
field.message = Array(bytes[index..<end])
index = end
case 5:
guard bytes.count - index >= 4 else { return nil }
let bits = UInt32(bytes[index]) | (UInt32(bytes[index + 1]) << 8)
| (UInt32(bytes[index + 2]) << 16) | (UInt32(bytes[index + 3]) << 24)
field.fixed32 = Float(bitPattern: bits)
index += 4
default:
return nil
}
return field
}

private static func readVarint(_ bytes: [UInt8], index: inout Int) -> UInt64? {
var value: UInt64 = 0
var shift: UInt64 = 0
while index < bytes.count, shift < 64 {
let byte = bytes[index]
index += 1
if shift == 63, byte > 1 { return nil }
value |= UInt64(byte & 0x7F) << shift
if byte & 0x80 == 0 { return value }
shift += 7
}
return nil
}
}
150 changes: 21 additions & 129 deletions Sources/CodexBarCore/Providers/Grok/GrokRemainingResetsFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,151 +365,43 @@ enum GrokRemainingResetsFetcher {
}

private static func parseMessage(_ data: Data, now: Date) -> ParsedMessage? {
let bytes = [UInt8](data)
guard let fields = GrokProtobufField.fields(in: Array(data)) else { return nil }
var tokens: [GrokRemainingReset] = []
var containsTokenRecord = false
var index = 0
while index < bytes.count {
guard let key = Self.readVarint(bytes, index: &index), key != 0 else { return nil }
let fieldNumber = key >> 3
let wireType = key & 0x07
switch wireType {
case 0:
guard Self.readVarint(bytes, index: &index) != nil else { return nil }
case 1:
guard index + 8 <= bytes.count else { return nil }
index += 8
case 2:
guard let length = Self.readVarint(bytes, index: &index),
length <= UInt64(bytes.count - index)
else {
return nil
}
let start = index
let end = index + Int(length)
if fieldNumber == 10 {
containsTokenRecord = true
guard let parsed = Self.parseToken(Data(bytes[start..<end]), now: now) else {
return nil
}
if let token = parsed.token {
tokens.append(token)
}
}
index = end
case 5:
guard index + 4 <= bytes.count else { return nil }
index += 4
default:
return nil
}
for field in fields where field.number == 10 {
guard let message = field.message else { continue }
containsTokenRecord = true
guard let parsed = Self.parseToken(Data(message), now: now) else { return nil }
if let token = parsed.token { tokens.append(token) }
}
return ParsedMessage(tokens: tokens, containsTokenRecord: containsTokenRecord)
}

private static func parseToken(_ data: Data, now: Date) -> ParsedToken? {
let bytes = [UInt8](data)
guard let fields = GrokProtobufField.fields(in: Array(data)) else { return nil }
var tokenID = ""
var grantedAt: Date?
var expiresAt: Date?
var index = 0
while index < bytes.count {
guard let key = Self.readVarint(bytes, index: &index), key != 0 else { return nil }
let fieldNumber = key >> 3
let wireType = key & 0x07
switch wireType {
case 0:
guard Self.readVarint(bytes, index: &index) != nil else { return nil }
case 1:
guard index + 8 <= bytes.count else { return nil }
index += 8
case 2:
guard let length = Self.readVarint(bytes, index: &index),
length <= UInt64(bytes.count - index)
else {
return nil
}
let start = index
let end = index + Int(length)
let payload = Data(bytes[start..<end])
if fieldNumber == 10 {
tokenID = String(data: payload, encoding: .utf8) ?? ""
} else if fieldNumber == 20 {
grantedAt = Self.timestamp(from: payload)
} else if fieldNumber == 30 {
expiresAt = Self.timestamp(from: payload)
}
index = end
case 5:
guard index + 4 <= bytes.count else { return nil }
index += 4
default:
return nil
for field in fields {
guard let message = field.message else { continue }
let payload = Data(message)
switch field.number {
case 10: tokenID = String(data: payload, encoding: .utf8) ?? ""
case 20: grantedAt = Self.timestamp(from: payload)
case 30: expiresAt = Self.timestamp(from: payload)
default: break
}
}
guard !tokenID.isEmpty, let expiresAt else { return nil }
guard expiresAt > now else { return ParsedToken(token: nil) }
return ParsedToken(token: GrokRemainingReset(
tokenID: tokenID,
grantedAt: grantedAt,
expiresAt: expiresAt))
return ParsedToken(token: GrokRemainingReset(tokenID: tokenID, grantedAt: grantedAt, expiresAt: expiresAt))
}

private static func timestamp(from data: Data) -> Date? {
let bytes = [UInt8](data)
var index = 0
while index < bytes.count {
let fieldStart = index
guard let key = Self.readVarint(bytes, index: &index), key != 0 else {
index = fieldStart + 1
continue
}
let fieldNumber = key >> 3
let wireType = key & 0x07
switch wireType {
case 0:
if let value = Self.readVarint(bytes, index: &index),
fieldNumber == 1,
value >= 1_700_000_000,
value <= 2_100_000_000
{
return Date(timeIntervalSince1970: TimeInterval(value))
}
case 1:
guard index + 8 <= bytes.count else { return nil }
index += 8
case 2:
guard let length = Self.readVarint(bytes, index: &index),
length <= UInt64(bytes.count - index)
else {
return nil
}
index += Int(length)
case 5:
guard index + 4 <= bytes.count else { return nil }
index += 4
default:
return nil
}
}
return nil
}

private static func readVarint(_ bytes: [UInt8], index: inout Int) -> UInt64? {
var value: UInt64 = 0
var shift: UInt64 = 0
while index < bytes.count {
let byte = bytes[index]
index += 1
value |= UInt64(byte & 0x7F) << shift
if byte & 0x80 == 0 {
return value
}
shift += 7
if shift > 63 {
return nil
}
}
return nil
guard let fields = GrokProtobufField.fields(in: Array(data)),
let seconds = fields.first(where: {
$0.number == 1 && $0.varint.map { (1_700_000_000...2_100_000_000).contains($0) } == true
})?.varint else { return nil }
return Date(timeIntervalSince1970: TimeInterval(seconds))
}
}
Loading
Loading