mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 19:02:17 +03:00
feat(grok-cli): show and redeem banked reset credits on Provider Limits (#12805)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. A decodificação dos campos aninhados 10/20/30 do `GetRemainingResets` ao vivo (último commit) é o que separa isto de um palpite sobre o formato do frame. Mostrar zero em vez de esconder a linha é a escolha certa: crédito zerado é informação, ausência de linha é ambiguidade.
This commit is contained in:
1
changelog.d/fixes/grok-cli-reset-credits-readonly.md
Normal file
1
changelog.d/fixes/grok-cli-reset-credits-readonly.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(grok-cli):** Provider Limits shows grok-cli banked reset credits from `GetRemainingResets` (including a real zero; a failed RPC omits the row) and the existing View credits button now calls `ConsumerUiSvc/RedeemReset` for grok-cli. Live tokens use nested fields 10/20/30 (id + Timestamp), not compact 1/2/3.
|
||||
214
open-sse/services/grokResetCredits.ts
Normal file
214
open-sse/services/grokResetCredits.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* grokResetCredits.ts — live read / redeem of Grok reset cards.
|
||||
*
|
||||
* POST grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets with the
|
||||
* grok-cli OAuth bearer. Decode failure / HTTP miss returns null so callers
|
||||
* omit bankedResetCredits rather than faking a zero. A decoded empty DATA
|
||||
* frame with grpc-status 0 is a real zero and must be returned as count 0.
|
||||
*
|
||||
* Redeem: POST .../RedeemReset with ConsumerRedeemResetReq.token_id as
|
||||
* protobuf field 10 (live X500 2026-09-05: fake field-10 → grpc-status 9
|
||||
* "does not exist"; field 1 / empty → grpc-status 3 "Invalid token_id").
|
||||
* Token ids stay server-side and are never logged.
|
||||
*/
|
||||
import {
|
||||
decodeGrokGrpcWebRpc,
|
||||
decodeGrokResetCreditsFrame,
|
||||
encodeGrpcWebRequest,
|
||||
encodeRedeemResetRequest,
|
||||
type GrokResetCreditToken,
|
||||
type GrokResetCreditsSnapshot,
|
||||
} from "./grokResetCreditsFrame.ts";
|
||||
|
||||
const GROK_RESET_CREDITS_URL =
|
||||
"https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets";
|
||||
const GROK_REDEEM_RESET_URL = "https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset";
|
||||
const GRPC_WEB_EMPTY_REQUEST_FRAME = Buffer.from([0, 0, 0, 0, 0]);
|
||||
const FETCH_TIMEOUT_MS = 8_000;
|
||||
const REDEEM_TIMEOUT_MS = 15_000;
|
||||
|
||||
export type GrokResetCreditOutcome = "reset" | "alreadyRedeemed";
|
||||
export type GrokRedeemMappedStatus = GrokResetCreditOutcome | "noCredit";
|
||||
|
||||
export class GrokResetCreditError extends Error {
|
||||
status: number;
|
||||
code: string;
|
||||
|
||||
constructor(status: number, code: string, message: string) {
|
||||
super(message);
|
||||
this.name = "GrokResetCreditError";
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export type PublicGrokResetCredit = {
|
||||
selectionToken: string;
|
||||
expiresAt: string | null;
|
||||
};
|
||||
|
||||
function grokRpcHeaders(accessToken: string): Record<string, string> {
|
||||
return {
|
||||
Authorization: ["Bearer", accessToken].join(" "),
|
||||
"Content-Type": "application/grpc-web+proto",
|
||||
"X-Grpc-Web": "1",
|
||||
};
|
||||
}
|
||||
|
||||
function expirySortValue(expiresAt: string | null): number {
|
||||
if (!expiresAt) return Number.POSITIVE_INFINITY;
|
||||
const ms = Date.parse(expiresAt);
|
||||
return Number.isFinite(ms) ? ms : Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
function toPublicCredits(tokens: GrokResetCreditToken[]): PublicGrokResetCredit[] {
|
||||
return tokens
|
||||
.map((token, index) => ({ token, index }))
|
||||
.sort(
|
||||
(left, right) =>
|
||||
expirySortValue(left.token.expiresAt) - expirySortValue(right.token.expiresAt) ||
|
||||
left.index - right.index
|
||||
)
|
||||
.map(({ token }) => ({
|
||||
selectionToken: token.tokenId,
|
||||
expiresAt: token.expiresAt,
|
||||
}));
|
||||
}
|
||||
|
||||
async function postGrokRpc(
|
||||
url: string,
|
||||
accessToken: string,
|
||||
payload: Buffer,
|
||||
fetchImpl: typeof fetch,
|
||||
timeoutMs: number
|
||||
): Promise<{ grpcStatus: string; grpcMessage: string | null }> {
|
||||
const response = await fetchImpl(url, {
|
||||
method: "POST",
|
||||
headers: grokRpcHeaders(accessToken),
|
||||
body: new Uint8Array(encodeGrpcWebRequest(payload)),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new GrokResetCreditError(
|
||||
response.status,
|
||||
"grok_reset_credit_upstream_error",
|
||||
`Grok reset-credit API returned HTTP ${response.status}.`
|
||||
);
|
||||
}
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
return decodeGrokGrpcWebRpc(
|
||||
buffer,
|
||||
response.headers.get("grpc-status"),
|
||||
response.headers.get("grpc-message")
|
||||
);
|
||||
}
|
||||
|
||||
export function mapGrokRedeemGrpcStatus(
|
||||
grpcStatus: string,
|
||||
grpcMessage: string | null
|
||||
): GrokRedeemMappedStatus {
|
||||
if (grpcStatus === "0") return "reset";
|
||||
const message = (grpcMessage ?? "").toLowerCase();
|
||||
if (grpcStatus === "9") {
|
||||
return message.includes("already") ? "alreadyRedeemed" : "noCredit";
|
||||
}
|
||||
if (grpcStatus === "3" && message.includes("token_id")) {
|
||||
return "noCredit";
|
||||
}
|
||||
throw new GrokResetCreditError(
|
||||
502,
|
||||
"unknown_reset_credit_response",
|
||||
grpcMessage ? `Grok reset failed: ${grpcMessage}` : `Grok reset failed (grpc-status ${grpcStatus})`
|
||||
);
|
||||
}
|
||||
|
||||
function throwMappedRedeemStatus(outcome: GrokRedeemMappedStatus): GrokResetCreditOutcome {
|
||||
if (outcome === "noCredit") {
|
||||
throw new GrokResetCreditError(409, "no_credit", "No Grok reset credits are available.");
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
|
||||
export async function fetchGrokResetCredits(
|
||||
accessToken: string,
|
||||
fetchImpl: typeof fetch = fetch
|
||||
): Promise<GrokResetCreditsSnapshot | null> {
|
||||
if (!accessToken) return null;
|
||||
try {
|
||||
const response = await fetchImpl(GROK_RESET_CREDITS_URL, {
|
||||
method: "POST",
|
||||
headers: grokRpcHeaders(accessToken),
|
||||
body: GRPC_WEB_EMPTY_REQUEST_FRAME,
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const decoded = decodeGrokResetCreditsFrame(Buffer.from(await response.arrayBuffer()));
|
||||
return decoded.ok ? decoded.snapshot : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInventory(
|
||||
accessToken: string,
|
||||
fetchImpl: typeof fetch
|
||||
): Promise<{ credits: PublicGrokResetCredit[]; availableCount: number }> {
|
||||
const response = await fetchImpl(GROK_RESET_CREDITS_URL, {
|
||||
method: "POST",
|
||||
headers: grokRpcHeaders(accessToken),
|
||||
body: GRPC_WEB_EMPTY_REQUEST_FRAME,
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new GrokResetCreditError(
|
||||
response.status,
|
||||
"grok_reset_credit_upstream_error",
|
||||
`Grok remaining-resets returned HTTP ${response.status}.`
|
||||
);
|
||||
}
|
||||
const decoded = decodeGrokResetCreditsFrame(Buffer.from(await response.arrayBuffer()));
|
||||
if (!decoded.ok) {
|
||||
throw new GrokResetCreditError(502, "grok_reset_credit_decode_failed", "Grok remaining-resets decode failed.");
|
||||
}
|
||||
const credits = toPublicCredits(decoded.tokens);
|
||||
return { credits, availableCount: credits.length };
|
||||
}
|
||||
|
||||
export async function listGrokResetCreditTokens(
|
||||
accessToken: string,
|
||||
fetchImpl: typeof fetch = fetch
|
||||
): Promise<{ credits: PublicGrokResetCredit[]; availableCount: number }> {
|
||||
if (!accessToken) {
|
||||
throw new GrokResetCreditError(401, "grok_access_token_missing", "Grok OAuth access token is missing.");
|
||||
}
|
||||
return loadInventory(accessToken, fetchImpl);
|
||||
}
|
||||
|
||||
export async function consumeGrokResetCredit(
|
||||
accessToken: string,
|
||||
options: { tokenId?: string } = {},
|
||||
fetchImpl: typeof fetch = fetch
|
||||
): Promise<GrokResetCreditOutcome> {
|
||||
if (!accessToken) {
|
||||
throw new GrokResetCreditError(401, "grok_access_token_missing", "Grok OAuth access token is missing.");
|
||||
}
|
||||
|
||||
const requested = options.tokenId?.trim() ?? "";
|
||||
let selectedTokenId = requested;
|
||||
if (!selectedTokenId) {
|
||||
const inventory = await loadInventory(accessToken, fetchImpl);
|
||||
selectedTokenId = inventory.credits[0]?.selectionToken ?? "";
|
||||
}
|
||||
if (!selectedTokenId) {
|
||||
throw new GrokResetCreditError(409, "no_credit", "No Grok reset credits are available.");
|
||||
}
|
||||
|
||||
const rpc = await postGrokRpc(
|
||||
GROK_REDEEM_RESET_URL,
|
||||
accessToken,
|
||||
encodeRedeemResetRequest(selectedTokenId),
|
||||
fetchImpl,
|
||||
REDEEM_TIMEOUT_MS
|
||||
);
|
||||
return throwMappedRedeemStatus(mapGrokRedeemGrpcStatus(rpc.grpcStatus, rpc.grpcMessage));
|
||||
}
|
||||
346
open-sse/services/grokResetCreditsFrame.ts
Normal file
346
open-sse/services/grokResetCreditsFrame.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* grokResetCreditsFrame.ts — gRPC-web decoder for
|
||||
* `prod_mc_billing.ConsumerUiSvc/GetRemainingResets`.
|
||||
*
|
||||
* Live shape (X500, 2026-09-06 hotmail SuperGrokPro): empty DATA +
|
||||
* grpc-status 0 = inventory 0; otherwise repeated top-level field 10,
|
||||
* each a ConsumerResetToken. Nested numbers are 10 / 20 / 30, all
|
||||
* length-delimited (id 13B string, granted/expires google.protobuf.Timestamp
|
||||
* with seconds in field 1). Compact 1 / 2 / 3 (id bytes, granted/expires
|
||||
* varint unix seconds) is still accepted. Token ids stay server-side.
|
||||
*
|
||||
* Do not reuse grokCliQuotaFrame.decodeFields: that Map last-wins and
|
||||
* would collapse repeated field 10 to a single token.
|
||||
*/
|
||||
import { probeFrameHeader } from "./grokCliQuotaFrame.ts";
|
||||
|
||||
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;
|
||||
|
||||
const FIELD_RESET_TOKEN = 10;
|
||||
/** Compact 1/2/3 (varint seconds) and live 10/20/30 (id + Timestamp). */
|
||||
const TOKEN_NESTED_FIELDS = {
|
||||
id: [1, 10],
|
||||
granted: [2, 20],
|
||||
expires: [3, 30],
|
||||
} as const;
|
||||
const TIMESTAMP_FIELD_SECONDS = 1;
|
||||
const REDEEM_REQUEST_TOKEN_ID_FIELD = 10;
|
||||
|
||||
type ProtoField =
|
||||
| { wireType: typeof WIRE_TYPE_VARINT; value: number }
|
||||
| {
|
||||
wireType: typeof WIRE_TYPE_FIXED64 | typeof WIRE_TYPE_FIXED32 | typeof WIRE_TYPE_LENGTH_DELIMITED;
|
||||
bytes: Buffer;
|
||||
};
|
||||
|
||||
type TaggedField = { fieldNumber: number; field: ProtoField };
|
||||
|
||||
export type GrokResetCreditsSnapshot = {
|
||||
count: number;
|
||||
nextExpiresAt: string | null;
|
||||
};
|
||||
|
||||
export type GrokResetCreditToken = {
|
||||
tokenId: string;
|
||||
expiresAt: string | null;
|
||||
};
|
||||
|
||||
export type GrokResetCreditsDecode =
|
||||
| { ok: true; snapshot: GrokResetCreditsSnapshot; tokens: GrokResetCreditToken[] }
|
||||
| { ok: false; reason: "empty-buffer" | "no-data-frame" | "malformed" | "trailer-nonzero" };
|
||||
|
||||
function encodeVarint(value: number): Buffer {
|
||||
const bytes: number[] = [];
|
||||
let n = Math.floor(value);
|
||||
while (n > 0x7f) {
|
||||
bytes.push((n & 0x7f) | 0x80);
|
||||
n = Math.floor(n / 128);
|
||||
}
|
||||
bytes.push(n);
|
||||
return Buffer.from(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* ConsumerRedeemResetReq.token_id is field 10 (live X500 2026-09-05:
|
||||
* fake field-10 id → grpc-status 9 "does not exist"; field 1 / empty →
|
||||
* grpc-status 3 "Invalid token_id").
|
||||
*/
|
||||
export function encodeRedeemResetRequest(tokenId: string): Buffer {
|
||||
const body = Buffer.from(tokenId, "utf8");
|
||||
return Buffer.concat([
|
||||
encodeVarint((REDEEM_REQUEST_TOKEN_ID_FIELD << 3) | WIRE_TYPE_LENGTH_DELIMITED),
|
||||
encodeVarint(body.length),
|
||||
body,
|
||||
]);
|
||||
}
|
||||
|
||||
export function encodeGrpcWebRequest(payload: Buffer): Buffer {
|
||||
const header = Buffer.alloc(5);
|
||||
header.writeUInt32BE(payload.length, 1);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function readVarint(buffer: Buffer, offset: number): { value: number; next: number } | null {
|
||||
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 readField(buffer: Buffer, offset: number): { tagged: TaggedField; next: number } | null {
|
||||
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 {
|
||||
tagged: { fieldNumber, field: { wireType: WIRE_TYPE_VARINT, value: valueResult.value } },
|
||||
next: valueResult.next,
|
||||
};
|
||||
}
|
||||
if (wireType === WIRE_TYPE_LENGTH_DELIMITED) {
|
||||
const lengthResult = readVarint(buffer, tagResult.next);
|
||||
if (!lengthResult) return null;
|
||||
const { value: length, next: bodyStart } = lengthResult;
|
||||
if (length < 0 || bodyStart + length > buffer.length) return null;
|
||||
return {
|
||||
tagged: {
|
||||
fieldNumber,
|
||||
field: { wireType: WIRE_TYPE_LENGTH_DELIMITED, bytes: buffer.subarray(bodyStart, bodyStart + length) },
|
||||
},
|
||||
next: bodyStart + length,
|
||||
};
|
||||
}
|
||||
if (wireType === WIRE_TYPE_FIXED64) {
|
||||
if (tagResult.next + 8 > buffer.length) return null;
|
||||
return {
|
||||
tagged: {
|
||||
fieldNumber,
|
||||
field: { wireType: WIRE_TYPE_FIXED64, bytes: buffer.subarray(tagResult.next, tagResult.next + 8) },
|
||||
},
|
||||
next: tagResult.next + 8,
|
||||
};
|
||||
}
|
||||
if (wireType === WIRE_TYPE_FIXED32) {
|
||||
if (tagResult.next + 4 > buffer.length) return null;
|
||||
return {
|
||||
tagged: {
|
||||
fieldNumber,
|
||||
field: { wireType: WIRE_TYPE_FIXED32, bytes: buffer.subarray(tagResult.next, tagResult.next + 4) },
|
||||
},
|
||||
next: tagResult.next + 4,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function walkFields(buffer: Buffer): TaggedField[] | null {
|
||||
const fields: TaggedField[] = [];
|
||||
let offset = 0;
|
||||
while (offset < buffer.length) {
|
||||
const result = readField(buffer, offset);
|
||||
if (!result) return null;
|
||||
fields.push(result.tagged);
|
||||
offset = result.next;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function decodeTrailerMessage(raw: string | null): string | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return decodeURIComponent(raw.replace(/\+/g, " "));
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function parseTrailer(trailerBody: Buffer): { status: number | null; message: string | null } {
|
||||
const text = trailerBody.toString("utf8");
|
||||
const statusMatch = text.match(/grpc-status:\s*(\d+)/);
|
||||
const messageMatch = text.match(/grpc-message:\s*([^\r\n]+)/);
|
||||
return {
|
||||
status: statusMatch ? Number(statusMatch[1]) : null,
|
||||
message: decodeTrailerMessage(messageMatch?.[1] ?? null),
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeGrokGrpcWebRpc(
|
||||
buffer: Buffer,
|
||||
headerStatus?: string | null,
|
||||
headerMessage?: string | null
|
||||
): { grpcStatus: string; grpcMessage: string | null } {
|
||||
let trailerStatus: number | null = null;
|
||||
let trailerMessage: string | null = null;
|
||||
let offset = 0;
|
||||
while (offset < buffer.length) {
|
||||
const frame = probeFrameHeader(buffer, offset);
|
||||
if (!frame) break;
|
||||
const frameEnd = frame.payloadStart + frame.payloadLength;
|
||||
const body = buffer.subarray(frame.payloadStart, frameEnd);
|
||||
if ((frame.flag & GRPC_WEB_TRAILER_FLAG_BIT) !== 0) {
|
||||
const parsed = parseTrailer(body);
|
||||
if (parsed.status !== null) trailerStatus = parsed.status;
|
||||
if (parsed.message) trailerMessage = parsed.message;
|
||||
}
|
||||
offset = frameEnd;
|
||||
}
|
||||
const grpcStatus =
|
||||
trailerStatus !== null ? String(trailerStatus) : headerStatus && headerStatus.trim() ? headerStatus.trim() : "13";
|
||||
const grpcMessage = trailerMessage ?? decodeTrailerMessage(headerMessage ?? null);
|
||||
return { grpcStatus, grpcMessage };
|
||||
}
|
||||
|
||||
function splitFrames(buffer: Buffer): {
|
||||
dataPayload: Buffer | null;
|
||||
sawData: boolean;
|
||||
trailerStatus: number | null;
|
||||
} {
|
||||
let offset = 0;
|
||||
let dataPayload: Buffer | null = null;
|
||||
let sawData = false;
|
||||
let trailerStatus: number | null = null;
|
||||
|
||||
while (offset < buffer.length) {
|
||||
const frame = probeFrameHeader(buffer, offset);
|
||||
if (!frame) break;
|
||||
const frameEnd = frame.payloadStart + frame.payloadLength;
|
||||
const body = buffer.subarray(frame.payloadStart, frameEnd);
|
||||
if ((frame.flag & GRPC_WEB_TRAILER_FLAG_BIT) !== 0) {
|
||||
const status = parseTrailer(body).status;
|
||||
if (status !== null) trailerStatus = status;
|
||||
} else if (!sawData) {
|
||||
sawData = true;
|
||||
dataPayload = body;
|
||||
}
|
||||
offset = frameEnd;
|
||||
}
|
||||
|
||||
return { dataPayload, sawData, trailerStatus };
|
||||
}
|
||||
|
||||
function timestampSeconds(field: ProtoField): number | null {
|
||||
if (field.wireType === WIRE_TYPE_VARINT) {
|
||||
return Number.isFinite(field.value) ? field.value : null;
|
||||
}
|
||||
if (field.wireType !== WIRE_TYPE_LENGTH_DELIMITED) return null;
|
||||
const nested = walkFields(field.bytes);
|
||||
if (!nested) return null;
|
||||
const seconds = nested.find(
|
||||
(item) => item.fieldNumber === TIMESTAMP_FIELD_SECONDS && item.field.wireType === WIRE_TYPE_VARINT
|
||||
);
|
||||
if (!seconds || seconds.field.wireType !== WIRE_TYPE_VARINT) return null;
|
||||
return Number.isFinite(seconds.field.value) ? seconds.field.value : null;
|
||||
}
|
||||
|
||||
function hasFieldNumber(field: TaggedField, numbers: readonly number[]): boolean {
|
||||
return (numbers as readonly number[]).includes(field.fieldNumber);
|
||||
}
|
||||
|
||||
function tokenExpiresAtMs(tokenFields: TaggedField[]): number | null {
|
||||
const expires = tokenFields.find((field) => hasFieldNumber(field, TOKEN_NESTED_FIELDS.expires));
|
||||
if (!expires) return null;
|
||||
const seconds = timestampSeconds(expires.field);
|
||||
return seconds === null ? null : seconds * 1000;
|
||||
}
|
||||
|
||||
function tokenIdFromFields(tokenFields: TaggedField[]): string | null {
|
||||
const id = tokenFields.find(
|
||||
(field) =>
|
||||
hasFieldNumber(field, TOKEN_NESTED_FIELDS.id) &&
|
||||
field.field.wireType === WIRE_TYPE_LENGTH_DELIMITED
|
||||
);
|
||||
if (!id || id.field.wireType !== WIRE_TYPE_LENGTH_DELIMITED) return null;
|
||||
const tokenId = id.field.bytes.toString("utf8").trim();
|
||||
return tokenId.length > 0 ? tokenId : null;
|
||||
}
|
||||
|
||||
function inventoryFromPayload(
|
||||
payload: Buffer,
|
||||
nowMs: number
|
||||
): { snapshot: GrokResetCreditsSnapshot; tokens: GrokResetCreditToken[] } | null {
|
||||
if (payload.length === 0) {
|
||||
return { snapshot: { count: 0, nextExpiresAt: null }, tokens: [] };
|
||||
}
|
||||
|
||||
const top = walkFields(payload);
|
||||
if (!top) return null;
|
||||
|
||||
const tokens: GrokResetCreditToken[] = [];
|
||||
const expiresMs: number[] = [];
|
||||
|
||||
for (const tagged of top) {
|
||||
if (tagged.fieldNumber !== FIELD_RESET_TOKEN) continue;
|
||||
if (tagged.field.wireType !== WIRE_TYPE_LENGTH_DELIMITED) return null;
|
||||
const tokenFields = walkFields(tagged.field.bytes);
|
||||
if (!tokenFields) return null;
|
||||
const tokenId = tokenIdFromFields(tokenFields);
|
||||
if (!tokenId) return null;
|
||||
const expires = tokenExpiresAtMs(tokenFields);
|
||||
if (expires !== null && expires < nowMs) continue;
|
||||
tokens.push({
|
||||
tokenId,
|
||||
expiresAt: expires === null ? null : new Date(expires).toISOString(),
|
||||
});
|
||||
if (expires !== null) expiresMs.push(expires);
|
||||
}
|
||||
|
||||
const next = expiresMs.length > 0 ? Math.min(...expiresMs) : null;
|
||||
return {
|
||||
snapshot: {
|
||||
count: tokens.length,
|
||||
nextExpiresAt: next === null ? null : new Date(next).toISOString(),
|
||||
},
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeGrokResetCreditsFrame(
|
||||
buffer: Buffer,
|
||||
nowMs = Date.now()
|
||||
): GrokResetCreditsDecode {
|
||||
if (!buffer || buffer.length === 0) {
|
||||
return { ok: false, reason: "empty-buffer" };
|
||||
}
|
||||
|
||||
try {
|
||||
const framed = probeFrameHeader(buffer, 0) !== null;
|
||||
if (!framed) {
|
||||
const inventory = inventoryFromPayload(buffer, nowMs);
|
||||
if (!inventory) return { ok: false, reason: "malformed" };
|
||||
return { ok: true, ...inventory };
|
||||
}
|
||||
|
||||
const { dataPayload, sawData, trailerStatus } = splitFrames(buffer);
|
||||
if (trailerStatus !== null && trailerStatus !== 0) {
|
||||
return { ok: false, reason: "trailer-nonzero" };
|
||||
}
|
||||
if (!sawData || dataPayload === null) {
|
||||
return { ok: false, reason: "no-data-frame" };
|
||||
}
|
||||
|
||||
const inventory = inventoryFromPayload(dataPayload, nowMs);
|
||||
if (!inventory) return { ok: false, reason: "malformed" };
|
||||
return { ok: true, ...inventory };
|
||||
} catch {
|
||||
return { ok: false, reason: "malformed" };
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
GROK_BUILD_ADDITIONAL_CREDITS_URL,
|
||||
type GrokAutoTopUpStatus,
|
||||
} from "../../../src/shared/utils/grokBilling.ts";
|
||||
import { fetchGrokResetCredits } from "../grokResetCredits.ts";
|
||||
|
||||
const GROK_BUILD_FETCH_TIMEOUT_MS = 10_000;
|
||||
const GROK_BUILD_MAX_RESPONSE_BYTES = 256 * 1024;
|
||||
@@ -223,11 +224,13 @@ export async function getGrokCliUsage(accessToken?: string) {
|
||||
const user = await fetchGrokBuildJson("/user?include=subscription", baseHeaders, userSchema);
|
||||
const userId = user?.userId || null;
|
||||
const tier = user?.subscriptionTier || null;
|
||||
const billing = await fetchGrokBuildJson(
|
||||
"/billing?format=credits",
|
||||
userId ? getGrokBuildModelsHeaders({ token: accessToken, userId }) : baseHeaders,
|
||||
billingSchema
|
||||
);
|
||||
const billingHeaders = userId
|
||||
? getGrokBuildModelsHeaders({ token: accessToken, userId })
|
||||
: baseHeaders;
|
||||
const [billing, resetCredits] = await Promise.all([
|
||||
fetchGrokBuildJson("/billing?format=credits", billingHeaders, billingSchema),
|
||||
fetchGrokResetCredits(accessToken),
|
||||
]);
|
||||
|
||||
if (!billing?.config) {
|
||||
return {
|
||||
@@ -258,6 +261,7 @@ export async function getGrokCliUsage(accessToken?: string) {
|
||||
return {
|
||||
quotas,
|
||||
...(tier ? { plan: tier } : {}),
|
||||
...(resetCredits ? { bankedResetCredits: resetCredits.count } : {}),
|
||||
billing: {
|
||||
currency: "USD",
|
||||
...(config.prepaidBalance ? { extraCreditsMinorUnits: config.prepaidBalance.val } : {}),
|
||||
|
||||
@@ -147,7 +147,12 @@ function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
|
||||
}
|
||||
|
||||
function parseGeneric(data: any) {
|
||||
return quotaEntries(data).map(([name, quota]) => normalizeQuotaEntry(name, quota));
|
||||
const quotas = quotaEntries(data).map(([name, quota]) => normalizeQuotaEntry(name, quota));
|
||||
const bankedResetCredits = Number(data?.bankedResetCredits);
|
||||
if (Number.isFinite(bankedResetCredits) && bankedResetCredits >= 0) {
|
||||
quotas.push(buildBankedResetCreditsQuota(bankedResetCredits));
|
||||
}
|
||||
return quotas;
|
||||
}
|
||||
|
||||
function parseGithub(data: any) {
|
||||
|
||||
@@ -80,7 +80,12 @@ function useOpenCodexResetCredits(
|
||||
const notify = useNotificationStore();
|
||||
return useCallback(
|
||||
async (connectionId: string, provider: string) => {
|
||||
if (provider !== "codex" || loadingResetCreditsId || redeemingResetCreditId) return;
|
||||
if (
|
||||
(provider !== "codex" && provider !== "grok-cli") ||
|
||||
loadingResetCreditsId ||
|
||||
redeemingResetCreditId
|
||||
)
|
||||
return;
|
||||
setLoadingResetCreditsId(connectionId);
|
||||
setErrors((prev) => ({ ...prev, [connectionId]: null }));
|
||||
try {
|
||||
|
||||
@@ -339,7 +339,7 @@ export function computeCanEditCutoff(quotas: any[]): boolean {
|
||||
|
||||
export function computeCanRedeemResetCredit(provider: string, quotas: any[]): boolean {
|
||||
return (
|
||||
provider === "codex" &&
|
||||
(provider === "codex" || provider === "grok-cli") &&
|
||||
quotas.some((q: any) => q?.isResetCredits && Number(q.creditCount ?? q.remaining ?? 0) > 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
consumeCodexResetCredit,
|
||||
listCodexResetCredits,
|
||||
} from "@/lib/usage/codexResetCredits";
|
||||
import {
|
||||
GrokResetCreditError,
|
||||
consumeGrokResetCredit,
|
||||
listGrokResetCredits,
|
||||
} from "@/lib/usage/grokResetCredits";
|
||||
import { getProviderConnectionById } from "@/lib/db/providers";
|
||||
|
||||
const ConnectionIdSchema = z.string().trim().min(1).max(256);
|
||||
|
||||
@@ -16,17 +22,40 @@ const CodexResetCreditBodySchema = z.object({
|
||||
creditId: z.string().trim().min(1).max(512).optional(),
|
||||
});
|
||||
|
||||
function isResetCreditError(
|
||||
error: unknown
|
||||
): error is CodexResetCreditError | GrokResetCreditError {
|
||||
return error instanceof CodexResetCreditError || error instanceof GrokResetCreditError;
|
||||
}
|
||||
|
||||
function buildErrorResponse(error: unknown) {
|
||||
const status = error instanceof CodexResetCreditError ? error.status : 500;
|
||||
const code = error instanceof CodexResetCreditError ? error.code : "codex_reset_credit_failed";
|
||||
const message =
|
||||
error instanceof CodexResetCreditError
|
||||
? sanitizeErrorMessage(error.message) || "Codex reset-credit request failed."
|
||||
: "Codex reset-credit request failed.";
|
||||
const status = isResetCreditError(error) ? error.status : 500;
|
||||
const code = isResetCreditError(error) ? error.code : "reset_credit_failed";
|
||||
const message = isResetCreditError(error)
|
||||
? sanitizeErrorMessage(error.message) || "Reset-credit request failed."
|
||||
: "Reset-credit request failed.";
|
||||
console.error("[API] /api/usage/codex-reset-credit error:", error);
|
||||
return NextResponse.json({ ok: false, code, error: message }, { status });
|
||||
}
|
||||
|
||||
function unsupportedResetCreditProvider(provider: string | null) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
code: provider ? "unsupported_reset_credit_provider" : "connection_not_found",
|
||||
error: provider
|
||||
? "Reset credits are only available for Codex and Grok Build accounts."
|
||||
: "Connection not found.",
|
||||
},
|
||||
{ status: provider ? 400 : 404 }
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveResetCreditProvider(connectionId: string): Promise<string | null> {
|
||||
const connection = await getProviderConnectionById(connectionId);
|
||||
return connection && typeof connection.provider === "string" ? connection.provider : null;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
@@ -41,8 +70,16 @@ export async function GET(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const result = await listCodexResetCredits(parsed.data);
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
const provider = await resolveResetCreditProvider(parsed.data);
|
||||
if (provider === "grok-cli") {
|
||||
const result = await listGrokResetCredits(parsed.data);
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
}
|
||||
if (provider === "codex") {
|
||||
const result = await listCodexResetCredits(parsed.data);
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
}
|
||||
return unsupportedResetCreditProvider(provider);
|
||||
} catch (error) {
|
||||
return buildErrorResponse(error);
|
||||
}
|
||||
@@ -62,12 +99,24 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const result = await consumeCodexResetCredit(
|
||||
parsed.data.connectionId,
|
||||
parsed.data.idempotencyKey,
|
||||
parsed.data.creditId
|
||||
);
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
const provider = await resolveResetCreditProvider(parsed.data.connectionId);
|
||||
if (provider === "grok-cli") {
|
||||
const result = await consumeGrokResetCredit(
|
||||
parsed.data.connectionId,
|
||||
parsed.data.idempotencyKey,
|
||||
parsed.data.creditId
|
||||
);
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
}
|
||||
if (provider === "codex") {
|
||||
const result = await consumeCodexResetCredit(
|
||||
parsed.data.connectionId,
|
||||
parsed.data.idempotencyKey,
|
||||
parsed.data.creditId
|
||||
);
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
}
|
||||
return unsupportedResetCreditProvider(provider);
|
||||
} catch (error) {
|
||||
return buildErrorResponse(error);
|
||||
}
|
||||
|
||||
143
src/lib/usage/grokResetCredits.ts
Normal file
143
src/lib/usage/grokResetCredits.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { getProviderConnectionById } from "@/lib/db/providers";
|
||||
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
|
||||
import {
|
||||
fetchAndPersistProviderLimits,
|
||||
refreshAndUpdateCredentials,
|
||||
} from "@/lib/usage/providerLimits";
|
||||
import { invalidateGrokCliQuotaCache } from "@omniroute/open-sse/services/grokCliQuotaFetcher.ts";
|
||||
import {
|
||||
consumeGrokResetCredit as consumeGrokResetCreditRpc,
|
||||
GrokResetCreditError,
|
||||
listGrokResetCreditTokens,
|
||||
type GrokResetCreditOutcome,
|
||||
type PublicGrokResetCredit,
|
||||
} from "@omniroute/open-sse/services/grokResetCredits.ts";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
export { GrokResetCreditError };
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
type GrokConnectionLike = JsonRecord & {
|
||||
id: string;
|
||||
provider: string;
|
||||
authType?: string;
|
||||
accessToken?: string;
|
||||
};
|
||||
|
||||
export interface GrokResetCreditList {
|
||||
credits: PublicGrokResetCredit[];
|
||||
availableCount: number;
|
||||
}
|
||||
|
||||
async function loadGrokConnection(connectionId: string): Promise<GrokConnectionLike> {
|
||||
if (await isConnectionUnavailableToAuxiliaryActivity(connectionId)) {
|
||||
throw new GrokResetCreditError(
|
||||
409,
|
||||
"exclusive_lease_active",
|
||||
"Reset-credit operations are deferred while an exclusive lease is active."
|
||||
);
|
||||
}
|
||||
const connection = (await getProviderConnectionById(
|
||||
connectionId
|
||||
)) as unknown as GrokConnectionLike | null;
|
||||
|
||||
if (!connection) {
|
||||
throw new GrokResetCreditError(404, "connection_not_found", "Connection not found.");
|
||||
}
|
||||
|
||||
if (connection.provider !== "grok-cli") {
|
||||
throw new GrokResetCreditError(
|
||||
400,
|
||||
"grok_provider_required",
|
||||
"Reset credits can only be redeemed for Grok Build accounts."
|
||||
);
|
||||
}
|
||||
|
||||
if (connection.authType !== "oauth") {
|
||||
throw new GrokResetCreditError(
|
||||
400,
|
||||
"grok_oauth_required",
|
||||
"Grok reset credits require an OAuth connection."
|
||||
);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
async function refreshGrokConnectionIfNeeded(
|
||||
connection: GrokConnectionLike,
|
||||
force = false
|
||||
): Promise<GrokConnectionLike> {
|
||||
const refreshed = await refreshAndUpdateCredentials(connection, {
|
||||
allowRotatingRefresh: true,
|
||||
force,
|
||||
});
|
||||
return refreshed.connection as GrokConnectionLike;
|
||||
}
|
||||
|
||||
function requireAccessToken(connection: GrokConnectionLike): string {
|
||||
const token = typeof connection.accessToken === "string" ? connection.accessToken.trim() : "";
|
||||
if (!token) {
|
||||
throw new GrokResetCreditError(
|
||||
401,
|
||||
"grok_access_token_missing",
|
||||
"Grok OAuth access token is missing."
|
||||
);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function listGrokResetCredits(connectionId: string): Promise<GrokResetCreditList> {
|
||||
if (!connectionId || typeof connectionId !== "string") {
|
||||
throw new GrokResetCreditError(400, "connection_id_required", "connectionId is required.");
|
||||
}
|
||||
|
||||
try {
|
||||
let connection = await loadGrokConnection(connectionId);
|
||||
connection = await refreshGrokConnectionIfNeeded(connection);
|
||||
return await listGrokResetCreditTokens(requireAccessToken(connection));
|
||||
} catch (error) {
|
||||
if (error instanceof GrokResetCreditError) throw error;
|
||||
throw new GrokResetCreditError(
|
||||
500,
|
||||
"grok_reset_credit_list_failed",
|
||||
sanitizeErrorMessage(error) || "Failed to load Grok reset credits."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function consumeGrokResetCredit(
|
||||
connectionId: string,
|
||||
// RedeemReset has no idempotency field (live X500 2026-09-05). Kept so the
|
||||
// shared /api/usage/codex-reset-credit body schema stays one shape.
|
||||
_idempotencyKey: string,
|
||||
creditId?: string
|
||||
): Promise<{
|
||||
outcome: GrokResetCreditOutcome;
|
||||
usage: JsonRecord;
|
||||
}> {
|
||||
if (!connectionId || typeof connectionId !== "string") {
|
||||
throw new GrokResetCreditError(400, "connection_id_required", "connectionId is required.");
|
||||
}
|
||||
|
||||
try {
|
||||
let connection = await loadGrokConnection(connectionId);
|
||||
connection = await refreshGrokConnectionIfNeeded(connection);
|
||||
const outcome = await consumeGrokResetCreditRpc(requireAccessToken(connection), {
|
||||
tokenId: creditId,
|
||||
});
|
||||
invalidateGrokCliQuotaCache(connectionId);
|
||||
const refreshed = await fetchAndPersistProviderLimits(connectionId, "manual", {
|
||||
allowRotatingRefresh: true,
|
||||
});
|
||||
return { outcome, usage: refreshed.usage };
|
||||
} catch (error) {
|
||||
if (error instanceof GrokResetCreditError) throw error;
|
||||
throw new GrokResetCreditError(
|
||||
500,
|
||||
"grok_reset_credit_failed",
|
||||
sanitizeErrorMessage(error) || "Failed to redeem Grok reset credit."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,75 @@ function response(value: unknown, init: ResponseInit = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function grpcFrame(flag: number, payload: Buffer): Buffer {
|
||||
const header = Buffer.alloc(5);
|
||||
header[0] = flag;
|
||||
header.writeUInt32BE(payload.length, 1);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function emptyResetCreditsResponse(): Response {
|
||||
const trailer = Buffer.from("grpc-status:0\r\n", "utf8");
|
||||
return new Response(Buffer.concat([grpcFrame(0x00, Buffer.alloc(0)), grpcFrame(0x80, trailer)]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/grpc-web+proto" },
|
||||
});
|
||||
}
|
||||
|
||||
function encodeVarint(value: number): Buffer {
|
||||
const bytes: number[] = [];
|
||||
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: number, wireType: number): Buffer {
|
||||
return encodeVarint((fieldNumber << 3) | wireType);
|
||||
}
|
||||
|
||||
function encodeLengthDelimited(fieldNumber: number, body: Buffer): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 2), encodeVarint(body.length), body]);
|
||||
}
|
||||
|
||||
function encodeVarintField(fieldNumber: number, value: number): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 0), encodeVarint(value)]);
|
||||
}
|
||||
|
||||
function oneResetTokenResponse(): Response {
|
||||
const token = Buffer.concat([
|
||||
encodeLengthDelimited(1, Buffer.from("test-token-id", "utf8")),
|
||||
encodeVarintField(2, 1786560540),
|
||||
encodeVarintField(3, 1789238940),
|
||||
]);
|
||||
const payload = encodeLengthDelimited(10, token);
|
||||
const trailer = Buffer.from("grpc-status:0\r\n", "utf8");
|
||||
return new Response(Buffer.concat([grpcFrame(0x00, payload), grpcFrame(0x80, trailer)]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/grpc-web+proto" },
|
||||
});
|
||||
}
|
||||
|
||||
/** Live X500 hotmail shape: nested fields 10/20/30, timestamps length-delimited. */
|
||||
function liveResetTokenResponse(): Response {
|
||||
const timestamp = (unixSeconds: number) => encodeVarintField(1, unixSeconds);
|
||||
const token = Buffer.concat([
|
||||
encodeLengthDelimited(10, Buffer.from("test-token-id", "utf8")),
|
||||
encodeLengthDelimited(20, timestamp(1786560540)),
|
||||
encodeLengthDelimited(30, timestamp(1789238940)),
|
||||
]);
|
||||
const payload = encodeLengthDelimited(10, token);
|
||||
const trailer = Buffer.from("grpc-status:0\r\n", "utf8");
|
||||
return new Response(Buffer.concat([grpcFrame(0x00, payload), grpcFrame(0x80, trailer)]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/grpc-web+proto" },
|
||||
});
|
||||
}
|
||||
|
||||
function successFixtures(
|
||||
options: {
|
||||
tier?: unknown;
|
||||
@@ -99,6 +168,9 @@ function successFixtures(
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.includes("prod_mc_billing.ConsumerUiSvc/GetRemainingResets")) {
|
||||
return emptyResetCreditsResponse();
|
||||
}
|
||||
return new Response(null, { status: 404 });
|
||||
};
|
||||
}
|
||||
@@ -106,6 +178,7 @@ function successFixtures(
|
||||
interface UsageResult {
|
||||
plan?: string;
|
||||
message?: string;
|
||||
bankedResetCredits?: number;
|
||||
quotas?: Record<
|
||||
string,
|
||||
{
|
||||
@@ -190,15 +263,35 @@ test("grok-cli fetches the fixed read-only surfaces with the full Grok client pr
|
||||
additionalCreditsUrl: "https://grok.com/build?_s=usage",
|
||||
});
|
||||
|
||||
assert.equal(usage.bankedResetCredits, 0);
|
||||
|
||||
const jsonCalls = calls.filter(
|
||||
(call) => !call.url.includes("prod_mc_billing.ConsumerUiSvc/GetRemainingResets")
|
||||
);
|
||||
const resetCall = calls.find((call) =>
|
||||
call.url.includes("prod_mc_billing.ConsumerUiSvc/GetRemainingResets")
|
||||
);
|
||||
assert.ok(resetCall);
|
||||
assert.equal(resetCall.init.method, "POST");
|
||||
assert.equal(
|
||||
new Headers(resetCall.init.headers).get("content-type"),
|
||||
"application/grpc-web+proto"
|
||||
);
|
||||
assert.equal(new Headers(resetCall.init.headers).get("x-grpc-web"), "1");
|
||||
assert.equal(
|
||||
new Headers(resetCall.init.headers).get("authorization"),
|
||||
"Bearer fixture-access-token"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
calls.map((call) => call.url),
|
||||
jsonCalls.map((call) => call.url),
|
||||
[
|
||||
"https://cli-chat-proxy.grok.com/v1/user?include=subscription",
|
||||
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
|
||||
"https://cli-chat-proxy.grok.com/v1/auto-topup-rule",
|
||||
]
|
||||
);
|
||||
for (const { init } of calls) {
|
||||
for (const { init } of jsonCalls) {
|
||||
assert.equal(init.method, "GET");
|
||||
assert.equal(init.redirect, "error");
|
||||
assert.equal(init.body, undefined);
|
||||
@@ -212,8 +305,8 @@ test("grok-cli fetches the fixed read-only surfaces with the full Grok client pr
|
||||
assert.ok(headers.get("x-grok-client-identifier"));
|
||||
assert.equal(headers.get("x-grok-client-mode"), "headless");
|
||||
}
|
||||
assert.equal(new Headers(calls[0].init.headers).has("x-userid"), false);
|
||||
assert.equal(new Headers(calls[2].init.headers).get("x-userid"), "canonical-user-id");
|
||||
assert.equal(new Headers(jsonCalls[0].init.headers).has("x-userid"), false);
|
||||
assert.equal(new Headers(jsonCalls[2].init.headers).get("x-userid"), "canonical-user-id");
|
||||
assert.deepEqual(grokTesting.networkPolicy, {
|
||||
method: "GET",
|
||||
redirect: "error",
|
||||
@@ -549,6 +642,39 @@ test("SuperGrokPro explicit null creditUsagePercent still yields a weekly quota
|
||||
});
|
||||
});
|
||||
|
||||
test("grok-cli surfaces bankedResetCredits when GetRemainingResets returns one token", async () => {
|
||||
const fixtureFetch = successFixtures();
|
||||
const usage = await getUsage((async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url.includes("GetRemainingResets")) return oneResetTokenResponse();
|
||||
return fixtureFetch(input);
|
||||
}) as typeof fetch);
|
||||
assert.equal(usage.bankedResetCredits, 1);
|
||||
assert.ok(usage.quotas?.weekly);
|
||||
});
|
||||
|
||||
test("grok-cli surfaces bankedResetCredits for live nested 10/20/30 tokens", async () => {
|
||||
const fixtureFetch = successFixtures();
|
||||
const usage = await getUsage((async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url.includes("GetRemainingResets")) return liveResetTokenResponse();
|
||||
return fixtureFetch(input);
|
||||
}) as typeof fetch);
|
||||
assert.equal(usage.bankedResetCredits, 1);
|
||||
assert.ok(usage.quotas?.weekly);
|
||||
});
|
||||
|
||||
test("grok-cli omits bankedResetCredits when GetRemainingResets fails (fail-open)", async () => {
|
||||
const fixtureFetch = successFixtures();
|
||||
const usage = await getUsage((async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url.includes("GetRemainingResets")) return new Response("nope", { status: 404 });
|
||||
return fixtureFetch(input);
|
||||
}) as typeof fetch);
|
||||
assert.equal("bankedResetCredits" in usage, false);
|
||||
assert.ok(usage.quotas?.weekly);
|
||||
});
|
||||
|
||||
test("SuperGrokPro omitted currentPeriod still yields a weekly bar with null resetAt", async () => {
|
||||
const usage = await getUsage(
|
||||
successFixtures({
|
||||
|
||||
195
tests/unit/grok-reset-credits-connection.test.ts
Normal file
195
tests/unit/grok-reset-credits-connection.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-grok-reset-credits-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = "test-grok-reset-credits-secret";
|
||||
process.env.STORAGE_ENCRYPTION_KEY = "grok-reset-credits-test-key-32-bytes-min";
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const grokReset = await import("../../src/lib/usage/grokResetCredits.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const LIST_URL = "https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets";
|
||||
const REDEEM_URL = "https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset";
|
||||
const GRANTED = 1786560540;
|
||||
const EXPIRES = 1789238940;
|
||||
const TOKEN_ID = "test-token-id";
|
||||
|
||||
function encodeVarint(value: number): Buffer {
|
||||
const bytes: number[] = [];
|
||||
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: number, wireType: number): Buffer {
|
||||
return encodeVarint((fieldNumber << 3) | wireType);
|
||||
}
|
||||
|
||||
function encodeLengthDelimited(fieldNumber: number, body: Buffer): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 2), encodeVarint(body.length), body]);
|
||||
}
|
||||
|
||||
function encodeVarintField(fieldNumber: number, value: number): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 0), encodeVarint(value)]);
|
||||
}
|
||||
|
||||
function grpcFrame(flag: number, payload: Buffer): Buffer {
|
||||
const header = Buffer.alloc(5);
|
||||
header[0] = flag;
|
||||
header.writeUInt32BE(payload.length, 1);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function listResponse(tokens: Array<{ id: string; expires: number }>): Response {
|
||||
const payload = Buffer.concat(
|
||||
tokens.map((token) =>
|
||||
encodeLengthDelimited(
|
||||
10,
|
||||
Buffer.concat([
|
||||
encodeLengthDelimited(1, Buffer.from(token.id, "utf8")),
|
||||
encodeVarintField(2, GRANTED),
|
||||
encodeVarintField(3, token.expires),
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
const trailer = Buffer.from("grpc-status:0\r\n", "utf8");
|
||||
return new Response(Buffer.concat([grpcFrame(0x00, payload), grpcFrame(0x80, trailer)]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/grpc-web+proto" },
|
||||
});
|
||||
}
|
||||
|
||||
function trailerResponse(status: number, message?: string): Response {
|
||||
const lines = [`grpc-status:${status}\r\n`];
|
||||
if (message) lines.push(`grpc-message:${encodeURIComponent(message)}\r\n`);
|
||||
const trailer = Buffer.from(lines.join(""), "utf8");
|
||||
return new Response(Buffer.concat([grpcFrame(0x00, Buffer.alloc(0)), grpcFrame(0x80, trailer)]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/grpc-web+proto" },
|
||||
});
|
||||
}
|
||||
|
||||
function usageJsonResponse(): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
plan: "SuperGrok Heavy",
|
||||
quotas: { weekly: { used: 0, total: 100, remainingPercentage: 100 } },
|
||||
bankedResetCredits: 0,
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function createGrokConnection(overrides: Record<string, unknown> = {}) {
|
||||
return providersDb.createProviderConnection({
|
||||
provider: "grok-cli",
|
||||
authType: "oauth",
|
||||
name: `Grok Reset ${Date.now()} ${Math.random()}`,
|
||||
email: `grok-${Date.now()}-${Math.random()}@example.test`,
|
||||
accessToken: "grok-access-token",
|
||||
refreshToken: "grok-refresh-token",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
});
|
||||
|
||||
test("listGrokResetCredits returns public rows without logging token ids in usage", async () => {
|
||||
const connection = (await createGrokConnection()) as { id: string };
|
||||
globalThis.fetch = async (url) => {
|
||||
if (String(url) === LIST_URL) return listResponse([{ id: TOKEN_ID, expires: EXPIRES }]);
|
||||
return new Response("unexpected", { status: 500 });
|
||||
};
|
||||
|
||||
const result = await grokReset.listGrokResetCredits(connection.id);
|
||||
assert.equal(result.availableCount, 1);
|
||||
assert.equal(result.credits[0]?.selectionToken, TOKEN_ID);
|
||||
assert.equal(result.credits[0]?.expiresAt, new Date(EXPIRES * 1000).toISOString());
|
||||
});
|
||||
|
||||
test("consumeGrokResetCredit posts RedeemReset then refreshes usage", async () => {
|
||||
const connection = (await createGrokConnection()) as { id: string };
|
||||
const calls: string[] = [];
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push(String(url));
|
||||
if (String(url) === LIST_URL) return listResponse([{ id: TOKEN_ID, expires: EXPIRES }]);
|
||||
if (String(url) === REDEEM_URL) {
|
||||
const headers = init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, ["Bearer", "grok-access-token"].join(" "));
|
||||
return trailerResponse(0);
|
||||
}
|
||||
if (String(url).includes("/user?include=subscription")) {
|
||||
return new Response(JSON.stringify({ userId: "u1", subscriptionTier: "SuperGrok Heavy" }), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/billing?format=credits")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
config: {
|
||||
creditUsagePercent: 0,
|
||||
currentPeriod: { type: "WEEKLY", end: "2026-09-12T00:00:00.000Z" },
|
||||
},
|
||||
}),
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
if (String(url).includes("/auto-topup-rule")) {
|
||||
return new Response(JSON.stringify({}), { status: 200 });
|
||||
}
|
||||
return usageJsonResponse();
|
||||
};
|
||||
|
||||
const result = await grokReset.consumeGrokResetCredit(connection.id, "redeem-1", TOKEN_ID);
|
||||
assert.equal(result.outcome, "reset");
|
||||
assert.equal(calls.includes(REDEEM_URL), true);
|
||||
assert.equal(typeof result.usage, "object");
|
||||
});
|
||||
|
||||
test("consumeGrokResetCredit rejects non-grok connections", async () => {
|
||||
const connection = (await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
name: "Codex decoy",
|
||||
email: `codex-${Date.now()}@example.test`,
|
||||
accessToken: "codex-access-token",
|
||||
refreshToken: "codex-refresh-token",
|
||||
})) as { id: string };
|
||||
|
||||
await assert.rejects(
|
||||
() => grokReset.consumeGrokResetCredit(connection.id, "redeem-wrong"),
|
||||
(error: unknown) =>
|
||||
error instanceof grokReset.GrokResetCreditError &&
|
||||
error.status === 400 &&
|
||||
error.code === "grok_provider_required"
|
||||
);
|
||||
});
|
||||
191
tests/unit/grok-reset-credits-frame.test.ts
Normal file
191
tests/unit/grok-reset-credits-frame.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
decodeGrokResetCreditsFrame,
|
||||
encodeRedeemResetRequest,
|
||||
} from "../../open-sse/services/grokResetCreditsFrame.ts";
|
||||
|
||||
const GRANTED = 1786560540;
|
||||
const EXPIRES = 1789238940;
|
||||
const TOKEN_ID = "test-token-id"; // 13 bytes
|
||||
|
||||
function encodeVarint(value: number): Buffer {
|
||||
const bytes: number[] = [];
|
||||
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: number, wireType: number): Buffer {
|
||||
return encodeVarint((fieldNumber << 3) | wireType);
|
||||
}
|
||||
|
||||
function encodeLengthDelimited(fieldNumber: number, body: Buffer): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 2), encodeVarint(body.length), body]);
|
||||
}
|
||||
|
||||
function encodeVarintField(fieldNumber: number, value: number): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 0), encodeVarint(value)]);
|
||||
}
|
||||
|
||||
function encodeToken(id: string, granted: number, expires: number): Buffer {
|
||||
return Buffer.concat([
|
||||
encodeLengthDelimited(1, Buffer.from(id, "utf8")),
|
||||
encodeVarintField(2, granted),
|
||||
encodeVarintField(3, expires),
|
||||
]);
|
||||
}
|
||||
|
||||
/** google.protobuf.Timestamp with seconds only (nanos omitted). Live inner length is 6. */
|
||||
function encodeTimestampSeconds(unixSeconds: number): Buffer {
|
||||
return encodeVarintField(1, unixSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* X500 2026-09-06 hotmail SuperGrokPro GetRemainingResets DATA:
|
||||
* top field 10, nested fields 10/20/30 all length-delimited
|
||||
* (id 13B, granted Timestamp 6B, expires Timestamp 6B).
|
||||
*/
|
||||
function encodeLiveToken(id: string, granted: number, expires: number): Buffer {
|
||||
return Buffer.concat([
|
||||
encodeLengthDelimited(10, Buffer.from(id, "utf8")),
|
||||
encodeLengthDelimited(20, encodeTimestampSeconds(granted)),
|
||||
encodeLengthDelimited(30, encodeTimestampSeconds(expires)),
|
||||
]);
|
||||
}
|
||||
|
||||
function frameData(payload: Buffer): Buffer {
|
||||
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"): Buffer {
|
||||
const body = Buffer.from(statusText, "utf8");
|
||||
const header = Buffer.alloc(5);
|
||||
header[0] = 0x80;
|
||||
header.writeUInt32BE(body.length, 1);
|
||||
return Buffer.concat([header, body]);
|
||||
}
|
||||
|
||||
test("empty DATA frame + grpc-status 0 is a real zero inventory", () => {
|
||||
const buffer = Buffer.concat([frameData(Buffer.alloc(0)), frameTrailer()]);
|
||||
const decoded = decodeGrokResetCreditsFrame(buffer);
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.snapshot.count, 0);
|
||||
assert.equal(decoded.snapshot.nextExpiresAt, null);
|
||||
});
|
||||
|
||||
test("one unexpired field-10 token counts as 1", () => {
|
||||
const payload = encodeLengthDelimited(10, encodeToken(TOKEN_ID, GRANTED, EXPIRES));
|
||||
const decoded = decodeGrokResetCreditsFrame(Buffer.concat([frameData(payload), frameTrailer()]));
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.snapshot.count, 1);
|
||||
assert.equal(decoded.snapshot.nextExpiresAt, new Date(EXPIRES * 1000).toISOString());
|
||||
assert.equal(decoded.tokens.length, 1);
|
||||
assert.equal(decoded.tokens[0]?.tokenId, TOKEN_ID);
|
||||
assert.equal(decoded.tokens[0]?.expiresAt, new Date(EXPIRES * 1000).toISOString());
|
||||
});
|
||||
|
||||
test("repeated field-10 is not collapsed by a Map walker", () => {
|
||||
const a = encodeLengthDelimited(10, encodeToken("test-token-aa", GRANTED, EXPIRES));
|
||||
const b = encodeLengthDelimited(10, encodeToken("test-token-bb", GRANTED, EXPIRES + 86400));
|
||||
const decoded = decodeGrokResetCreditsFrame(
|
||||
Buffer.concat([frameData(Buffer.concat([a, b])), frameTrailer()])
|
||||
);
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.snapshot.count, 2);
|
||||
assert.equal(decoded.snapshot.nextExpiresAt, new Date(EXPIRES * 1000).toISOString());
|
||||
assert.deepEqual(
|
||||
decoded.tokens.map((token) => token.tokenId),
|
||||
["test-token-aa", "test-token-bb"]
|
||||
);
|
||||
});
|
||||
|
||||
test("expired tokens are dropped from the count", () => {
|
||||
const expired = encodeLengthDelimited(10, encodeToken("test-token-ex", GRANTED, 1_700_000_000));
|
||||
const live = encodeLengthDelimited(10, encodeToken(TOKEN_ID, GRANTED, EXPIRES));
|
||||
const decoded = decodeGrokResetCreditsFrame(
|
||||
Buffer.concat([frameData(Buffer.concat([expired, live])), frameTrailer()])
|
||||
);
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.snapshot.count, 1);
|
||||
assert.equal(decoded.snapshot.nextExpiresAt, new Date(EXPIRES * 1000).toISOString());
|
||||
});
|
||||
|
||||
test("nonzero grpc-status is not a zero inventory", () => {
|
||||
const decoded = decodeGrokResetCreditsFrame(
|
||||
Buffer.concat([frameData(Buffer.alloc(0)), frameTrailer("grpc-status:13\r\n")])
|
||||
);
|
||||
assert.equal(decoded.ok, false);
|
||||
if (decoded.ok) return;
|
||||
assert.equal(decoded.reason, "trailer-nonzero");
|
||||
});
|
||||
|
||||
test("trailer-only buffer is not a zero inventory", () => {
|
||||
const decoded = decodeGrokResetCreditsFrame(frameTrailer());
|
||||
assert.equal(decoded.ok, false);
|
||||
if (decoded.ok) return;
|
||||
assert.equal(decoded.reason, "no-data-frame");
|
||||
});
|
||||
|
||||
test("empty buffer is not a zero inventory", () => {
|
||||
const decoded = decodeGrokResetCreditsFrame(Buffer.alloc(0));
|
||||
assert.equal(decoded.ok, false);
|
||||
if (decoded.ok) return;
|
||||
assert.equal(decoded.reason, "empty-buffer");
|
||||
});
|
||||
|
||||
test("13-byte token id is a string, not a nested protobuf message", () => {
|
||||
const payload = encodeLengthDelimited(10, encodeToken(TOKEN_ID, GRANTED, EXPIRES));
|
||||
const decoded = decodeGrokResetCreditsFrame(Buffer.concat([frameData(payload), frameTrailer()]));
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.snapshot.count, 1);
|
||||
assert.equal(decoded.tokens[0]?.tokenId, TOKEN_ID);
|
||||
assert.equal(decoded.tokens[0]?.tokenId.length, 13);
|
||||
});
|
||||
|
||||
test("live nested fields 10/20/30 are not malformed", () => {
|
||||
const liveInner = encodeLiveToken(TOKEN_ID, GRANTED, EXPIRES);
|
||||
assert.equal(encodeTimestampSeconds(GRANTED).length, 6);
|
||||
assert.equal(encodeTimestampSeconds(EXPIRES).length, 6);
|
||||
const payload = encodeLengthDelimited(10, liveInner);
|
||||
const decoded = decodeGrokResetCreditsFrame(Buffer.concat([frameData(payload), frameTrailer()]));
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.snapshot.count, 1);
|
||||
assert.equal(decoded.tokens[0]?.tokenId, TOKEN_ID);
|
||||
assert.equal(decoded.tokens[0]?.expiresAt, new Date(EXPIRES * 1000).toISOString());
|
||||
});
|
||||
|
||||
test("live nested field-30 expiry still drops expired cards", () => {
|
||||
const expired = encodeLengthDelimited(10, encodeLiveToken("test-token-ex", GRANTED, 1_700_000_000));
|
||||
const live = encodeLengthDelimited(10, encodeLiveToken(TOKEN_ID, GRANTED, EXPIRES));
|
||||
const decoded = decodeGrokResetCreditsFrame(
|
||||
Buffer.concat([frameData(Buffer.concat([expired, live])), frameTrailer()])
|
||||
);
|
||||
assert.equal(decoded.ok, true);
|
||||
if (!decoded.ok) return;
|
||||
assert.equal(decoded.snapshot.count, 1);
|
||||
assert.equal(decoded.tokens[0]?.tokenId, TOKEN_ID);
|
||||
});
|
||||
|
||||
test("RedeemReset request encodes token_id as protobuf field 10", () => {
|
||||
const encoded = encodeRedeemResetRequest(TOKEN_ID);
|
||||
const tag = encoded[0];
|
||||
const length = encoded[1];
|
||||
assert.equal(tag, (10 << 3) | 2);
|
||||
assert.equal(length, TOKEN_ID.length);
|
||||
assert.equal(encoded.subarray(2).toString("utf8"), TOKEN_ID);
|
||||
});
|
||||
176
tests/unit/grok-reset-credits-redeem.test.ts
Normal file
176
tests/unit/grok-reset-credits-redeem.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
consumeGrokResetCredit,
|
||||
listGrokResetCreditTokens,
|
||||
mapGrokRedeemGrpcStatus,
|
||||
} from "../../open-sse/services/grokResetCredits.ts";
|
||||
import { encodeRedeemResetRequest } from "../../open-sse/services/grokResetCreditsFrame.ts";
|
||||
|
||||
const GRANTED = 1786560540;
|
||||
const EXPIRES = 1789238940;
|
||||
const TOKEN_ID = "test-token-id";
|
||||
const REDEEM_URL = "https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset";
|
||||
const LIST_URL = "https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets";
|
||||
|
||||
function encodeVarint(value: number): Buffer {
|
||||
const bytes: number[] = [];
|
||||
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: number, wireType: number): Buffer {
|
||||
return encodeVarint((fieldNumber << 3) | wireType);
|
||||
}
|
||||
|
||||
function encodeLengthDelimited(fieldNumber: number, body: Buffer): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 2), encodeVarint(body.length), body]);
|
||||
}
|
||||
|
||||
function encodeVarintField(fieldNumber: number, value: number): Buffer {
|
||||
return Buffer.concat([encodeTag(fieldNumber, 0), encodeVarint(value)]);
|
||||
}
|
||||
|
||||
function grpcFrame(flag: number, payload: Buffer): Buffer {
|
||||
const header = Buffer.alloc(5);
|
||||
header[0] = flag;
|
||||
header.writeUInt32BE(payload.length, 1);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function trailerResponse(status: number, message?: string): Response {
|
||||
const lines = [`grpc-status:${status}\r\n`];
|
||||
if (message) lines.push(`grpc-message:${encodeURIComponent(message)}\r\n`);
|
||||
const trailer = Buffer.from(lines.join(""), "utf8");
|
||||
return new Response(Buffer.concat([grpcFrame(0x00, Buffer.alloc(0)), grpcFrame(0x80, trailer)]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/grpc-web+proto" },
|
||||
});
|
||||
}
|
||||
|
||||
function listResponse(tokens: Array<{ id: string; expires: number }>): Response {
|
||||
const payload = Buffer.concat(
|
||||
tokens.map((token) =>
|
||||
encodeLengthDelimited(
|
||||
10,
|
||||
Buffer.concat([
|
||||
encodeLengthDelimited(1, Buffer.from(token.id, "utf8")),
|
||||
encodeVarintField(2, GRANTED),
|
||||
encodeVarintField(3, token.expires),
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
const trailer = Buffer.from("grpc-status:0\r\n", "utf8");
|
||||
return new Response(Buffer.concat([grpcFrame(0x00, payload), grpcFrame(0x80, trailer)]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/grpc-web+proto" },
|
||||
});
|
||||
}
|
||||
|
||||
test("mapGrokRedeemGrpcStatus treats grpc 0 as reset", () => {
|
||||
assert.equal(mapGrokRedeemGrpcStatus("0", null), "reset");
|
||||
});
|
||||
|
||||
test("mapGrokRedeemGrpcStatus treats missing token as noCredit", () => {
|
||||
assert.equal(
|
||||
mapGrokRedeemGrpcStatus("9", "The token cannot be redeemed: it does not exist or is expired"),
|
||||
"noCredit"
|
||||
);
|
||||
});
|
||||
|
||||
test("mapGrokRedeemGrpcStatus treats already-redeemed as alreadyRedeemed", () => {
|
||||
assert.equal(mapGrokRedeemGrpcStatus("9", "token already redeemed"), "alreadyRedeemed");
|
||||
});
|
||||
|
||||
test("mapGrokRedeemGrpcStatus treats invalid token_id as noCredit", () => {
|
||||
assert.equal(mapGrokRedeemGrpcStatus("3", "redeem_reset(), Invalid token_id"), "noCredit");
|
||||
});
|
||||
|
||||
test("listGrokResetCreditTokens returns public rows ordered by expiry", async () => {
|
||||
const listed = await listGrokResetCreditTokens("fixture-access-token", async (url) => {
|
||||
assert.equal(String(url), LIST_URL);
|
||||
return listResponse([
|
||||
{ id: "test-token-bb", expires: EXPIRES + 86400 },
|
||||
{ id: "test-token-aa", expires: EXPIRES },
|
||||
]);
|
||||
});
|
||||
assert.equal(listed.availableCount, 2);
|
||||
assert.deepEqual(
|
||||
listed.credits.map((credit) => credit.selectionToken),
|
||||
["test-token-aa", "test-token-bb"]
|
||||
);
|
||||
assert.equal(listed.credits[0]?.expiresAt, new Date(EXPIRES * 1000).toISOString());
|
||||
});
|
||||
|
||||
test("consumeGrokResetCredit posts RedeemReset with protobuf field 10 and skips inventory when tokenId is given", async () => {
|
||||
const calls: Array<{ url: string; init: RequestInit }> = [];
|
||||
const outcome = await consumeGrokResetCredit(
|
||||
"fixture-access-token",
|
||||
{ tokenId: TOKEN_ID },
|
||||
async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
if (String(url) === LIST_URL) return listResponse([{ id: TOKEN_ID, expires: EXPIRES }]);
|
||||
return trailerResponse(0);
|
||||
}
|
||||
);
|
||||
assert.equal(outcome, "reset");
|
||||
assert.equal(
|
||||
calls.some((call) => call.url === LIST_URL),
|
||||
false
|
||||
);
|
||||
const redeem = calls.find((call) => call.url === REDEEM_URL);
|
||||
assert.ok(redeem);
|
||||
assert.equal(redeem?.init.method, "POST");
|
||||
const body = Buffer.from(redeem?.init.body as Buffer);
|
||||
const payload = body.subarray(5);
|
||||
assert.deepEqual(payload, encodeRedeemResetRequest(TOKEN_ID));
|
||||
const headers = redeem?.init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, ["Bearer", "fixture-access-token"].join(" "));
|
||||
});
|
||||
|
||||
test("consumeGrokResetCredit picks the token that expires first when none is selected", async () => {
|
||||
const calls: Array<{ url: string; body: Buffer | null }> = [];
|
||||
const outcome = await consumeGrokResetCredit("fixture-access-token", {}, async (url, init = {}) => {
|
||||
const body = init.body ? Buffer.from(init.body as Buffer) : null;
|
||||
calls.push({ url: String(url), body });
|
||||
if (String(url) === LIST_URL) {
|
||||
return listResponse([
|
||||
{ id: "test-token-bb", expires: EXPIRES + 86400 },
|
||||
{ id: "test-token-aa", expires: EXPIRES },
|
||||
]);
|
||||
}
|
||||
return trailerResponse(0);
|
||||
});
|
||||
assert.equal(outcome, "reset");
|
||||
const redeem = calls.find((call) => call.url === REDEEM_URL);
|
||||
assert.ok(redeem?.body);
|
||||
const payload = redeem!.body!.subarray(5);
|
||||
assert.deepEqual(payload, encodeRedeemResetRequest("test-token-aa"));
|
||||
});
|
||||
|
||||
test("consumeGrokResetCredit maps a missing selected token via RedeemReset grpc-status 9", async () => {
|
||||
const { GrokResetCreditError } = await import("../../open-sse/services/grokResetCredits.ts");
|
||||
await assert.rejects(
|
||||
() =>
|
||||
consumeGrokResetCredit(
|
||||
"fixture-access-token",
|
||||
{ tokenId: "missing-token" },
|
||||
async (url) => {
|
||||
if (String(url) === LIST_URL) return listResponse([{ id: TOKEN_ID, expires: EXPIRES }]);
|
||||
return trailerResponse(
|
||||
9,
|
||||
"The token cannot be redeemed: it does not exist or is expired"
|
||||
);
|
||||
}
|
||||
),
|
||||
(error: unknown) =>
|
||||
error instanceof GrokResetCreditError && error.status === 409 && error.code === "no_credit"
|
||||
);
|
||||
});
|
||||
@@ -425,3 +425,38 @@ test("provider quota auto-refresh settings are accepted by the settings schema",
|
||||
|
||||
assert.equal(result.success, true);
|
||||
});
|
||||
|
||||
test("grok-cli banked reset credits parse as an integer reset-credit counter including zero", () => {
|
||||
const parsed = providerLimitUtils.parseQuotaData("grok-cli", {
|
||||
quotas: {
|
||||
weekly: { used: 37.25, total: 100, remainingPercentage: 62.75, isPercentageOnly: true },
|
||||
},
|
||||
bankedResetCredits: 0,
|
||||
}) as ParsedQuota[];
|
||||
const resetCredits = parsed.find((quota) => quota.name === "banked_reset_credits");
|
||||
assert.ok(resetCredits);
|
||||
assert.equal(resetCredits.isResetCredits, true);
|
||||
assert.equal(resetCredits.creditCount, 0);
|
||||
});
|
||||
|
||||
test("grok-cli omits the reset-credit row when bankedResetCredits is absent", () => {
|
||||
const parsed = providerLimitUtils.parseQuotaData("grok-cli", {
|
||||
quotas: {
|
||||
weekly: { used: 37.25, total: 100, remainingPercentage: 62.75 },
|
||||
},
|
||||
}) as ParsedQuota[];
|
||||
assert.equal(
|
||||
parsed.some((quota) => quota.name === "banked_reset_credits"),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("grok-cli exposes the redeem button when banked reset credits are present", () => {
|
||||
const parsed = providerLimitUtils.parseQuotaData("grok-cli", {
|
||||
quotas: { weekly: { used: 0, total: 100, remainingPercentage: 100 } },
|
||||
bankedResetCredits: 2,
|
||||
});
|
||||
assert.equal(providerLimitUtils.computeCanRedeemResetCredit("grok-cli", parsed), true);
|
||||
assert.equal(providerLimitUtils.computeCanRedeemResetCredit("codex", parsed), true);
|
||||
assert.equal(providerLimitUtils.computeCanRedeemResetCredit("claude", parsed), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user