diff --git a/open-sse/services/usage/grok-cli.js b/open-sse/services/usage/grok-cli.js index 8fcddee0..80d89677 100644 --- a/open-sse/services/usage/grok-cli.js +++ b/open-sse/services/usage/grok-cli.js @@ -29,11 +29,19 @@ import { GROK_CLI_USER_AGENT, GROK_CLI_VERSION, } from "../../config/grokCli.js"; +import { decodeGrokCreditsFrame } from "./grokCliQuotaFrame.js"; const USAGE = U("grok-cli"); const BILLING_URL = USAGE.url || "https://cli-chat-proxy.grok.com/v1/billing?format=credits"; const USER_URL = USAGE.userUrl || "https://cli-chat-proxy.grok.com/v1/user?include=subscription"; +// SuperGrok weekly pool — same endpoint OmniRoute #6844 / steipete CodexBar docs. +const GRPC_CREDITS_URL = + "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig"; +// Empty gRPC-web request frame (flag 0 + length 0). Without it upstream returns +// grpc-status 13 "Missing request message." with a 0-byte body. +const GRPC_WEB_EMPTY_REQUEST_FRAME = Buffer.from([0, 0, 0, 0, 0]); + /** Unwrap protobuf-json `{ val: n }` or plain numbers/strings. */ function unwrapVal(value, fallback = 0) { if (value == null) return fallback; @@ -271,6 +279,50 @@ export function parseGrokCliBilling(billing, user = null) { }; } +/** + * Live SuperGrok weekly pool via gRPC-web GetGrokCreditsConfig. + * Fail-open: any network/auth/parse failure returns null. + * @returns {{ percentUsed: number, resetAt: string|null } | null} + */ +export async function fetchGrokCliCreditsConfig(accessToken, proxyOptions = null) { + if (!accessToken) return null; + try { + const res = await proxyAwareFetch( + GRPC_CREDITS_URL, + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/grpc-web+proto", + "X-Grpc-Web": "1", + Accept: "application/grpc-web+proto", + }, + body: GRPC_WEB_EMPTY_REQUEST_FRAME, + }, + proxyOptions, + ); + if (!res?.ok) return null; + const arrayBuffer = await res.arrayBuffer().catch(() => null); + if (!arrayBuffer) return null; + return decodeGrokCreditsFrame(Buffer.from(arrayBuffer)); + } catch { + return null; + } +} + +function quotasFromGrpcCredits(decoded) { + if (!decoded || !Number.isFinite(decoded.percentUsed)) return null; + // Round for bar display (fixed32 ratio * 100 can be 34.999… for 0.35) + const used = Math.round(Math.max(0, Math.min(100, decoded.percentUsed))); + return { + "Weekly SuperGrok": makeQuota({ + used, + total: 100, + resetAt: decoded.resetAt || null, + }), + }; +} + /** * @param {string} accessToken * @param {object|null} providerSpecificData @@ -321,6 +373,16 @@ export async function getGrokCliUsage(accessToken, providerSpecificData = null, const parsed = parseGrokCliBilling(billing, user); if (!parsed.quotas || Object.keys(parsed.quotas).length === 0) { + // Paid SuperGrok often returns cap=0 over REST but exposes the shared + // weekly pool on GetGrokCreditsConfig — try that before giving up. + const grpc = await fetchGrokCliCreditsConfig(accessToken, proxyOptions); + const grpcQuotas = quotasFromGrpcCredits(grpc); + if (grpcQuotas) { + return { + plan: parsed.plan, + quotas: grpcQuotas, + }; + } return { plan: parsed.plan, message: parsed.subscriptionAccess diff --git a/open-sse/services/usage/grokCliQuotaFrame.js b/open-sse/services/usage/grokCliQuotaFrame.js new file mode 100644 index 00000000..1578bec6 --- /dev/null +++ b/open-sse/services/usage/grokCliQuotaFrame.js @@ -0,0 +1,191 @@ +/** + * gRPC-web frame decoder for xAI GetGrokCreditsConfig + * (grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig). + * + * Real response shape (live capture 2026-07-20): + * top-level field 1 (length-delimited) — nested credits info + * subfield 1 (fixed32 float) — usage ratio 0..1 + * subfield 5 (Timestamp{seconds,nanos}) — credit-pool reset time + * + * Fail-open: any malformed buffer returns null, never throws. + */ + +const FIELD_CREDITS_INFO = 1; +const CREDITS_FIELD_USAGE_RATIO = 1; +const CREDITS_FIELD_RESET_TIMESTAMP = 5; +const TIMESTAMP_FIELD_SECONDS = 1; +const TIMESTAMP_FIELD_NANOS = 2; + +const WIRE_TYPE_VARINT = 0; +const WIRE_TYPE_FIXED64 = 1; +const WIRE_TYPE_LENGTH_DELIMITED = 2; +const WIRE_TYPE_FIXED32 = 5; + +const GRPC_WEB_TRAILER_FLAG_BIT = 0x80; +const MAX_VARINT_SHIFT_BITS = 70n; + +/** + * Validate a gRPC-web frame header at `offset`. + * @returns {{ flag: number, payloadStart: number, payloadLength: number } | null} + */ +export function probeFrameHeader(buffer, offset = 0) { + if (!Buffer.isBuffer(buffer) || offset < 0 || buffer.length - offset < 5) return null; + const flag = buffer[offset]; + if (flag !== 0x00 && flag !== 0x01 && flag !== 0x80 && flag !== 0x81) return null; + const payloadStart = offset + 5; + const payloadLength = buffer.readUInt32BE(offset + 1); + if (payloadLength > buffer.length - payloadStart) return null; + return { flag, payloadStart, payloadLength }; +} + +function readVarint(buffer, offset) { + let result = 0n; + let shift = 0n; + let pos = offset; + for (;;) { + if (pos >= buffer.length) return null; + const byte = buffer[pos]; + result |= BigInt(byte & 0x7f) << shift; + pos += 1; + if ((byte & 0x80) === 0) break; + shift += 7n; + if (shift > MAX_VARINT_SHIFT_BITS) return null; + } + return { value: Number(result), next: pos }; +} + +function readLengthDelimitedField(buffer, offset) { + const lengthResult = readVarint(buffer, offset); + if (!lengthResult) return null; + const { value: length, next: bodyStart } = lengthResult; + if (length < 0 || bodyStart + length > buffer.length) return null; + return { + field: { wireType: WIRE_TYPE_LENGTH_DELIMITED, bytes: buffer.subarray(bodyStart, bodyStart + length) }, + next: bodyStart + length, + }; +} + +function readFixedWidthField(buffer, offset, width, wireType) { + if (offset + width > buffer.length) return null; + return { + field: { wireType, bytes: buffer.subarray(offset, offset + width) }, + next: offset + width, + }; +} + +function readField(buffer, offset) { + const tagResult = readVarint(buffer, offset); + if (!tagResult) return null; + const fieldNumber = tagResult.value >>> 3; + const wireType = tagResult.value & 0x7; + if (fieldNumber === 0) return null; + + if (wireType === WIRE_TYPE_VARINT) { + const valueResult = readVarint(buffer, tagResult.next); + if (!valueResult) return null; + return { + fieldNumber, + field: { wireType: WIRE_TYPE_VARINT, value: valueResult.value }, + next: valueResult.next, + }; + } + if (wireType === WIRE_TYPE_LENGTH_DELIMITED) { + const result = readLengthDelimitedField(buffer, tagResult.next); + return result ? { fieldNumber, field: result.field, next: result.next } : null; + } + if (wireType === WIRE_TYPE_FIXED64) { + const result = readFixedWidthField(buffer, tagResult.next, 8, WIRE_TYPE_FIXED64); + return result ? { fieldNumber, field: result.field, next: result.next } : null; + } + if (wireType === WIRE_TYPE_FIXED32) { + const result = readFixedWidthField(buffer, tagResult.next, 4, WIRE_TYPE_FIXED32); + return result ? { fieldNumber, field: result.field, next: result.next } : null; + } + return null; +} + +function decodeFields(buffer) { + const fields = new Map(); + let offset = 0; + while (offset < buffer.length) { + const result = readField(buffer, offset); + if (!result) return null; + fields.set(result.fieldNumber, result.field); + offset = result.next; + } + return fields; +} + +function findDataFramePayload(buffer) { + let offset = 0; + while (offset < buffer.length) { + const frame = probeFrameHeader(buffer, offset); + if (!frame) return null; + const frameEnd = frame.payloadStart + frame.payloadLength; + const isTrailer = (frame.flag & GRPC_WEB_TRAILER_FLAG_BIT) !== 0; + if (!isTrailer) { + return buffer.subarray(frame.payloadStart, frameEnd); + } + offset = frameEnd; + } + return null; +} + +function extractNestedMessage(field) { + if (!field || field.wireType !== WIRE_TYPE_LENGTH_DELIMITED) return null; + return decodeFields(field.bytes); +} + +function extractUsageRatio(field) { + if (!field) return 0; // proto3 omission = 0% used + if (field.wireType === WIRE_TYPE_FIXED32) return field.bytes.readFloatLE(0); + if (field.wireType === WIRE_TYPE_FIXED64) return field.bytes.readDoubleLE(0); + return null; +} + +function extractResetAt(field) { + if (!field || field.wireType !== WIRE_TYPE_LENGTH_DELIMITED) return null; + + const timestampFields = decodeFields(field.bytes); + if (!timestampFields) return null; + + const secondsField = timestampFields.get(TIMESTAMP_FIELD_SECONDS); + const nanosField = timestampFields.get(TIMESTAMP_FIELD_NANOS); + const seconds = secondsField?.wireType === WIRE_TYPE_VARINT ? secondsField.value : 0; + const nanos = nanosField?.wireType === WIRE_TYPE_VARINT ? nanosField.value : 0; + + const millis = seconds * 1000 + Math.round(nanos / 1_000_000); + const parsed = new Date(millis); + return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(); +} + +/** + * Decode GetGrokCreditsConfig response → `{ percentUsed: 0-100, resetAt }` or null. + * @param {Buffer} buffer + * @returns {{ percentUsed: number, resetAt: string|null } | null} + */ +export function decodeGrokCreditsFrame(buffer) { + if (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) return null; + + try { + const framed = probeFrameHeader(buffer, 0) !== null; + const payload = framed ? findDataFramePayload(buffer) : buffer; + if (!payload) return null; + + const topLevelFields = decodeFields(payload); + if (!topLevelFields) return null; + + const creditsInfo = extractNestedMessage(topLevelFields.get(FIELD_CREDITS_INFO)); + if (!creditsInfo) return null; + + const usageRatio = extractUsageRatio(creditsInfo.get(CREDITS_FIELD_USAGE_RATIO)); + if (usageRatio === null || !Number.isFinite(usageRatio) || usageRatio < 0) return null; + + return { + percentUsed: Math.min(100, usageRatio * 100), + resetAt: extractResetAt(creditsInfo.get(CREDITS_FIELD_RESET_TIMESTAMP)), + }; + } catch { + return null; + } +} diff --git a/tests/unit/grok-cli-quota-frame.test.js b/tests/unit/grok-cli-quota-frame.test.js new file mode 100644 index 00000000..cd86ab4a --- /dev/null +++ b/tests/unit/grok-cli-quota-frame.test.js @@ -0,0 +1,229 @@ +import { describe, it, expect } from "vitest"; +import { + decodeGrokCreditsFrame, + probeFrameHeader, +} from "../../open-sse/services/usage/grokCliQuotaFrame.js"; + +/** + * Minimal protobuf encoder for fixtures — real GetGrokCreditsConfig wire shape + * (nested field 1 / fixed32 ratio / Timestamp reset + optional trailer 0x80). + */ + +function encodeVarint(value) { + const bytes = []; + let v = BigInt(value); + do { + let byte = Number(v & 0x7fn); + v >>= 7n; + if (v !== 0n) byte |= 0x80; + bytes.push(byte); + } while (v !== 0n); + return Buffer.from(bytes); +} + +function encodeTag(fieldNumber, wireType) { + return encodeVarint((fieldNumber << 3) | wireType); +} + +function encodeFixed32Field(fieldNumber, value) { + const body = Buffer.alloc(4); + body.writeFloatLE(value, 0); + return Buffer.concat([encodeTag(fieldNumber, 5), body]); +} + +function encodeLengthDelimited(fieldNumber, body) { + return Buffer.concat([encodeTag(fieldNumber, 2), encodeVarint(body.length), body]); +} + +function encodeVarintField(fieldNumber, value) { + return Buffer.concat([encodeTag(fieldNumber, 0), encodeVarint(value)]); +} + +function encodeTimestampField(fieldNumber, seconds, nanos) { + const parts = []; + if (seconds !== 0) parts.push(encodeVarintField(1, seconds)); + if (nanos !== 0) parts.push(encodeVarintField(2, nanos)); + return encodeLengthDelimited(fieldNumber, Buffer.concat(parts)); +} + +function encodeCreditsInfo(shape) { + const parts = []; + if (shape.usageRatio !== undefined) parts.push(encodeFixed32Field(1, shape.usageRatio)); + if (shape.asOfSeconds !== undefined) { + parts.push(encodeTimestampField(4, shape.asOfSeconds, shape.asOfNanos ?? 0)); + } + if (shape.resetSeconds !== undefined) { + parts.push(encodeTimestampField(5, shape.resetSeconds, shape.resetNanos ?? 0)); + } + return Buffer.concat(parts); +} + +function encodeTopLevelMessage(creditsInfo) { + return encodeLengthDelimited(1, creditsInfo); +} + +function frameData(payload) { + const header = Buffer.alloc(5); + header[0] = 0x00; + header.writeUInt32BE(payload.length, 1); + return Buffer.concat([header, payload]); +} + +function frameTrailer(statusText = "grpc-status:0\r\n") { + const body = Buffer.from(statusText, "utf8"); + const header = Buffer.alloc(5); + header[0] = 0x80; + header.writeUInt32BE(body.length, 1); + return Buffer.concat([header, body]); +} + +const REAL_USAGE_RATIO = 1.0; +const REAL_ASOF_SECONDS = 1784221140; +const REAL_ASOF_NANOS = 867850000; +const REAL_RESET_SECONDS = 1784825940; +const REAL_RESET_NANOS = 867850000; +const PERCENT_TOLERANCE = 1e-4; + +function isoFromEpoch(seconds, nanos) { + return new Date(seconds * 1000 + Math.round(nanos / 1_000_000)).toISOString(); +} + +describe("decodeGrokCreditsFrame", () => { + it("decodes real GetGrokCreditsConfig shape (nested, fixed32, Timestamp, trailer)", () => { + const creditsInfo = encodeCreditsInfo({ + usageRatio: REAL_USAGE_RATIO, + asOfSeconds: REAL_ASOF_SECONDS, + asOfNanos: REAL_ASOF_NANOS, + resetSeconds: REAL_RESET_SECONDS, + resetNanos: REAL_RESET_NANOS, + }); + const buffer = Buffer.concat([frameData(encodeTopLevelMessage(creditsInfo)), frameTrailer()]); + + const result = decodeGrokCreditsFrame(buffer); + expect(result).toBeTruthy(); + expect(result.percentUsed).toBe(100); + expect(result.resetAt).toBe(isoFromEpoch(REAL_RESET_SECONDS, REAL_RESET_NANOS)); + }); + + it("ignores trailing gRPC-web trailer frame (flag 0x80)", () => { + const creditsInfo = encodeCreditsInfo({ + usageRatio: 0.5, + resetSeconds: REAL_RESET_SECONDS, + resetNanos: 0, + }); + const topMessage = encodeTopLevelMessage(creditsInfo); + const withoutTrailer = frameData(topMessage); + const withTrailer = Buffer.concat([frameData(topMessage), frameTrailer()]); + + const a = decodeGrokCreditsFrame(withoutTrailer); + const b = decodeGrokCreditsFrame(withTrailer); + expect(a).toBeTruthy(); + expect(b).toBeTruthy(); + expect(b.percentUsed).toBe(a.percentUsed); + expect(b.resetAt).toBe(a.resetAt); + expect(b.percentUsed).toBe(50); + }); + + it("decodes raw unframed protobuf payload", () => { + const creditsInfo = encodeCreditsInfo({ + usageRatio: 0.75, + resetSeconds: REAL_RESET_SECONDS, + resetNanos: REAL_RESET_NANOS, + }); + const payload = encodeTopLevelMessage(creditsInfo); + expect(probeFrameHeader(payload)).toBeNull(); + + const result = decodeGrokCreditsFrame(payload); + expect(result).toBeTruthy(); + expect(Math.abs(result.percentUsed - 75)).toBeLessThan(PERCENT_TOLERANCE); + expect(result.resetAt).toBe(isoFromEpoch(REAL_RESET_SECONDS, REAL_RESET_NANOS)); + }); + + it("treats omitted usage-ratio as 0% (proto3 default)", () => { + const creditsInfo = encodeCreditsInfo({ + resetSeconds: REAL_RESET_SECONDS, + resetNanos: REAL_RESET_NANOS, + }); + const result = decodeGrokCreditsFrame(frameData(encodeTopLevelMessage(creditsInfo))); + expect(result).toBeTruthy(); + expect(result.percentUsed).toBe(0); + expect(result.resetAt).toBe(isoFromEpoch(REAL_RESET_SECONDS, REAL_RESET_NANOS)); + }); + + it("clamps usage ratio above 1.0 to percentUsed 100", () => { + const creditsInfo = encodeCreditsInfo({ usageRatio: 1.5 }); + const result = decodeGrokCreditsFrame(frameData(encodeTopLevelMessage(creditsInfo))); + expect(result).toBeTruthy(); + expect(result.percentUsed).toBe(100); + }); + + it("returns null for negative usage ratio", () => { + const creditsInfo = encodeCreditsInfo({ usageRatio: -0.1 }); + expect(decodeGrokCreditsFrame(frameData(encodeTopLevelMessage(creditsInfo)))).toBeNull(); + }); + + it("returns null when top-level field 1 is not length-delimited", () => { + expect(decodeGrokCreditsFrame(frameData(encodeVarintField(1, 42)))).toBeNull(); + }); + + it("returns null when nested usage-ratio has unexpected wire type", () => { + const creditsInfo = encodeLengthDelimited(1, Buffer.from("not-a-float", "utf8")); + expect(decodeGrokCreditsFrame(frameData(encodeTopLevelMessage(creditsInfo)))).toBeNull(); + }); + + it("returns null when top-level has no field 1", () => { + expect(decodeGrokCreditsFrame(frameData(encodeVarintField(9, 1)))).toBeNull(); + }); + + it("returns null for truncated buffer", () => { + const creditsInfo = encodeCreditsInfo({ + usageRatio: 0.5, + resetSeconds: REAL_RESET_SECONDS, + resetNanos: REAL_RESET_NANOS, + }); + const buffer = frameData(encodeTopLevelMessage(creditsInfo)); + expect(decodeGrokCreditsFrame(buffer.subarray(0, buffer.length - 3))).toBeNull(); + }); + + it("returns null for trailer-only body", () => { + expect(decodeGrokCreditsFrame(frameTrailer())).toBeNull(); + }); + + it("returns null for empty buffer", () => { + expect(decodeGrokCreditsFrame(Buffer.alloc(0))).toBeNull(); + }); +}); + +describe("probeFrameHeader", () => { + it("rejects declared length that exceeds body", () => { + const header = Buffer.alloc(5); + header[0] = 0x00; + header.writeUInt32BE(9999, 1); + expect(probeFrameHeader(Buffer.concat([header, Buffer.from([0x01, 0x02])]))).toBeNull(); + }); + + it("rejects invalid compression flag", () => { + const header = Buffer.alloc(5); + header[0] = 0x07; + expect(probeFrameHeader(header)).toBeNull(); + }); + + it("accepts trailer frame header (flag 0x80)", () => { + const result = probeFrameHeader(frameTrailer()); + expect(result).toBeTruthy(); + expect(result.flag).toBe(0x80); + }); + + it("reads frame header at non-zero offset", () => { + const creditsInfo = encodeCreditsInfo({ usageRatio: 0.5 }); + const buffer = Buffer.concat([ + frameData(encodeTopLevelMessage(creditsInfo)), + frameTrailer(), + ]); + const first = probeFrameHeader(buffer); + expect(first).toBeTruthy(); + const second = probeFrameHeader(buffer, first.payloadStart + first.payloadLength); + expect(second).toBeTruthy(); + expect(second.flag).toBe(0x80); + }); +}); diff --git a/tests/unit/grok-cli-usage.test.js b/tests/unit/grok-cli-usage.test.js index 84414b90..a0e28a5c 100644 --- a/tests/unit/grok-cli-usage.test.js +++ b/tests/unit/grok-cli-usage.test.js @@ -169,6 +169,67 @@ describe("parseGrokCliBilling", () => { }); }); +function encodeVarint(value) { + const bytes = []; + let v = BigInt(value); + do { + let byte = Number(v & 0x7fn); + v >>= 7n; + if (v !== 0n) byte |= 0x80; + bytes.push(byte); + } while (v !== 0n); + return Buffer.from(bytes); +} + +function encodeTag(fieldNumber, wireType) { + return encodeVarint((fieldNumber << 3) | wireType); +} + +function encodeFixed32Field(fieldNumber, value) { + const body = Buffer.alloc(4); + body.writeFloatLE(value, 0); + return Buffer.concat([encodeTag(fieldNumber, 5), body]); +} + +function encodeLengthDelimited(fieldNumber, body) { + return Buffer.concat([encodeTag(fieldNumber, 2), encodeVarint(body.length), body]); +} + +function encodeVarintField(fieldNumber, value) { + return Buffer.concat([encodeTag(fieldNumber, 0), encodeVarint(value)]); +} + +function encodeTimestampField(fieldNumber, seconds, nanos) { + const parts = []; + if (seconds !== 0) parts.push(encodeVarintField(1, seconds)); + if (nanos !== 0) parts.push(encodeVarintField(2, nanos)); + return encodeLengthDelimited(fieldNumber, Buffer.concat(parts)); +} + +/** Framed GetGrokCreditsConfig response for a usage ratio 0..1. */ +function buildCreditsResponseBuffer(usageRatio, resetSeconds = 1784825940, resetNanos = 867850000) { + const creditsInfo = Buffer.concat([ + encodeFixed32Field(1, usageRatio), + encodeTimestampField(5, resetSeconds, resetNanos), + ]); + const topMessage = encodeLengthDelimited(1, creditsInfo); + const header = Buffer.alloc(5); + header[0] = 0x00; + header.writeUInt32BE(topMessage.length, 1); + return Buffer.concat([header, topMessage]); +} + +function binaryResponse(buffer, status = 200) { + return new Response(buffer, { + status, + headers: { "content-type": "application/grpc-web+proto" }, + }); +} + +const EMPTY_GRPC_WEB_FRAME = Buffer.from([0, 0, 0, 0, 0]); +const GRPC_CREDITS_URL = + "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig"; + describe("getUsageForProvider(grok-cli)", () => { beforeEach(() => { vi.clearAllMocks(); @@ -211,6 +272,8 @@ describe("getUsageForProvider(grok-cli)", () => { expect(billingCall[1].headers["x-userid"]).toBe( "d84768dd-224d-4052-ba49-0d336fa9160c", ); + // REST already has numeric quotas — do not hit gRPC fallback + expect(proxyAwareFetch.mock.calls).toHaveLength(2); }); it("surfaces auth-expired message on 401", async () => { @@ -224,6 +287,8 @@ describe("getUsageForProvider(grok-cli)", () => { }); expect(usage.message).toMatch(/expired|re-authorize/i); + // Auth failure must not attempt gRPC fallback + expect(proxyAwareFetch.mock.calls).toHaveLength(2); }); it("returns depleted on-demand bar without blocking message when cap is zero", async () => { @@ -241,15 +306,62 @@ describe("getUsageForProvider(grok-cli)", () => { expect(usage.message).toBeUndefined(); expect(usage.quotas["On-demand"].remainingPercentage).toBe(0); expect(usage.quotas["On-demand"].total).toBe(1); + // Exhausted free already has a quota bar — no gRPC fallback + expect(proxyAwareFetch.mock.calls).toHaveLength(2); }); - it("reports active paid access when provider exposes no numeric quota", async () => { + it("falls back to GetGrokCreditsConfig gRPC when paid sub has no REST numeric quota", async () => { + const resetSeconds = 1784825940; + const resetNanos = 867850000; + const resetAt = new Date( + resetSeconds * 1000 + Math.round(resetNanos / 1_000_000), + ).toISOString(); + proxyAwareFetch .mockResolvedValueOnce(jsonResponse(EXHAUSTED_BILLING)) - .mockResolvedValueOnce(jsonResponse({ - ...USER_PROFILE, - subscriptionTier: "XPremiumPlus", - })); + .mockResolvedValueOnce( + jsonResponse({ + ...USER_PROFILE, + subscriptionTier: "XPremiumPlus", + }), + ) + .mockResolvedValueOnce(binaryResponse(buildCreditsResponseBuffer(0.35, resetSeconds, resetNanos))); + + const usage = await getUsageForProvider({ + provider: "grok-cli", + accessToken: "test-token", + }); + + expect(usage.message).toBeUndefined(); + expect(usage.plan).toBe("XPremiumPlus"); + expect(usage.quotas["Weekly SuperGrok"]).toMatchObject({ + used: 35, + total: 100, + remainingPercentage: 65, + resetAt, + unlimited: false, + }); + + const grpcCall = proxyAwareFetch.mock.calls[2]; + expect(grpcCall[0]).toBe(GRPC_CREDITS_URL); + expect(grpcCall[1].method).toBe("POST"); + expect(grpcCall[1].headers.Authorization).toBe("Bearer test-token"); + expect(grpcCall[1].headers["Content-Type"]).toBe("application/grpc-web+proto"); + expect(grpcCall[1].headers["X-Grpc-Web"]).toBe("1"); + // Empty gRPC-web request frame is required (flag 0 + length 0) + expect(Buffer.from(grpcCall[1].body)).toEqual(EMPTY_GRPC_WEB_FRAME); + }); + + it("keeps subscription message when REST empty and gRPC fails open", async () => { + proxyAwareFetch + .mockResolvedValueOnce(jsonResponse(EXHAUSTED_BILLING)) + .mockResolvedValueOnce( + jsonResponse({ + ...USER_PROFILE, + subscriptionTier: "XPremiumPlus", + }), + ) + .mockResolvedValueOnce(binaryResponse(Buffer.alloc(0), 500)); const usage = await getUsageForProvider({ provider: "grok-cli", @@ -260,6 +372,26 @@ describe("getUsageForProvider(grok-cli)", () => { expect(usage.message).toMatch(/active.*numeric included quota/i); expect(usage.quotas).toEqual({}); }); + + it("does not throw when gRPC network fails after empty REST quotas", async () => { + proxyAwareFetch + .mockResolvedValueOnce(jsonResponse(EXHAUSTED_BILLING)) + .mockResolvedValueOnce( + jsonResponse({ + ...USER_PROFILE, + subscriptionTier: "XPremiumPlus", + }), + ) + .mockRejectedValueOnce(new Error("network down")); + + const usage = await getUsageForProvider({ + provider: "grok-cli", + accessToken: "test-token", + }); + + expect(usage.message).toMatch(/active.*numeric included quota/i); + expect(usage.quotas).toEqual({}); + }); }); describe("parseQuotaData(grok-cli)", () => {