Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}

Expand Down Expand Up @@ -174,6 +181,9 @@ public struct GrokStatusProbe: Sendable {
rpcError = error
}

// 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.
Expand Down Expand Up @@ -204,6 +214,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?,
Expand All @@ -222,6 +249,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(
Expand Down
Loading
Loading