// Core of the xAI Grok Build integration: model metadata, OIDC discovery, // OAuth device authorization/refresh, and credential narrowing. OAuth only — // API keys are deliberately unsupported in this project (the subscription // token is the whole point). This module has ZERO pi-ai runtime imports so it // can be shared by both the pi-ai adapter (provider.ts) and the pi extension // adapter (extension.ts) — the extension loader's jiti alias mangles pi-ai // subpath imports, so anything the extension reaches must stay free of them. // // The OAuth flow borrows the Grok CLI's public client id and scope because xAI // has no public client registration. xAI can break this flow at any time. // Type-only: erased at runtime, so the extension path never resolves pi-ai. import type { Model, StreamOptions } from "@earendil-works/pi-ai"; export const XAI_PROVIDER_ID = "xai-grok-build"; /** Default model used by live tests and smoke scripts. */ export const XAI_MODEL_ID = "grok-4.5"; export const XAI_BASE_URL = "https://cli-chat-proxy.grok.com/v1"; // Tracks the installed Grok CLI release; bump alongside `grok update`. const XAI_CLI_CLIENT_VERSION = "0.2.101"; /** * Identity headers the Grok CLI chat proxy expects on subscription traffic; * they impersonate the real Grok CLI. The proxy can reject chat requests that * omit the Grok CLI User-Agent, so keep it in lockstep with the version above. */ export const XAI_CLI_HEADERS = { "X-XAI-Token-Auth": "xai-grok-cli", "x-grok-client-version": XAI_CLI_CLIENT_VERSION, "User-Agent": `xai-grok-workspace/${XAI_CLI_CLIENT_VERSION}`, } as const; /** * Per-model routing header. The CLI proxy routes to a model's inference * cluster from this header, not the JSON `model` field; it is safe to always * include and required off the default cluster, so every model sends it. */ export const XAI_MODEL_OVERRIDE_HEADER = "x-grok-model-override"; /** Grok CLI identity headers plus the per-model cluster-routing override. */ function xaiModelHeaders(modelId: string) { return { ...XAI_CLI_HEADERS, [XAI_MODEL_OVERRIDE_HEADER]: modelId }; } /** OpenAI Responses include value that returns encrypted reasoning for session recovery. */ export const REASONING_ENCRYPTED_INCLUDE = "reasoning.encrypted_content"; export class XaiToolSchemaCompatibilityError extends TypeError { readonly code = "XAI_INCOMPATIBLE_TOOL_SCHEMA"; readonly toolName: string | undefined; readonly schemaPath: string; constructor(toolName: string | undefined, schemaPath: string, reason: string) { const toolLabel = toolName === undefined ? schemaPath.split(".parameters", 1)[0] : `tool ${JSON.stringify(toolName)}`; super( `xAI-incompatible schema for ${toolLabel}: ${schemaPath} ${reason}; ` + "xAI requires every root anyOf/oneOf branch to declare an object-only type", ); this.name = "XaiToolSchemaCompatibilityError"; this.toolName = toolName; this.schemaPath = schemaPath; } } const XAI_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration"; const XAI_ISSUER = "https://auth.x.ai"; const XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"; const XAI_SCOPE = "openid profile email offline_access grok-cli:access api:access"; const XAI_DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"; const XAI_REFRESH_LEAD_MS = 5 * 60 * 1000; const XAI_DEFAULT_POLL_INTERVAL_MS = 5_000; const XAI_MAX_POLL_DURATION_MS = 30 * 60 * 1000; const XAI_REQUEST_TIMEOUT_MS = 30_000; const MODEL_COMPAT = { sendSessionIdHeader: false, supportsLongCacheRetention: false, } as const; // Catalog notes: // - The CLI proxy's live GET /v1/models returns grok-4.5 and // grok-composer-2.5-fast for this subscription credential. Both also pass // live /responses probes through the same endpoint. // - maxTokens values are registry metadata, unverified against live output // caps: pi-ai sends max_output_tokens only when stream options set maxTokens, // which they normally don't. // - grok-4.5 is a reasoning model. xAI's docs specify reasoning_effort // low/medium/high (default high, cannot be disabled); a live probe showed // "minimal" and "xhigh" also return 200 but they are undocumented (and // "xhigh" means agent count on grok-4.20-multi-agent, not depth), so only // the documented levels are exposed: off/minimal/xhigh/max are nulled. // Nulling "off" also stops pi's openai-responses path from sending // `reasoning.effort: "none"` (a live 400) when no effort is set. // - Reasoning models always need include:["reasoning.encrypted_content"] for // durable thinking recovery under store:false. pi-ai only adds that include // when an effort is set; we inject it for every reasoning:true catalog entry // via withXaiPayloadInvariants in both provider adapters. /** Documented efforts only: low / medium / high (default high; see catalog notes). */ const GROK_45_THINKING_LEVEL_MAP = { off: null, minimal: null, xhigh: null, max: null, } as const; export const GROK_45_MODEL = { id: "grok-4.5", name: "Grok 4.5", api: "openai-responses", provider: XAI_PROVIDER_ID, baseUrl: XAI_BASE_URL, headers: xaiModelHeaders("grok-4.5"), reasoning: true, thinkingLevelMap: GROK_45_THINKING_LEVEL_MAP, input: ["text", "image"], cost: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0, tiers: [ { inputTokensAbove: 200_000, input: 4, output: 12, cacheRead: 1, cacheWrite: 0, }, ], }, contextWindow: 500_000, maxTokens: 65_536, compat: MODEL_COMPAT, } satisfies Model<"openai-responses">; export const GROK_COMPOSER_MODEL = { id: "grok-composer-2.5-fast", name: "Grok Composer 2.5 Fast", api: "openai-responses", provider: XAI_PROVIDER_ID, baseUrl: XAI_BASE_URL, headers: xaiModelHeaders("grok-composer-2.5-fast"), reasoning: false, input: ["text"], cost: { input: 3, output: 15, cacheRead: 0.5, cacheWrite: 0, }, contextWindow: 200_000, maxTokens: 32_768, compat: MODEL_COMPAT, } satisfies Model<"openai-responses">; export const GROK_MODELS = [GROK_45_MODEL, GROK_COMPOSER_MODEL] as const; /** * Reject function parameter schemas that xAI cannot sample against. * * xAI requires every branch of a root anyOf/oneOf to resolve exclusively to * an object. Do not rewrite invalid schemas: changing them would hide the * caller's incompatibility and weaken the model-facing tool contract. */ export function assertXaiCompatibleToolSchemas(payload: unknown): void { if (!isRecord(payload)) return; const tools = payload["tools"]; if (!Array.isArray(tools)) return; for (const [toolIndex, tool] of tools.entries()) { if (!isRecord(tool) || tool["type"] !== "function") continue; const parameters = tool["parameters"]; if (!isRecord(parameters)) continue; const toolName = typeof tool["name"] === "string" ? tool["name"] : undefined; for (const unionName of ["anyOf", "oneOf"] as const) { const union = parameters[unionName]; if (union === undefined) continue; const unionPath = `tools[${toolIndex}].parameters.${unionName}`; if (!Array.isArray(union) || union.length === 0) { throw new XaiToolSchemaCompatibilityError(toolName, unionPath, "must be a non-empty array"); } for (const [branchIndex, branch] of union.entries()) { const branchPath = `${unionPath}[${branchIndex}]`; if (!isRecord(branch)) { throw new XaiToolSchemaCompatibilityError( toolName, branchPath, 'must be a schema object with type "object"', ); } const branchType = branch["type"]; if (typeof branchType === "string") { if (branchType === "object") continue; throw new XaiToolSchemaCompatibilityError( toolName, `${branchPath}.type`, `is ${JSON.stringify(branchType)}`, ); } if ( Array.isArray(branchType) && branchType.length > 0 && branchType.every((value) => value === "object") ) { continue; } throw new XaiToolSchemaCompatibilityError( toolName, `${branchPath}.type`, branchType === undefined ? 'does not explicitly declare type "object"' : `allows ${JSON.stringify(branchType)}`, ); } } } } /** Enforce xAI payload invariants after the caller's onPayload callback runs. */ export function withXaiPayloadInvariants(options: StreamOptions | undefined): StreamOptions { return { ...options, onPayload: async (payload, model) => { let current: unknown = payload; const user = await options?.onPayload?.(payload, model); if (user !== undefined) current = user; assertXaiCompatibleToolSchemas(current); const injected = ensureEncryptedReasoningInclude(current); return injected !== undefined ? injected : current; }, }; } /** * Ensure reasoning models request encrypted reasoning content. * * pi-ai only adds `include: ["reasoning.encrypted_content"]` when an effort is * set. Live wire probes showed encrypted content is returned whenever include * is present — even without effort — and that path is stronger for session * reload under store:false. Return undefined when no change is needed * (`onPayload` uses undefined to mean "keep payload"). */ export function ensureEncryptedReasoningInclude( payload: unknown, ): Record | undefined { if (!isRecord(payload)) return undefined; const modelId = payload["model"]; if (typeof modelId !== "string") return undefined; const catalog = GROK_MODELS.find((m) => m.id === modelId); if (catalog === undefined || !catalog.reasoning) return undefined; const include = payload["include"]; let existing: string[]; if (include === undefined) { existing = []; } else if (Array.isArray(include) && include.every((value) => typeof value === "string")) { existing = include; } else { throw new TypeError("Responses payload include must be an array of strings"); } if (existing.includes(REASONING_ENCRYPTED_INCLUDE)) return undefined; return { ...payload, include: [...existing, REASONING_ENCRYPTED_INCLUDE], }; } export type XaiOptions = { /** @deprecated Device authorization has no authorize-URL referrer; ignored. */ referrer?: string; /** * Optional lower bound for aborting the authorization wait. Values above * the production 30-minute cap are ignored. Primarily useful to hosts that * impose a shorter interactive-login budget. */ maxPollDurationMs?: number; /** Optional lower per-request timeout; values above 30 seconds are ignored. */ requestTimeoutMs?: number; }; export type XaiDeviceCodeInfo = { userCode: string; verificationUri: string; intervalSeconds?: number; expiresInSeconds?: number; }; /** * Everything the login flow needs from its host. Deliberately smaller than * either pi surface: xAI's flow never prompts, only notifies. */ export type XaiLoginHost = { deviceCode(info: XaiDeviceCodeInfo): void; progress(message: string): void; signal?: AbortSignal; }; /** * `type`, not `interface` — needs an implicit index signature to be assignable * to pi's OAuthCredential / OAuthCredentials (`[key: string]: unknown`). * Exactly these four fields, nothing more: pi coding-agent's refresh path does * a FULL REPLACE of the stored credential, so anything refresh omits is gone * forever. Identity fields (idToken/email/subject) are deliberately not * persisted — nothing reads them and the refresh grant may omit id_token. */ export type XaiOAuthCredential = { access: string; refresh: string; expires: number; tokenEndpoint: string; }; /** pi hands back `Record`. Narrow at the boundary; fail fast. */ export function requireXaiOAuthCredential(value: Record): XaiOAuthCredential { const access = stringValue(value["access"]); const refresh = stringValue(value["refresh"]); const tokenEndpoint = stringValue(value["tokenEndpoint"]); const expires = numberValue(value["expires"]); if ( access === undefined || refresh === undefined || tokenEndpoint === undefined || expires === undefined ) { throw new Error("Stored xAI credential is incomplete; log in again"); } return { access, refresh, expires, tokenEndpoint }; } type XaiDiscovery = { deviceAuthorizationEndpoint: string; tokenEndpoint: string; }; type XaiDeviceAuthorization = { deviceCode: string; userCode: string; verificationUri: string; verificationUriComplete?: string; expiresIn: number; pollIntervalMs: number; }; type XaiTokenData = { accessToken: string; refreshToken?: string; expiresIn: number; }; export async function loginXai( host: XaiLoginHost, opts: XaiOptions = {}, ): Promise { const requestTimeoutMs = boundedDuration(opts.requestTimeoutMs, XAI_REQUEST_TIMEOUT_MS); let discovery: XaiDiscovery; let authorization: XaiDeviceAuthorization; try { discovery = await discoverXaiOAuth(host.signal, requestTimeoutMs); authorization = await requestDeviceAuthorization( discovery.deviceAuthorizationEndpoint, host.signal, requestTimeoutMs, ); } catch (error) { if (host.signal?.aborted === true) throw authorizationWaitError(host.signal, error); throw error; } host.deviceCode({ userCode: authorization.userCode, verificationUri: authorization.verificationUriComplete ?? authorization.verificationUri, intervalSeconds: authorization.pollIntervalMs / 1000, expiresInSeconds: authorization.expiresIn, }); host.progress("Waiting for xAI device authorization..."); const maxPollDurationMs = Math.min( boundedDuration(opts.maxPollDurationMs, XAI_MAX_POLL_DURATION_MS), authorization.expiresIn * 1000, ); const timeoutSignal = AbortSignal.timeout(maxPollDurationMs); const pollSignal = host.signal === undefined ? timeoutSignal : AbortSignal.any([host.signal, timeoutSignal]); const token = await pollForDeviceToken( authorization, discovery.tokenEndpoint, pollSignal, host.signal, requestTimeoutMs, ); return tokenToCredential(token, discovery.tokenEndpoint); } export async function refreshXai(credential: XaiOAuthCredential): Promise { const token = await postTokenForm(credential.tokenEndpoint, { grant_type: "refresh_token", client_id: XAI_CLIENT_ID, refresh_token: credential.refresh, }); return tokenToCredential(token, credential.tokenEndpoint, credential.refresh); } async function discoverXaiOAuth( signal: AbortSignal | undefined, requestTimeoutMs: number, ): Promise { const response = await fetchOAuth( XAI_DISCOVERY_URL, { headers: { Accept: "application/json" }, }, signal, requestTimeoutMs, "xAI discovery request", ); const text = await response.text(); if (!response.ok) { throw new Error( `xAI discovery failed (${response.status}): ${errorBodySnippet(text) || response.statusText}`, ); } const body = parseJsonObject(text, "xAI discovery response"); // OIDC discovery requires the metadata issuer to match the issuer the // document was fetched for; a mismatch means misconfiguration or tampering. const issuer = requiredString(body["issuer"], "issuer"); if (issuer.replace(/\/$/, "") !== XAI_ISSUER) { throw new Error(`xAI discovery issuer mismatch: ${errorBodySnippet(issuer)}`); } const deviceAuthorizationEndpoint = validateXaiOAuthEndpoint( body["device_authorization_endpoint"], "device_authorization_endpoint", ); const tokenEndpoint = validateXaiOAuthEndpoint(body["token_endpoint"], "token_endpoint"); return { deviceAuthorizationEndpoint, tokenEndpoint }; } async function requestDeviceAuthorization( endpoint: string, signal: AbortSignal | undefined, requestTimeoutMs: number, ): Promise { const response = await fetchOAuth( endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, body: new URLSearchParams({ client_id: XAI_CLIENT_ID, scope: XAI_SCOPE }), }, signal, requestTimeoutMs, "xAI device authorization request", ); const text = await response.text(); if (!response.ok) { throw new Error( `xAI device authorization failed (${response.status}): ${errorBodySnippet(text) || response.statusText}`, ); } const body = parseJsonObject(text, "xAI device authorization response"); const expiresIn = positiveNumber(body["expires_in"], "expires_in"); const interval = body["interval"]; const intervalSeconds = interval === undefined ? undefined : numberValue(interval); if (interval !== undefined && (intervalSeconds === undefined || intervalSeconds < 0)) { throw new Error("xAI device authorization response has invalid interval"); } // RFC 8628 requires verification_uri; verification_uri_complete is an // optional convenience. Both are validated before the host sees them. const verificationUri = validateVerificationUri(body["verification_uri"], "verification_uri"); const verificationUriComplete = body["verification_uri_complete"] === undefined ? undefined : validateVerificationUri(body["verification_uri_complete"], "verification_uri_complete"); return { deviceCode: requiredString(body["device_code"], "device_code"), userCode: requiredString(body["user_code"], "user_code"), verificationUri, ...(verificationUriComplete !== undefined ? { verificationUriComplete } : {}), expiresIn, pollIntervalMs: Math.max( (intervalSeconds ?? XAI_DEFAULT_POLL_INTERVAL_MS / 1000) * 1000, XAI_DEFAULT_POLL_INTERVAL_MS, ), }; } /** * The verification URI is rendered as a clickable link and may be opened in a * browser by library hosts, so it must be an https URL on x.ai before the * host ever sees it. Returns the parser-normalized href, which also * percent-encodes stray control characters. */ function validateVerificationUri(value: unknown, field: string): string { const raw = requiredString(value, field); let url: URL; try { url = new URL(raw); } catch (error) { throw new Error(`xAI device authorization ${field} is not a valid URL`, { cause: error }); } assertHttpsOnXai(url, `xAI device authorization ${field}`); return url.href; } async function pollForDeviceToken( authorization: XaiDeviceAuthorization, tokenEndpoint: string, signal: AbortSignal, hostSignal: AbortSignal | undefined, requestTimeoutMs: number, ): Promise { let intervalMs = authorization.pollIntervalMs; for (;;) { // Wait before every poll, including the first: the user cannot have // authorized yet, and RFC 8628 §3.3 sets the interval as the minimum // spacing between token requests. try { await waitForPoll(intervalMs, signal); } catch (error) { throw authorizationWaitError(hostSignal, error); } let outcome: XaiDeviceTokenOutcome; try { outcome = await requestDeviceToken( tokenEndpoint, authorization.deviceCode, signal, requestTimeoutMs, ); } catch (error) { if (signal.aborted) throw authorizationWaitError(hostSignal, error); throw error; } switch (outcome.type) { case "success": return outcome.token; case "authorization_pending": break; case "slow_down": // RFC 8628 §3.5 requires at least +5s; a server-provided interval is // a floor, never a way to speed polling back up. intervalMs = Math.max( intervalMs + XAI_DEFAULT_POLL_INTERVAL_MS, (outcome.intervalSeconds ?? 0) * 1000, ); break; case "transient": // RFC 8628 §3.5: back off and keep polling on connection trouble. // The per-request timeout and the overall deadline bound the retries. intervalMs += XAI_DEFAULT_POLL_INTERVAL_MS; break; case "expired_token": throw new Error("xAI device authorization expired"); case "access_denied": throw new Error("xAI device authorization denied"); case "error": throw new Error( `xAI device token error: ${outcome.code}${outcome.description === undefined ? "" : `: ${outcome.description}`}`, ); default: { const exhaustive: never = outcome; throw new Error(`Unhandled xAI device token outcome: ${String(exhaustive)}`); } } } } type XaiDeviceTokenOutcome = | { type: "success"; token: XaiTokenData } | { type: "authorization_pending" } | { type: "slow_down"; intervalSeconds?: number } | { type: "transient"; cause: unknown } | { type: "expired_token" } | { type: "access_denied" } | { type: "error"; code: string; description?: string }; async function requestDeviceToken( tokenEndpoint: string, deviceCode: string, signal: AbortSignal, requestTimeoutMs: number, ): Promise { const endpoint = validateXaiOAuthEndpoint(tokenEndpoint, "token_endpoint"); let response: Response; let text: string; try { response = await fetchOAuth( endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: new URLSearchParams({ grant_type: XAI_DEVICE_CODE_GRANT, device_code: deviceCode, client_id: XAI_CLIENT_ID, }), }, signal, requestTimeoutMs, "xAI device token request", ); text = await response.text(); } catch (error) { // Cancellation propagates; anything else (timeout, connection reset) is // a retryable hiccup while the device code is still valid. if (signal.aborted) throw error; return { type: "transient", cause: error }; } let body: Record; try { body = parseJsonObject(text, `xAI device token response (HTTP ${response.status})`); } catch (error) { // A non-JSON error body (proxy 502 page and the like) is a server // hiccup; a non-JSON success body is a protocol violation. if (!response.ok) return { type: "transient", cause: error }; throw error; } const errorCode = stringValue(body["error"]); if (errorCode !== undefined) { switch (errorCode) { case "authorization_pending": case "expired_token": case "access_denied": return { type: errorCode }; case "slow_down": { const interval = numberValue(body["interval"]); return { type: "slow_down", ...(interval !== undefined && interval > 0 ? { intervalSeconds: interval } : {}), }; } default: { const description = stringValue(body["error_description"]); return { type: "error", code: errorCode, ...(description !== undefined ? { description } : {}), }; } } } if (!response.ok) { throw new Error( `xAI device token request failed (${response.status}): ${errorBodySnippet(text)}`, ); } return { type: "success", token: parseTokenResponse(text) }; } async function postTokenForm( tokenEndpoint: string, fields: Record, signal?: AbortSignal, ): Promise { const endpoint = validateXaiOAuthEndpoint(tokenEndpoint, "token_endpoint"); const response = await fetchOAuth( endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" }, body: new URLSearchParams(fields), }, signal, XAI_REQUEST_TIMEOUT_MS, "xAI token request", ); const text = await response.text(); if (!response.ok) { throw new Error( `xAI token request failed (${response.status}): ${errorBodySnippet(text) || response.statusText}`, ); } return parseTokenResponse(text); } function parseTokenResponse(text: string): XaiTokenData { const body = parseJsonObject(text, "xAI token response"); const accessToken = requiredString(body["access_token"], "access_token"); const refreshToken = stringValue(body["refresh_token"]); // Zero or negative lifetimes would store an already-expired credential and // churn refresh forever; reject them with the missing-field error. const expiresIn = positiveNumber(body["expires_in"], "expires_in"); assertBearer(stringValue(body["token_type"])); return { accessToken, ...(refreshToken !== undefined ? { refreshToken } : {}), expiresIn, }; } // RFC 6749 §7.1: token_type values are case-insensitive; xAI sends "Bearer". function assertBearer(tokenType: string | undefined): void { if (tokenType !== undefined && tokenType.toLowerCase() !== "bearer") { throw new Error(`xAI returned unsupported token_type: ${tokenType}`); } } function tokenToCredential( token: XaiTokenData, tokenEndpoint: string, fallbackRefreshToken?: string, ): XaiOAuthCredential { const refresh = token.refreshToken ?? fallbackRefreshToken; if (refresh === undefined) { throw new Error("xAI token response missing refresh_token"); } // Refresh ahead of expiry, but never so far ahead that a short-lived token // stores as already expired: cap the lead at half the token's lifetime. const lifetimeMs = token.expiresIn * 1000; const leadMs = Math.min(XAI_REFRESH_LEAD_MS, Math.floor(lifetimeMs / 2)); return { access: token.accessToken, refresh, expires: Date.now() + lifetimeMs - leadMs, tokenEndpoint, }; } async function fetchOAuth( url: string, init: RequestInit, signal: AbortSignal | undefined, requestTimeoutMs: number, label: string, ): Promise { const timeoutSignal = AbortSignal.timeout(requestTimeoutMs); const requestSignal = signal === undefined ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]); try { requestSignal.throwIfAborted(); return await fetch(url, { ...init, signal: requestSignal }); } catch (error) { throw new Error(`${label} failed`, { cause: error }); } } function waitForPoll(delayMs: number, signal: AbortSignal): Promise { if (signal.aborted) return Promise.reject(signal.reason); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, delayMs); const onAbort = () => { clearTimeout(timeout); reject(signal.reason); }; signal.addEventListener("abort", onAbort, { once: true }); }); } function authorizationWaitError(hostSignal: AbortSignal | undefined, cause: unknown): Error { if (hostSignal?.aborted === true) { return new Error("xAI device authorization cancelled", { cause }); } return new Error("xAI device authorization expired or timed out", { cause }); } function boundedDuration(value: number | undefined, maximum: number): number { if (value === undefined) return maximum; if (!Number.isFinite(value) || value <= 0) { throw new TypeError("xAI OAuth timeout must be a positive finite number"); } return Math.min(Math.ceil(value), maximum); } function validateXaiOAuthEndpoint(value: unknown, field: string): string { const endpoint = requiredString(value, field); assertHttpsOnXai(new URL(endpoint), `xAI discovery ${field}`); return endpoint; } function assertHttpsOnXai(url: URL, label: string): void { if (url.protocol !== "https:") { throw new Error(`${label} must use https`); } const host = url.hostname.toLowerCase(); if (host !== "x.ai" && !host.endsWith(".x.ai")) { throw new Error(`${label} host is not on x.ai`); } } /** Bound and sanitize server response text before embedding it in an error. */ function errorBodySnippet(text: string): string { // eslint-disable-next-line no-control-regex const sanitized = text.replace(/[\u0000-\u001f\u007f]+/g, " ").trim(); return sanitized.length > 256 ? `${sanitized.slice(0, 256)}…` : sanitized; } function parseJsonObject(text: string, label: string): Record { let parsed: unknown; try { parsed = JSON.parse(text); } catch (error) { throw new Error(`${label} is not valid JSON`, { cause: error }); } if (!isRecord(parsed)) throw new Error(`${label} must be a JSON object`); return parsed; } function requiredString(value: unknown, field: string): string { const result = stringValue(value); if (result === undefined) throw new Error(`xAI response missing ${field}`); return result; } function numberValue(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } function positiveNumber(value: unknown, field: string): number { const result = numberValue(value); if (result === undefined || result <= 0) { throw new Error(`xAI response missing or invalid ${field}`); } return result; } function stringValue(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }