From 51995eff0f3c31464d97093395ca17a209a66cce Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:39:44 -0300 Subject: [PATCH 1/3] fix(grok): restore the weekly credits window on a zero-usage week The ACP `_x.ai/billing` credits payload can describe an active weekly period and a prepaid balance while omitting `creditUsagePercent` right after a period rollover. `GrokRPCClient.fetchBilling()` accepted that wallet-only snapshot, so `GrokStatusProbe.fetch()` treated billing as successful and the auto pipeline never reached the #3261 web fallback; `toUsageSnapshot()` then had no primary window for the rest of the week. Reuse the existing bounded, same-account usage enrichment for ACP credits whose percentage is unknown: ask the grok.com bearer surface with the same auth-file token under the six-second budget and adopt only a wire-published percentage or a parser-validated implicit zero for an active period. The ACP period, plan, and prepaid balance stay authoritative, a failed or slow enrichment keeps the already-valid wallet-only billing, and missing or expired tokens never reach the network. Adds synthetic ACP regression coverage for the zero-usage week, enrichment unknown/failure/cancellation/timeout paths, credential guards, published-percent no-retry behavior, and unknown-usage preservation. Updates docs/grok.md and the 0.79.1 changelog. Refs #4674 --- CHANGELOG.md | 1 + .../Providers/Grok/GrokStatusProbe.swift | 60 +++ .../GrokACPUnknownUsageTests.swift | 362 ++++++++++++++++++ docs/grok.md | 7 + 4 files changed, 430 insertions(+) create mode 100644 Tests/CodexBarTests/GrokACPUnknownUsageTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index e2737694df8..fbdda0c1840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.79.1 — Unreleased - Linux: preserve autostart preferences on reinstall when desktop entries contain whitespace, comments, or other groups. (#4662) Thanks @Lucenx9! +- Grok: restore the weekly Credits window on a zero-usage week when the ACP `_x.ai/billing` answer omits the usage percentage, by enriching wallet-only responses with the bounded, validated grok.com usage reading while keeping the prepaid balance and leaving usage unknown when the retry cannot validate a percent (#4674). ## 0.79.0 — 2026-10-20 diff --git a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift index 9b6f5b4cd05..e3d4bd0accd 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift @@ -126,6 +126,13 @@ public struct GrokStatusProbe: Sendable { var settingsTransport: any ProviderHTTPTransport = ProviderHTTPClient.shared var identityOnlyFallback: @Sendable (GrokCredentials?, Bool, Error?) -> Bool = GrokStatusProbe.shouldUseIdentityOnlyFallback + /// Best-effort grok.com usage lookup for ACP credits that carry an active period and a wallet + /// balance but omit the usage percentage (a zero-usage week). Injectable so probe tests stay + /// hermetic instead of reaching the network. + var enrichUnknownCredits: @Sendable (GrokWebBillingSnapshot, GrokCredentials?) async throws + -> GrokWebBillingSnapshot = { snapshot, credentials in + try await GrokStatusProbe.enrichingUnknownCredits(snapshot, credentials: credentials) + } public init() {} @@ -174,6 +181,13 @@ public struct GrokStatusProbe: Sendable { rpcError = error } + // ACP reports an active period and prepaid balance while omitting `creditUsagePercent` + // early in a billing period. Ask the grok.com bearer surface for a validated percentage + // before the wallet-only answer ends the pipeline; keep the ACP answer when it has none. + if let credits = webBilling, credits.usedPercent == nil { + webBilling = try await self.enrichingCredits(credits, credentials: credentials) + } + 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. @@ -204,6 +218,23 @@ public struct GrokStatusProbe: Sendable { subscriptionTier: subscriptionTier) } + /// Enrichment is best-effort: cancellation propagates, but any other failure keeps the + /// already-valid wallet-only billing instead of discarding it. + func enrichingCredits( + _ snapshot: GrokWebBillingSnapshot, + credentials: GrokCredentials?) async throws -> GrokWebBillingSnapshot + { + do { + return try await self.enrichUnknownCredits(snapshot, credentials) + } catch is CancellationError { + throw CancellationError() + } catch let error as URLError where error.code == .cancelled { + throw error + } catch { + return snapshot + } + } + static func identityOnlySnapshot( credentials: GrokCredentials, localSummary: GrokLocalSessionSummary?, @@ -222,6 +253,35 @@ public struct GrokStatusProbe: Sendable { subscriptionTier: subscriptionTier) } + /// The ACP credits answer can describe an active billing period and a prepaid balance while + /// omitting `creditUsagePercent` (a zero-usage week). That is unknown usage, and an unknown + /// percent yields no rate window, so ask the grok.com bearer surface with the same auth-file + /// token (no browser cookies) before accepting the wallet-only answer. This reuses the CLI-proxy + /// retry's bounded budget and strict validation: only a wire-published percentage or a + /// parser-validated implicit zero from an active period can replace unknown usage, and the + /// ACP period, plan, and wallet metadata stay authoritative. + static func enrichingUnknownCredits( + _ snapshot: GrokWebBillingSnapshot, + credentials: GrokCredentials?, + budget: Duration = GrokOAuthFetchStrategy.unknownUsageEnrichmentBudget, + grpcBilling: @escaping GrokWebFetchStrategy.ProxyBillingFetch = { + try await GrokWebBillingFetcher.fetch(credentials: $0) + }) async throws -> GrokWebBillingSnapshot + { + guard snapshot.usedPercent == nil, + let credentials, + !credentials.isExpired + else { + return snapshot + } + return try await GrokOAuthFetchStrategy.resolvingUnknownUsage( + snapshot, + credentials: credentials, + budget: budget, + grpcBilling: grpcBilling) + .snapshot + } + static let settingsJoinGrace = Duration.seconds(2) static func loadSettingsTier( diff --git a/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift b/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift new file mode 100644 index 00000000000..c947e90c8d7 --- /dev/null +++ b/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift @@ -0,0 +1,362 @@ +import Foundation +import Testing +@testable import CodexBarCore + +/// Regression coverage for #4674: the ACP `_x.ai/billing` answer can describe an active weekly +/// period and a prepaid balance while omitting `creditUsagePercent` (a zero-usage week). That +/// wallet-only answer is valid and must keep its prepaid balance, but it must also regain the +/// weekly Credits window through the bounded, validated grok.com usage enrichment rather than +/// ending the pipeline with unknown usage. +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() } + let probe = Self.makeProbe { snapshot, credentials in + #expect(credentials?.accessToken == "synthetic-token") + return try await GrokStatusProbe.enrichingUnknownCredits( + snapshot, + credentials: credentials, + grpcBilling: { _ in + // grok.com reports its own reset; the ACP period stays authoritative. + GrokWebBillingSnapshot( + usedPercent: 0, + resetsAt: fixture.periodEnd.addingTimeInterval(-90), + windowMinutes: 10080, + usedPercentIsWirePublished: false, + usedPercentIsImplicitZero: true) + }) + } + + let snapshot = try await probe.fetch(env: fixture.environment) + #expect(snapshot.billing == nil) + #expect(snapshot.webBilling?.usedPercent == 0) + #expect(snapshot.webBilling?.usedPercentIsImplicitZero == true) + #expect(snapshot.webBilling?.windowMinutes == 10080) + #expect(snapshot.webBilling?.resetsAt == fixture.periodEnd) + #expect(snapshot.webBilling?.prepaidBalanceUSD == 1.23) + #expect(snapshot.diagnostic == nil) + + 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) + } + + @Test + func `published ACP percentage never triggers the grok.com retry`() async throws { + let fixture = try Self.makeFixture(credentials: .valid, publishedPercent: 5) + defer { fixture.cleanup() } + let probe = Self.makeProbe { _, _ in + Issue.record("Enrichment must not run when ACP already published a percentage") + throw GrokWebBillingError.parseFailed + } + + let usage = try await probe.fetch(env: fixture.environment).toUsageSnapshot() + #expect(usage.primary?.usedPercent == 5) + } + + @Test + func `enrichment without a validated percentage keeps unknown usage and the wallet`() async throws { + let fixture = try Self.makeFixture(credentials: .valid, prepaidCents: 123) + defer { fixture.cleanup() } + let probe = Self.makeProbe { snapshot, credentials in + try await GrokStatusProbe.enrichingUnknownCredits( + snapshot, + credentials: credentials, + grpcBilling: { _ in GrokWebBillingSnapshot(usedPercent: nil, resetsAt: nil) }) + } + + let snapshot = try await probe.fetch(env: fixture.environment) + let usage = snapshot.toUsageSnapshot() + #expect(snapshot.webBilling?.usedPercent == nil) + #expect(usage.primary == nil) + #expect(usage.providerCost?.balance == 1.23) + } + + @Test + func `enrichment failure cannot discard already-valid wallet-only billing`() async throws { + let fixture = try Self.makeFixture(credentials: .valid, prepaidCents: 123) + defer { fixture.cleanup() } + let probe = Self.makeProbe { _, _ in + throw GrokWebBillingError.requestFailed(503, "synthetic outage") + } + + let snapshot = try await probe.fetch(env: fixture.environment) + let usage = snapshot.toUsageSnapshot() + #expect(snapshot.webBilling?.prepaidBalanceUSD == 1.23) + #expect(usage.primary == nil) + #expect(usage.providerCost?.balance == 1.23) + } + + @Test(arguments: [CredentialsMode.expired, .missing]) + func `expired or missing credentials skip enrichment but keep the wallet`(_ mode: CredentialsMode) async throws { + let fixture = try Self.makeFixture(credentials: mode, prepaidCents: 123) + defer { fixture.cleanup() } + // The default seam must short-circuit before any grok.com request: this test would hang or + // fail hermeticity if it reached the network. + let probe = GrokStatusProbe() + + let snapshot = try await probe.fetch(env: fixture.environment) + let usage = snapshot.toUsageSnapshot() + #expect(usage.primary == nil) + #expect(usage.providerCost?.balance == 1.23) + } + + @Test + func `enrichment cancellation propagates instead of settling for unknown usage`() async throws { + let fixture = try Self.makeFixture(credentials: .valid, prepaidCents: 123) + defer { fixture.cleanup() } + let probe = Self.makeProbe { _, _ in + throw CancellationError() + } + + do { + _ = try await probe.fetch(env: fixture.environment) + Issue.record("Expected ACP usage enrichment cancellation to propagate") + } catch is CancellationError { + // Expected: the caller canceled instead of waiting for wallet-only data. + } + } + + @Test + func `published percentage and live credentials never enter the bounded retry`() async throws { + let snapshot = GrokWebBillingSnapshot( + usedPercent: 5, + resetsAt: Date(timeIntervalSince1970: 1_900_000_000), + windowMinutes: 10080, + subscriptionTier: "SuperGrok Heavy", + prepaidBalanceUSD: 1.23) + let enriched = try await GrokStatusProbe.enrichingUnknownCredits( + snapshot, + credentials: Self.validCredentials, + grpcBilling: { _ in + Issue.record("Published ACP usage must not schedule another request") + throw GrokWebBillingError.parseFailed + }) + #expect(enriched == snapshot) + } + + @Test + func `missing or expired credentials never enter the bounded retry`() async throws { + let snapshot = Self.unknownCreditsSnapshot() + let credentialSets: [GrokCredentials?] = [nil, Self.expiredCredentials] + for credentials in credentialSets { + let enriched = try await GrokStatusProbe.enrichingUnknownCredits( + snapshot, + credentials: credentials, + grpcBilling: { _ in + Issue.record("grok.com billing must not run without non-expired ACP credentials") + throw GrokWebBillingError.missingCredentials + }) + #expect(enriched == snapshot) + } + } + + @Test + func `unvalidated zero cannot replace unknown ACP usage`() async throws { + let snapshot = Self.unknownCreditsSnapshot() + let enriched = try await GrokStatusProbe.enrichingUnknownCredits( + snapshot, + credentials: Self.validCredentials, + grpcBilling: { _ in + GrokWebBillingSnapshot( + usedPercent: 0, + resetsAt: nil, + usedPercentIsWirePublished: false, + usedPercentIsImplicitZero: false) + }) + #expect(enriched == snapshot) + #expect(enriched.usedPercent == nil) + } + + @Test + func `bounded retry timeout keeps the ACP period, plan, and wallet`() async throws { + let snapshot = Self.unknownCreditsSnapshot() + let enriched = try await GrokStatusProbe.enrichingUnknownCredits( + snapshot, + credentials: Self.validCredentials, + budget: .milliseconds(20), + grpcBilling: { _ in + try await Task.sleep(for: .seconds(30)) + return GrokWebBillingSnapshot(usedPercent: 99, resetsAt: nil) + }) + #expect(enriched == snapshot) + } + + @Test + func `published grok.com percentage adopts the ACP period, plan, and wallet`() async throws { + let snapshot = Self.unknownCreditsSnapshot() + let enriched = try await GrokStatusProbe.enrichingUnknownCredits( + snapshot, + credentials: Self.validCredentials, + grpcBilling: { _ in + GrokWebBillingSnapshot( + usedPercent: 12, + resetsAt: Date(timeIntervalSince1970: 1_950_000_000), + windowMinutes: 10080, + usedPercentIsWirePublished: true) + }) + #expect(enriched.usedPercent == 12) + #expect(enriched.usedPercentIsWirePublished) + #expect(enriched.resetsAt == snapshot.resetsAt) + #expect(enriched.windowMinutes == snapshot.windowMinutes) + #expect(enriched.subscriptionTier == snapshot.subscriptionTier) + #expect(enriched.prepaidBalanceUSD == snapshot.prepaidBalanceUSD) + } + + // MARK: - Synthetic ACP fixture + + private 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"] + let requestLog = root.appendingPathComponent("billing-requests.jsonl") + + 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' "$billing_request" >> "\(requestLog.path)" + 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))) + } + + private static func makeProbe( + enrich: @escaping @Sendable (GrokWebBillingSnapshot, GrokCredentials?) async throws + -> GrokWebBillingSnapshot) -> GrokStatusProbe + { + 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)) + return (Data("{}".utf8), response) + } + probe.enrichUnknownCredits = enrich + return probe + } + + private static func unknownCreditsSnapshot() -> GrokWebBillingSnapshot { + GrokWebBillingSnapshot( + usedPercent: nil, + resetsAt: Date(timeIntervalSince1970: 1_900_000_000), + windowMinutes: 10080, + subscriptionTier: "SuperGrok Heavy", + prepaidBalanceUSD: 1.23) + } + + private static let validCredentials = GrokCredentials( + accessToken: "synthetic-token", + refreshToken: nil, + scope: "https://auth.x.ai::test", + authMode: "oidc", + userId: nil, + email: nil, + firstName: nil, + lastName: nil, + teamId: nil, + oidcIssuer: nil, + oidcClientId: nil, + expiresAt: .distantFuture, + createTime: nil) + + private static let expiredCredentials = GrokCredentials( + accessToken: "synthetic-token", + refreshToken: nil, + scope: "https://auth.x.ai::test", + authMode: "oidc", + userId: nil, + email: nil, + firstName: nil, + lastName: nil, + teamId: nil, + oidcIssuer: nil, + oidcClientId: nil, + expiresAt: Date(timeIntervalSince1970: 1_000_000), + createTime: nil) +} diff --git a/docs/grok.md b/docs/grok.md index 93872a5ebe9..fd94f2adb09 100644 --- a/docs/grok.md +++ b/docs/grok.md @@ -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 without `creditUsagePercent` is a zero-usage week, not + data to forget: the ACP snapshot alone has no percentage, 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 From a7b3d1298754813c00f056f36c414b097759cd26 Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:12:07 -0300 Subject: [PATCH 2/3] fix(grok): expose the ACP regression test's credentials mode `CredentialsMode` is a parameter of an internal `@Test(arguments:)` entry point, so declaring the nested enum private fails the macOS test-target compile: "method must be declared private because its parameter uses a private type". Nesting it in the internal suite already keeps it test-target-local. Refs #4674 --- Tests/CodexBarTests/GrokACPUnknownUsageTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift b/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift index c947e90c8d7..af1fea667f1 100644 --- a/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift +++ b/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift @@ -209,7 +209,8 @@ struct GrokACPUnknownUsageTests { // MARK: - Synthetic ACP fixture - private enum CredentialsMode: Sendable { + /// The `@Test(arguments:)` entry point is internal, so its parameter type must not be private. + enum CredentialsMode: Sendable { case valid, expired, missing } From 10dbc839a1c021563b515bf466496706b2dbf25e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 21 Oct 2026 01:41:32 -0700 Subject: [PATCH 3/3] fix(grok): reuse bounded enrichment for ACP credit usage Use the existing quota enrichment path for wallet-only ACP replies, preserving validated zero usage and the ACP billing period without adding another fetch hook. Keep the end-to-end synthetic ACP regression and remove duplicate result assembly. Fixes #4674 Co-authored-by: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> --- CHANGELOG.md | 5 +- .../Grok/GrokProviderDescriptor.swift | 7 +- .../Providers/Grok/GrokStatusProbe.swift | 69 +---- .../GrokACPUnknownUsageTests.swift | 282 +++--------------- docs/grok.md | 4 +- 5 files changed, 63 insertions(+), 304 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbdda0c1840..745581bed3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,10 @@ ## 0.79.1 — Unreleased - Linux: preserve autostart preferences on reinstall when desktop entries contain whitespace, comments, or other groups. (#4662) Thanks @Lucenx9! -- Grok: restore the weekly Credits window on a zero-usage week when the ACP `_x.ai/billing` answer omits the usage percentage, by enriching wallet-only responses with the bounded, validated grok.com usage reading while keeping the prepaid balance and leaving usage unknown when the retry cannot validate a percent (#4674). + +### 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! ## 0.79.0 — 2026-10-20 diff --git a/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift index fb6e9521516..e3c689c7bd6 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift @@ -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): diff --git a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift index e3d4bd0accd..712508315cd 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift @@ -126,13 +126,6 @@ public struct GrokStatusProbe: Sendable { var settingsTransport: any ProviderHTTPTransport = ProviderHTTPClient.shared var identityOnlyFallback: @Sendable (GrokCredentials?, Bool, Error?) -> Bool = GrokStatusProbe.shouldUseIdentityOnlyFallback - /// Best-effort grok.com usage lookup for ACP credits that carry an active period and a wallet - /// balance but omit the usage percentage (a zero-usage week). Injectable so probe tests stay - /// hermetic instead of reaching the network. - var enrichUnknownCredits: @Sendable (GrokWebBillingSnapshot, GrokCredentials?) async throws - -> GrokWebBillingSnapshot = { snapshot, credentials in - try await GrokStatusProbe.enrichingUnknownCredits(snapshot, credentials: credentials) - } public init() {} @@ -181,11 +174,12 @@ public struct GrokStatusProbe: Sendable { rpcError = error } - // ACP reports an active period and prepaid balance while omitting `creditUsagePercent` - // early in a billing period. Ask the grok.com bearer surface for a validated percentage - // before the wallet-only answer ends the pipeline; keep the ACP answer when it has none. - if let credits = webBilling, credits.usedPercent == nil { - webBilling = try await self.enrichingCredits(credits, credentials: credentials) + // 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 @@ -218,23 +212,6 @@ public struct GrokStatusProbe: Sendable { subscriptionTier: subscriptionTier) } - /// Enrichment is best-effort: cancellation propagates, but any other failure keeps the - /// already-valid wallet-only billing instead of discarding it. - func enrichingCredits( - _ snapshot: GrokWebBillingSnapshot, - credentials: GrokCredentials?) async throws -> GrokWebBillingSnapshot - { - do { - return try await self.enrichUnknownCredits(snapshot, credentials) - } catch is CancellationError { - throw CancellationError() - } catch let error as URLError where error.code == .cancelled { - throw error - } catch { - return snapshot - } - } - static func identityOnlySnapshot( credentials: GrokCredentials, localSummary: GrokLocalSessionSummary?, @@ -253,35 +230,6 @@ public struct GrokStatusProbe: Sendable { subscriptionTier: subscriptionTier) } - /// The ACP credits answer can describe an active billing period and a prepaid balance while - /// omitting `creditUsagePercent` (a zero-usage week). That is unknown usage, and an unknown - /// percent yields no rate window, so ask the grok.com bearer surface with the same auth-file - /// token (no browser cookies) before accepting the wallet-only answer. This reuses the CLI-proxy - /// retry's bounded budget and strict validation: only a wire-published percentage or a - /// parser-validated implicit zero from an active period can replace unknown usage, and the - /// ACP period, plan, and wallet metadata stay authoritative. - static func enrichingUnknownCredits( - _ snapshot: GrokWebBillingSnapshot, - credentials: GrokCredentials?, - budget: Duration = GrokOAuthFetchStrategy.unknownUsageEnrichmentBudget, - grpcBilling: @escaping GrokWebFetchStrategy.ProxyBillingFetch = { - try await GrokWebBillingFetcher.fetch(credentials: $0) - }) async throws -> GrokWebBillingSnapshot - { - guard snapshot.usedPercent == nil, - let credentials, - !credentials.isExpired - else { - return snapshot - } - return try await GrokOAuthFetchStrategy.resolvingUnknownUsage( - snapshot, - credentials: credentials, - budget: budget, - grpcBilling: grpcBilling) - .snapshot - } - static let settingsJoinGrace = Duration.seconds(2) static func loadSettingsTier( @@ -343,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 { diff --git a/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift b/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift index af1fea667f1..ad23e5ee370 100644 --- a/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift +++ b/Tests/CodexBarTests/GrokACPUnknownUsageTests.swift @@ -2,214 +2,86 @@ import Foundation import Testing @testable import CodexBarCore -/// Regression coverage for #4674: the ACP `_x.ai/billing` answer can describe an active weekly -/// period and a prepaid balance while omitting `creditUsagePercent` (a zero-usage week). That -/// wallet-only answer is valid and must keep its prepaid balance, but it must also regain the -/// weekly Credits window through the bounded, validated grok.com usage enrichment rather than -/// ending the pipeline with unknown usage. 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() } - let probe = Self.makeProbe { snapshot, credentials in - #expect(credentials?.accessToken == "synthetic-token") - return try await GrokStatusProbe.enrichingUnknownCredits( - snapshot, - credentials: credentials, - grpcBilling: { _ in - // grok.com reports its own reset; the ACP period stays authoritative. - GrokWebBillingSnapshot( - usedPercent: 0, - resetsAt: fixture.periodEnd.addingTimeInterval(-90), - windowMinutes: 10080, - usedPercentIsWirePublished: false, - usedPercentIsImplicitZero: true) - }) + 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) - #expect(snapshot.billing == nil) - #expect(snapshot.webBilling?.usedPercent == 0) - #expect(snapshot.webBilling?.usedPercentIsImplicitZero == true) - #expect(snapshot.webBilling?.windowMinutes == 10080) - #expect(snapshot.webBilling?.resetsAt == fixture.periodEnd) - #expect(snapshot.webBilling?.prepaidBalanceUSD == 1.23) - #expect(snapshot.diagnostic == nil) - 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 percentage never triggers the grok.com retry`() async throws { + func `published ACP usage skips enrichment`() async throws { let fixture = try Self.makeFixture(credentials: .valid, publishedPercent: 5) defer { fixture.cleanup() } - let probe = Self.makeProbe { _, _ in - Issue.record("Enrichment must not run when ACP already published a percentage") - throw GrokWebBillingError.parseFailed - } - - let usage = try await probe.fetch(env: fixture.environment).toUsageSnapshot() - #expect(usage.primary?.usedPercent == 5) - } - - @Test - func `enrichment without a validated percentage keeps unknown usage and the wallet`() async throws { - let fixture = try Self.makeFixture(credentials: .valid, prepaidCents: 123) - defer { fixture.cleanup() } - let probe = Self.makeProbe { snapshot, credentials in - try await GrokStatusProbe.enrichingUnknownCredits( - snapshot, - credentials: credentials, - grpcBilling: { _ in GrokWebBillingSnapshot(usedPercent: nil, resetsAt: nil) }) - } - - let snapshot = try await probe.fetch(env: fixture.environment) - let usage = snapshot.toUsageSnapshot() - #expect(snapshot.webBilling?.usedPercent == nil) - #expect(usage.primary == nil) - #expect(usage.providerCost?.balance == 1.23) - } - - @Test - func `enrichment failure cannot discard already-valid wallet-only billing`() async throws { - let fixture = try Self.makeFixture(credentials: .valid, prepaidCents: 123) - defer { fixture.cleanup() } - let probe = Self.makeProbe { _, _ in - throw GrokWebBillingError.requestFailed(503, "synthetic outage") + 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))) } - - let snapshot = try await probe.fetch(env: fixture.environment) - let usage = snapshot.toUsageSnapshot() - #expect(snapshot.webBilling?.prepaidBalanceUSD == 1.23) - #expect(usage.primary == nil) - #expect(usage.providerCost?.balance == 1.23) + #expect(try await probe.fetch(env: fixture.environment).toUsageSnapshot().primary?.usedPercent == 5) } - @Test(arguments: [CredentialsMode.expired, .missing]) - func `expired or missing credentials skip enrichment but keep the wallet`(_ mode: CredentialsMode) async throws { + @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() } - // The default seam must short-circuit before any grok.com request: this test would hang or - // fail hermeticity if it reached the network. - let probe = GrokStatusProbe() - - let snapshot = try await probe.fetch(env: fixture.environment) - let usage = snapshot.toUsageSnapshot() + 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) } - @Test - func `enrichment cancellation propagates instead of settling for unknown usage`() async throws { - let fixture = try Self.makeFixture(credentials: .valid, prepaidCents: 123) - defer { fixture.cleanup() } - let probe = Self.makeProbe { _, _ in - throw CancellationError() - } - - do { - _ = try await probe.fetch(env: fixture.environment) - Issue.record("Expected ACP usage enrichment cancellation to propagate") - } catch is CancellationError { - // Expected: the caller canceled instead of waiting for wallet-only data. + 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 } - } - - @Test - func `published percentage and live credentials never enter the bounded retry`() async throws { - let snapshot = GrokWebBillingSnapshot( - usedPercent: 5, - resetsAt: Date(timeIntervalSince1970: 1_900_000_000), - windowMinutes: 10080, - subscriptionTier: "SuperGrok Heavy", - prepaidBalanceUSD: 1.23) - let enriched = try await GrokStatusProbe.enrichingUnknownCredits( - snapshot, - credentials: Self.validCredentials, - grpcBilling: { _ in - Issue.record("Published ACP usage must not schedule another request") - throw GrokWebBillingError.parseFailed - }) - #expect(enriched == snapshot) - } - - @Test - func `missing or expired credentials never enter the bounded retry`() async throws { - let snapshot = Self.unknownCreditsSnapshot() - let credentialSets: [GrokCredentials?] = [nil, Self.expiredCredentials] - for credentials in credentialSets { - let enriched = try await GrokStatusProbe.enrichingUnknownCredits( - snapshot, - credentials: credentials, - grpcBilling: { _ in - Issue.record("grok.com billing must not run without non-expired ACP credentials") - throw GrokWebBillingError.missingCredentials - }) - #expect(enriched == snapshot) + 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)) } - @Test - func `unvalidated zero cannot replace unknown ACP usage`() async throws { - let snapshot = Self.unknownCreditsSnapshot() - let enriched = try await GrokStatusProbe.enrichingUnknownCredits( - snapshot, - credentials: Self.validCredentials, - grpcBilling: { _ in - GrokWebBillingSnapshot( - usedPercent: 0, - resetsAt: nil, - usedPercentIsWirePublished: false, - usedPercentIsImplicitZero: false) - }) - #expect(enriched == snapshot) - #expect(enriched.usedPercent == nil) - } - - @Test - func `bounded retry timeout keeps the ACP period, plan, and wallet`() async throws { - let snapshot = Self.unknownCreditsSnapshot() - let enriched = try await GrokStatusProbe.enrichingUnknownCredits( - snapshot, - credentials: Self.validCredentials, - budget: .milliseconds(20), - grpcBilling: { _ in - try await Task.sleep(for: .seconds(30)) - return GrokWebBillingSnapshot(usedPercent: 99, resetsAt: nil) - }) - #expect(enriched == snapshot) - } - - @Test - func `published grok.com percentage adopts the ACP period, plan, and wallet`() async throws { - let snapshot = Self.unknownCreditsSnapshot() - let enriched = try await GrokStatusProbe.enrichingUnknownCredits( - snapshot, - credentials: Self.validCredentials, - grpcBilling: { _ in - GrokWebBillingSnapshot( - usedPercent: 12, - resetsAt: Date(timeIntervalSince1970: 1_950_000_000), - windowMinutes: 10080, - usedPercentIsWirePublished: true) - }) - #expect(enriched.usedPercent == 12) - #expect(enriched.usedPercentIsWirePublished) - #expect(enriched.resetsAt == snapshot.resetsAt) - #expect(enriched.windowMinutes == snapshot.windowMinutes) - #expect(enriched.subscriptionTier == snapshot.subscriptionTier) - #expect(enriched.prepaidBalanceUSD == snapshot.prepaidBalanceUSD) - } - - // MARK: - Synthetic ACP fixture - - /// The `@Test(arguments:)` entry point is internal, so its parameter type must not be private. enum CredentialsMode: Sendable { case valid, expired, missing } @@ -271,7 +143,6 @@ struct GrokACPUnknownUsageTests { config["creditUsagePercent"] = publishedPercent } let billing: [String: Any] = ["config": config, "subscription_tier": "SuperGrok Heavy"] - let requestLog = root.appendingPathComponent("billing-requests.jsonl") func reply(id: Int, result: [String: Any]) throws -> String { let data = try JSONSerialization.data( @@ -290,7 +161,6 @@ struct GrokACPUnknownUsageTests { IFS= read -r initialize_request printf '%s\\n' '\(initialize)' IFS= read -r billing_request || exit 0 - printf '%s\\n' "$billing_request" >> "\(requestLog.path)" printf '%s\\n' '\(billingReply)' """ try script.write(to: binary, atomically: true, encoding: .utf8) @@ -306,58 +176,4 @@ struct GrokACPUnknownUsageTests { ], periodEnd: #require(ISO8601DateParser.parse(endString))) } - - private static func makeProbe( - enrich: @escaping @Sendable (GrokWebBillingSnapshot, GrokCredentials?) async throws - -> GrokWebBillingSnapshot) -> GrokStatusProbe - { - 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)) - return (Data("{}".utf8), response) - } - probe.enrichUnknownCredits = enrich - return probe - } - - private static func unknownCreditsSnapshot() -> GrokWebBillingSnapshot { - GrokWebBillingSnapshot( - usedPercent: nil, - resetsAt: Date(timeIntervalSince1970: 1_900_000_000), - windowMinutes: 10080, - subscriptionTier: "SuperGrok Heavy", - prepaidBalanceUSD: 1.23) - } - - private static let validCredentials = GrokCredentials( - accessToken: "synthetic-token", - refreshToken: nil, - scope: "https://auth.x.ai::test", - authMode: "oidc", - userId: nil, - email: nil, - firstName: nil, - lastName: nil, - teamId: nil, - oidcIssuer: nil, - oidcClientId: nil, - expiresAt: .distantFuture, - createTime: nil) - - private static let expiredCredentials = GrokCredentials( - accessToken: "synthetic-token", - refreshToken: nil, - scope: "https://auth.x.ai::test", - authMode: "oidc", - userId: nil, - email: nil, - firstName: nil, - lastName: nil, - teamId: nil, - oidcIssuer: nil, - oidcClientId: nil, - expiresAt: Date(timeIntervalSince1970: 1_000_000), - createTime: nil) } diff --git a/docs/grok.md b/docs/grok.md index fd94f2adb09..5987b49c5fa 100644 --- a/docs/grok.md +++ b/docs/grok.md @@ -38,8 +38,8 @@ 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 without `creditUsagePercent` is a zero-usage week, not - data to forget: the ACP snapshot alone has no percentage, so the fetch applies the same bounded + - 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