From d0a11a4aac53d63948f031275d837260f8e9b89e Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sat, 26 Sep 2026 15:02:52 -0700 Subject: [PATCH] feat(grok): decode the product breakdown on the grok.com billing path The "Usage breakdown" rows came only from the CLI credits proxy. When the grok CLI login is gone (grok deletes ~/.grok/auth.json after a rejected refresh), CodexBar falls back to grok.com GetGrokCreditsConfig, which carries the same shares but was never paired, so the rows vanished. Decode the repeated [1, 7] entries ({1: product id, 2: float percent, omitted = 0}) from a single complete data frame whose aggregate is [1, 1], and run them through the same all-or-nothing add-up guard as the proxy (now a shared GrokProductUsage.composing). Only live-verified ids are named (2 = GrokBuild, 4 = GrokChat); an unnamed id with a nonzero share, a malformed or duplicate entry, or a mismatched sum drops the breakdown. The aggregate, reset, and usage flags are unchanged. The live grok.com response (6 = Chat 4 + Build 2) is checked in verbatim as a fixture. Co-Authored-By: Claude Opus 5.5 --- .../grok-grpc-product-usage/README.md | 22 ++ .../grok-grpc-product-usage/after.png | Bin 0 -> 31836 bytes .../grok-grpc-product-usage/before.png | Bin 0 -> 18776 bytes .../Grok/GrokCreditsProxyFetcher.swift | 14 +- .../Grok/GrokWebBillingFetcher.swift | 127 +++++++- .../GrokWebBillingProductUsageTests.swift | 287 ++++++++++++++++++ docs/grok.md | 12 + 7 files changed, 447 insertions(+), 15 deletions(-) create mode 100644 .github/pr-proof/grok-grpc-product-usage/README.md create mode 100644 .github/pr-proof/grok-grpc-product-usage/after.png create mode 100644 .github/pr-proof/grok-grpc-product-usage/before.png create mode 100644 Tests/CodexBarTests/GrokWebBillingProductUsageTests.swift diff --git a/.github/pr-proof/grok-grpc-product-usage/README.md b/.github/pr-proof/grok-grpc-product-usage/README.md new file mode 100644 index 0000000000..7b82a3fd10 --- /dev/null +++ b/.github/pr-proof/grok-grpc-product-usage/README.md @@ -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`. diff --git a/.github/pr-proof/grok-grpc-product-usage/after.png b/.github/pr-proof/grok-grpc-product-usage/after.png new file mode 100644 index 0000000000000000000000000000000000000000..dadc4ec593cf5269de28051e8cd24902ba2403ce GIT binary patch literal 31836 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 MkWrOJNSX!y52OQG!~g&Q literal 0 HcmV?d00001 diff --git a/.github/pr-proof/grok-grpc-product-usage/before.png b/.github/pr-proof/grok-grpc-product-usage/before.png new file mode 100644 index 0000000000000000000000000000000000000000..b48a57a4f8eb48c2fb2b289773985b9b9e25cde9 GIT binary patch literal 18776 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 [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, diff --git a/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift b/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift index b88992ab3b..7c7db80019 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift @@ -14,6 +14,17 @@ public struct GrokProductUsage: Sendable, Equatable { } } +extension GrokProductUsage { + static let compositionTolerancePercent = 1.0 + + static func composing(_ 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.compositionTolerancePercent ? products : [] + } +} + public struct GrokWebBillingSnapshot: Sendable, Equatable { public let usedPercent: Double? public let resetsAt: Date? @@ -306,14 +317,14 @@ public enum GrokWebBillingFetcher { scan.merge(Self.scanProtobuf(payload, depth: 0)) } - let parsedPercent = scan.fixed32Fields + let parsedPercentField = scan.fixed32Fields .filter { field in field.path.last == 1 && field.value.isFinite && field.value >= 0 && field.value <= 100 } .min { lhs, rhs in lhs.path.count == rhs.path.count ? lhs.order < rhs.order : lhs.path.count < rhs.path.count } - .map { Double($0.value) } + let parsedPercent = parsedPercentField.map { Double($0.value) } let resetFields = scan.varintFields.compactMap { field -> (path: [UInt64], date: Date)? in let raw = field.value @@ -344,11 +355,20 @@ public enum GrokWebBillingFetcher { guard let percent = parsedPercent ?? (noUsageYet ? 0 : nil) else { throw GrokWebBillingError.parseFailed } + let productUsage: [GrokProductUsage] = if payloads.count == 1, scan.isComplete, + let parsedPercent, parsedPercentField?.path == [1, 1] + { + GrokProductUsage.composing( + Self.decodeProductUsage(payloads[0]), creditUsagePercent: parsedPercent) + } else { + [] + } return GrokWebBillingSnapshot( usedPercent: percent, resetsAt: reset, usedPercentIsWirePublished: parsedPercent != nil, - usedPercentIsImplicitZero: noUsageYet && payloads.count == 1 && scan.isComplete && hasActiveCurrentPeriod) + usedPercentIsImplicitZero: noUsageYet && payloads.count == 1 && scan.isComplete && hasActiveCurrentPeriod, + productUsage: productUsage) } static func looksLikeProtobufPayload(_ data: Data) -> Bool { @@ -570,6 +590,107 @@ public enum GrokWebBillingFetcher { } } + private struct ProductWireField { + let number: UInt64 + let varint: UInt64? + let fixed32: Float? + let message: [UInt8]? + } + + private static func decodeProductUsage(_ payload: Data) -> [GrokProductUsage] { + let bytes = [UInt8](payload) + var index = 0 + var config: [UInt8]? + while index < bytes.count { + guard let field = Self.readProductWireField(bytes, index: &index) else { return [] } + if field.number == 1 { + guard let message = field.message else { return [] } + guard config == nil else { return [] } + config = message + } + } + guard let config else { return [] } + + var products: [GrokProductUsage] = [] + var seenIDs: Set = [] + index = 0 + while index < config.count { + guard let field = Self.readProductWireField(config, index: &index) else { return [] } + guard field.number == 7 else { continue } + guard let entry = field.message else { return [] } + guard let (id, percent) = Self.decodeProductEntry(entry), seenIDs.insert(id).inserted else { + return [] + } + // Only live-verified product IDs are named. + let name: String? = switch id { + case 2: "GrokBuild" + case 4: "GrokChat" + default: nil + } + if let name { + products.append(GrokProductUsage(product: name, usedPercent: percent)) + } else if percent > 0 { + return [] + } + } + return products + } + + private static func decodeProductEntry(_ bytes: [UInt8]) -> (UInt64, Double)? { + var index = 0 + var id: UInt64? + var percent = 0.0 + while index < bytes.count { + guard let field = Self.readProductWireField(bytes, index: &index) else { return nil } + switch field.number { + case 1: + guard let value = field.varint else { return nil } + id = value + case 2: + guard let value = field.fixed32 else { return nil } + percent = Double(value) + default: + break + } + } + guard let id, percent.isFinite, percent >= 0 else { return nil } + return (id, percent) + } + + private static func readProductWireField(_ bytes: [UInt8], index: inout Int) -> ProductWireField? { + guard let key = readVarint(bytes, index: &index), + key >> 3 > 0, key >> 3 <= 536_870_911 + else { return nil } + let number = key >> 3 + switch key & 0x07 { + case 0: + guard let value = Self.readVarint(bytes, index: &index) else { return nil } + return ProductWireField(number: number, varint: value, fixed32: nil, message: nil) + case 1: + guard index + 8 <= bytes.count else { return nil } + index += 8 + return ProductWireField(number: number, varint: nil, fixed32: nil, message: nil) + case 2: + guard let length = Self.readVarint(bytes, index: &index), + length <= UInt64(bytes.count - index) + else { return nil } + let end = index + Int(length) + let message = Array(bytes[index.. UInt64? { var value: UInt64 = 0 var shift: UInt64 = 0 diff --git a/Tests/CodexBarTests/GrokWebBillingProductUsageTests.swift b/Tests/CodexBarTests/GrokWebBillingProductUsageTests.swift new file mode 100644 index 0000000000..7e0ca89495 --- /dev/null +++ b/Tests/CodexBarTests/GrokWebBillingProductUsageTests.swift @@ -0,0 +1,287 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct GrokWebBillingProductUsageTests { + private static let now = Date(timeIntervalSince1970: 1_790_456_400) + private static let periodStart: UInt64 = 1_789_929_765 + private static let periodEnd: UInt64 = 1_790_534_565 + + @Test + func `live grok dot com billing frame decodes the product breakdown`() throws { + let hex = """ + 000000005f0a5d0d0000c04012001a00220c08a5d2c0d5061088ccb580022a0c08a5c7e5d5061088ccb58002 + 3a07080415000080403a0708021500000040421e0802120c08a5d2c0d5061088ccb580021a0c08a5c7e5d506 + 1088ccb58002580162006801800000000f677270632d7374617475733a300d0a + """ + let data = try #require(Self.data(hex: hex)) + let parsed = try GrokWebBillingFetcher.parseGRPCWebResponse(data, now: Self.now) + + #expect(parsed.usedPercent == 6) + #expect(parsed.usedPercentIsWirePublished) + #expect(parsed.productUsage == [ + GrokProductUsage(product: "GrokChat", usedPercent: 4), + GrokProductUsage(product: "GrokBuild", usedPercent: 2), + ]) + + let usage = GrokUsageSnapshot( + billing: nil, + webBilling: parsed, + credentials: nil, + localSummary: nil, + cliVersion: nil, + updatedAt: Self.now).toUsageSnapshot() + let section = try #require(usage.details.first) + #expect(usage.primary?.usedPercent == 6) + #expect(usage.details.count == 1) + #expect(section.title == "Usage breakdown") + #expect(section.rows.map(\.label) == ["Grok Chat", "Grok Build"]) + #expect(section.rows.map(\.value) == ["4%", "2%"]) + #expect(section.rows.allSatisfy { $0.progress == nil }) + #expect(usage.secondary == nil) + #expect(usage.tertiary == nil) + } + + @Test + func `unnamed products keep only complete named shares`() throws { + let named = [Self.entry(id: 4, percent: 4), Self.entry(id: 2, percent: 2)] + let baseline = Self.frame(Self.payload(aggregate: 6)) + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: 6, entries: named + [Self.entry(id: 7, percent: 1)])), + baseline: baseline, + products: []) + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: 6, entries: [Self.entry(id: 7)] + named)), + baseline: baseline, + products: [ + GrokProductUsage(product: "GrokChat", usedPercent: 4), + GrokProductUsage(product: "GrokBuild", usedPercent: 2), + ]) + } + + @Test + func `missing duplicate and malformed product ids drop the breakdown`() throws { + let baseline = Self.frame(Self.payload(aggregate: 6)) + let invalidEntries: [[Data]] = [ + [Self.entry(id: nil, percent: 4)], + [Self.entry(id: 4, percent: 4), Self.entry(id: 4, percent: 2)], + [Data([0x08])], // Truncated field 1 varint inside an entry. + [Self.fixed32(1, 4), Self.entry(id: 2, percent: 6)], + [Self.entry(id: 4) + Self.varintField(2, 1), Self.entry(id: 2, percent: 6)], + ] + for entries in invalidEntries { + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: 6, entries: entries)), + baseline: baseline, + products: []) + } + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: 6, extra: Self.varintField(7, 4))), + baseline: baseline, + products: []) + } + + @Test + func `omitted percentages default to zero and unknown entry fields are skipped`() throws { + let baseline = Self.frame(Self.payload(aggregate: 6)) + var chat = Self.entry(id: 4, percent: 6) + chat.append(Self.varintField(3, 42)) + chat.append(Self.message(4, Data([0xFF]))) + chat.append(Self.fixed32(5, 3)) + chat.append(contentsOf: [0x31] + Array(repeating: 0, count: 8)) // Field 6, fixed64. + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: 6, entries: [chat, Self.entry(id: 2)])), + baseline: baseline, + products: [ + GrokProductUsage(product: "GrokChat", usedPercent: 6), + GrokProductUsage(product: "GrokBuild", usedPercent: 0), + ]) + } + + @Test + func `invalid or noncomposing percentages drop the breakdown`() throws { + let baseline = Self.frame(Self.payload(aggregate: 6)) + let cases: [[Data]] = [ + [Self.entry(id: 4, percent: -1), Self.entry(id: 2, percent: 7)], + [Self.entry(id: 4, percent: .nan), Self.entry(id: 2, percent: 2)], + [Self.entry(id: 2, percent: 2)], + ] + for entries in cases { + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: 6, entries: entries)), + baseline: baseline, + products: []) + } + } + + @Test + func `products require one complete payload with a published config aggregate`() throws { + let named = [Self.entry(id: 4, percent: 4), Self.entry(id: 2, percent: 2)] + let baseline = Self.frame(Self.payload(aggregate: 6)) + try Self.expectSameBilling(baseline, baseline: baseline, products: []) + + let implicitZero = Self.frame(Self.payload(aggregate: nil, entries: [Self.entry(id: 4)])) + let implicitBaseline = Self.frame(Self.payload(aggregate: nil)) + let implicitSnapshot = try GrokWebBillingFetcher.parseGRPCWebResponse(implicitBaseline, now: Self.now) + #expect(implicitSnapshot.usedPercent == 0) + #expect(implicitSnapshot.usedPercentIsImplicitZero) + try Self.expectSameBilling( + implicitZero, + baseline: implicitBaseline, + products: []) + + let twoFrames = Self.frame(Self.payload(aggregate: 6, entries: named)) + + Self.frame(Self.payload(aggregate: 6)) + let twoFramesBaseline = Self.frame(Self.payload(aggregate: 6)) + + Self.frame(Self.payload(aggregate: 6)) + try Self.expectSameBilling(twoFrames, baseline: twoFramesBaseline, products: []) + + let nestedAggregate = Self.message(2, Self.fixed32(1, 6)) + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: nil, entries: named, extra: nestedAggregate)), + baseline: Self.frame(Self.payload(aggregate: nil, extra: nestedAggregate)), + products: []) + + let malformedOtherField = Data([0x62, 0x02, 0x08]) + try Self.expectSameBilling( + Self.frame(Self.payload(aggregate: 6, entries: named, extra: malformedOtherField)), + baseline: Self.frame(Self.payload(aggregate: 6, extra: malformedOtherField)), + products: []) + } + + @Test + func `cookie fallback shows decoded product rows`() async throws { + let liveHex = """ + 000000005f0a5d0d0000c04012001a00220c08a5d2c0d5061088ccb580022a0c08a5c7e5d5061088ccb58002 + 3a07080415000080403a0708021500000040421e0802120c08a5d2c0d5061088ccb580021a0c08a5c7e5d506 + 1088ccb58002580162006801800000000f677270632d7374617475733a300d0a + """ + let live = try #require(Self.data(hex: liveHex)) + var strategy = GrokWebFetchStrategy() + strategy.loadCredentials = { _ in .failure(GrokWebBillingError.missingCredentials) } + strategy.localSummary = { _ in nil } + strategy.cliVersion = { _ in nil } + let home = FileManager.default.temporaryDirectory + .appendingPathComponent("CodexBar-GrokProductUsage-\(UUID().uuidString)", isDirectory: true) + let browserDetection = BrowserDetection(cacheTTL: 0) + let context = ProviderFetchContext( + runtime: .cli, + sourceMode: .web, + includeCredits: true, + includeOptionalUsage: false, + webTimeout: 1, + webDebugDumpHTML: false, + verbose: false, + env: ["GROK_HOME": home.path], + settings: nil, + fetcher: UsageFetcher(), + claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection), + browserDetection: browserDetection) + let result = try await strategy.fetch( + context, + webBilling: { _ in + try GrokWebBillingResult( + snapshot: GrokWebBillingFetcher.parseGRPCWebResponse(live, now: Self.now), + sourceLabel: "Chrome", + authContext: .cookie("sso=test")) + }, + settingsTier: { _ in nil }, + remainingResets: { _, _, _ in .empty }) + + #expect(result.usage.primary?.usedPercent == 6) + #expect(result.usage.details.count == 1) + #expect(result.usage.details.first?.title == "Usage breakdown") + #expect(result.usage.details.first?.rows.map(\.label) == ["Grok Chat", "Grok Build"]) + #expect(result.usage.details.first?.rows.map(\.value) == ["4%", "2%"]) + } + + private static func expectSameBilling( + _ data: Data, + baseline: Data, + products: [GrokProductUsage]) throws + { + let actual = try GrokWebBillingFetcher.parseGRPCWebResponse(data, now: Self.now) + let withoutProducts = try GrokWebBillingFetcher.parseGRPCWebResponse(baseline, now: Self.now) + #expect(actual.usedPercent == withoutProducts.usedPercent) + #expect(actual.resetsAt == withoutProducts.resetsAt) + #expect(actual.usedPercentIsWirePublished == withoutProducts.usedPercentIsWirePublished) + #expect(actual.usedPercentIsImplicitZero == withoutProducts.usedPercentIsImplicitZero) + #expect(actual.productUsage == products) + } + + private static func payload(aggregate: Float?, entries: [Data] = [], extra: Data = Data()) -> Data { + var config = Data() + if let aggregate { config.append(Self.fixed32(1, aggregate)) } + config.append(Self.message(5, Self.varintField(1, Self.periodEnd))) + var currentPeriod = Self.varintField(1, 2) + currentPeriod.append(Self.message(2, Self.varintField(1, Self.periodStart))) + currentPeriod.append(Self.message(3, Self.varintField(1, Self.periodEnd))) + config.append(Self.message(8, currentPeriod)) + for entry in entries { + config.append(Self.message(7, entry)) + } + config.append(extra) + return Self.message(1, config) + } + + private static func entry(id: UInt64?, percent: Float? = nil) -> Data { + var data = Data() + if let id { data.append(Self.varintField(1, id)) } + if let percent { data.append(Self.fixed32(2, percent)) } + return data + } + + private static func varintField(_ number: UInt64, _ value: UInt64) -> Data { + var data = Self.varint(number << 3) + data.append(Self.varint(value)) + return data + } + + private static func fixed32(_ number: UInt64, _ value: Float) -> Data { + var data = Self.varint((number << 3) | 5) + var bits = value.bitPattern.littleEndian + withUnsafeBytes(of: &bits) { data.append(contentsOf: $0) } + return data + } + + private static func message(_ number: UInt64, _ value: Data) -> Data { + var data = Self.varint((number << 3) | 2) + data.append(Self.varint(UInt64(value.count))) + data.append(value) + return data + } + + private static func varint(_ value: UInt64) -> Data { + var remaining = value + var bytes: [UInt8] = [] + repeat { + var byte = UInt8(remaining & 0x7F) + remaining >>= 7 + if remaining != 0 { byte |= 0x80 } + bytes.append(byte) + } while remaining != 0 + return Data(bytes) + } + + private static func frame(_ payload: Data) -> Data { + var data = Data([0]) + var length = UInt32(payload.count).bigEndian + withUnsafeBytes(of: &length) { data.append(contentsOf: $0) } + data.append(payload) + return data + } + + private static func data(hex: String) -> Data? { + let digits = hex.filter { !$0.isWhitespace } + guard digits.count.isMultiple(of: 2) else { return nil } + var data = Data() + var index = digits.startIndex + while index < digits.endIndex { + let next = digits.index(index, offsetBy: 2) + guard let byte = UInt8(digits[index..