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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Usage & Spend: keep stalled or failed Codex catch-up paused until explicit Refresh, preventing background synchronization from restarting CPU-heavy scans (partial fix for #3316). Thanks @heyajulia!
- Xiaomi MiMo: prevent overlapping local usage tracker updates from colliding on a shared temporary cache file, preserving atomic publication (#3321). Thanks @Lucenx9!
- Claude: avoid duplicated “Resets Reset” labels when CLI usage supplies a singular reset description (#3317). Thanks @Aternus!
- Grok: send a nonempty billing request while preserving legacy monthly usage and leaving unknown percentages unchanged (#3336). Thanks @CharlieLZ!

## 0.56.2 — 2026-08-31

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ public enum GrokWebBillingFetcher {
URL(string: "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig")!
private static let requestTimeoutSeconds: TimeInterval = 15

/// Explicit field 1 = false (exclude_legacy_monthly_usage) keeps the request nonempty
/// without changing the default billing semantics. This field is not a period selector.
private static let creditsConfigRequest = Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00])

public static func fetch(
credentials: GrokCredentials,
session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
Expand Down Expand Up @@ -208,7 +212,7 @@ public enum GrokWebBillingFetcher {
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.timeoutInterval = Self.requestTimeoutSeconds
request.httpBody = Data([0x00, 0x00, 0x00, 0x00, 0x00])
request.httpBody = Self.creditsConfigRequest
if let authorizationHeader {
request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization")
}
Expand Down
60 changes: 60 additions & 0 deletions Tests/CodexBarTests/GrokCreditsProxyFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,66 @@ struct GrokCreditsProxyFetcherTests {
#expect(snapshot.subscriptionTier == nil)
}

@Test
func `unified billing without a published percent keeps subscription usage unknown`() async throws {
let snapshot = try GrokCreditsProxyFetcher.parseSnapshot(
Data(
"""
{
"config": {
"isUnifiedBillingUser": true,
"currentPeriod": {
"type": "USAGE_PERIOD_TYPE_WEEKLY",
"start": "2026-08-31T12:35:57.056343+00:00",
"end": "2026-09-07T12:35:57.056343+00:00"
},
"onDemandCap": { "val": 0 },
"onDemandUsed": { "val": 0 },
"prepaidBalance": { "val": 0 },
"topUpMethod": "TOP_UP_METHOD_SAVED_PAYMENT_METHOD",
"billingPeriodEnd": "2026-09-07T12:35:57.056343+00:00"
}
}
""".utf8))

let expectedReset = try Self.date("2026-09-07T12:35:57.056343+00:00")
// Zero on-demand spending does not establish the included subscription usage.
#expect(snapshot.usedPercent == nil)
#expect(snapshot.resetsAt == expectedReset)
#expect(snapshot.subscriptionTier == nil)

let result = try await GrokOAuthFetchStrategy.resolvingUnknownUsage(
snapshot,
credentials: Self.credentials,
grpcBilling: { _ in GrokWebBillingSnapshot(usedPercent: 35, resetsAt: nil) })

#expect(result.snapshot.usedPercent == 35)
#expect(result.snapshot.resetsAt == expectedReset)
#expect(result.sourceLabel == "grok-web")
}

@Test
func `period only proxy payload without unified flag stays unknown`() throws {
let snapshot = try GrokCreditsProxyFetcher.parseSnapshot(
Data(
"""
{
"config": {
"currentPeriod": {
"type": "USAGE_PERIOD_TYPE_WEEKLY",
"start": "2026-08-31T12:35:57.056343+00:00",
"end": "2026-09-07T12:35:57.056343+00:00"
},
"onDemandCap": { "val": 0 },
"onDemandUsed": { "val": 0 },
"billingPeriodEnd": "2026-09-07T12:35:57.056343+00:00"
}
}
""".utf8))

#expect(snapshot.usedPercent == nil)
}

@Test
func `derives percent from on demand cap and usage`() throws {
let snapshot = try GrokCreditsProxyFetcher.parseSnapshot(
Expand Down
80 changes: 79 additions & 1 deletion Tests/CodexBarTests/GrokWebBillingFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,76 @@ struct GrokWebBillingFetcherTests {
// no-usage-yet reading and must never travel as a published percent.
#expect(!snapshot.usedPercentIsWirePublished)
}
}

extension GrokWebBillingFetcherTests {
@Test
func `fractional billing timestamps do not publish a usage percent`() async throws {
// Captured frame from #3336. The nonzero [1,4,2]/[1,5,2] varints are timestamp
// nanoseconds, not limits or usage; the frame has no credit_usage_percent field.
let data = Data([
0x00, 0x00, 0x00, 0x00, 0x44, 0x0A, 0x42, 0x12,
0x00, 0x1A, 0x00, 0x22, 0x0B, 0x08, 0xAD, 0xEA,
0xD5, 0xD4, 0x06, 0x10, 0xD8, 0xF3, 0xEE, 0x1A,
0x2A, 0x0B, 0x08, 0xAD, 0xDF, 0xFA, 0xD4, 0x06,
0x10, 0xD8, 0xF3, 0xEE, 0x1A, 0x42, 0x1C, 0x08,
0x02, 0x12, 0x0B, 0x08, 0xAD, 0xEA, 0xD5, 0xD4,
0x06, 0x10, 0xD8, 0xF3, 0xEE, 0x1A, 0x1A, 0x0B,
0x08, 0xAD, 0xDF, 0xFA, 0xD4, 0x06, 0x10, 0xD8,
0xF3, 0xEE, 0x1A, 0x58, 0x01, 0x62, 0x00, 0x68,
0x01, 0x80, 0x00, 0x00, 0x00, 0x0F, 0x67, 0x72,
0x70, 0x63, 0x2D, 0x73, 0x74, 0x61, 0x74, 0x75,
0x73, 0x3A, 0x30, 0x0D, 0x0A,
])

let snapshot = try GrokWebBillingFetcher.parseGRPCWebResponse(
data,
now: Date(timeIntervalSince1970: 1_788_000_000))

#expect(snapshot.usedPercent == 0)
#expect(snapshot.resetsAt == Date(timeIntervalSince1970: 1_788_784_557))
#expect(snapshot.subscriptionTier == nil)
#expect(!snapshot.usedPercentIsWirePublished)

let proxy = GrokWebBillingSnapshot(
usedPercent: nil,
resetsAt: Date(timeIntervalSince1970: 1_788_700_000),
subscriptionTier: "SuperGrok Heavy")
let result = try await GrokOAuthFetchStrategy.resolvingUnknownUsage(
proxy,
credentials: Self.credentials,
grpcBilling: { _ in snapshot })

#expect(result.snapshot == proxy)
#expect(result.sourceLabel == "grok-cli-proxy")
let usage = GrokUsageSnapshot(
billing: nil,
webBilling: result.snapshot,
credentials: Self.credentials,
localSummary: nil,
cliVersion: nil,
updatedAt: Date(timeIntervalSince1970: 1_788_000_000)).toUsageSnapshot()
#expect(usage.primary == nil)
#expect(usage.loginMethod(for: .grok) == "SuperGrok Heavy")
}

@Test(arguments: [Float(0), Float(20)])
func `parsed wire percentages replace unknown proxy usage`(percent: Float) async throws {
let snapshot = try GrokWebBillingFetcher.parseGRPCWebResponse(
Self.grpcFrame(Self.protobufPayload(usedPercent: percent, resetEpoch: 1_800_604_803)),
now: Date(timeIntervalSince1970: 1_799_000_000))
let proxyReset = Date(timeIntervalSince1970: 1_800_000_003)
let result = try await GrokOAuthFetchStrategy.resolvingUnknownUsage(
GrokWebBillingSnapshot(usedPercent: nil, resetsAt: proxyReset, subscriptionTier: "SuperGrok Heavy"),
credentials: Self.credentials,
grpcBilling: { _ in snapshot })

#expect(snapshot.usedPercentIsWirePublished)
#expect(result.snapshot.usedPercent == Double(percent))
#expect(result.snapshot.resetsAt == proxyReset)
#expect(result.snapshot.subscriptionTier == "SuperGrok Heavy")
#expect(result.sourceLabel == "grok-web")
}

@Test
func `parses omitted zero percent with current billing period`() throws {
Expand Down Expand Up @@ -770,7 +840,7 @@ struct GrokWebBillingFetcherTests {
endpoint: endpoint)

#expect(GrokWebBillingStubURLProtocol.requests.count == 1)
#expect(GrokWebBillingStubURLProtocol.requestBodies == [Data([0x00, 0x00, 0x00, 0x00, 0x00])])
#expect(GrokWebBillingStubURLProtocol.requestBodies == [Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00])])
#expect(snapshot.usedPercent == 55.5)
#expect(snapshot.resetsAt == Date(timeIntervalSince1970: TimeInterval(reset)))
}
Expand Down Expand Up @@ -816,6 +886,10 @@ struct GrokWebBillingFetcherTests {

#expect(attempts.current() == 2)
#expect(GrokWebBillingStubURLProtocol.requests.count == 2)
#expect(GrokWebBillingStubURLProtocol.requests.allSatisfy { $0.timeoutInterval == 15 })
#expect(GrokWebBillingStubURLProtocol.requestBodies == Array(
repeating: Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00]),
count: 2))
#expect(snapshot.usedPercent == 25)
#expect(snapshot.resetsAt == Date(timeIntervalSince1970: TimeInterval(reset)))
}
Expand Down Expand Up @@ -920,6 +994,7 @@ extension GrokWebBillingFetcherTests {
#expect(request.value(forHTTPHeaderField: "Cookie") == "sso=session; sso-rw=session")
#expect(request.value(forHTTPHeaderField: "Authorization") == nil)
#expect(request.value(forHTTPHeaderField: "x-user-agent") == "connect-es/2.1.1")
#expect(request.timeoutInterval == 15)
let response = HTTPURLResponse(
url: endpoint,
statusCode: 200,
Expand All @@ -935,6 +1010,7 @@ extension GrokWebBillingFetcherTests {
endpoint: endpoint)

#expect(snapshot.usedPercent == 9)
#expect(GrokWebBillingStubURLProtocol.requestBodies == [Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00])])
}

@Test
Expand All @@ -955,6 +1031,7 @@ extension GrokWebBillingFetcherTests {
GrokWebBillingStubURLProtocol.handler = { request in
#expect(request.value(forHTTPHeaderField: "Cookie") == "sso=session")
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer token-123")
#expect(request.timeoutInterval == 15)
let response = HTTPURLResponse(
url: endpoint,
statusCode: 200,
Expand All @@ -970,6 +1047,7 @@ extension GrokWebBillingFetcherTests {
endpoint: endpoint)

#expect(snapshot.usedPercent == 9)
#expect(GrokWebBillingStubURLProtocol.requestBodies == [Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00])])
}

@Test
Expand Down
19 changes: 16 additions & 3 deletions docs/grok.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,19 @@ The grok.com billing gRPC-web endpoint remains a best-effort fallback.
that omit `subscription_tier_display` all drop the plan overlay and fall
back to the OIDC SuperGrok label. There is no process-lifetime tier cache.
4) **grok.com billing gRPC-web fallback** (best-effort)
- POSTs an empty gRPC-web protobuf request to
- POSTs `GetGrokCreditsConfigRequest { exclude_legacy_monthly_usage: false }`
(gRPC-web binary frame `00 00 00 00 02 08 00`) to
`https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig`.
Explicitly encoding the default boolean supplies a nonempty protobuf message
for the reported `grpc-status 13` ("Missing request message.") rejection,
without excluding legacy monthly usage or selecting a billing period.
The current public web-client descriptor defines field 1 as this boolean,
not a weekly/monthly enum, and the web client requests `{}`. False preserves
that implicit proto3 default; whether the nonempty encoding restores the
affected account's response still requires live confirmation.
Protocol references: the public [billing descriptor](https://cdn.grok.com/_next/static/chunks/32g78bk5hhe1q.js),
[web billing query](https://cdn.grok.com/_next/static/chunks/062ueaj23tfo3.js)
(checked 2026-09-01), and [proto3 field presence](https://protobuf.dev/programming-guides/field_presence/).
- This endpoint now requires the browser-held Web Key Exchange (WKE) keypair.
Cookie-only authentication can fail with gRPC status 16 and
`no-credentials`; signing in through Chrome alone cannot provide that proof
Expand All @@ -109,8 +120,10 @@ The grok.com billing gRPC-web endpoint remains a best-effort fallback.
- Parses the returned protobuf enough to recover used percent and
reset timestamp, accepting both gRPC-web frames and the raw protobuf form
returned by some successful requests. A current billing period with an
omitted proto3 `credit_usage_percent` is treated as zero usage. This keeps
billing visible when `grok agent stdio` returns `Method not found`.
omitted proto3 `credit_usage_percent` is treated as zero usage only on this
surface; it is not wire-published and cannot replace an unknown proxy percent.
Nonzero timestamp nanoseconds at `[1,4,2]`/`[1,5,2]` do not change that rule.
This keeps billing visible when `grok agent stdio` returns `Method not found`.
5) **Local session signals** (informational fallback)
- Walks `~/.grok/sessions/<encoded-cwd>/<session-id>/signals.json` files (last 30 days).
- Aggregates `totalTokensBeforeCompaction`, `contextTokensUsed`, `modelsUsed`,
Expand Down
Loading