Skip to content
Closed
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
41 changes: 29 additions & 12 deletions Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ public enum GrokProviderDescriptor {
supportsTokenCost: false,
noDataMessage: { "Grok cost summary is not supported yet." }),
pace: ProviderPaceCapability(resetWindowPace: .custom { window, now in
guard Self.primaryLabel(window: window, now: now) == "Weekly",
guard let label = Self.primaryLabel(window: window, now: now),
label == "Weekly" || label == "Monthly",
let resetsAt = window.resetsAt
else { return false }
let windowMinutes = window.windowMinutes ?? 7 * 24 * 60
// SuperGrok included usage is a weekly pool; monthly only appears when a measured
// billing-period length is present (legacy CLI / older payloads).
let defaultMinutes = label == "Weekly" ? 7 * 24 * 60 : 30 * 24 * 60
let windowMinutes = window.windowMinutes ?? defaultMinutes
let timeUntilReset = resetsAt.timeIntervalSince(now)
return windowMinutes > 0
&& timeUntilReset > 0
Expand Down Expand Up @@ -69,28 +73,41 @@ public enum GrokProviderDescriptor {
}

/// Returns a contextual label for Grok's primary usage bar ("Weekly" or "Monthly").
/// Prefer the billing period duration when available; fall back to reset distance for
/// web billing payloads that expose only a reset timestamp.
/// Prefer a measured billing-period duration when available. For reset-only web payloads,
/// treat remaining time within a week-sized horizon as Weekly — SuperGrok's included usage
/// pool resets weekly, so late-cycle windows must not fall back to "Credits".
public static func primaryLabel(window: RateWindow?, now: Date = .now) -> String? {
if let minutes = window?.windowMinutes {
return self.primaryLabel(duration: TimeInterval(minutes) * 60)
return self.primaryLabel(duration: TimeInterval(minutes) * 60, fromMeasuredDuration: true)
}
return self.primaryLabel(resetsAt: window?.resetsAt, now: now)
}

public static func primaryLabel(resetsAt: Date?, now: Date = .now) -> String? {
guard let resetsAt else { return nil }
return self.primaryLabel(duration: resetsAt.timeIntervalSince(now))
return self.primaryLabel(
duration: resetsAt.timeIntervalSince(now),
fromMeasuredDuration: false)
}

private static func primaryLabel(duration seconds: TimeInterval) -> String? {
private static func primaryLabel(duration seconds: TimeInterval, fromMeasuredDuration: Bool) -> String? {
guard seconds > 3600 else { return nil }
let days = Int((seconds / 86400).rounded(.toNearestOrAwayFromZero))
if (4...12).contains(days) {
return "Weekly"
if fromMeasuredDuration {
let days = Int((seconds / 86400).rounded(.toNearestOrAwayFromZero))
if (4...12).contains(days) {
return "Weekly"
}
if (20...45).contains(days) {
return "Monthly"
}
return nil
}
if (20...45).contains(days) {
return "Monthly"
// Reset-distance only: SuperGrok usage is weekly. Compare the remaining interval directly
// against a week-sized horizon so late-cycle partial-day remainders (1–12h) stay Weekly
// instead of rounding to 0 days and falling back to Credits.
let weekSizedHorizonSeconds: TimeInterval = 12 * 86400
if seconds <= weekSizedHorizonSeconds {
return "Weekly"
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public struct GrokUsageSnapshot: Sendable {
{
primary = RateWindow(
usedPercent: percent,
windowMinutes: nil,
windowMinutes: webBilling.windowMinutes,
resetsAt: webBilling.resetsAt,
resetDescription: nil)
}
Expand Down
50 changes: 46 additions & 4 deletions Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import FoundationNetworking
public struct GrokWebBillingSnapshot: Sendable, Equatable {
public let usedPercent: Double?
public let resetsAt: Date?
/// Billing-period length in minutes when the protobuf exposes both period start and end.
/// Late-cycle web windows need this so Weekly/Monthly labeling and pace can still resolve.
public let windowMinutes: Int?

public init(usedPercent: Double?, resetsAt: Date?) {
public init(usedPercent: Double?, resetsAt: Date?, windowMinutes: Int? = nil) {
self.usedPercent = usedPercent
self.resetsAt = resetsAt
self.windowMinutes = windowMinutes
}
}

Expand Down Expand Up @@ -233,18 +237,20 @@ public enum GrokWebBillingFetcher {
}
.map { Double($0.value) }

let resetFields = scan.varintFields.compactMap { field -> (path: [UInt64], date: Date)? in
let timestampFields = scan.varintFields.compactMap { field -> (path: [UInt64], date: Date)? in
let raw = field.value
guard raw >= 1_700_000_000, raw <= 2_100_000_000 else { return nil }
return (field.path, Date(timeIntervalSince1970: TimeInterval(raw)))
}
let futureResetFields = resetFields.filter { $0.date > now }
let futureResetFields = timestampFields.filter { $0.date > now }
let reset = futureResetFields
.filter { $0.path == [1, 5, 1] }
.map(\.date)
.min() ?? futureResetFields
.map(\.date)
.min()
let periodStart = Self.billingPeriodStart(from: timestampFields, resetsAt: reset, now: now)
let windowMinutes = Self.billingWindowMinutes(from: periodStart, to: reset)

let hasUsagePeriod = scan.varintFields.contains { field in
field.path.starts(with: [1, 6]) ||
Expand All @@ -257,7 +263,43 @@ public enum GrokWebBillingFetcher {
guard let percent = parsedPercent ?? (noUsageYet ? 0 : nil) else {
throw GrokWebBillingError.parseFailed
}
return GrokWebBillingSnapshot(usedPercent: percent, resetsAt: reset)
return GrokWebBillingSnapshot(
usedPercent: percent,
resetsAt: reset,
windowMinutes: windowMinutes)
}

/// Prefers the protobuf period-start siblings (`[1,4,1]` / `[1,8,2,1]`), then any past timestamp
/// that precedes the chosen reset.
static func billingPeriodStart(
from timestampFields: [(path: [UInt64], date: Date)],
resetsAt: Date?,
now: Date) -> Date?
{
guard let resetsAt else { return nil }

let preferredStarts = timestampFields
.filter { field in
field.path == [1, 4, 1] || field.path == [1, 8, 2, 1]
}
.map(\.date)
.filter { $0 <= now && $0 < resetsAt }
if let preferred = preferredStarts.max() {
return preferred
}

return timestampFields
.map(\.date)
.filter { $0 <= now && $0 < resetsAt }
.max()
}

static func billingWindowMinutes(from start: Date?, to end: Date?) -> Int? {
guard let start, let end else { return nil }
let minutes = end.timeIntervalSince(start) / 60
// Accept common Grok credit cycles (~daily through ~monthly); reject noise.
guard minutes.isFinite, minutes >= 24 * 60, minutes <= 45 * 24 * 60 else { return nil }
return Int(minutes.rounded())
}

static func looksLikeProtobufPayload(_ data: Data) -> Bool {
Expand Down
70 changes: 64 additions & 6 deletions Tests/CodexBarTests/GrokMenuCardModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct GrokMenuCardModelTests {
}

@Test
func `monthly quota does not show weekly projection`() throws {
func `monthly quota shows projection and pace marker`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
Expand All @@ -73,20 +73,78 @@ struct GrokMenuCardModelTests {

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Monthly")
#expect(metric.detailLeftText == nil)
#expect(metric.detailRightText == nil)
#expect(metric.pacePercent == nil)
#expect(metric.detailLeftText == "17% in deficit")
#expect(metric.detailRightText == "Runs out in 10d")
#expect(metric.pacePercent != nil)
#expect(metric.paceOnTop == false)
}

@Test
func `unclassified quota does not show weekly projection`() throws {
func `late cycle web weekly quota keeps label and projection`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 37,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval((2 * 24 + 9) * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Weekly")
#expect(metric.detailLeftText == "29% in reserve")
#expect(metric.detailRightText == "Lasts until reset")
#expect(metric.pacePercent != nil)
#expect(metric.paceOnTop == true)
}

@Test
func `late cycle web monthly quota keeps label and projection`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 37,
windowMinutes: 30 * 24 * 60,
resetsAt: now.addingTimeInterval((2 * 24 + 9) * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Monthly")
#expect(metric.detailLeftText == "55% in reserve")
#expect(metric.detailRightText == "Lasts until reset")
#expect(metric.pacePercent != nil)
#expect(metric.paceOnTop == true)
}

@Test
func `late cycle reset-only web quota defaults to weekly projection`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 37,
windowMinutes: nil,
resetsAt: now.addingTimeInterval((2 * 24 + 9) * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Weekly")
#expect(metric.detailLeftText == "29% in reserve")
#expect(metric.detailRightText == "Lasts until reset")
#expect(metric.pacePercent != nil)
#expect(metric.paceOnTop == true)
}

@Test
func `far reset without measured duration does not invent weekly projection`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 50,
windowMinutes: nil,
resetsAt: now.addingTimeInterval(2 * 24 * 3600),
resetsAt: now.addingTimeInterval(20 * 24 * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
Expand Down
59 changes: 53 additions & 6 deletions Tests/CodexBarTests/GrokWebBillingFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ struct GrokWebBillingFetcherTests {
}

@Test
func `primaryLabel derives Weekly or Monthly from resetsAt`() {
func `primaryLabel prefers Weekly for reset-only SuperGrok usage windows`() {
let now = Date()
let in2Days = now.addingTimeInterval(2 * 86400)
let in6Days = now.addingTimeInterval(6 * 86400)
let in30Days = now.addingTimeInterval(30 * 86400)
let in90Days = now.addingTimeInterval(90 * 86400)
Expand All @@ -46,11 +47,21 @@ struct GrokWebBillingFetcherTests {
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(86400),
resetDescription: nil)
let measuredMonthlyWindow = RateWindow(
usedPercent: 25,
windowMinutes: 30 * 24 * 60,
resetsAt: now.addingTimeInterval(86400),
resetDescription: nil)

let in6Hours = now.addingTimeInterval(6 * 3600)

#expect(GrokProviderDescriptor.primaryLabel(resetsAt: in2Days, now: now) == "Weekly")
#expect(GrokProviderDescriptor.primaryLabel(resetsAt: in6Days, now: now) == "Weekly")
#expect(GrokProviderDescriptor.primaryLabel(resetsAt: in30Days, now: now) == "Monthly")
#expect(GrokProviderDescriptor.primaryLabel(resetsAt: in6Hours, now: now) == "Weekly")
#expect(GrokProviderDescriptor.primaryLabel(resetsAt: in30Days, now: now) == nil)
#expect(GrokProviderDescriptor.primaryLabel(resetsAt: in90Days, now: now) == nil)
#expect(GrokProviderDescriptor.primaryLabel(window: lateWeeklyWindow, now: now) == "Weekly")
#expect(GrokProviderDescriptor.primaryLabel(window: measuredMonthlyWindow, now: now) == "Monthly")
#expect(GrokProviderDescriptor.primaryLabel(resetsAt: nil) == nil)
}

Expand Down Expand Up @@ -111,6 +122,7 @@ struct GrokWebBillingFetcherTests {

#expect(snapshot.usedPercent == 42.5)
#expect(snapshot.resetsAt == Date(timeIntervalSince1970: TimeInterval(reset)))
#expect(snapshot.windowMinutes == nil)
}

@Test
Expand All @@ -122,10 +134,11 @@ struct GrokWebBillingFetcherTests {

let snapshot = try GrokWebBillingFetcher.parseGRPCWebResponse(
data,
now: Date(timeIntervalSince1970: 1_780_000_000))
now: Date(timeIntervalSince1970: 1_781_000_000))

#expect(snapshot.usedPercent == 1.222000002861023)
#expect(snapshot.resetsAt == Date(timeIntervalSince1970: 1_782_864_000))
#expect(snapshot.windowMinutes == 30 * 24 * 60)
}

@Test
Expand Down Expand Up @@ -563,10 +576,11 @@ struct GrokWebBillingFetcherTests {

let snapshot = try GrokWebBillingFetcher.parseGRPCWebResponse(
data,
now: Date(timeIntervalSince1970: 1_768_000_000))
now: Date(timeIntervalSince1970: 1_778_500_000))

#expect(snapshot.usedPercent == 0)
#expect(snapshot.resetsAt == Date(timeIntervalSince1970: 1_780_272_000))
#expect(snapshot.windowMinutes == 31 * 24 * 60)
}

@Test
Expand All @@ -589,6 +603,7 @@ struct GrokWebBillingFetcherTests {

#expect(snapshot.usedPercent == 0)
#expect(snapshot.resetsAt == Date(timeIntervalSince1970: 1_782_864_000))
#expect(snapshot.windowMinutes == 30 * 24 * 60)
}

@Test
Expand Down Expand Up @@ -626,6 +641,37 @@ struct GrokWebBillingFetcherTests {
now: Date(timeIntervalSince1970: TimeInterval(recentStart + 1800)))

#expect(snapshot.resetsAt == Date(timeIntervalSince1970: TimeInterval(billingEnd)))
#expect(snapshot.windowMinutes == Int((TimeInterval(billingEnd) - TimeInterval(recentStart)) / 60))
}

@Test
func `rejects future preferred period start when deriving window length`() throws {
let nowEpoch = UInt64(1_800_000_000)
let futureStart = nowEpoch + 86400
let billingEnd = nowEpoch + 8 * 86400
var payload = Data()
payload.append(0x0A) // field 1, length-delimited billing message
var inner = Data()
inner.append(0x0D) // field 1, fixed32 usage percent
var percentBits = Float(40).bitPattern.littleEndian
withUnsafeBytes(of: &percentBits) { inner.append(contentsOf: $0) }
inner.append(0x22) // field 4, nested period start
inner.append(0x06)
inner.append(0x08) // nested field 1 varint
inner.append(contentsOf: Self.varint(futureStart))
inner.append(0x2A) // field 5, nested period end
inner.append(0x06)
inner.append(0x08)
inner.append(contentsOf: Self.varint(billingEnd))
payload.append(UInt8(inner.count))
payload.append(inner)

let snapshot = try GrokWebBillingFetcher.parseGRPCWebResponse(
Self.grpcFrame(payload),
now: Date(timeIntervalSince1970: TimeInterval(nowEpoch)))

#expect(snapshot.resetsAt == Date(timeIntervalSince1970: TimeInterval(billingEnd)))
#expect(snapshot.windowMinutes == nil)
}

@Test
Expand Down Expand Up @@ -914,7 +960,8 @@ extension GrokWebBillingFetcherTests {
billing: nil,
webBilling: GrokWebBillingSnapshot(
usedPercent: 67.25,
resetsAt: Date(timeIntervalSince1970: 1_800_000_003)),
resetsAt: Date(timeIntervalSince1970: 1_800_000_003),
windowMinutes: 7 * 24 * 60),
credentials: Self.credentials,
localSummary: nil,
cliVersion: nil,
Expand All @@ -923,7 +970,7 @@ extension GrokWebBillingFetcherTests {
let usage = snapshot.toUsageSnapshot()

#expect(usage.primary?.usedPercent == 67.25)
#expect(usage.primary?.windowMinutes == nil)
#expect(usage.primary?.windowMinutes == 7 * 24 * 60)
#expect(usage.primary?.resetsAt == Date(timeIntervalSince1970: 1_800_000_003))
#expect(usage.accountEmail(for: .grok) == "grok@example.com")
#expect(usage.loginMethod(for: .grok) == "SuperGrok")
Expand Down
Loading
Loading