fix(grok-cli): align Grok Build with current subscription protocol (#2590)

This commit is contained in:
ryanngit
2026-07-16 15:33:19 +07:00
committed by decolua
parent d6761c6fb0
commit 59b7828237
13 changed files with 839 additions and 84 deletions
+10
View File
@@ -0,0 +1,10 @@
export const GROK_CLI_VERSION = "0.2.99";
export const GROK_CLI_MODEL = "grok-build";
export const GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
export const GROK_CLI_CLIENT_IDENTIFIER = "grok-shell";
export const GROK_CLI_USER_AGENT = `grok-shell/${GROK_CLI_VERSION} (linux; x86_64)`;
export function supportsGrokCliReasoningEffort(model) {
// ponytail: unknown models omit effort until live metadata reaches dispatch.
return /^grok-4\.5(?:$|-)/.test(String(model || ""));
}
+194 -39
View File
@@ -7,6 +7,12 @@ import {
} from "../services/oauthCredentialManager.js";
import { normalizeResponsesInput } from "../translator/formats/responsesApi.js";
import { getModelUpstreamId } from "../config/providerModels.js";
import {
GROK_CLI_CLIENT_IDENTIFIER,
GROK_CLI_VERSION,
supportsGrokCliReasoningEffort,
} from "../config/grokCli.js";
import { MEMORY_CONFIG } from "../config/runtimeConfig.js";
import { resolveSessionId } from "../utils/sessionManager.js";
import { getConsistentMachineId } from "../shared/machineId.js";
@@ -45,10 +51,18 @@ const RESPONSES_API_ALLOWLIST = new Set([
"prompt_cache_key",
]);
const EFFORT_LEVELS = ["low", "medium", "high"];
const EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
const GROK_CLI_TURN_STORE_MAX = 5000;
const GROK_CLI_NATIVE_ITEM_ID = /^(?:rs|msg|fc)_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const GROK_CLI_FREEFORM_TOOL_PARAMETERS = {
type: "object",
properties: { input: { type: "string" } },
required: ["input"],
};
// Per-session last turn index so multi-turn headers never go backwards within this process
const sessionTurnStore = new Map();
let requestTurnStore = new WeakMap();
/**
* Count user turns in a Responses `input` array.
@@ -72,18 +86,138 @@ export function countGrokCliUserTurns(input) {
* Prefers user-message count from the payload (full history clients), but never
* decreases vs the last index observed for the same sessionId in this process.
*/
export function resolveGrokCliTurnIdx(sessionId, input) {
export function resolveGrokCliTurnIdx(sessionId, input, requestKey = null) {
const fromInput = countGrokCliUserTurns(input);
if (!sessionId) return fromInput;
const prev = sessionTurnStore.get(sessionId) || 0;
const turn = Math.max(fromInput, prev);
sessionTurnStore.set(sessionId, turn);
if (requestKey && requestTurnStore.has(requestKey)) {
return requestTurnStore.get(requestKey);
}
const now = Date.now();
const existing = sessionTurnStore.get(sessionId);
const prev = existing && now - existing.lastUsed <= MEMORY_CONFIG.sessionTtlMs
? existing.turn
: 0;
if (existing) sessionTurnStore.delete(sessionId);
// A new delta-style request advances the turn; retries reuse requestKey.
const turn = prev > 0 ? Math.max(fromInput, prev + (requestKey ? 1 : 0)) : fromInput;
while (sessionTurnStore.size >= GROK_CLI_TURN_STORE_MAX) {
sessionTurnStore.delete(sessionTurnStore.keys().next().value);
}
sessionTurnStore.set(sessionId, { turn, lastUsed: now });
if (requestKey) requestTurnStore.set(requestKey, turn);
return turn;
}
/** Test helper — clear in-memory turn counters */
export function _resetGrokCliTurnStore() {
sessionTurnStore.clear();
requestTurnStore = new WeakMap();
}
export function _getGrokCliTurnStoreSize() {
return sessionTurnStore.size;
}
export function normalizeGrokCliEffort(value) {
const effort = typeof value === "string" ? value.trim().toLowerCase() : "";
if (effort === "max") return "xhigh";
if (EFFORT_LEVELS.includes(effort)) return effort;
return "high";
}
export { supportsGrokCliReasoningEffort } from "../config/grokCli.js";
export function resolveGrokCliSessionId(credentials, body) {
// ponytail: clients without stable thread metadata share one connection session;
// split further when their wire format exposes a durable conversation id.
const explicitSessionBody = {
prompt_cache_key: body?.prompt_cache_key,
session_id: body?.session_id,
conversation_id: body?.conversation_id,
metadata: body?.metadata,
};
return resolveSessionId({
headers: credentials?.rawHeaders,
body: explicitSessionBody,
connectionId: credentials?.connectionId || credentials?.id,
workspaceId: credentials?.providerSpecificData?.workspaceId,
scope: "grok-cli",
});
}
function stringifyGrokCliToolOutput(output) {
if (typeof output === "string") return output;
if (output === undefined) return "";
return JSON.stringify(output);
}
function isNativeGrokCliItemId(id) {
return typeof id === "string" && GROK_CLI_NATIVE_ITEM_ID.test(id);
}
function normalizeGrokCliInputItem(item) {
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
const { internal_chat_message_metadata_passthrough: _metadata, ...clean } = item;
if (item.type === "reasoning") {
if (!isNativeGrokCliItemId(item.id) || typeof item.encrypted_content !== "string") return null;
return clean;
}
if (item.type === "custom_tool_call") {
const callId = item.call_id || item.id;
const name = typeof item.name === "string" ? item.name.trim() : "";
if (!callId || !name) return null;
return {
type: "function_call",
call_id: callId,
name,
arguments: JSON.stringify({ input: stringifyGrokCliToolOutput(item.input ?? item.arguments) }),
};
}
if (item.type === "custom_tool_call_output" || item.type === "function_call_output") {
const callId = item.call_id || item.id;
if (!callId) return null;
return {
type: "function_call_output",
call_id: callId,
output: stringifyGrokCliToolOutput(item.output),
};
}
if (item.type === "function_call") {
const callId = item.call_id || item.id;
const name = typeof item.name === "string" ? item.name.trim() : "";
if (!callId || !name) return null;
return {
type: "function_call",
...(isNativeGrokCliItemId(item.id) ? { id: item.id } : {}),
call_id: callId,
name,
arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {}),
...(typeof item.status === "string" ? { status: item.status } : {}),
};
}
return clean;
}
export function normalizeGrokCliInput(body) {
if (!Array.isArray(body?.input)) return body;
const normalized = body.input.map(normalizeGrokCliInputItem).filter(Boolean);
const callIds = new Set(
normalized
.filter((item) => item?.type === "function_call" && item.call_id)
.map((item) => item.call_id)
);
body.input = normalized.filter(
(item) => item?.type !== "function_call_output" || callIds.has(item.call_id)
);
return body;
}
function stripStoredItemReferences(body) {
@@ -92,7 +226,11 @@ function stripStoredItemReferences(body) {
if (typeof item === "string" && SERVER_ID_PATTERN.test(item)) return false;
if (item && typeof item === "object" && !Array.isArray(item)) {
if (item.type === "item_reference") return false;
if (typeof item.id === "string" && SERVER_ID_PATTERN.test(item.id)) delete item.id;
if (
typeof item.id === "string" &&
SERVER_ID_PATTERN.test(item.id) &&
!isNativeGrokCliItemId(item.id)
) delete item.id;
}
return true;
});
@@ -103,15 +241,23 @@ function stripStoredItemReferences(body) {
* Keep hosted tools (web_search / x_search) passthrough.
*/
function normalizeGrokCliTools(body) {
if (!Array.isArray(body.tools)) return;
if (!Array.isArray(body.tools) || body.tools.length === 0) {
delete body.tools;
delete body.tool_choice;
return;
}
const validNames = new Set();
const hostedTypes = new Set();
body.tools = body.tools.filter((tool) => {
if (!tool || typeof tool !== "object" || Array.isArray(tool)) return false;
const type = typeof tool.type === "string" ? tool.type : "";
if (type !== "function") {
// Hosted tools: { type: "web_search" } / { type: "x_search" }
if (HOSTED_TOOL_TYPES.has(type)) return true;
if (HOSTED_TOOL_TYPES.has(type)) {
hostedTypes.add(type);
return true;
}
// Nested function shape without type
if (!type && tool.function) {
// fall through to function flatten below
@@ -143,8 +289,9 @@ function normalizeGrokCliTools(body) {
: typeof fn?.description === "string"
? fn.description
: "";
const parameters =
tool.parameters && typeof tool.parameters === "object" && !Array.isArray(tool.parameters)
const parameters = type === "custom"
? GROK_CLI_FREEFORM_TOOL_PARAMETERS
: tool.parameters && typeof tool.parameters === "object" && !Array.isArray(tool.parameters)
? tool.parameters
: fn?.parameters && typeof fn.parameters === "object" && !Array.isArray(fn.parameters)
? fn.parameters
@@ -155,14 +302,25 @@ function normalizeGrokCliTools(body) {
tool.name = name.slice(0, 128);
if (description) tool.description = description;
tool.parameters = parameters;
validNames.add(name);
validNames.add(tool.name);
return true;
});
if (body.tools.length === 0) {
delete body.tools;
delete body.tool_choice;
return;
}
if (body.tool_choice && typeof body.tool_choice === "object" && !Array.isArray(body.tool_choice)) {
if (body.tool_choice.type === "function") {
const n = typeof body.tool_choice.name === "string" ? body.tool_choice.name.trim() : "";
if (!n || !validNames.has(n)) delete body.tool_choice;
const choiceType = typeof body.tool_choice.type === "string" ? body.tool_choice.type : "";
if (choiceType === "function" || choiceType === "custom") {
const rawName = body.tool_choice.name ?? body.tool_choice.function?.name;
const name = typeof rawName === "string" ? rawName.trim().slice(0, 128) : "";
if (!name || !validNames.has(name)) delete body.tool_choice;
else body.tool_choice = { type: "function", name };
} else if (!hostedTypes.has(choiceType)) {
delete body.tool_choice;
}
}
}
@@ -192,9 +350,9 @@ export class GrokCliExecutor extends BaseExecutor {
return this.config.baseUrl;
}
async refreshCredentials(credentials, log) {
async refreshCredentials(credentials, log, proxyOptions = null) {
if (!credentials?.refreshToken) return null;
return refreshProviderCredentials("grok-cli", credentials, log);
return refreshProviderCredentials("grok-cli", credentials, log, proxyOptions);
}
needsRefresh(credentials) {
@@ -210,13 +368,10 @@ export class GrokCliExecutor extends BaseExecutor {
if (v != null && headers[k] === undefined) headers[k] = v;
}
// Ensure token-auth marker is present even if headers map was overridden
headers["x-xai-token-auth"] = this.config.tokenAuth || "xai-grok-cli";
headers["x-grok-client-identifier"] =
this.config.clientIdentifier || headers["x-grok-client-identifier"] || "grok-pager";
this.config.clientIdentifier || headers["x-grok-client-identifier"] || GROK_CLI_CLIENT_IDENTIFIER;
headers["x-grok-client-version"] =
this.config.clientVersion || headers["x-grok-client-version"] || "0.2.93";
headers["x-authenticateresponse"] = "authenticate-response";
this.config.clientVersion || headers["x-grok-client-version"] || GROK_CLI_VERSION;
const sessionId = this._currentSessionId || credentials?.connectionId || crypto.randomUUID();
const reqId = this._currentReqId || crypto.randomUUID();
@@ -231,10 +386,6 @@ export class GrokCliExecutor extends BaseExecutor {
// Surface model override (CLI always sets this)
if (this._currentModel) headers["x-grok-model-override"] = this._currentModel;
if (this.config.compactionAt) {
headers["x-compaction-at"] = String(this.config.compactionAt);
}
// Identity: mapTokens stores email top-level AND in providerSpecificData;
// fall back either way so OAuth connections always fingerprint like the CLI.
const psd = credentials?.providerSpecificData || {};
@@ -267,13 +418,8 @@ export class GrokCliExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
// Session / request ids for headers — stable per client conversation when possible
this._currentSessionId = resolveSessionId({
headers: credentials?.rawHeaders,
body,
connectionId: credentials?.connectionId || credentials?.id,
workspaceId: credentials?.providerSpecificData?.workspaceId,
scope: "grok-cli",
});
const requestKey = body;
this._currentSessionId = resolveGrokCliSessionId(credentials, body);
this._currentReqId = crypto.randomUUID();
this._agentId =
credentials?.providerSpecificData?.deviceId ||
@@ -302,11 +448,12 @@ export class GrokCliExecutor extends BaseExecutor {
// Keep role:"system" as-is — official grok-pager HAR sends system, not developer
// (Codex converts system→developer; Grok CLI does not).
normalizeGrokCliInput(body);
stripStoredItemReferences(body);
normalizeGrokCliTools(body);
// Turn index after input is finalized (user-message count, monotonic per session)
this._currentTurnIdx = resolveGrokCliTurnIdx(this._currentSessionId, body.input);
this._currentTurnIdx = resolveGrokCliTurnIdx(this._currentSessionId, body.input, requestKey);
body.stream = true;
body.store = false;
@@ -325,20 +472,28 @@ export class GrokCliExecutor extends BaseExecutor {
body.model = resolvedModel;
this._currentModel = resolvedModel;
// Reasoning effort priority: explicit > reasoning_effort > model suffix > default high
// Reasoning effort priority: explicit > reasoning_effort > model suffix > default high.
// grok-build and Composer reject reasoningEffort but still accept summary/encrypted continuity.
const supportsReasoningEffort = supportsGrokCliReasoningEffort(resolvedModel);
if (!body.reasoning || typeof body.reasoning !== "object") {
const effort = body.reasoning_effort || modelEffort || "high";
body.reasoning = { effort, summary: "concise" };
body.reasoning = { summary: "concise" };
if (supportsReasoningEffort) {
body.reasoning.effort = normalizeGrokCliEffort(body.reasoning_effort || modelEffort);
}
} else {
if (!body.reasoning.effort) {
body.reasoning.effort = body.reasoning_effort || modelEffort || "high";
if (supportsReasoningEffort) {
body.reasoning.effort = normalizeGrokCliEffort(
body.reasoning.effort || body.reasoning_effort || modelEffort,
);
} else {
delete body.reasoning.effort;
}
if (!body.reasoning.summary) body.reasoning.summary = "concise";
}
delete body.reasoning_effort;
// Encrypted reasoning for multi-turn continuity (CLI always requests this)
if (body.reasoning?.effort && body.reasoning.effort !== "none") {
if (body.reasoning && body.reasoning.effort !== "none") {
const include = Array.isArray(body.include) ? body.include : [];
if (!include.includes("reasoning.encrypted_content")) {
include.push("reasoning.encrypted_content");
+3 -1
View File
@@ -13,6 +13,7 @@ import { HTTP_STATUS, TOKEN_SAVER_HEADER } from "../config/runtimeConfig.js";
import { handleBypassRequest } from "../utils/bypassHandler.js";
import { trackPendingRequest, appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js";
import { getExecutor } from "../executors/index.js";
import { supportsGrokCliReasoningEffort } from "../config/grokCli.js";
import { buildRequestDetail, extractRequestConfig } from "./chatCore/requestDetail.js";
import { handleForcedSSEToJson } from "./chatCore/sseToJsonHandler.js";
import { handleNonStreamingResponse } from "./chatCore/nonStreamingHandler.js";
@@ -168,7 +169,8 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const msgN = translatedBody.messages?.length || translatedBody.input?.length || translatedBody.contents?.length || body.messages?.length || body.input?.length || 0;
const toolN = translatedBody.tools?.length || body.tools?.length || 0;
const fmtStr = passthrough ? `FMT: ${sourceFormat} (passthrough)` : `FMT: ${sourceFormat}${targetFormat}`;
const think = log.fmtThink?.(extractThinking(translatedBody));
const showThinking = provider !== "grok-cli" || supportsGrokCliReasoningEffort(model);
const think = showThinking ? log.fmtThink?.(extractThinking(translatedBody)) : null;
const acc = credentials?.connectionName || credentials?.connectionId?.slice(0, 8) || "-";
const parts = [
`POST ${clientModel}${provider}/${model}`,
+28 -18
View File
@@ -1,13 +1,21 @@
/**
* Grok CLI / Grok Build (cli-chat-proxy.grok.com)
*
* Source of truth: HAR capture of official grok-shell/grok-pager 0.2.93
* Source of truth: wire capture of official @xai-official/grok 0.2.99
* talking to https://cli-chat-proxy.grok.com (OpenAI Responses API).
*
* Distinct from:
* - `xai` → api.x.ai (API key / Grok Build OAuth PKCE)
* - `xai` → api.x.ai (API key / xAI API OAuth PKCE)
* - `grok-web` → grok.com web SSO cookie
*/
import {
GROK_CLI_BASE_URL,
GROK_CLI_CLIENT_IDENTIFIER,
GROK_CLI_MODEL,
GROK_CLI_USER_AGENT,
GROK_CLI_VERSION,
} from "../../config/grokCli.js";
export default {
id: "grok-cli",
priority: 275,
@@ -29,32 +37,28 @@ export default {
authModes: ["oauth"],
hasOAuth: true,
thinkingConfig: {
options: ["low", "medium", "high"],
options: ["low", "medium", "high", "xhigh"],
defaultMode: "high",
},
transport: {
baseUrl: "https://cli-chat-proxy.grok.com/v1/responses",
baseUrl: `${GROK_CLI_BASE_URL}/responses`,
format: "openai-responses",
forceStream: true,
modelsUrl: "https://cli-chat-proxy.grok.com/v1/models",
userUrl: "https://cli-chat-proxy.grok.com/v1/user",
billingUrl: "https://cli-chat-proxy.grok.com/v1/billing",
clientVersion: "0.2.93",
clientIdentifier: "grok-pager",
modelsUrl: `${GROK_CLI_BASE_URL}/models`,
userUrl: `${GROK_CLI_BASE_URL}/user`,
billingUrl: `${GROK_CLI_BASE_URL}/billing`,
clientVersion: GROK_CLI_VERSION,
clientIdentifier: GROK_CLI_CLIENT_IDENTIFIER,
tokenAuth: "xai-grok-cli",
headers: {
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
"x-xai-token-auth": "xai-grok-cli",
"x-grok-client-identifier": "grok-pager",
"x-grok-client-version": "0.2.93",
"x-authenticateresponse": "authenticate-response",
"User-Agent": GROK_CLI_USER_AGENT,
"x-grok-client-identifier": GROK_CLI_CLIENT_IDENTIFIER,
"x-grok-client-version": GROK_CLI_VERSION,
},
// Compaction threshold mirrored from CLI (x-compaction-at)
compactionAt: 400000,
// Quota tracker: official CLI polls billing?format=credits + user?include=subscription
usage: {
url: "https://cli-chat-proxy.grok.com/v1/billing?format=credits",
userUrl: "https://cli-chat-proxy.grok.com/v1/user?include=subscription",
url: `${GROK_CLI_BASE_URL}/billing?format=credits`,
userUrl: `${GROK_CLI_BASE_URL}/user?include=subscription`,
},
retry: {
429: { attempts: 2, delayMs: 2000 },
@@ -63,6 +67,12 @@ export default {
},
},
models: [
{
id: GROK_CLI_MODEL,
name: "Grok Build",
contextLength: 500000,
maxOutputTokens: 64000,
},
{ id: "grok-4.5", name: "Grok 4.5" },
{ id: "grok-4.5-high", name: "Grok 4.5 (High)", upstreamModelId: "grok-4.5" },
{ id: "grok-4.5-medium", name: "Grok 4.5 (Medium)", upstreamModelId: "grok-4.5" },
+127
View File
@@ -0,0 +1,127 @@
import {
GROK_CLI_BASE_URL,
GROK_CLI_CLIENT_IDENTIFIER,
GROK_CLI_MODEL,
GROK_CLI_USER_AGENT,
GROK_CLI_VERSION,
} from "../config/grokCli.js";
import { refreshProviderCredentials } from "./oauthCredentialManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
const MODELS_URL = `${GROK_CLI_BASE_URL}/models`;
function modelEntries(data) {
const value = Array.isArray(data) ? data : data?.data ?? data?.models ?? data?.results ?? [];
if (Array.isArray(value)) return value.map((item) => [null, item]);
if (value && typeof value === "object") return Object.entries(value);
return [];
}
export function parseGrokCliModels(data) {
const seen = new Set();
const models = [];
for (const [key, raw] of modelEntries(data)) {
const item = typeof raw === "string" ? { id: raw } : raw;
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const id = String(
item.id ?? item.model_id ?? item.modelId ?? item.model ?? item.slug ?? key ?? item.name ?? "",
).trim();
if (!id || seen.has(id)) continue;
seen.add(id);
const model = {
...item,
id,
name: item.display_name ?? item.displayName ?? item.name ?? id,
};
const contextLength = Number(
item.context_length ?? item.contextLength ?? item.context_window ?? item.contextWindow,
);
const maxOutputTokens = Number(item.max_output_tokens ?? item.maxOutputTokens);
if (Number.isFinite(contextLength) && contextLength > 0) model.contextLength = contextLength;
if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0) {
model.maxOutputTokens = maxOutputTokens;
}
if (id === GROK_CLI_MODEL) {
model.contextLength ||= 500000;
model.maxOutputTokens ||= 64000;
}
models.push(model);
}
return models;
}
function buildHeaders(accessToken, providerSpecificData = {}) {
const headers = {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"User-Agent": GROK_CLI_USER_AGENT,
"x-xai-token-auth": "xai-grok-cli",
"x-grok-client-version": GROK_CLI_VERSION,
"x-grok-client-identifier": GROK_CLI_CLIENT_IDENTIFIER,
"x-grok-client-mode": "headless",
};
const email = providerSpecificData?.email;
const userId = providerSpecificData?.userId || providerSpecificData?.principalId;
if (email) headers["x-email"] = email;
if (userId) headers["x-userid"] = userId;
return headers;
}
export async function resolveGrokCliModels(credentials, options = {}) {
const {
fetchFn = proxyAwareFetch,
log = console,
proxyOptions = null,
onCredentialsRefreshed,
} = options;
let accessToken = credentials?.accessToken;
if (!accessToken) return { models: [], warning: "Grok CLI access token is missing." };
const request = (token) => fetchFn(
MODELS_URL,
{
method: "GET",
headers: buildHeaders(token, credentials?.providerSpecificData),
},
proxyOptions,
);
try {
let response = await request(accessToken);
if ((response.status === 401 || response.status === 403) && credentials?.refreshToken) {
const refreshed = await refreshProviderCredentials(
"grok-cli",
credentials,
log,
proxyOptions,
);
if (refreshed?.accessToken) {
accessToken = refreshed.accessToken;
try {
await onCredentialsRefreshed?.(refreshed);
} catch (error) {
log?.warn?.("Grok CLI credential persistence failed", error);
}
response = await request(accessToken);
}
}
if (!response.ok) {
const detail = await response.text().catch(() => "");
return {
models: [],
warning: `Grok CLI model discovery failed (${response.status})${detail ? `: ${detail.slice(0, 160)}` : ""}`,
};
}
const models = parseGrokCliModels(await response.json());
return models.length
? { models }
: { models: [], warning: "Grok CLI returned no selectable models." };
} catch (error) {
return { models: [], warning: `Grok CLI model discovery failed: ${error.message}` };
}
}
+7 -1
View File
@@ -17,6 +17,10 @@ for (const entry of REGISTRY) {
for (const a of entry.aliases || []) ALIAS_TO_PROVIDER_ID[a] = entry.id;
}
const BUILTIN_MODEL_ALIASES = {
"grok-build": "gcli/grok-build",
};
/**
* Resolve provider alias to provider ID
*/
@@ -104,7 +108,9 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
: aliasesOrGetter;
// Resolve alias
const resolved = resolveModelAliasFromMap(parsed.model, aliases);
const resolved =
resolveModelAliasFromMap(parsed.model, aliases) ||
resolveModelAliasFromMap(parsed.model, BUILTIN_MODEL_ALIASES);
if (resolved) {
return resolved;
}
+61 -7
View File
@@ -24,6 +24,11 @@
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { U, parseResetTime, toFiniteNumber } from "./shared.js";
import {
GROK_CLI_CLIENT_IDENTIFIER,
GROK_CLI_USER_AGENT,
GROK_CLI_VERSION,
} from "../../config/grokCli.js";
const USAGE = U("grok-cli");
const BILLING_URL = USAGE.url || "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
@@ -43,10 +48,11 @@ function buildGrokCliHeaders(accessToken, providerSpecificData = {}) {
const headers = {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
"User-Agent": GROK_CLI_USER_AGENT,
"x-xai-token-auth": "xai-grok-cli",
"x-grok-client-identifier": "grok-pager",
"x-grok-client-version": "0.2.93",
"x-grok-client-identifier": GROK_CLI_CLIENT_IDENTIFIER,
"x-grok-client-version": GROK_CLI_VERSION,
"x-grok-client-mode": "headless",
};
const email = psd.email;
const userId = psd.userId || psd.principalId;
@@ -55,8 +61,18 @@ function buildGrokCliHeaders(accessToken, providerSpecificData = {}) {
return headers;
}
function subscriptionTier(user, config) {
const rawTier =
user?.subscriptionTier ??
user?.subscription_tier ??
user?.subscription?.tier ??
config?.subscriptionTier ??
config?.subscription_tier;
return typeof rawTier === "string" ? rawTier.trim() : "";
}
function resolvePlan(user, config) {
const tier = typeof user?.subscriptionTier === "string" ? user.subscriptionTier.trim() : "";
const tier = subscriptionTier(user, config);
if (tier) {
return tier
.replace(/[_-]+/g, " ")
@@ -105,11 +121,42 @@ export function parseGrokCliBilling(billing, user = null) {
const periodEnd =
parseResetTime(config.billingPeriodEnd) ||
parseResetTime(config.billing_period_end) ||
parseResetTime(config.currentPeriod?.end) ||
parseResetTime(config.resetAt || config.resetsAt || config.periodEnd) ||
parseResetTime(root.billingPeriodEnd) ||
parseResetTime(root.billing_period_end) ||
parseResetTime(root.resetAt || root.resetsAt || root.periodEnd) ||
null;
const quotas = {};
const tier = subscriptionTier(user, config);
const subscriptionAccess = Boolean(tier) && !/^(free|none|null)$/i.test(tier);
// Current Grok Build responses expose included monthly usage at top level.
const monthlyLimit = unwrapVal(
config.monthlyLimit ?? config.monthly_limit ?? root.monthlyLimit ?? root.monthly_limit,
NaN,
);
const includedUsed = unwrapVal(
config.includedUsed ?? config.included_used ?? root.includedUsed ?? root.included_used,
NaN,
);
const totalUsed = unwrapVal(
config.totalUsed ?? config.total_used ?? root.totalUsed ?? root.total_used,
NaN,
);
if (Number.isFinite(monthlyLimit) && monthlyLimit > 0) {
quotas["Monthly included"] = makeQuota({
used: Number.isFinite(includedUsed)
? includedUsed
: Number.isFinite(totalUsed)
? totalUsed
: 0,
total: monthlyLimit,
resetAt: periodEnd,
});
}
// Primary: on-demand spending window (subscription / promo credits)
const onDemandCap = unwrapVal(config.onDemandCap ?? root.onDemandCap, NaN);
@@ -121,7 +168,12 @@ export function parseGrokCliBilling(billing, user = null) {
total: onDemandCap,
resetAt: periodEnd,
});
} else if (Number.isFinite(onDemandCap) && onDemandCap === 0 && Number.isFinite(onDemandUsed)) {
} else if (
!subscriptionAccess &&
Number.isFinite(onDemandCap) &&
onDemandCap === 0 &&
Number.isFinite(onDemandUsed)
) {
// Cap 0 is the exhausted free/promo state (chat returns 402 spending-limit).
// UI treats total===0 as unlimited, so use a synthetic 1/1 depleted row.
quotas["On-demand"] = {
@@ -199,6 +251,7 @@ export function parseGrokCliBilling(billing, user = null) {
quotas,
periodEnd,
exhausted,
subscriptionAccess,
rawConfig: config,
};
}
@@ -255,8 +308,9 @@ export async function getGrokCliUsage(accessToken, providerSpecificData = null,
if (!parsed.quotas || Object.keys(parsed.quotas).length === 0) {
return {
plan: parsed.plan,
message:
"Grok Build connected, but no credit allotment was returned. Free promo may be exhausted — upgrade at https://grok.com/supergrok or add credits at https://grok.com/?_s=usage.",
message: parsed.subscriptionAccess
? "Subscription access is active; Grok does not expose a numeric included quota."
: "Grok Build connected, but no credit allotment was returned. Free promo may be exhausted.",
quotas: {},
};
}