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

### Added

- Grok: show product usage shares beneath the quota bar using the existing billing response, while preserving the total and reset credits (#3975). Thanks @olddonkey!
- Agent sessions: add opt-in Stay Awake for live local agent processes, including idle sessions, with automatic release and a menu status indicator (#2740). Thanks @kocaemre!
- Notifications: add opt-in, account-scoped credential-expiry alerts and route Augment keepalive through shared delivery without repeated refresh notifications (#2512). Thanks @LeoLin990405!
- LiteLLM: optionally show per-model input/output/total tokens and logged requests for the last 30 days while preserving personal/team budgets (#3432). Thanks @anyingiit!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum GrokCreditsProxyFetcher {
public static let defaultEndpoint = URL(
string: "https://cli-chat-proxy.grok.com/v1/billing?format=credits")!
private static let requestTimeoutSeconds: TimeInterval = 15
private static let productCompositionTolerancePercent = 1.0

public static func fetch(
credentials: GrokCredentials,
Expand Down Expand Up @@ -67,7 +68,8 @@ public enum GrokCreditsProxyFetcher {
usedPercent: min(100, max(0, percent)),
resetsAt: resetsAt,
windowMinutes: windowMinutes,
subscriptionTier: subscriptionTier)
subscriptionTier: subscriptionTier,
productUsage: Self.composingProducts(config.productUsage?.values ?? [], creditUsagePercent: percent))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject product details when the total is negative

When the proxy returns a negative creditUsagePercent, the primary total is clamped to 0, but product composition is still checked against the raw negative value. For example, a total of -0.5 with one product at 0.5 falls exactly within the one-point tolerance, so the UI shows a 0% total alongside a nonzero product row. Drop the breakdown for negative raw totals (or validate it against the normalized total) so malformed responses cannot present contradictory usage.

Useful? React with 👍 / 👎.

}

if let cap = config.onDemandCap?.val,
Expand All @@ -93,6 +95,16 @@ public enum GrokCreditsProxyFetcher {
throw GrokWebBillingError.parseFailed
}

private static func composingProducts(
_ products: [GrokProductUsage],
creditUsagePercent: Double) -> [GrokProductUsage]
{
// Shares must compose this payload's credit percentage; any malformed entry drops the breakdown.
guard !products.isEmpty else { return [] }
let sum = products.reduce(0) { $0 + $1.usedPercent }
return abs(sum - creditUsagePercent) <= Self.productCompositionTolerancePercent ? products : []
}

private static func windowMinutes(start: String?, end: Date?, now: Date) -> Int? {
guard let start = ISO8601DateParser.parse(start),
let end, end > start, start <= now,
Expand All @@ -115,6 +127,38 @@ public enum GrokCreditsProxyFetcher {
let onDemandCap: CreditsAmount?
let onDemandUsed: CreditsAmount?
let subscriptionTier: String?
let productUsage: LossyProductUsageArray?
}

private struct LossyProductUsageArray: Decodable {
let values: [GrokProductUsage]?

init(from decoder: Decoder) {
self.values = (try? decoder.singleValueContainer().decode([LossyProductUsage].self))?
.map(\.value)
}
}

private struct LossyProductUsage: Decodable {
let value: GrokProductUsage

private enum CodingKeys: String, CodingKey {
case product
case usagePercent
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let product = try container.decode(String.self, forKey: .product)
.trimmingCharacters(in: .whitespacesAndNewlines)
let percent = try container.decode(Double.self, forKey: .usagePercent)
guard !product.isEmpty, percent.isFinite, percent >= 0 else {
throw DecodingError.dataCorrupted(.init(
codingPath: decoder.codingPath,
debugDescription: "Invalid product usage"))
}
self.value = GrokProductUsage(product: product, usedPercent: percent)
}
}

private struct CurrentPeriod: Decodable {
Expand Down
30 changes: 18 additions & 12 deletions Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ public enum GrokProviderDescriptor {
manualCookieHeader: nil).sourceMode ?? base
})

fileprivate static func withResetCreditDetails(
usage: UsageSnapshot,
resetCredits: GrokRateLimitResetCreditsSnapshot?,
now: Date) -> UsageSnapshot
{
let enriched = usage.withGrokResetCredits(resetCredits)
let resetSections = GrokRemainingResetsFetcher.detailSections(snapshot: resetCredits, now: now)
return enriched.replacing(details: .value(resetSections + enriched.details))
}

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .grok,
Expand Down Expand Up @@ -201,12 +211,10 @@ struct GrokCLIFetchStrategy: ProviderFetchStrategy {
at: snapshot.updatedAt,
requiresCompleteness: context.requiresOptionalUsageCompleteness)
return self.makeResult(
usage: usage
.withGrokResetCredits(resetResolution.snapshot)
.replacing(details: .value(
GrokRemainingResetsFetcher.detailSections(
snapshot: resetResolution.snapshot,
now: snapshot.updatedAt))),
usage: GrokProviderDescriptor.withResetCreditDetails(
usage: usage,
resetCredits: resetResolution.snapshot,
now: snapshot.updatedAt),
sourceLabel: "grok-cli",
supplementalUsageTask: resetResolution.supplementalUsageTask,
diagnostic: snapshot.diagnostic)
Expand Down Expand Up @@ -530,12 +538,10 @@ struct GrokWebFetchStrategy: ProviderFetchStrategy {
at: snapshot.updatedAt,
requiresCompleteness: context.requiresOptionalUsageCompleteness)
return self.makeResult(
usage: usage
.withGrokResetCredits(resetResolution.snapshot)
.replacing(details: .value(
GrokRemainingResetsFetcher.detailSections(
snapshot: resetResolution.snapshot,
now: snapshot.updatedAt))),
usage: GrokProviderDescriptor.withResetCreditDetails(
usage: usage,
resetCredits: resetResolution.snapshot,
now: snapshot.updatedAt),
sourceLabel: billingResult.sourceLabel,
supplementalUsageTask: resetResolution.supplementalUsageTask,
diagnostic: enrichedBilling.usedPercent == nil ? GrokStatusProbe.usageUnavailableMessage : nil)
Expand Down
31 changes: 31 additions & 0 deletions Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import Foundation

enum GrokProductUsageDetails {
static func sections(for products: [GrokProductUsage]) -> [ProviderDetailSection] {
let rows = products.enumerated()
.filter { $0.element.usedPercent.isFinite && $0.element.usedPercent > 0 }
.sorted {
$0.element.usedPercent == $1.element.usedPercent
? $0.offset < $1.offset
: $0.element.usedPercent > $1.element.usedPercent
}
.map { _, usage in
let product = usage.product.trimmingCharacters(in: .whitespacesAndNewlines)
let label = switch product {
case "GrokBuild": "Grok Build"
case "GrokChat": "Grok Chat"
case "GrokImagine": "Grok Imagine"
case "GrokAppBuilder": "Grok App Builder"
default: product
}
return ProviderDetailSection.makeRow(
id: "grok.product.\(product)",
label: label,
value: UsageFormatter.percentString(usage.usedPercent))
}
guard !rows.isEmpty else { return [] }
return [.makeSection(title: "Usage breakdown", rows: rows)]
}
}

public struct GrokUsageSnapshot: Sendable {
public let billing: GrokBillingResponse?
public let webBilling: GrokWebBillingSnapshot?
Expand Down Expand Up @@ -34,6 +62,7 @@ public struct GrokUsageSnapshot: Sendable {
// Primary window: credit usage (against included limit) from the CLI RPC,
// falling back to the web billing RPC used by grok.com when the agent surface lacks billing.
var primary: RateWindow?
var details: [ProviderDetailSection] = []
if let billing,
let percent = billing.monthlyUsedPercent
{
Expand All @@ -52,6 +81,7 @@ public struct GrokUsageSnapshot: Sendable {
windowMinutes: webBilling.windowMinutes,
resetsAt: webBilling.resetsAt,
resetDescription: nil)
details = GrokProductUsageDetails.sections(for: webBilling.productUsage)
}

let identity = ProviderIdentitySnapshot(
Expand All @@ -68,6 +98,7 @@ public struct GrokUsageSnapshot: Sendable {
tertiary: nil,
costUsage: self.localSummary?.toCostUsageTokenSnapshot(
historyDays: GrokLocalSessionScanner.defaultLookbackDays),
details: details,
updatedAt: self.updatedAt,
identity: identity)
}
Expand Down
25 changes: 21 additions & 4 deletions Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import Foundation
import FoundationNetworking
#endif

public struct GrokProductUsage: Sendable, Equatable {
public let product: String
public let usedPercent: Double

public init(product: String, usedPercent: Double) {
self.product = product
self.usedPercent = usedPercent
}
}

public struct GrokWebBillingSnapshot: Sendable, Equatable {
public let usedPercent: Double?
public let resetsAt: Date?
Expand All @@ -17,21 +27,25 @@ public struct GrokWebBillingSnapshot: Sendable, Equatable {
public let usedPercentIsWirePublished: Bool
/// The parser validated an active current period with an omitted proto3 usage scalar.
public let usedPercentIsImplicitZero: Bool
/// Shares compose this snapshot's credit `usedPercent` from the same payload before clamping; empty if unverified.
public let productUsage: [GrokProductUsage]

public init(
usedPercent: Double?,
resetsAt: Date?,
windowMinutes: Int? = nil,
subscriptionTier: String? = nil,
usedPercentIsWirePublished: Bool = true,
usedPercentIsImplicitZero: Bool = false)
usedPercentIsImplicitZero: Bool = false,
productUsage: [GrokProductUsage] = [])
{
self.usedPercent = usedPercent
self.resetsAt = resetsAt
self.windowMinutes = windowMinutes
self.subscriptionTier = subscriptionTier
self.usedPercentIsWirePublished = usedPercentIsWirePublished
self.usedPercentIsImplicitZero = usedPercentIsImplicitZero
self.productUsage = productUsage
}

/// Overlay the CLI settings plan name. Usage percent stays on the existing credits rules.
Expand All @@ -42,20 +56,23 @@ public struct GrokWebBillingSnapshot: Sendable, Equatable {
windowMinutes: self.windowMinutes,
subscriptionTier: GrokPlan.displayName(from: raw) ?? self.subscriptionTier,
usedPercentIsWirePublished: self.usedPercentIsWirePublished,
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero)
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero,
productUsage: self.productUsage)
}

/// Keep period and plan metadata a second billing surface did not publish. Usage percent
/// always stays with the surface that produced this snapshot, so an unknown percent is
/// never backfilled from another response.
/// never backfilled from another response. Product shares compose that same payload's
/// `usedPercent` and are never borrowed from another billing surface.
func completing(with other: GrokWebBillingSnapshot) -> GrokWebBillingSnapshot {
GrokWebBillingSnapshot(
usedPercent: self.usedPercent,
resetsAt: other.resetsAt ?? self.resetsAt,
windowMinutes: other.resetsAt == nil ? self.windowMinutes : other.windowMinutes,
subscriptionTier: self.subscriptionTier ?? other.subscriptionTier,
usedPercentIsWirePublished: self.usedPercentIsWirePublished,
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero)
usedPercentIsImplicitZero: self.usedPercentIsImplicitZero,
productUsage: self.productUsage)
}
}

Expand Down
Loading
Loading