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 @@ -7,6 +7,7 @@

### Fixed

- Grok: restore the weekly Credits window on zero-usage weeks when ACP omits the percentage, while preserving prepaid balances and leaving unvalidated usage unknown (#4674, #4679). Thanks @LeoLin990405 and @psl75011!
- Linux CLI: prevent static musl builds from crashing when scanning non-empty Codex session files, with a regression check for musl and glibc builds (#4672, #4673). Thanks @harjothkhara and @psl75011!

## 0.79.0 — 2026-10-20
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,16 +367,11 @@ struct GrokOAuthFetchStrategy: ProviderFetchStrategy {
try await GrokWebBillingFetcher.fetch(credentials: $0)
}) async throws -> GrokWebBillingResult
{
guard proxySnapshot.usedPercent == nil else {
return GrokWebBillingResult(
snapshot: proxySnapshot,
sourceLabel: "grok-cli-proxy",
authContext: .oauth(credentials))
}
let proxyAnswer = GrokWebBillingResult(
snapshot: proxySnapshot,
sourceLabel: "grok-cli-proxy",
authContext: .oauth(credentials))
guard proxySnapshot.usedPercent == nil else { return proxyAnswer }
let join = BoundedTaskJoin(sourceTask: Task { try await grpcBilling(credentials) })
switch await join.value(joinGrace: budget) {
case let .value(grpcSnapshot):
Expand Down
13 changes: 9 additions & 4 deletions Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ public struct GrokStatusProbe: Sendable {
rpcError = error
}

// ACP wallet-only success needs the same bounded quota enrichment as the proxy.
if let credits = webBilling, let credentials, !credentials.isExpired {
webBilling = try await GrokOAuthFetchStrategy.resolvingUnknownUsage(
credits, credentials: credentials, grpcBilling: {
try await GrokWebBillingFetcher.fetch(credentials: $0, session: self.settingsTransport)
}).snapshot
}

let hasBilling = billing != nil || webBilling != nil
let isIdentityOnly = !hasBilling && self.identityOnlyFallback(credentials, billingAttempted, rpcError)
// Terminal CLI failures must reach the provider's web fallback without scanning discarded history.
Expand Down Expand Up @@ -283,10 +291,7 @@ public struct GrokStatusProbe: Sendable {
{
// If remote usage succeeded, xAI accepted auth and the local
// identity is still useful even when the persisted expires_at is stale.
if billing != nil || webBilling != nil {
return credentials
}
return credentials.flatMap { $0.isExpired ? nil : $0 }
credentials.flatMap { billing != nil || webBilling != nil || !$0.isExpired ? $0 : nil }
}

static func shouldSurfaceRemoteAuthError(_ error: Error?) -> Bool {
Expand Down
179 changes: 179 additions & 0 deletions Tests/CodexBarTests/GrokACPUnknownUsageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import Foundation
import Testing
@testable import CodexBarCore

struct GrokACPUnknownUsageTests {
@Test
func `active ACP period with omitted percentage adopts validated implicit zero`() async throws {
let fixture = try Self.makeFixture(credentials: .valid, prepaidCents: 123)
defer { fixture.cleanup() }
var probe = GrokStatusProbe()
probe.localSummary = { _ in nil }
probe.settingsTransport = ProviderHTTPTransportHandler { request in
let response = try #require(HTTPURLResponse(
url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil))
if request.url == GrokCLISettingsFetcher.defaultEndpoint {
return (Data("{}".utf8), response)
}
#expect(request.url == GrokWebBillingFetcher.defaultEndpoint)
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer synthetic-token")
#expect(request.value(forHTTPHeaderField: "Cookie") == nil)
// The bearer response validates zero but must not replace the ACP reset or wallet.
return (Self.zeroUsagePayload(endingAt: fixture.periodEnd.addingTimeInterval(-90)), response)
}
let snapshot = try await probe.fetch(env: fixture.environment)
let usage = snapshot.toUsageSnapshot()
let primary = try #require(usage.primary)
#expect(primary.usedPercent == 0)
#expect(primary.windowMinutes == 10080)
#expect(primary.resetsAt == fixture.periodEnd)
#expect(usage.providerCost?.balance == 1.23)
#expect(snapshot.webBilling?.usedPercentIsImplicitZero == true)
}

@Test
func `published ACP usage skips enrichment`() async throws {
let fixture = try Self.makeFixture(credentials: .valid, publishedPercent: 5)
defer { fixture.cleanup() }
var probe = GrokStatusProbe()
probe.localSummary = { _ in nil }
probe.settingsTransport = ProviderHTTPTransportHandler { request in
#expect(request.url == GrokCLISettingsFetcher.defaultEndpoint)
return try (Data("{}".utf8), #require(HTTPURLResponse(
url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)))
}
#expect(try await probe.fetch(env: fixture.environment).toUsageSnapshot().primary?.usedPercent == 5)
}

@Test(arguments: [CredentialsMode.valid, .expired, .missing])
func `unavailable enrichment preserves prepaid billing`(_ mode: CredentialsMode) async throws {
let fixture = try Self.makeFixture(credentials: mode, prepaidCents: 123)
defer { fixture.cleanup() }
var probe = GrokStatusProbe()
probe.localSummary = { _ in nil }
probe.settingsTransport = ProviderHTTPTransportHandler { request in
if request.url == GrokWebBillingFetcher.defaultEndpoint {
#expect(mode == .valid)
}
throw URLError(.timedOut)
}
let usage = try await probe.fetch(env: fixture.environment).toUsageSnapshot()
#expect(usage.primary == nil)
#expect(usage.providerCost?.balance == 1.23)
}

private static func zeroUsagePayload(endingAt end: Date) -> Data {
func varint(_ number: UInt64) -> Data {
var number = number
var bytes = Data()
while number >= 0x80 {
bytes.append(UInt8(number & 0x7F) | 0x80)
number >>= 7
}
bytes.append(UInt8(number))
return bytes
}
func message(_ field: UInt8, _ data: Data) -> Data {
Data([field << 3 | 2, UInt8(data.count)]) + data
}
let start = Data([0x08]) + varint(UInt64(end.timeIntervalSince1970) - 7 * 86400)
let finish = Data([0x08]) + varint(UInt64(end.timeIntervalSince1970))
let period = Data([0x08, 0x02]) + message(2, start) + message(3, finish)
return message(1, message(8, period))
}

enum CredentialsMode: Sendable {
case valid, expired, missing
}

private struct ACPFixture: Sendable {
let root: URL
let environment: [String: String]
let periodEnd: Date

func cleanup() {
try? FileManager.default.removeItem(at: self.root)
}
}

private static func makeFixture(
credentials: CredentialsMode,
prepaidCents: Int? = nil,
publishedPercent: Double? = nil) throws -> ACPFixture
{
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("grok-acp-unknown-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime]
let now = Date()
let periodStart = Date(timeIntervalSince1970: now.timeIntervalSince1970.rounded(.down) - 3 * 86400)
let periodEnd = Date(timeIntervalSince1970: now.timeIntervalSince1970.rounded(.down) + 4 * 86400)
let startString = formatter.string(from: periodStart)
let endString = formatter.string(from: periodEnd)

if credentials != .missing {
let expiresAt = credentials == .expired ? "2000-01-01T00:00:00Z" : "2099-01-01T00:00:00Z"
let auth: [String: Any] = [
"https://auth.x.ai::fake-client": [
"key": "synthetic-token",
"refresh_token": "synthetic-refresh",
"email": "grok@example.com",
"auth_mode": "oidc",
"principal_type": "User",
"expires_at": expiresAt,
],
]
try JSONSerialization.data(withJSONObject: auth)
.write(to: root.appendingPathComponent("auth.json"))
}

var config: [String: Any] = [
"currentPeriod": [
"type": "USAGE_PERIOD_TYPE_WEEKLY",
"start": startString,
"end": endString,
],
]
if let prepaidCents {
config["prepaidBalance"] = ["val": prepaidCents]
}
if let publishedPercent {
config["creditUsagePercent"] = publishedPercent
}
let billing: [String: Any] = ["config": config, "subscription_tier": "SuperGrok Heavy"]

func reply(id: Int, result: [String: Any]) throws -> String {
let data = try JSONSerialization.data(
withJSONObject: ["jsonrpc": "2.0", "id": id, "result": result])
return try #require(String(bytes: data, encoding: .utf8))
}
let initialize = try reply(id: 1, result: [:])
let billingReply = try reply(id: 2, result: billing)
let binary = root.appendingPathComponent("grok-fixture")
let script = """
#!/bin/sh
if [ "$1" = "--version" ]; then
printf '%s\\n' 'grok synthetic-version'
exit 0
fi
IFS= read -r initialize_request
printf '%s\\n' '\(initialize)'
IFS= read -r billing_request || exit 0
printf '%s\\n' '\(billingReply)'
"""
try script.write(to: binary, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)

return try ACPFixture(
root: root,
environment: [
"HOME": root.path,
"GROK_HOME": root.path,
"GROK_CLI_PATH": binary.path,
"PATH": "/usr/bin:/bin",
],
periodEnd: #require(ISO8601DateParser.parse(endString)))
}
}
7 changes: 7 additions & 0 deletions docs/grok.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ The grok.com billing gRPC-web endpoint remains a best-effort fallback.
periods, product usage, and prepaid balance. The RPC wrapper's `subscription_tier` supplies the plan.
Older `monthlyLimit` / `usage.totalUsed` payloads remain supported. A prepaid balance remains
useful without a quota percentage; results with neither are unavailable instead of empty snapshots.
- An active period plus prepaid balance can omit `creditUsagePercent` at the start of a week.
The ACP snapshot alone has unknown usage, so the fetch applies the same bounded
grok.com bearer retry described for the CLI-proxy credits path (step 3) before accepting the
wallet-only answer. Only a wire-published percentage or a parser-validated implicit zero for an
active period can restore the weekly Credits window; the ACP period, plan, and prepaid balance
stay authoritative, and failed, slow, or absent enrichment leaves usage unknown without
dropping the wallet. Missing or expired `auth.json` tokens skip the retry entirely.
- If the extension returns `-32601`, personal/unknown principals continue to the web fallback,
while a team principal degrades to identity-only with the unsupported-team-usage diagnostic.
- Missing methods are classified by JSON-RPC code `-32601`, independently of
Expand Down
Loading