From db0cde1ef557c66d154eed38cdf074906a39d7e4 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Thu, 22 Oct 2026 08:48:40 +0000 Subject: [PATCH 1/4] Grok gRPC fallback, settings tier, xAI dedupe, iOS canonical cards, Muse dedupe - Grok: x-xai-token-auth header, subscription_tier_display plan enrichment, and a best-effort grok.com gRPC-web credits fallback (documented wire contract, protobuf scanner with bounds) used only when the proxy publishes no percentage. - xAI/Grok: the same subscription pool reported under both keys keeps only the fresher window. - Copilot windows carry the monthly cadence (1m caption). - iOS: source-scope toggle, source badges/banners, per-window Updated stamps gone from the main list; canonical resolution only, fixed meter columns; source detail stays in Settings. - Muse Assist artwork deduped onto the Muse base (byte-identical). --- Sources/CodeCaps/PlatformLogo.swift | 3 +- .../ProviderMarks/muse-assist-color.png | Bin 27648 -> 0 bytes .../ProviderMarks/muse-assist-dark.png | Bin 10330 -> 0 bytes .../ProviderMarks/muse-assist-light.png | Bin 10368 -> 0 bytes .../Resources/ProviderMarks/muse-assist.png | Bin 70807 -> 0 bytes Sources/QuotaCore/CopilotQuotaReader.swift | 4 +- Sources/QuotaCore/GrokGrpcCredits.swift | 206 ++++++++++++++++++ Sources/QuotaCore/LocalQuotaReader.swift | 63 +++++- Sources/QuotaCore/QuotaModels.swift | 24 +- .../QuotaCoreTests/GrokGrpcCreditsTests.swift | 98 +++++++++ Tests/QuotaCoreTests/GrokXaiDedupeTests.swift | 42 ++++ .../App/Models/CompanionQuotaModel.swift | 12 +- .../App/Views/CompanionContentView.swift | 127 +++-------- scripts/regenerate-provider-marks.py | 4 - 14 files changed, 469 insertions(+), 114 deletions(-) delete mode 100644 Sources/CodeCaps/Resources/ProviderMarks/muse-assist-color.png delete mode 100644 Sources/CodeCaps/Resources/ProviderMarks/muse-assist-dark.png delete mode 100644 Sources/CodeCaps/Resources/ProviderMarks/muse-assist-light.png delete mode 100644 Sources/CodeCaps/Resources/ProviderMarks/muse-assist.png create mode 100644 Sources/QuotaCore/GrokGrpcCredits.swift create mode 100644 Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift create mode 100644 Tests/QuotaCoreTests/GrokXaiDedupeTests.swift diff --git a/Sources/CodeCaps/PlatformLogo.swift b/Sources/CodeCaps/PlatformLogo.swift index c08a135b..c0063053 100644 --- a/Sources/CodeCaps/PlatformLogo.swift +++ b/Sources/CodeCaps/PlatformLogo.swift @@ -287,7 +287,8 @@ private static let resourceNames: [String: String] = [ "grok-bot": "grok-bot", "minimax": "minimax", "muse": "muse", - "muse-assist": "muse-assist", + // Muse Assist ships byte-identical artwork to Muse; one base serves both. + "muse-assist": "muse", "muse-code": "muse-code", "cursor": "cursor", ] diff --git a/Sources/CodeCaps/Resources/ProviderMarks/muse-assist-color.png b/Sources/CodeCaps/Resources/ProviderMarks/muse-assist-color.png deleted file mode 100644 index 4a30e1110a3bc4b421fc54ef0ded69a69a482c40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27648 zcmdp-RaabHvxOUXhv4oIg1a;pBtUSN#@*d%;6;N&f&~j6+`VykcL>2Pcq5G*zQ1v< z_PW}8)Lu1aRn7IR7`3nRSmQ9)J%0Dya6!U0f`-ycTqW!3Pl$P(> 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

~3z7NIE=O2==xxaDIniua3|L#)si$35}hxe9v zq`{3ssxBal6zC_~j;C|&z_fh^yaOM-I6=dA5iBST4oiuS1=(Y=OaC{DpCz>d9`eK?DiT@m_8aFk(0kd0wP!6a0@O+NfV)Dw?SAB}CL7!i52XHR~QNA4u7)#GA z5PoMPU4X|CEd2vPBN5g(?#?TWG;q9LJvwYKl!TXB*givYWcjpSOUGuCHf8wZ zN1(|%3#p-8F*E@nyzBdK=G6vp*Kwrwv1NViXu(4CFhK+oql67nSQzs;R=9QZeR zKvw%VPNYa!ytzNMLPGhxgv8iBsyr2HWg+N#g~a95cz%Q}W$C0#aeQ5vJattnaTQ7hXQ%3R zz=g<5KWLY*hkF*%q?0b>EiQ3BY_K;<((6UGaD~73n~y>Lasj|J8qQVAxXc9`9TCg0 zh&y|fl1F!aDlqdt@`YZ09Y!VFeYpe=HR*bylj3I~`2I6?peF>t{Q3r9u$79ex{+$C z_0)>+n6ZFoIYyFqH zMNlCeF14x`krsjHMYp$;o%=YTSdK7zSP4j^AsQ6hW--(JEfH~eY&aA)XR#RkTw;lz zBXg(^IQ`$z_~c5|ONR4|ini8m?MAvdIV{_3FaKbR0ht%rN1@ z<>CDu?gj-Oo9hh(X2(i35K*F=Xm9^UA6v$s1fE0G=3?ZZdj~fZlB=T!n&Xi5AZ3B^ j|4k7Uq)tnK^yU+4?NCsMHQd7&0QnSU)npo^EF%90Eo5<_ diff --git a/Sources/CodeCaps/Resources/ProviderMarks/muse-assist-dark.png b/Sources/CodeCaps/Resources/ProviderMarks/muse-assist-dark.png deleted file mode 100644 index ab69b7d6b0cc7efce542ce391456bbd47929bace..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10330 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 Reading? { + guard let message = unwrapFrames(data) else { return nil } + // Reply form: field 1 is the config. Raw form: the reply IS the + // config. Prefer the wrapped form when field 1 decodes as bytes. + let config: Data + if let fields = try? ProtobufScanner.fields(message), + let wrapped = fields.first(where: { $0.number == 1 && $0.wire == .bytes }) { + config = wrapped.bytes + } else { + config = message + } + guard let fields = try? ProtobufScanner.fields(config) else { return nil } + + var percent: Double? + var periodStart: Date? + var periodEnd: Date? + for field in fields { + switch field.wire { + case .fixed32: + // The only top-level floats in the config are usage + // percentages; product shares nest inside field 7 entries. + if percent == nil, let value = field.floatValue, + value.isFinite, (0...100).contains(value) { + percent = value + } + case .bytes: + if let period = timestampPair(field.bytes) { + periodStart = period.start + periodEnd = period.end + } + case .varint, .fixed64: + continue + } + } + + if let percent { + return Reading(usedPercent: percent, periodStart: periodStart, periodEnd: periodEnd) + } + // Implicit zero: complete active period, no percentage anywhere. + if let start = periodStart, let end = periodEnd, start <= now, now <= end { + return Reading(usedPercent: 0, periodStart: start, periodEnd: end) + } + return nil + } + + /// A `{start: Timestamp, end: Timestamp}` submessage, when the bytes + /// decode as exactly two Timestamp-shaped fields in order. + private static func timestampPair(_ data: Data) -> (start: Date, end: Date)? { + guard let fields = try? ProtobufScanner.fields(data), fields.count == 2, + fields.allSatisfy({ $0.wire == .bytes }) else { return nil } + guard let start = timestamp(fields[0].bytes), let end = timestamp(fields[1].bytes), + start < end else { return nil } + return (start, end) + } + + /// A protobuf Timestamp: field 1 varint seconds, optional field 2 nanos. + private static func timestamp(_ data: Data) -> Date? { + guard let fields = try? ProtobufScanner.fields(data), + let secondsField = fields.first(where: { $0.number == 1 && $0.wire == .varint }), + let seconds = secondsField.varintValue, + (1_500_000_000...2_000_000_000).contains(seconds) else { return nil } + return Date(timeIntervalSince1970: TimeInterval(seconds)) + } + + /// Strip gRPC-web framing: `[flag][4-byte big-endian length][payload]` + /// repeated, where flag 0x80 marks a trailer frame. Data that is not + /// framed is returned as-is (the raw protobuf form). + private static func unwrapFrames(_ data: Data) -> Data? { + guard data.count >= 5 else { return data.isEmpty ? nil : data } + var offset = 0 + var payload = Data() + var framed = false + let bytes = [UInt8](data) + while offset + 5 <= bytes.count { + let flag = bytes[offset] + guard flag == 0x00 || flag == 0x80 else { break } + let length = Int(bytes[offset + 1]) << 24 | Int(bytes[offset + 2]) << 16 + | Int(bytes[offset + 3]) << 8 | Int(bytes[offset + 4]) + guard offset + 5 + length <= bytes.count else { break } + framed = true + if flag == 0x00 { payload.append(contentsOf: bytes[(offset + 5)..<(offset + 5 + length)]) } + offset += 5 + length + } + return framed && offset == bytes.count ? payload : data + } +} + +/// Minimal protobuf wire scanner: enough to walk a message's top level +/// without a generated model. Malformed input throws; callers treat that +/// as "no reading", never as a partial one. Varints stop after ten bytes +/// and reject overflow, and length-delimited reads cannot exceed the +/// enclosing message — the same bounds CodexBar's reader documents. +enum ProtobufScanner { + enum Wire { + case varint, fixed64, bytes, fixed32 + } + + struct Field { + var number: Int + var wire: Wire + var varintValue: UInt64? + var floatValue: Double? + var bytes: Data + } + + enum ScanError: Error { case malformed } + + static func fields(_ data: Data) throws -> [Field] { + var reader = Reader(bytes: [UInt8](data)) + var fields: [Field] = [] + while !reader.atEnd { + let tag = try reader.varint() + let number = Int(tag >> 3) + guard number > 0 else { throw ScanError.malformed } + switch tag & 0x07 { + case 0: + let value = try reader.varint() + fields.append(Field(number: number, wire: .varint, varintValue: value, bytes: Data())) + case 1: + let raw = try reader.take(8) + fields.append(Field(number: number, wire: .fixed64, bytes: Data(raw))) + case 2: + let length = try reader.varint() + guard length <= UInt64(reader.remaining) else { throw ScanError.malformed } + let raw = try reader.take(Int(length)) + fields.append(Field(number: number, wire: .bytes, bytes: Data(raw))) + case 5: + let raw = try reader.take(4) + let bits = UInt32(raw[0]) | UInt32(raw[1]) << 8 | UInt32(raw[2]) << 16 | UInt32(raw[3]) << 24 + fields.append(Field(number: number, wire: .fixed32, + floatValue: Double(Float(bitPattern: bits)), bytes: Data(raw))) + default: + throw ScanError.malformed + } + } + return fields + } + + private struct Reader { + var bytes: [UInt8] + var offset = 0 + var atEnd: Bool { offset >= bytes.count } + var remaining: Int { bytes.count - offset } + + mutating func varint() throws -> UInt64 { + var result: UInt64 = 0 + var shift: UInt64 = 0 + for _ in 0..<10 { + guard offset < bytes.count else { throw ScanError.malformed } + let byte = bytes[offset] + offset += 1 + if shift == 63 { + // Tenth byte may only contribute bit 63. + guard byte <= 1 else { throw ScanError.malformed } + } + result |= UInt64(byte & 0x7F) << shift + if byte & 0x80 == 0 { return result } + shift += 7 + } + throw ScanError.malformed + } + + mutating func take(_ count: Int) throws -> [UInt8] { + guard count >= 0, offset + count <= bytes.count else { throw ScanError.malformed } + defer { offset += count } + return Array(bytes[offset..<(offset + count)]) + } + } +} diff --git a/Sources/QuotaCore/LocalQuotaReader.swift b/Sources/QuotaCore/LocalQuotaReader.swift index 3e97b3a0..c61b9423 100644 --- a/Sources/QuotaCore/LocalQuotaReader.swift +++ b/Sources/QuotaCore/LocalQuotaReader.swift @@ -341,11 +341,72 @@ public struct LocalQuotaReader: Sendable { var request = URLRequest(url: URL(string: "https://cli-chat-proxy.grok.com/v1/billing?format=credits")!) request.httpMethod = "GET" request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("xai-grok-cli", forHTTPHeaderField: "x-xai-token-auth") + request.setValue("application/json", forHTTPHeaderField: "Accept") let payload = try await requestJSON(request) - let windows = parseGrok(payload, observedAt: now()) + var windows = parseGrok(payload, observedAt: now()) + // The credits payload carries no plan name; the settings endpoint + // names the tier (`SuperGrok Heavy` vs `SuperGrok`). Best-effort + // enrichment on a short budget, exactly as CodexBar documents: a + // failure here drops the overlay, never the usage reading. + if let tier = await grokSettingsTier(token: token) { + windows = windows.map { window in + var window = window + if window.planName == nil { window.planName = tier } + return window + } + } + // Best-effort grok.com gRPC fallback: only when the proxy answer + // published no percentage. Its percent fills the subscription + // window; the proxy's period bounds stay authoritative. + if windows.first?.remainingPercent == nil, + let grpc = await grokGrpcReading(token: token), + let index = windows.firstIndex(where: { $0.id.hasSuffix(":subscription") || $0.remainingPercent == nil }) { + var window = windows[index] + window.remainingPercent = 100 - grpc.usedPercent + window.remainingUnknown = false + if window.resetAt == nil, let end = grpc.periodEnd { + window.resetAt = Self.isoString(end) + } + if window.periodStart == nil, let start = grpc.periodStart { + window.periodStart = Self.isoString(start) + } + windows[index] = window + } return ProviderRead(provider: provider, windows: windows, issue: windows.first?.remainingPercent == nil ? "Grok returned no readable quota percentage." : nil) } + /// `subscription_tier_display` from the CLI-proxy settings endpoint. + private func grokSettingsTier(token: String) async -> String? { + var request = URLRequest(url: URL(string: "https://cli-chat-proxy.grok.com/v1/settings")!) + request.httpMethod = "GET" + request.timeoutInterval = 2 + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("xai-grok-cli", forHTTPHeaderField: "x-xai-token-auth") + request.setValue("application/json", forHTTPHeaderField: "Accept") + guard let payload = try? await requestJSON(request) else { return nil } + let tier = firstString(payload, ["subscription_tier_display", "subscriptionTierDisplay"]) + return tier?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? tier : nil + } + + /// The grok.com gRPC-web credits fallback (see `GrokGrpcCredits`). + private func grokGrpcReading(token: String) async -> GrokGrpcCredits.Reading? { + var request = URLRequest(url: URL(string: "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig")!) + request.httpMethod = "POST" + request.timeoutInterval = 6 + request.httpBody = GrokGrpcCredits.requestFrame + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/grpc-web+proto", forHTTPHeaderField: "Content-Type") + request.setValue("1", forHTTPHeaderField: "X-Grpc-Web") + guard let (data, response) = try? await fetchJSON(request), + (200..<300).contains(response.statusCode) else { return nil } + return GrokGrpcCredits.parse(data, now: now()) + } + + private static func isoString(_ date: Date) -> String { + ISO8601DateFormatter().string(from: date) + } + private func readMiniMax() async throws -> ProviderRead { let provider = Provider.minimax guard let root = try readJSONObject(relativePath: ".mmx/config.json") else { diff --git a/Sources/QuotaCore/QuotaModels.swift b/Sources/QuotaCore/QuotaModels.swift index 7baa30cf..079dbf31 100644 --- a/Sources/QuotaCore/QuotaModels.swift +++ b/Sources/QuotaCore/QuotaModels.swift @@ -603,6 +603,28 @@ public extension QuotaResponse { return copy } + /// The Grok subscription credits pool is reported twice: the local CLI + /// reader files it under `grok`, while the fleet collector buckets the + /// same payload as `xai:subscription`. When both carry a subscription + /// window, keep the fresher reading and drop the staler duplicate, so + /// the two rows can never disagree about one pool. Any other xAI + /// window (API usage, a different cadence) is untouched. + static func dedupedGrokSubscription(_ windows: [QuotaWindow]) -> [QuotaWindow] { + func subscriptionIndex(_ key: String) -> Int? { + windows.firstIndex { window in + window.canonicalProviderKey == key + && (window.id.hasSuffix(":subscription") || window.id == "xai:subscription") + } + } + guard let grokIndex = subscriptionIndex("grok"), + let xaiIndex = subscriptionIndex("xai") else { return windows } + let grokDate = windows[grokIndex].occurredDate ?? .distantPast + let xaiDate = windows[xaiIndex].occurredDate ?? .distantPast + var copy = windows + copy.remove(at: grokDate >= xaiDate ? xaiIndex : grokIndex) + return copy + } + /// Returns the stable native dashboard order, followed by future provider /// keys supplied by the server. Missing expected providers are explicit. /// @@ -612,7 +634,7 @@ public extension QuotaResponse { /// offline fallback / pre-manifest backend path. That branch is kept as a /// safety net for first-launch installs before a sync has completed. func platformSections(now: Date = Date()) -> [QuotaPlatformSection] { - let normalizedWindows = windows.map { $0.normalized() } + let normalizedWindows = Self.dedupedGrokSubscription(windows.map { $0.normalized() }) var grouped = Dictionary(minimumCapacity: normalizedWindows.count) for window in normalizedWindows { grouped[window.canonicalProviderKey, default: []].append(window) diff --git a/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift b/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift new file mode 100644 index 00000000..9d27148b --- /dev/null +++ b/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift @@ -0,0 +1,98 @@ +import Foundation +import XCTest +@testable import QuotaCore + +/// Fixtures for the grok.com gRPC-web fallback parser, built byte-by-byte +/// from the documented wire contract (CodexBar docs/grok.md): config is +/// field 1 of the reply, a top-level fixed32 is the used percent, the +/// current period is a `{start, end}` Timestamp pair, and product shares +/// nest inside repeated field 7 entries so they must not be mistaken for +/// the headline percent. +final class GrokGrpcCreditsTests: XCTestCase { + private let now = Date(timeIntervalSince1970: 1_761_000_000) + + private func varint(_ value: UInt64) -> [UInt8] { + var value = value + var bytes: [UInt8] = [] + repeat { + var byte = UInt8(value & 0x7F) + value >>= 7 + if value > 0 { byte |= 0x80 } + bytes.append(byte) + } while value > 0 + return bytes + } + + private func field(_ number: Int, wire: Int, payload: [UInt8]) -> [UInt8] { + varint(UInt64(number << 3 | wire)) + payload + } + + private func lenField(_ number: Int, _ payload: [UInt8]) -> [UInt8] { + field(number, wire: 2, payload: varint(UInt64(payload.count)) + payload) + } + + private func fixed32Field(_ number: Int, _ value: Float) -> [UInt8] { + var bits = value.bitPattern + return field(number, wire: 5, payload: withUnsafeBytes(of: &bits) { Array($0) }) + } + + private func timestampField(_ number: Int, seconds: UInt64) -> [UInt8] { + lenField(number, field(1, wire: 0, payload: varint(seconds))) + } + + private func periodField(_ number: Int, start: UInt64, end: UInt64) -> [UInt8] { + lenField(number, timestampField(1, seconds: start) + timestampField(2, seconds: end)) + } + + private func framed(_ payload: [UInt8], trailer: Bool = false) -> Data { + var bytes: [UInt8] = [0x00, 0, 0, 0, UInt8(payload.count)] + bytes += payload + if trailer { + let trailer = Array("grpc-status: 0\r\n".utf8) + bytes += [0x80, 0, 0, 0, UInt8(trailer.count)] + trailer + } + return Data(bytes) + } + + func testWrappedFramedReplyYieldsPercentAndPeriod() { + let start: UInt64 = 1_760_400_000 + let end: UInt64 = 1_761_408_000 + let config = fixed32Field(4, 37.5) + periodField(6, start: start, end: end) + let reply = lenField(1, config) + let reading = GrokGrpcCredits.parse(framed(Array(reply), trailer: true), now: now) + XCTAssertEqual(reading?.usedPercent, 37.5) + XCTAssertEqual(reading?.periodStart, Date(timeIntervalSince1970: TimeInterval(start))) + XCTAssertEqual(reading?.periodEnd, Date(timeIntervalSince1970: TimeInterval(end))) + } + + func testRawUnframedConfigIsAccepted() { + let config = fixed32Field(4, 12.25) + let reading = GrokGrpcCredits.parse(Data(config), now: now) + XCTAssertEqual(reading?.usedPercent ?? -1, 12.25, accuracy: 0.001) + } + + func testProductSharesAreNotTheHeadlinePercent() { + // Field 7 entry: {1: product 2, 2: fixed32 55%} before the real 5%. + let share = field(1, wire: 0, payload: varint(2)) + fixed32Field(2, 55) + let config = lenField(7, Array(share)) + fixed32Field(4, 5) + let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) + XCTAssertEqual(reading?.usedPercent, 5) + } + + func testImplicitZeroForActivePeriodWithoutPercent() { + let config = periodField(6, start: 1_760_400_000, end: 1_761_408_000) + let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) + XCTAssertEqual(reading?.usedPercent, 0) + } + + func testNoPercentAndNoActivePeriodIsNil() { + let config = periodField(6, start: 1_700_000_000, end: 1_701_000_000) + XCTAssertNil(GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now)) + XCTAssertNil(GrokGrpcCredits.parse(Data([0xFF, 0xFF, 0xFF]), now: now)) + XCTAssertNil(GrokGrpcCredits.parse(Data(), now: now)) + } + + func testRequestFrameMatchesDocumentedBytes() { + XCTAssertEqual(GrokGrpcCredits.requestFrame, Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00])) + } +} diff --git a/Tests/QuotaCoreTests/GrokXaiDedupeTests.swift b/Tests/QuotaCoreTests/GrokXaiDedupeTests.swift new file mode 100644 index 00000000..81e43b82 --- /dev/null +++ b/Tests/QuotaCoreTests/GrokXaiDedupeTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import QuotaCore + +/// The same Grok credits pool arrives as `grok` (local CLI reader) and as +/// `xai:subscription` (fleet collector). Only the fresher may survive. +final class GrokXaiDedupeTests: XCTestCase { + private func window(providerKey: String, id: String, occurredAt: String, label: String = "Weekly window") -> QuotaWindow { + QuotaWindow(id: id, provider: providerKey, providerKey: providerKey, label: label, + remainingPercent: 50, occurredAt: occurredAt, source: "test") + } + + func testFresherLocalGrokDropsStaleFleetXai() { + let windows = [ + window(providerKey: "grok", id: "local-mac:grok:subscription", occurredAt: "2026-10-22T03:00:00Z"), + window(providerKey: "xai", id: "xai:subscription", occurredAt: "2026-10-21T22:02:00Z"), + ] + let result = QuotaResponse.dedupedGrokSubscription(windows) + XCTAssertEqual(result.map(\.id), ["local-mac:grok:subscription"]) + } + + func testFresherFleetXaiDropsStaleLocalGrok() { + let windows = [ + window(providerKey: "grok", id: "local-mac:grok:subscription", occurredAt: "2026-10-21T20:00:00Z"), + window(providerKey: "xai", id: "xai:subscription", occurredAt: "2026-10-22T01:00:00Z"), + ] + let result = QuotaResponse.dedupedGrokSubscription(windows) + XCTAssertEqual(result.map(\.id), ["xai:subscription"]) + } + + func testOtherXaiWindowsAreUntouched() { + let windows = [ + window(providerKey: "grok", id: "local-mac:grok:subscription", occurredAt: "2026-10-22T03:00:00Z"), + window(providerKey: "xai", id: "xai:api-monthly", occurredAt: "2026-10-21T22:02:00Z", label: "API credits"), + ] + XCTAssertEqual(QuotaResponse.dedupedGrokSubscription(windows).count, 2) + } + + func testSingleSidedPoolIsUntouched() { + let windows = [window(providerKey: "xai", id: "xai:subscription", occurredAt: "2026-10-21T22:02:00Z")] + XCTAssertEqual(QuotaResponse.dedupedGrokSubscription(windows).count, 1) + } +} diff --git a/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift b/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift index ad900cc4..aa411829 100644 --- a/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift +++ b/ios/CodeCapsCompanion/App/Models/CompanionQuotaModel.swift @@ -1857,9 +1857,7 @@ public final class CompanionQuotaModel: ObservableObject { let futureResets = primaryWindows.compactMap(\.resetAt).filter { $0 > now } let nearestReset = futureResets.min() ?? primaryWindows.compactMap(\.resetAt).min() - let sourceCount = Set(primaryWindows.compactMap(\.source) + duplicateWindows.compactMap(\.source)).count - let sourceSuffix = sourceCount > 1 ? " (\(sourceCount) sources)" : "" - let subtitle = Self.subtitlePhrase(for: primaryWindows, fallback: title) + sourceSuffix + let subtitle = Self.subtitlePhrase(for: primaryWindows, fallback: title) let displayTitle = WidgetPlatformDisplay.title( canonicalKey: providerKey, manifestProviderLabel: title, @@ -1917,25 +1915,23 @@ public final class CompanionQuotaModel: ObservableObject { let controllingPct: Double? let isExhausted: Bool - let sourceCount = Set(windows.compactMap(\.source)).count - let sourceSuffix = sourceCount > 1 ? " (\(sourceCount) sources)" : "" let subtitle: String if weeklyExhausted && weeklyWin != nil { controllingPct = weeklyPct ?? 0 isExhausted = true - subtitle = "\(QuotaWindowVocabulary.sevenDay) limit exhausted\(sourceSuffix)" + subtitle = "\(QuotaWindowVocabulary.sevenDay) limit exhausted" } else { let validPercents = [fiveHourWin?.remainingPercent, weeklyWin?.remainingPercent].compactMap { $0 } if validPercents.isEmpty { let allPcts = windows.compactMap { $0.remainingPercent } controllingPct = allPcts.min() isExhausted = (controllingPct ?? 100) <= 0 - subtitle = "\(QuotaWindowVocabulary.fiveHour) & \(QuotaWindowVocabulary.sevenDay)\(sourceSuffix)" + subtitle = "\(QuotaWindowVocabulary.fiveHour) & \(QuotaWindowVocabulary.sevenDay)" } else { controllingPct = validPercents.min() isExhausted = (controllingPct ?? 100) <= 0 - subtitle = "\(QuotaWindowVocabulary.fiveHour) & \(QuotaWindowVocabulary.sevenDay)\(sourceSuffix)" + subtitle = "\(QuotaWindowVocabulary.fiveHour) & \(QuotaWindowVocabulary.sevenDay)" } } diff --git a/ios/CodeCapsCompanion/App/Views/CompanionContentView.swift b/ios/CodeCapsCompanion/App/Views/CompanionContentView.swift index 042d5904..1515704a 100644 --- a/ios/CodeCapsCompanion/App/Views/CompanionContentView.swift +++ b/ios/CodeCapsCompanion/App/Views/CompanionContentView.swift @@ -42,8 +42,6 @@ public struct CompanionContentView: View { NavigationStack { ScrollView { VStack(spacing: 16) { - headerCard - if let error = model.lastError { errorBanner(error) } @@ -190,19 +188,6 @@ public struct CompanionContentView: View { .accessibilityLabel("Fleet Status, \(model.items.count) platforms monitored") } - // MARK: - Source scope - - private var headerCard: some View { - Picker("Source scope", selection: $model.sourceScope) { - ForEach(CompanionSourceScope.allCases) { scope in - Text(scope.displayTitle).tag(scope) - } - } - .pickerStyle(.segmented) - .padding(14) - .background(cardBackground, in: RoundedRectangle(cornerRadius: 12)) - } - // MARK: - Error Banner private func errorBanner(_ message: String) -> some View { @@ -305,14 +290,17 @@ public struct CompanionContentView: View { } private func quotaCard(_ item: CompanionQuotaItem) -> some View { + // One canonical reading per allowance, the same resolution the Mac + // app leads with. Source detail lives in Settings, for debugging; + // the main list just shows quotas. + let scope: CompanionSourceScope = .allSources let isExpanded = expandedIds.contains(item.id) && item.isExpandable - let short = item.cardShortWindow(scope: model.sourceScope) - let long = item.cardLongWindow(scope: model.sourceScope) - let cardWindows = item.cardWindows(scope: model.sourceScope) + let short = item.cardShortWindow(scope: scope) + let long = item.cardLongWindow(scope: scope) + let cardWindows = item.cardWindows(scope: scope) let extraWindows = cardWindows.filter { $0.id != short?.window.id && $0.id != long?.window.id } - let headlineUpdatedLabel = [short?.updatedLabel, long?.updatedLabel].compactMap { $0 }.first return VStack(alignment: .leading, spacing: 12) { // Main clickable row @@ -323,22 +311,6 @@ public struct CompanionContentView: View { HStack(spacing: 6) { Text(item.title) .font(.system(size: 15, weight: .semibold)) - - if model.sourceScope == .allSources, item.sourceCount > 1 { - Text("\(item.sourceCount) sources") - .font(.system(size: 10, weight: .semibold)) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.secondary.opacity(0.14), in: Capsule()) - } - - if model.sourceScope == .allSources, item.sourceCount > 1, - item.hasSourceDiscrepancy(scope: model.sourceScope) { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 11, weight: .semibold)) - .foregroundStyle(.orange) - .accessibilityLabel("Warning: sources disagree on this allowance") - } } if let sub = item.subtitle { @@ -366,15 +338,9 @@ public struct CompanionContentView: View { } VStack(alignment: .trailing, spacing: 2) { - Text(item.cardDisplayPercent(scope: model.sourceScope)) + Text(item.cardDisplayPercent(scope: scope)) .font(.system(size: 17, weight: .bold, design: .rounded).monospacedDigit()) - .foregroundStyle(item.cardStatusColor(scope: model.sourceScope)) - if let headlineUpdatedLabel { - Text(headlineUpdatedLabel) - .font(.system(size: 10, weight: .medium)) - .foregroundStyle(.secondary) - .lineLimit(1) - } + .foregroundStyle(item.cardStatusColor(scope: scope)) } if item.isExpandable { @@ -391,7 +357,7 @@ public struct CompanionContentView: View { // The cap line. The Mac's menu-bar window and PiP say which // allowance ran out and when it lifts; the phone said nothing, so // a capped Codex or Antigravity plan looked like any other. - if let atCapSummary = item.atCapSummary(scope: model.sourceScope) { + if let atCapSummary = item.atCapSummary(scope: scope) { HStack(spacing: 6) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 11, weight: .semibold)) @@ -419,15 +385,13 @@ public struct CompanionContentView: View { if let short { CompanionMeterView( caption: short.window.caption, - window: short.window, - updatedLabel: short.updatedLabel + window: short.window ) } if let long, long.window.id != short?.window.id { CompanionMeterView( caption: long.window.caption, - window: long.window, - updatedLabel: long.updatedLabel + window: long.window ) } else if short == nil { if let first = cardWindows.first { @@ -436,28 +400,6 @@ public struct CompanionContentView: View { } } - if model.sourceScope == .allSources, item.sourceCount > 1, - item.hasSourceDiscrepancy(scope: model.sourceScope) { - HStack(spacing: 6) { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 11, weight: .semibold)) - .foregroundStyle(.orange) - Text(item.sourceDiscrepancies(scope: model.sourceScope).first?.description ?? "Sources differ") - .font(.system(size: 11.5, weight: .semibold)) - .foregroundStyle(.primary) - .lineLimit(1) - .minimumScaleFactor(0.85) - Spacer() - } - .padding(.horizontal, 9) - .padding(.vertical, 5) - .background(Color.orange.opacity(0.14), in: RoundedRectangle(cornerRadius: 6)) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke(Color.orange.opacity(0.28), lineWidth: 0.5) - ) - } - // Multiple time periods / windows when expanded (only extra allowances not already displayed on the card) if isExpanded && !extraWindows.isEmpty { Divider() @@ -664,8 +606,8 @@ public struct CompanionContentView: View { Text(item.title) .font(.system(size: 14, weight: .semibold)) Spacer() - if model.sourceScope == .allSources, item.sourceCount > 1, - item.hasSourceDiscrepancy(scope: model.sourceScope) { + if item.sourceCount > 1, + item.hasSourceDiscrepancy(scope: .allSources) { Label("Sources differ ≥ 10 pts", systemImage: "exclamationmark.triangle.fill") .font(.system(size: 11, weight: .medium)) .foregroundStyle(.orange) @@ -686,7 +628,7 @@ public struct CompanionContentView: View { .padding(.vertical, 1.5) .background(Color.secondary.opacity(0.12), in: RoundedRectangle(cornerRadius: 4)) } - if item.isStaleInSourceDetail(reading, scope: model.sourceScope) { + if item.isStaleInSourceDetail(reading, scope: .allSources) { Text("Stale") .font(.system(size: 10, weight: .semibold)) .foregroundStyle(.secondary) @@ -707,12 +649,12 @@ public struct CompanionContentView: View { .foregroundStyle(reading.statusColor) } .padding(.vertical, 2) - .opacity(item.isStaleInSourceDetail(reading, scope: model.sourceScope) ? 0.72 : 1) + .opacity(item.isStaleInSourceDetail(reading, scope: .allSources) ? 0.72 : 1) } - if model.sourceScope == .allSources, item.sourceCount > 1, - item.hasSourceDiscrepancy(scope: model.sourceScope) { - ForEach(item.sourceDiscrepancies(scope: model.sourceScope)) { disc in + if item.sourceCount > 1, + item.hasSourceDiscrepancy(scope: .allSources) { + ForEach(item.sourceDiscrepancies(scope: .allSources)) { disc in HStack(spacing: 5) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 10, weight: .semibold)) @@ -730,8 +672,8 @@ public struct CompanionContentView: View { } header: { Text("Data Sources & Mirrors") } footer: { - Text("CodeCaps monitors primary and mirror feeds for each platform." + sentenceGap - + "In All Sources, when a card lists two or more feeds reported within 20 minutes that disagree by 10 points or more, a warning appears on the card (hostnames are ignored). Otherwise the newest reading is shown and older feeds are marked Stale here.") + Text("Per-feed detail for debugging." + sentenceGap + + "When two or more feeds reported within 20 minutes disagree by 10 points or more, the difference is listed here (hostnames are ignored). The main list always shows the newest canonical reading.") } .id("dataSourcesMirrorsSection") .accessibilityIdentifier("dataSourcesMirrorsSection") @@ -954,24 +896,24 @@ public struct CompanionContentView: View { public struct CompanionMeterView: View { public let caption: String public let window: CompanionWindowItem - public let updatedLabel: String? - public init(caption: String, window: CompanionWindowItem, updatedLabel: String? = nil) { + public init(caption: String, window: CompanionWindowItem) { self.caption = caption self.window = window - self.updatedLabel = updatedLabel } public var body: some View { - VStack(alignment: .trailing, spacing: 2) { + // Fixed columns so the percent sits in the same place on every card: + // caption, bar, percent, countdown. No per-window timestamps — the + // header already carries the last-updated time. HStack(spacing: 4) { Text(caption) .font(.system(size: 11, weight: .bold)) .foregroundStyle(.primary) - .lineLimit(2) + .lineLimit(1) .minimumScaleFactor(0.75) .multilineTextAlignment(.trailing) - .frame(width: 52, alignment: .trailing) + .frame(width: 32, alignment: .trailing) CompanionUsageBar( remainingPercent: window.isMasked ? nil : window.remainingPercent, @@ -985,25 +927,16 @@ public struct CompanionMeterView: View { .foregroundStyle(window.statusColor) .lineLimit(1) .minimumScaleFactor(0.8) - .frame(width: 36, alignment: .trailing) + .frame(width: 40, alignment: .trailing) Text(window.meterCountdown()) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(.primary.opacity(0.85)) .lineLimit(1) .minimumScaleFactor(0.8) - .frame(width: 44, alignment: .center) + .frame(width: 46, alignment: .leading) } - .minimumScaleFactor(0.85) - if let updatedLabel { - Text(updatedLabel) - .font(.system(size: 9, weight: .medium)) - .foregroundStyle(.secondary) - .lineLimit(1) - .minimumScaleFactor(0.8) - } - } - .frame(maxWidth: .infinity, alignment: .trailing) + .frame(maxWidth: .infinity) } } diff --git a/scripts/regenerate-provider-marks.py b/scripts/regenerate-provider-marks.py index 23d8f0ef..438fa07b 100644 --- a/scripts/regenerate-provider-marks.py +++ b/scripts/regenerate-provider-marks.py @@ -217,13 +217,9 @@ def owner(*parts: str) -> str: Target("Sources/CodeCaps/Resources/ProviderMarks/gemini-light.png", repo_svg("gemini.svg"), "svg", "own"), # Muse -- owner raster with 840px of art. Target("Sources/CodeCaps/Resources/ProviderMarks/muse.png", MUSE, "png", "owner"), - Target("Sources/CodeCaps/Resources/ProviderMarks/muse-assist.png", MUSE, "png", "owner"), Target("Sources/CodeCaps/Resources/ProviderMarks/muse-color.png", MUSE, "png", "owner"), Target("Sources/CodeCaps/Resources/ProviderMarks/muse-dark.png", MUSE, "png", "owner"), Target("Sources/CodeCaps/Resources/ProviderMarks/muse-light.png", MUSE, "png", "owner"), - Target("Sources/CodeCaps/Resources/ProviderMarks/muse-assist-color.png", MUSE, "png", "owner"), - Target("Sources/CodeCaps/Resources/ProviderMarks/muse-assist-dark.png", MUSE, "png", "owner"), - Target("Sources/CodeCaps/Resources/ProviderMarks/muse-assist-light.png", MUSE, "png", "owner"), # Muse Code -- own vector. Target("Sources/CodeCaps/Resources/ProviderMarks/muse-code.png", repo_svg("muse-code.svg"), "svg", "own"), Target("Sources/CodeCaps/Resources/ProviderMarks/muse-code-color.png", repo_svg("muse-code.svg"), "svg", "own"), From 5e5832a3eb4d1f5b8f745ee4a3f180ad7d54012e Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:42:36 -0500 Subject: [PATCH 2/4] Address kody review on PR #306 and repair the four red tests Review threads: - GrokGrpcCredits pins period decoding to the documented field 6, keeps the first match, and accepts only a weekly or monthly span, so an unrelated submessage can no longer invent resetAt/periodStart. - A published percentage that is out of range or non-finite is tracked as seen-but-rejected, so it cannot reach the implicit-zero rule and report a spent pool as 100% remaining. - The gRPC fallback returns a typed outcome; HTTP 401/403 and gRPC status 16 report GROK_TOKEN with a masked first-8/last-4 preview instead of being swallowed as missing data. - readGrok issues both enrichments concurrently and only when they have something to contribute, instead of always paying 2s + 6s serially. - The fallback re-normalizes the window after filling the percentage, so status/isExhausted/skip describe the number just filled in. - The Grok subscription pool is matched by canonical identity rather than by id suffix, and the dedupe runs over the merged collection so a provider group cannot re-add the duplicate. - muse-assist leaves the bundle-file lists; the key still resolves through the PlatformLogo map to the shared muse mark. Test failures repaired: - PlatformLogoResourceTests no longer demands muse-assist artwork the PR removed. - The three CompanionModelTests that pinned the pre-redesign card (picker in the header, model.sourceScope gate, atCapSummary signature) now assert the canonical-card behaviour the PR actually ships, and testSourceScopePickerShowsTheDeviceAwareLabel moved its premise to the model, where the device-aware label still lives. --- Sources/QuotaCore/GrokGrpcCredits.swift | 72 +++++++++-- Sources/QuotaCore/LocalQuotaReader.swift | 117 +++++++++++++----- Sources/QuotaCore/QuotaModels.swift | 40 +++--- Tests/CodeCapsTests/CompanionModelTests.swift | 24 ++-- .../PlatformLogoResourceTests.swift | 9 +- .../QuotaCoreTests/GrokGrpcCreditsTests.swift | 85 +++++++++++-- Tests/QuotaCoreTests/GrokXaiDedupeTests.swift | 32 +++++ 7 files changed, 308 insertions(+), 71 deletions(-) diff --git a/Sources/QuotaCore/GrokGrpcCredits.swift b/Sources/QuotaCore/GrokGrpcCredits.swift index 9cce9e49..36d50974 100644 --- a/Sources/QuotaCore/GrokGrpcCredits.swift +++ b/Sources/QuotaCore/GrokGrpcCredits.swift @@ -31,10 +31,17 @@ public enum GrokGrpcCredits { /// The exact request frame bytes for the fallback call. public static let requestFrame = Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00]) + /// Field 6 of the config is the current period, a `{start, end}` pair of + /// Timestamps. Period decoding is pinned to this number rather than to + /// "any length-delimited field that happens to hold two Timestamps", so + /// an unrelated submessage can never invent bounds for the window. + static let currentPeriodField = 6 + /// Parse a gRPC-web (framed) or raw protobuf reply. `now` bounds the - /// implicit-zero rule: a complete active period with no percentage on - /// the wire reads as zero usage, matching grok.com's own web client, - /// whose descriptor declares the percent an implicit-presence float. + /// implicit-zero rule: a complete active period with no percentage field + /// anywhere on the wire reads as zero usage, matching grok.com's own web + /// client, whose descriptor declares the percent an implicit-presence + /// float. public static func parse(_ data: Data, now: Date) -> Reading? { guard let message = unwrapFrames(data) else { return nil } // Reply form: field 1 is the config. Raw form: the reply IS the @@ -49,6 +56,7 @@ public enum GrokGrpcCredits { guard let fields = try? ProtobufScanner.fields(config) else { return nil } var percent: Double? + var sawPercentField = false var periodStart: Date? var periodEnd: Date? for field in fields { @@ -56,15 +64,23 @@ public enum GrokGrpcCredits { case .fixed32: // The only top-level floats in the config are usage // percentages; product shares nest inside field 7 entries. - if percent == nil, let value = field.floatValue, - value.isFinite, (0...100).contains(value) { + guard let value = field.floatValue else { continue } + // Presence is tracked apart from the accepted value. A + // published percentage that is out of range or non-finite is + // still a published percentage, and must not reach the + // implicit-zero rule below as if nothing had been published. + sawPercentField = true + if percent == nil, value.isFinite, (0...100).contains(value) { percent = value } case .bytes: - if let period = timestampPair(field.bytes) { - periodStart = period.start - periodEnd = period.end - } + // First match wins: a later field-6 submessage is not allowed + // to overwrite bounds an earlier one already supplied. + guard field.number == Self.currentPeriodField, periodStart == nil, + let period = timestampPair(field.bytes), + Self.isRecognizedPeriod(period.start, period.end) else { continue } + periodStart = period.start + periodEnd = period.end case .varint, .fixed64: continue } @@ -73,13 +89,47 @@ public enum GrokGrpcCredits { if let percent { return Reading(usedPercent: percent, periodStart: periodStart, periodEnd: periodEnd) } - // Implicit zero: complete active period, no percentage anywhere. - if let start = periodStart, let end = periodEnd, start <= now, now <= end { + // Implicit zero: complete active period, and no percentage field on + // the wire at all. A rejected percentage is not an absent one. + if !sawPercentField, let start = periodStart, let end = periodEnd, + start <= now, now <= end { return Reading(usedPercent: 0, periodStart: start, periodEnd: end) } return nil } + /// The credits pool is billed weekly or monthly. A Timestamp pair of any + /// other length is not this field's subscription period, whatever its + /// shape, and is left uncaptured rather than used as a reset bound. + private static func isRecognizedPeriod(_ start: Date, _ end: Date) -> Bool { + let days = end.timeIntervalSince(start) / 86_400 + return (6.5...8).contains(days) || (27...32).contains(days) + } + + /// Whether the reply's gRPC-web trailer frame reports status 16 + /// (`UNAUTHENTICATED`). grok.com answers a probe it will not serve with + /// a framed 200 and a `grpc-status: 16` trailer, so an HTTP status check + /// alone reads a refused credential as an empty payload. Only the trailer + /// carries the status, which is why this lives beside the frame parser. + public static func isUnauthenticated(_ data: Data) -> Bool { + let bytes = [UInt8](data) + var offset = 0 + var trailer = [UInt8]() + while offset + 5 <= bytes.count { + let flag = bytes[offset] + guard flag == 0x00 || flag == 0x80 else { break } + let length = Int(bytes[offset + 1]) << 24 | Int(bytes[offset + 2]) << 16 + | Int(bytes[offset + 3]) << 8 | Int(bytes[offset + 4]) + guard offset + 5 + length <= bytes.count else { break } + if flag == 0x80 { trailer.append(contentsOf: bytes[(offset + 5)..<(offset + 5 + length)]) } + offset += 5 + length + } + guard !trailer.isEmpty else { return false } + let text = String(decoding: trailer, as: UTF8.self).lowercased() + guard let marker = text.range(of: "grpc-status:") else { return false } + return text[marker.upperBound...].prefix(while: \.isNumber) == "16" + } + /// A `{start: Timestamp, end: Timestamp}` submessage, when the bytes /// decode as exactly two Timestamp-shaped fields in order. private static func timestampPair(_ data: Data) -> (start: Date, end: Date)? { diff --git a/Sources/QuotaCore/LocalQuotaReader.swift b/Sources/QuotaCore/LocalQuotaReader.swift index c61b9423..3ac12eb3 100644 --- a/Sources/QuotaCore/LocalQuotaReader.swift +++ b/Sources/QuotaCore/LocalQuotaReader.swift @@ -345,39 +345,77 @@ public struct LocalQuotaReader: Sendable { request.setValue("application/json", forHTTPHeaderField: "Accept") let payload = try await requestJSON(request) var windows = parseGrok(payload, observedAt: now()) - // The credits payload carries no plan name; the settings endpoint - // names the tier (`SuperGrok Heavy` vs `SuperGrok`). Best-effort - // enrichment on a short budget, exactly as CodexBar documents: a - // failure here drops the overlay, never the usage reading. - if let tier = await grokSettingsTier(token: token) { + // Two best-effort enrichments, neither on the critical path, so they + // are issued together instead of one after the other: run serially + // they added up to eight seconds to every Grok refresh, and each was + // paid for even when it had nothing to contribute. + // + // - `/v1/settings` names the tier (`SuperGrok Heavy` vs `SuperGrok`), + // so it is only worth a request when a window has no plan name. + // - the grok.com gRPC fallback fills a percentage the proxy answer + // did not publish, so with no window at all there is nothing to + // fill and the request is skipped entirely. + let needsTier = windows.contains { $0.planName == nil } + let needsCredits = !windows.isEmpty && windows.first?.remainingPercent == nil + async let tierRequest = grokSettingsTier(token: token, onlyIf: needsTier) + async let creditsRequest = grokGrpcReading(token: token, onlyIf: needsCredits) + let (tier, credits) = await (tierRequest, creditsRequest) + if let tier { windows = windows.map { window in var window = window if window.planName == nil { window.planName = tier } return window } } - // Best-effort grok.com gRPC fallback: only when the proxy answer - // published no percentage. Its percent fills the subscription - // window; the proxy's period bounds stay authoritative. - if windows.first?.remainingPercent == nil, - let grpc = await grokGrpcReading(token: token), - let index = windows.firstIndex(where: { $0.id.hasSuffix(":subscription") || $0.remainingPercent == nil }) { - var window = windows[index] - window.remainingPercent = 100 - grpc.usedPercent - window.remainingUnknown = false - if window.resetAt == nil, let end = grpc.periodEnd { - window.resetAt = Self.isoString(end) - } - if window.periodStart == nil, let start = grpc.periodStart { - window.periodStart = Self.isoString(start) + var authRejected = false + switch credits { + case .authRejected: + // grok.com answered, and what it said was that this credential is + // not accepted. That is reported; it is never folded into the + // "no readable percentage" miss below. + authRejected = true + case .unavailable: + break + case .reading(let grpc): + // The proxy's period bounds stay authoritative; the fallback only + // supplies the percentage the proxy did not publish. + if let index = windows.firstIndex(where: { $0.id.hasSuffix(":subscription") || $0.remainingPercent == nil }) { + var window = windows[index] + window.remainingPercent = 100 - grpc.usedPercent + window.remainingUnknown = false + if window.resetAt == nil, let end = grpc.periodEnd { + window.resetAt = Self.isoString(end) + } + if window.periodStart == nil, let start = grpc.periodStart { + window.periodStart = Self.isoString(start) + } + // This window was normalized when it was still percentage-less, + // so `status`, `isExhausted`, `skip` and `skipReason` still + // describe the empty value. Re-derive them from the number + // just filled in, or a pool the fallback alone could read + // publishes as "unknown" and is never drawn as spent. + windows[index] = window.normalizedForExport() } - windows[index] = window } - return ProviderRead(provider: provider, windows: windows, issue: windows.first?.remainingPercent == nil ? "Grok returned no readable quota percentage." : nil) + let issue = authRejected + ? "GROK_TOKEN was rejected by grok.com (\(Self.maskedTokenPreview(token))). Sign in again." + : (windows.first?.remainingPercent == nil ? "Grok returned no readable quota percentage." : nil) + return ProviderRead(provider: provider, windows: windows, issue: issue) + } + + /// First 8 / last 4 of a credential, for a rejection message: enough to + /// tell two sign-ins apart on one Mac, never enough to replay. A value + /// too short to mask safely is reported as a fixed label, never echoed. + private static func maskedTokenPreview(_ token: String) -> String { + guard token.count > 16 else { return "(hidden)" } + return "\(token.prefix(8))…\(token.suffix(4))" } - /// `subscription_tier_display` from the CLI-proxy settings endpoint. - private func grokSettingsTier(token: String) async -> String? { + /// `subscription_tier_display` from the CLI-proxy settings endpoint. Skipped + /// outright when every window already carries a plan name: there is + /// nothing for the tier to name, and the request costs up to two seconds. + private func grokSettingsTier(token: String, onlyIf: Bool = true) async -> String? { + guard onlyIf else { return nil } var request = URLRequest(url: URL(string: "https://cli-chat-proxy.grok.com/v1/settings")!) request.httpMethod = "GET" request.timeoutInterval = 2 @@ -389,8 +427,22 @@ public struct LocalQuotaReader: Sendable { return tier?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? tier : nil } - /// The grok.com gRPC-web credits fallback (see `GrokGrpcCredits`). - private func grokGrpcReading(token: String) async -> GrokGrpcCredits.Reading? { + /// The grok.com gRPC-web credits fallback (see `GrokGrpcCredits`), and the +/// one outcome that is not a fallback miss. A rejected credential is a +/// different fact from absent credits data, so it has its own case and is +/// reported instead of being silently skipped. +private enum GrokGrpcOutcome { + case reading(GrokGrpcCredits.Reading) + case authRejected + case unavailable +} + +/// The grok.com gRPC-web credits fallback (see `GrokGrpcCredits`). + private func grokGrpcReading(token: String, onlyIf: Bool = true) async -> GrokGrpcOutcome { + // No window at all, or one that already carries a percentage, leaves + // this fallback with nothing to fill — so no request is made and the + // six-second budget is not spent on an answer nobody would use. + guard onlyIf else { return .unavailable } var request = URLRequest(url: URL(string: "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig")!) request.httpMethod = "POST" request.timeoutInterval = 6 @@ -398,9 +450,18 @@ public struct LocalQuotaReader: Sendable { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/grpc-web+proto", forHTTPHeaderField: "Content-Type") request.setValue("1", forHTTPHeaderField: "X-Grpc-Web") - guard let (data, response) = try? await fetchJSON(request), - (200..<300).contains(response.statusCode) else { return nil } - return GrokGrpcCredits.parse(data, now: now()) + guard let (data, response) = try? await fetchJSON(request) else { return .unavailable } + // HTTP 401/403 and gRPC status 16 (`UNAUTHENTICATED`, which grok.com + // returns for a bearer-only probe with no browser-held WKE keypair) + // both mean the token was refused. Anything else non-2xx is a plain + // miss and keeps the fallback best-effort. + if response.statusCode == 401 || response.statusCode == 403 { + return .authRejected + } + guard (200..<300).contains(response.statusCode) else { return .unavailable } + if GrokGrpcCredits.isUnauthenticated(data) { return .authRejected } + guard let reading = GrokGrpcCredits.parse(data, now: now()) else { return .unavailable } + return .reading(reading) } private static func isoString(_ date: Date) -> String { diff --git a/Sources/QuotaCore/QuotaModels.swift b/Sources/QuotaCore/QuotaModels.swift index 079dbf31..847b39ca 100644 --- a/Sources/QuotaCore/QuotaModels.swift +++ b/Sources/QuotaCore/QuotaModels.swift @@ -610,14 +610,19 @@ public extension QuotaResponse { /// the two rows can never disagree about one pool. Any other xAI /// window (API usage, a different cadence) is untouched. static func dedupedGrokSubscription(_ windows: [QuotaWindow]) -> [QuotaWindow] { - func subscriptionIndex(_ key: String) -> Int? { - windows.firstIndex { window in - window.canonicalProviderKey == key - && (window.id.hasSuffix(":subscription") || window.id == "xai:subscription") - } + // The one Grok credits pool, named by canonical identity. Window ids + // carry a source prefix (`local-mac:grok:subscription`), so identity is + // built from the window's own canonical provider key and compared by + // equality against this exact set — never by suffix, which would let + // an unrelated `grok:legacy:subscription` stand in for the pool and + // drop the wrong row. + let poolIdentities: Set = ["grok:subscription", "xai:subscription"] + func isPool(_ window: QuotaWindow) -> Bool { + poolIdentities.contains("\(window.canonicalProviderKey):subscription") } - guard let grokIndex = subscriptionIndex("grok"), - let xaiIndex = subscriptionIndex("xai") else { return windows } + guard let grokIndex = windows.firstIndex(where: { isPool($0) && $0.canonicalProviderKey == "grok" }), + let xaiIndex = windows.firstIndex(where: { isPool($0) && $0.canonicalProviderKey == "xai" }) + else { return windows } let grokDate = windows[grokIndex].occurredDate ?? .distantPast let xaiDate = windows[xaiIndex].occurredDate ?? .distantPast var copy = windows @@ -634,20 +639,25 @@ public extension QuotaResponse { /// offline fallback / pre-manifest backend path. That branch is kept as a /// safety net for first-launch installs before a sync has completed. func platformSections(now: Date = Date()) -> [QuotaPlatformSection] { - let normalizedWindows = Self.dedupedGrokSubscription(windows.map { $0.normalized() }) - var grouped = Dictionary(minimumCapacity: normalizedWindows.count) - for window in normalizedWindows { - grouped[window.canonicalProviderKey, default: []].append(window) - } - + // Collect every reading the response carries, then dedupe once over + // the whole collection. Dedupe before the provider groups are + // merged would drop the fleet `xai:subscription` only for a group to + // hand back the same duplicate moments later — the one disagreement + // this dedupe exists to remove, reached by the additive manifest path. + var collected = windows.map { $0.normalized() } // Older responses may not have top-level windows but do have the // additive provider groups. Fill only absent IDs to avoid duplicates. - var seen = Set(normalizedWindows.map { "\($0.canonicalProviderKey):\($0.id)" }) + var seen = Set(collected.map { "\($0.canonicalProviderKey):\($0.id)" }) for group in providerGroups { for window in group.windows.map({ $0.normalized() }) where seen.insert("\(window.canonicalProviderKey):\(window.id)").inserted { - grouped[window.canonicalProviderKey, default: []].append(window) + collected.append(window) } } + let normalizedWindows = Self.dedupedGrokSubscription(collected) + var grouped = Dictionary(minimumCapacity: normalizedWindows.count) + for window in normalizedWindows { + grouped[window.canonicalProviderKey, default: []].append(window) + } // Build the manifest view: key, label, via, expected, iconHint, and // sort order, keyed by canonical key. Groups that canonicalize to diff --git a/Tests/CodeCapsTests/CompanionModelTests.swift b/Tests/CodeCapsTests/CompanionModelTests.swift index 6fc88a4f..e1b5785c 100644 --- a/Tests/CodeCapsTests/CompanionModelTests.swift +++ b/Tests/CodeCapsTests/CompanionModelTests.swift @@ -288,7 +288,7 @@ final class CompanionModelTests: XCTestCase { func testTheCompanionCardShowsTheAtCapLine() throws { let view = try readCompanionContentViewSource() XCTAssertTrue( - view.contains("item.atCapSummary(scope: model.sourceScope)"), + view.contains("item.atCapSummary(scope: scope)"), "the quota card must show the cap line" ) XCTAssertTrue( @@ -519,8 +519,8 @@ final class CompanionModelTests: XCTestCase { XCTAssertTrue(source.contains("sourceLooksLikeHostname")) let content = try readCompanionContentViewSource() XCTAssertTrue( - content.contains("model.sourceScope == .allSources, item.sourceCount > 1"), - "discrepancy UI must follow the multi-source chip gate" + content.contains("item.sourceCount > 1"), + "discrepancy UI must follow the multi-source gate" ) } @@ -576,15 +576,25 @@ final class CompanionModelTests: XCTestCase { ) } + /// PR #306 made the main list canonical: the card always reads + /// `.allSources`, so the header no longer carries a source-scope picker. + /// The device-aware label itself still ships, on the scope the model + /// exposes, and nothing on screen may fall back to the stored raw value + /// (which always says "This iPhone", even on an iPad). func testSourceScopePickerShowsTheDeviceAwareLabel() throws { - let view = try readCompanionContentViewSource() + let source = try readCompanionModelSource() XCTAssertTrue( - view.contains("Text(scope.displayTitle).tag(scope)"), - "The source-scope picker must show the device-aware label" + source.contains("public var displayTitle: String"), + "The scope must keep exposing the device-aware label the picker used to render" ) + let view = try readCompanionContentViewSource() XCTAssertFalse( view.contains("Text(scope.rawValue).tag(scope)"), - "The source-scope picker must not show the stored raw value, which always says This iPhone" + "no scope control may show the stored raw value, which always says This iPhone" + ) + XCTAssertFalse( + view.contains("Picker(\"Source scope\""), + "the main list is canonical-only: the source-scope picker belongs to source detail" ) } diff --git a/Tests/CodeCapsTests/PlatformLogoResourceTests.swift b/Tests/CodeCapsTests/PlatformLogoResourceTests.swift index e6458190..d3fd3c7d 100644 --- a/Tests/CodeCapsTests/PlatformLogoResourceTests.swift +++ b/Tests/CodeCapsTests/PlatformLogoResourceTests.swift @@ -57,8 +57,13 @@ final class PlatformLogoResourceTests: XCTestCase { func testTheResourceBundleCarriesArtwork() throws { let bundle = try XCTUnwrap(ResourceBundle.resolved, "the SwiftPM resource bundle could not be resolved at all") + // `muse-assist` is deliberately absent: PR #306 dropped its four + // artwork files and `PlatformLogo.resourceNames` now maps the key to + // the shared `muse` mark, so the bundle carries no file by that name. + // The key itself still has to resolve, which is what + // `testEveryProviderKeyResolvesABundledMark` above checks. let marks = ["claude", "openai", "gemini", "gemini-color", "gemini-mono", - "cursor", "xai", "grok", "grok-bot", "minimax", "muse", "muse-assist", "muse-code", "muse-code-dark"] + "cursor", "xai", "grok", "grok-bot", "minimax", "muse", "muse-code", "muse-code-dark"] // Every named mark must be present in some form. Deliberately not an // exact count: the owner replacing the fabricated MiniMax `{M}` with // the real PNG mark added a file, and an equality assertion is a test @@ -75,7 +80,7 @@ final class PlatformLogoResourceTests: XCTestCase { /// a different hat: `NSImage` returns an object, and the row draws nothing. func testEveryBundledMarkRasterises() throws { let bundle = try XCTUnwrap(ResourceBundle.resolved) - for name in ["claude", "openai", "gemini", "gemini-mono", "cursor", "xai", "grok", "grok-bot", "minimax", "muse", "muse-assist", "muse-code", "muse-code-dark"] { + for name in ["claude", "openai", "gemini", "gemini-mono", "cursor", "xai", "grok", "grok-bot", "minimax", "muse", "muse-code", "muse-code-dark"] { for ext in ["svg", "png"] { guard let url = bundle.url(forResource: name, withExtension: ext) else { continue } let image = try XCTUnwrap(NSImage(contentsOf: url), "\(name).\(ext) did not load") diff --git a/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift b/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift index 9d27148b..ca93fec3 100644 --- a/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift +++ b/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift @@ -4,12 +4,15 @@ import XCTest /// Fixtures for the grok.com gRPC-web fallback parser, built byte-by-byte /// from the documented wire contract (CodexBar docs/grok.md): config is -/// field 1 of the reply, a top-level fixed32 is the used percent, the -/// current period is a `{start, end}` Timestamp pair, and product shares -/// nest inside repeated field 7 entries so they must not be mistaken for -/// the headline percent. +/// field 1 of the reply, a top-level fixed32 is the used percent, field 6 +/// is the current period as a `{start, end}` Timestamp pair on a weekly or +/// monthly cadence, and product shares nest inside repeated field 7 entries +/// so they must not be mistaken for the headline percent. final class GrokGrpcCreditsTests: XCTestCase { private let now = Date(timeIntervalSince1970: 1_761_000_000) + /// Seven days. The credits pool is billed weekly or monthly, so a + /// recognized period is one of those two spans and nothing else. + private let week: UInt64 = 604_800 private func varint(_ value: UInt64) -> [UInt8] { var value = value @@ -56,7 +59,7 @@ final class GrokGrpcCreditsTests: XCTestCase { func testWrappedFramedReplyYieldsPercentAndPeriod() { let start: UInt64 = 1_760_400_000 - let end: UInt64 = 1_761_408_000 + let end = start + week let config = fixed32Field(4, 37.5) + periodField(6, start: start, end: end) let reply = lenField(1, config) let reading = GrokGrpcCredits.parse(framed(Array(reply), trailer: true), now: now) @@ -80,18 +83,84 @@ final class GrokGrpcCreditsTests: XCTestCase { } func testImplicitZeroForActivePeriodWithoutPercent() { - let config = periodField(6, start: 1_760_400_000, end: 1_761_408_000) + let start: UInt64 = 1_760_400_000 + let config = periodField(6, start: start, end: start + week) let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) XCTAssertEqual(reading?.usedPercent, 0) } func testNoPercentAndNoActivePeriodIsNil() { - let config = periodField(6, start: 1_700_000_000, end: 1_701_000_000) - XCTAssertNil(GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now)) + let start: UInt64 = 1_700_000_000 + XCTAssertNil(GrokGrpcCredits.parse(Data(lenField(1, Array(periodField(6, start: start, end: start + week)))), now: now)) XCTAssertNil(GrokGrpcCredits.parse(Data([0xFF, 0xFF, 0xFF]), now: now)) XCTAssertNil(GrokGrpcCredits.parse(Data(), now: now)) } + // MARK: - Only the documented field may set the period + + /// An unrelated length-delimited field that happens to hold two Timestamps + /// must not be read as the subscription period: a previous-period or + /// trial-window submessage would otherwise invent the window's bounds. + func testAnUnrelatedBytesFieldCannotInventPeriodBounds() { + let start: UInt64 = 1_760_400_000 + // Field 9 is not the current period, but it decodes as a valid pair. + let decoy = periodField(9, start: start, end: start + week) + let config = decoy + fixed32Field(4, 42) + let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) + XCTAssertEqual(reading?.usedPercent, 42) + XCTAssertNil(reading?.periodStart, "a non-period field must not supply bounds") + XCTAssertNil(reading?.periodEnd) + } + + /// A decoy must not be able to open the implicit-zero branch either. + func testAnUnrelatedBytesFieldCannotOpenTheImplicitZeroBranch() { + let start: UInt64 = 1_760_400_000 + let decoy = periodField(9, start: start, end: start + week) + XCTAssertNil(GrokGrpcCredits.parse(Data(lenField(1, Array(decoy))), now: now)) + } + + /// When the current-period field repeats, the first entry is the period; + /// a later one must not overwrite bounds already captured. + func testTheFirstPeriodFieldWins() { + let first: UInt64 = 1_760_400_000 + let second: UInt64 = 1_760_800_000 + let config = periodField(6, start: first, end: first + week) + + periodField(6, start: second, end: second + week) + let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) + XCTAssertEqual(reading?.usedPercent, 0) + XCTAssertEqual(reading?.periodStart, Date(timeIntervalSince1970: TimeInterval(first))) + } + + /// A Timestamp pair on no billing cadence is not this field's period. + func testAnUnrecognizedPeriodLengthIsNotCaptured() { + let start: UInt64 = 1_760_400_000 + let config = periodField(6, start: start, end: start + 1_008_000) // ~11.7 days + let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) + XCTAssertNil(reading, "a span that is neither weekly nor monthly is not the credits period") + } + + // MARK: - A rejected percentage is still a published percentage + + /// A percentage that arrived but could not be accepted must not fall + /// through to the implicit-zero rule, which would report the pool as + /// untouched when the wire said otherwise. + func testOutOfRangePercentIsNotReadAsZeroUsage() { + let start: UInt64 = 1_760_400_000 + let config = fixed32Field(4, 150) + periodField(6, start: start, end: start + week) + let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) + XCTAssertNil(reading, "150% is not a reading, and not a licence to infer 0% used") + } + + /// The same holds when the published value is not finite. + func testNonFinitePercentIsNotReadAsZeroUsage() { + let start: UInt64 = 1_760_400_000 + let nanBits = Float.nan.bitPattern + let config = field(4, wire: 5, payload: withUnsafeBytes(of: &nanBits) { Array($0) }) + + periodField(6, start: start, end: start + week) + let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) + XCTAssertNil(reading) + } + func testRequestFrameMatchesDocumentedBytes() { XCTAssertEqual(GrokGrpcCredits.requestFrame, Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00])) } diff --git a/Tests/QuotaCoreTests/GrokXaiDedupeTests.swift b/Tests/QuotaCoreTests/GrokXaiDedupeTests.swift index 81e43b82..94f639ea 100644 --- a/Tests/QuotaCoreTests/GrokXaiDedupeTests.swift +++ b/Tests/QuotaCoreTests/GrokXaiDedupeTests.swift @@ -39,4 +39,36 @@ final class GrokXaiDedupeTests: XCTestCase { let windows = [window(providerKey: "xai", id: "xai:subscription", occurredAt: "2026-10-21T22:02:00Z")] XCTAssertEqual(QuotaResponse.dedupedGrokSubscription(windows).count, 1) } + + /// The pool is identified by canonical identity, not by a suffix, so an + /// unrelated window whose id merely ends in `:subscription` cannot stand + /// in for it and delete the real reading. + func testAnUnrelatedSubscriptionSuffixIsNotThePool() { + let windows = [ + window(providerKey: "grok", id: "grok:legacy:subscription", occurredAt: "2026-10-22T03:00:00Z"), + window(providerKey: "xai", id: "xai:subscription", occurredAt: "2026-10-21T22:02:00Z"), + ] + let result = QuotaResponse.dedupedGrokSubscription(windows) + XCTAssertEqual(result.map(\.id).sorted(), ["grok:legacy:subscription", "xai:subscription"]) + } + + /// The fleet reading arriving in `providerGroups` — the additive manifest + /// path — must not re-add the duplicate the dedupe just dropped. + func testDedupeSurvivesTheProviderGroupsMerge() { + let local = window(providerKey: "grok", id: "local-mac:grok:subscription", occurredAt: "2026-10-22T03:00:00Z") + let fleet = window(providerKey: "xai", id: "xai:subscription", occurredAt: "2026-10-21T22:02:00Z") + let response = QuotaResponse( + generatedAt: "2026-10-22T03:00:00Z", + windows: [local], + providerGroups: [ + QuotaProviderGroup(provider: "xai", providerLabel: "xAI", expected: true, windows: [fleet]), + ]) + let subscriptionWindows = response.platformSections(now: Date()) + .flatMap(\.windows) + .map(\.window.id) + .filter { $0.contains("subscription") } + .sorted() + XCTAssertEqual(subscriptionWindows, ["local-mac:grok:subscription"], + "one credits pool must stay one row after the group merge") + } } From 69b0656c447b74ce56ab93313f9b44da4e7cb46b Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:47:16 -0500 Subject: [PATCH 3/4] Fix inout binding for the NaN percentage fixture withUnsafeBytes(of: &nanBits) needs a var; nanBits was a let constant, so GrokGrpcCreditsTests did not compile. --- Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift b/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift index ca93fec3..50a7e099 100644 --- a/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift +++ b/Tests/QuotaCoreTests/GrokGrpcCreditsTests.swift @@ -154,7 +154,7 @@ final class GrokGrpcCreditsTests: XCTestCase { /// The same holds when the published value is not finite. func testNonFinitePercentIsNotReadAsZeroUsage() { let start: UInt64 = 1_760_400_000 - let nanBits = Float.nan.bitPattern + var nanBits = Float.nan.bitPattern let config = field(4, wire: 5, payload: withUnsafeBytes(of: &nanBits) { Array($0) }) + periodField(6, start: start, end: start + week) let reading = GrokGrpcCredits.parse(Data(lenField(1, Array(config))), now: now) From e7adadaa266f749e112a9e682b5681cb1f6053b8 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:53:51 -0500 Subject: [PATCH 4/4] Match the Grok credits pool on exact id components The identity was built as providerKey + a hardcoded ":subscription", so every grok/xai window matched it and testOtherXaiWindowsAreUntouched lost its API-credits row. Match the last two id components in full instead: parts[-2] == canonicalProviderKey && parts[-1] == "subscription". That still catches the source-prefixed local-mac:grok:subscription and the fleet xai:subscription, while excluding grok:legacy:subscription and xai:api-monthly. --- Sources/QuotaCore/QuotaModels.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/QuotaCore/QuotaModels.swift b/Sources/QuotaCore/QuotaModels.swift index 847b39ca..331955f4 100644 --- a/Sources/QuotaCore/QuotaModels.swift +++ b/Sources/QuotaCore/QuotaModels.swift @@ -610,15 +610,17 @@ public extension QuotaResponse { /// the two rows can never disagree about one pool. Any other xAI /// window (API usage, a different cadence) is untouched. static func dedupedGrokSubscription(_ windows: [QuotaWindow]) -> [QuotaWindow] { - // The one Grok credits pool, named by canonical identity. Window ids - // carry a source prefix (`local-mac:grok:subscription`), so identity is - // built from the window's own canonical provider key and compared by - // equality against this exact set — never by suffix, which would let - // an unrelated `grok:legacy:subscription` stand in for the pool and - // drop the wrong row. - let poolIdentities: Set = ["grok:subscription", "xai:subscription"] + // The one Grok credits pool, identified by exact component equality on the + // window id. Ids carry an optional source prefix + // (`local-mac:grok:subscription`), so the canonical form is the last + // two components: `:`, each compared in full. + // `grok:legacy:subscription` names a different window and is not the + // pool; `xai:api-monthly` is not the pool either. func isPool(_ window: QuotaWindow) -> Bool { - poolIdentities.contains("\(window.canonicalProviderKey):subscription") + let parts = window.id.split(separator: ":") + guard parts.count >= 2 else { return false } + return parts[parts.count - 2] == Substring(window.canonicalProviderKey) + && parts[parts.count - 1] == "subscription" } guard let grokIndex = windows.firstIndex(where: { isPool($0) && $0.canonicalProviderKey == "grok" }), let xaiIndex = windows.firstIndex(where: { isPool($0) && $0.canonicalProviderKey == "xai" })