From 928ffca29a0e8194aa65b3e3e0894f52b4ba41e4 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 | 4 + .../Providers/Grok/GrokStatusProbe.swift | 60 +++ .../GrokACPUnknownUsageTests.swift | 362 ++++++++++++++++++ docs/grok.md | 7 + 4 files changed, 433 insertions(+) create mode 100644 Tests/CodexBarTests/GrokACPUnknownUsageTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c360c74e1..e735ce0eddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.79.1 — Unreleased +### Fixed + +- 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 ### Highlights 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 270f6148b04533c07c9ce1de11f5816f62047fa0 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 179adfef4cef4de77bad9d329814601fdeca591b Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:12:23 -0300 Subject: [PATCH 3/3] test(repro): show #4674 regression fails without ACP enrichment wiring Proof-only variant: keeps the fix commit's seam, helpers, tests, docs, and changelog (plus the test access-level fix), but removes the production call in `GrokStatusProbe.fetch()` that enriches ACP credits whose usage percentage is unknown. The focused `GrokACPUnknownUsageTests` regression is expected to fail on this tree, demonstrating that it is a failing-before-fix test. Refs #4674 Do not merge. --- .../CodexBarCore/Providers/Grok/GrokStatusProbe.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift index e3d4bd0accd..04d5595a5e9 100644 --- a/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift @@ -181,13 +181,9 @@ 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) - } - + // PROOF VARIANT (#4674): the ACP unknown-usage enrichment wiring is intentionally reverted + // here so the regression in `GrokACPUnknownUsageTests` shows its failing-before-fix state. + // The seam and helpers below stay in place; only the call from `fetch()` is removed. 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.