Files
2026-07-13 13:39:12 +08:00

254 lines
7.4 KiB
TypeScript

/**
* GrokCliExecutor — Grok Build Provider
*
* Routes requests through Grok's chat proxy endpoint using OAuth authentication.
* Uses Node.js https module directly with IPv4 forced to bypass Cloudflare blocking.
* Supports automatic token refresh via refresh_token.
*/
import {
BaseExecutor,
type ExecuteInput,
type ExecutorLog,
type ProviderCredentials,
} from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { resolvePublicCred } from "../utils/publicCreds.ts";
import https from "node:https";
const GROK_TOKEN_URL = "https://auth.x.ai/oauth2/token";
const REQUEST_TIMEOUT_MS = 60_000;
export class GrokCliExecutor extends BaseExecutor {
constructor() {
super("grok-cli", PROVIDERS["grok-cli"]);
}
async execute(input: ExecuteInput) {
const { model, body, stream, credentials, signal } = input;
const url = this.buildUrl(model, stream, 0, credentials);
const headers = this.buildHeaders(credentials, stream);
const transformedBody = this.transformRequest(model, body, stream, credentials);
const bodyStr = JSON.stringify(transformedBody);
const response = await this.nativePost(url, headers, bodyStr, signal);
return { response, url, headers, transformedBody };
}
async refreshCredentials(
credentials: ProviderCredentials,
log?: ExecutorLog | null
): Promise<Partial<ProviderCredentials> | null> {
if (!credentials?.refreshToken) {
log?.warn?.("TOKEN_REFRESH", "Grok Build: no refresh token available");
return null;
}
const clientId = resolvePublicCred("grok_id", "GROK_OAUTH_CLIENT_ID");
try {
const body = new URLSearchParams({
grant_type: "refresh_token",
client_id: clientId,
refresh_token: credentials.refreshToken,
});
const result = await this.nativeHttpsPost(
GROK_TOKEN_URL,
{
"Content-Type": "application/x-www-form-urlencoded",
},
body.toString(),
10_000
);
if (result.status !== 200) {
log?.warn?.("TOKEN_REFRESH", `Grok Build: refresh failed with status ${result.status}`);
return null;
}
const data = JSON.parse(result.body);
if (!data.access_token) {
log?.warn?.("TOKEN_REFRESH", "Grok Build: no access_token in refresh response");
return null;
}
const expiresIn = data.expires_in || 21600;
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
log?.info?.("TOKEN_REFRESH", `Grok Build: token refreshed, expires ${expiresAt}`);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token || credentials.refreshToken,
expiresAt,
};
} catch (error) {
log?.warn?.(
"TOKEN_REFRESH",
`Grok Build: refresh error: ${error instanceof Error ? error.message : String(error)}`
);
return null;
}
}
private nativeHttpsPost(
url: string,
headers: Record<string, string>,
bodyStr: string,
timeoutMs = 10_000
): Promise<{ status: number; body: string }> {
const urlObj = new URL(url);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => req.destroy(new Error("Timeout")), timeoutMs);
const req = https.request(
{
hostname: urlObj.hostname,
port: 443,
path: urlObj.pathname + urlObj.search,
method: "POST",
family: 4,
headers: {
...headers,
"Content-Length": Buffer.byteLength(bodyStr),
},
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("end", () => {
clearTimeout(timer);
resolve({
status: res.statusCode ?? 500,
body: Buffer.concat(chunks).toString("utf-8"),
});
});
}
);
req.on("error", (err) => {
clearTimeout(timer);
reject(err);
});
req.write(bodyStr);
req.end();
});
}
private nativePost(
url: string,
headers: Record<string, string>,
bodyStr: string,
signal?: AbortSignal | null
): Promise<Response> {
const urlObj = new URL(url);
if (signal?.aborted) {
return Promise.reject(new Error("Aborted"));
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => req.destroy(new Error("Timeout")), REQUEST_TIMEOUT_MS);
let settled = false;
const settle = (fn: () => void) => {
if (settled) return;
settled = true;
clearTimeout(timer);
fn();
};
const req = https.request(
{
hostname: urlObj.hostname,
port: 443,
path: urlObj.pathname + urlObj.search,
method: "POST",
family: 4,
headers: {
...headers,
"Content-Length": Buffer.byteLength(bodyStr),
},
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("end", () => {
settle(() => {
const responseBody = Buffer.concat(chunks).toString("utf-8");
const responseHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(res.headers)) {
if (typeof value === "string") responseHeaders[key] = value;
else if (Array.isArray(value)) responseHeaders[key] = value.join(", ");
}
resolve(
new Response(responseBody, {
status: res.statusCode ?? 500,
headers: responseHeaders,
})
);
});
});
}
);
if (signal) {
const onAbort = () => settle(() => reject(new Error("Aborted")));
signal.addEventListener("abort", onAbort, { once: true });
// Clean up listener when request finishes naturally
req.on("close", () => signal.removeEventListener("abort", onAbort));
}
req.on("error", (err) => settle(() => reject(err)));
req.write(bodyStr);
req.end();
});
}
buildHeaders(credentials: ProviderCredentials, stream = true) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (credentials.accessToken) {
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
} else if (credentials.apiKey) {
headers["Authorization"] = `Bearer ${credentials.apiKey}`;
}
headers["Accept"] = stream ? "text/event-stream" : "application/json";
headers["x-grok-client-version"] = "0.2.72";
headers["x-grok-client-identifier"] = "grok_cli_rs";
headers["User-Agent"] = "grok-cli/0.2.72 (Windows 10.0.26200; x64)";
return headers;
}
transformRequest(
model: string,
body: unknown,
stream: boolean,
_credentials: ProviderCredentials
) {
const transformed =
body && typeof body === "object" ? { ...(body as Record<string, unknown>) } : {};
if (!transformed.model) {
transformed.model = model || "grok-composer-2.5-fast";
}
transformed.stream = !!stream;
// Grok Build rejects unsupported parameters with 400.
const UNSUPPORTED = ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"];
for (const param of UNSUPPORTED) {
if (param in transformed) {
delete transformed[param];
}
}
return transformed;
}
}