@demicodes/provider-grok-build 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +32 -0
- package/dist/index.d.mts +125 -0
- package/dist/index.mjs +1217 -0
- package/package.json +39 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
import { delay, errorCode, errorMessage, isAbortError, isRecord, nonEmptyString, normalizeBaseUrl, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, stringOrNull } from "@demicodes/utils";
|
|
2
|
+
import { createProviderQuota, defineProvider, httpRequestFailedEvent, normalizeErrorCode, numberHeader, providerErrorFromUnknown, redactCredentialText, severityFromUsedPercent, toolResultContentToText, usedPercentFromRatio } from "@demicodes/provider";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { zeroUsage } from "@demicodes/core";
|
|
9
|
+
import { FileCredentialPool, credentialIdFromIdentity, runVendorLoginCommand } from "@demicodes/provider/credentials-pool";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
//#region src/auth.ts
|
|
12
|
+
/** Grok auth.json stores the access token under `key`; redact it alongside the standard fields. */
|
|
13
|
+
const SECRET_FIELD_PATTERNS = ["\\bkey\\b"];
|
|
14
|
+
function redactGrokSecretText(text) {
|
|
15
|
+
return redactCredentialText(text, SECRET_FIELD_PATTERNS);
|
|
16
|
+
}
|
|
17
|
+
const DEFAULT_TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token";
|
|
18
|
+
const REFRESH_EXPIRY_SKEW_MS = 300 * 1e3;
|
|
19
|
+
async function grokBuildAuthStatus(options = {}) {
|
|
20
|
+
return new FileGrokAuthStore(options).status();
|
|
21
|
+
}
|
|
22
|
+
var FileGrokAuthStore = class {
|
|
23
|
+
grokHome;
|
|
24
|
+
authFile;
|
|
25
|
+
entryKey;
|
|
26
|
+
refreshImpl;
|
|
27
|
+
now;
|
|
28
|
+
lockRetryDelayMs;
|
|
29
|
+
lockTimeoutMs;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.grokHome = options.grokHome ?? defaultGrokHome();
|
|
32
|
+
this.authFile = options.authFile ?? join(this.grokHome, "auth.json");
|
|
33
|
+
this.entryKey = nonEmptyString(options.entryKey) ?? null;
|
|
34
|
+
this.refreshImpl = options.refresh ?? refreshGrokOidcToken;
|
|
35
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
36
|
+
this.lockRetryDelayMs = options.lockRetryDelayMs ?? 25;
|
|
37
|
+
this.lockTimeoutMs = options.lockTimeoutMs ?? 3e4;
|
|
38
|
+
}
|
|
39
|
+
async status() {
|
|
40
|
+
try {
|
|
41
|
+
const auth = await this.resolveAuth();
|
|
42
|
+
return {
|
|
43
|
+
status: "authenticated",
|
|
44
|
+
accountLabel: auth.email ?? auth.entryKey
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof GrokAuthError && error.code === "auth_missing") return {
|
|
48
|
+
status: "unauthenticated",
|
|
49
|
+
message: error.message
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
status: "error",
|
|
53
|
+
message: redactGrokSecretText(error instanceof Error ? error.message : String(error))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async resolveAuth(options = {}) {
|
|
58
|
+
const file = await this.readAuthFile();
|
|
59
|
+
const selected = this.entryKey ? selectAuthEntryByKey(file, this.entryKey) : selectAuthEntry(file);
|
|
60
|
+
if (!selected) throw new GrokAuthError("auth_missing", this.entryKey ? `No Grok OAuth entry "${this.entryKey}" in ${this.authFile}` : `No Grok OAuth session found in ${this.authFile}. Run \`grok login\` first.`);
|
|
61
|
+
const { entryKey, entry } = selected;
|
|
62
|
+
const accessToken = nonEmptyString(entry.key);
|
|
63
|
+
if (!accessToken) throw new GrokAuthError("auth_missing", `Grok auth entry "${entryKey}" has no access token (key)`);
|
|
64
|
+
const refreshToken = nonEmptyString(entry.refresh_token) ?? null;
|
|
65
|
+
const clientId = nonEmptyString(entry.oidc_client_id) ?? parseClientIdFromEntryKey(entryKey);
|
|
66
|
+
const issuer = nonEmptyString(entry.oidc_issuer) ?? parseIssuerFromEntryKey(entryKey);
|
|
67
|
+
const expiresAt = parseExpiresAt(entry.expires_at) ?? parseJwtExpiration(accessToken);
|
|
68
|
+
if ((options.forceRefresh === true || expiresWithin(expiresAt, this.now(), REFRESH_EXPIRY_SKEW_MS)) && refreshToken && clientId) return this.refreshAndResolve(accessToken, entryKey, {
|
|
69
|
+
refreshToken,
|
|
70
|
+
clientId,
|
|
71
|
+
tokenEndpoint: tokenEndpointForIssuer(issuer)
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
accessToken,
|
|
75
|
+
refreshToken,
|
|
76
|
+
expiresAt,
|
|
77
|
+
email: stringOrNull(entry.email),
|
|
78
|
+
issuer,
|
|
79
|
+
clientId,
|
|
80
|
+
entryKey,
|
|
81
|
+
authFile: this.authFile
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async refreshAndResolve(staleAccessToken, entryKey, refreshInput) {
|
|
85
|
+
return this.withAuthFileLock(async () => {
|
|
86
|
+
const latest = await this.readAuthFile();
|
|
87
|
+
const latestEntry = latest[entryKey];
|
|
88
|
+
if (!latestEntry || typeof latestEntry !== "object") throw new GrokAuthError("auth_missing", `Grok auth entry "${entryKey}" disappeared during refresh`);
|
|
89
|
+
const refreshToken = nonEmptyString(latestEntry.refresh_token) ?? refreshInput.refreshToken;
|
|
90
|
+
const clientId = nonEmptyString(latestEntry.oidc_client_id) ?? refreshInput.clientId;
|
|
91
|
+
const issuer = nonEmptyString(latestEntry.oidc_issuer) ?? parseIssuerFromEntryKey(entryKey);
|
|
92
|
+
const latestAccessToken = nonEmptyString(latestEntry.key);
|
|
93
|
+
if (latestAccessToken && latestAccessToken !== staleAccessToken) {
|
|
94
|
+
const latestExpiresAt = parseExpiresAt(latestEntry.expires_at) ?? parseJwtExpiration(latestAccessToken);
|
|
95
|
+
if (!expiresWithin(latestExpiresAt, this.now(), REFRESH_EXPIRY_SKEW_MS)) return {
|
|
96
|
+
accessToken: latestAccessToken,
|
|
97
|
+
refreshToken: nonEmptyString(latestEntry.refresh_token) ?? null,
|
|
98
|
+
expiresAt: latestExpiresAt,
|
|
99
|
+
email: stringOrNull(latestEntry.email),
|
|
100
|
+
issuer,
|
|
101
|
+
clientId,
|
|
102
|
+
entryKey,
|
|
103
|
+
authFile: this.authFile
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const response = await this.refreshImpl({
|
|
107
|
+
refreshToken,
|
|
108
|
+
clientId,
|
|
109
|
+
tokenEndpoint: tokenEndpointForIssuer(issuer) || refreshInput.tokenEndpoint
|
|
110
|
+
});
|
|
111
|
+
const accessToken = nonEmptyString(response.access_token);
|
|
112
|
+
if (!accessToken) throw new GrokAuthError("auth_refresh_failed", "Grok token refresh returned no access_token");
|
|
113
|
+
const expiresAt = typeof response.expires_in === "number" && Number.isFinite(response.expires_in) ? new Date(this.now().getTime() + response.expires_in * 1e3) : parseJwtExpiration(accessToken);
|
|
114
|
+
const nextEntry = {
|
|
115
|
+
...latestEntry,
|
|
116
|
+
key: accessToken,
|
|
117
|
+
...nonEmptyString(response.refresh_token) ? { refresh_token: response.refresh_token } : {},
|
|
118
|
+
...expiresAt ? { expires_at: expiresAt.toISOString() } : {}
|
|
119
|
+
};
|
|
120
|
+
const nextFile = {
|
|
121
|
+
...latest,
|
|
122
|
+
[entryKey]: nextEntry
|
|
123
|
+
};
|
|
124
|
+
await writeAuthJsonAtomic(this.authFile, nextFile);
|
|
125
|
+
return {
|
|
126
|
+
accessToken,
|
|
127
|
+
refreshToken: nonEmptyString(nextEntry.refresh_token) ?? null,
|
|
128
|
+
expiresAt,
|
|
129
|
+
email: stringOrNull(nextEntry.email),
|
|
130
|
+
issuer,
|
|
131
|
+
clientId,
|
|
132
|
+
entryKey,
|
|
133
|
+
authFile: this.authFile
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async readAuthFile() {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(await readFile(this.authFile, "utf8"));
|
|
140
|
+
if (!isRecord(parsed)) throw new GrokAuthError("auth_invalid", `Grok auth file is not an object: ${this.authFile}`);
|
|
141
|
+
return parsed;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof GrokAuthError) throw error;
|
|
144
|
+
if (errorCode(error) === "ENOENT") throw new GrokAuthError("auth_missing", `Grok auth file not found: ${this.authFile}. Run \`grok login\` first.`);
|
|
145
|
+
throw new GrokAuthError("auth_invalid", `Failed to read Grok auth file ${this.authFile}: ${redactGrokSecretText(errorMessage(error))}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async withAuthFileLock(fn) {
|
|
149
|
+
const lockFile = `${this.authFile}.lock`;
|
|
150
|
+
await mkdir(dirname(this.authFile), { recursive: true });
|
|
151
|
+
const started = Date.now();
|
|
152
|
+
let handle = null;
|
|
153
|
+
let brokeStaleLock = false;
|
|
154
|
+
while (!handle) try {
|
|
155
|
+
handle = await open(lockFile, "wx", 384);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (errorCode(error) !== "EEXIST") throw new GrokAuthError("auth_lock_failed", `Failed to lock Grok auth file: ${redactGrokSecretText(errorMessage(error))}`);
|
|
158
|
+
const staleIdentity = brokeStaleLock ? null : await fileIdentity(lockFile);
|
|
159
|
+
if (staleIdentity && await isAbandonedGrokAuthLock(lockFile, this.now())) {
|
|
160
|
+
if (await removeLockFileIfSame(lockFile, staleIdentity)) brokeStaleLock = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (Date.now() - started > this.lockTimeoutMs) throw new GrokAuthError("auth_lock_failed", `Timed out waiting for Grok auth lock ${lockFile}. If no other Grok process is running, delete the lock file and retry.`);
|
|
164
|
+
await delay(this.lockRetryDelayMs);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await writeFile(lockFile, `${process.pid}:${Math.floor(this.now().getTime() / 1e3)}`, { mode: 384 });
|
|
168
|
+
return await fn();
|
|
169
|
+
} finally {
|
|
170
|
+
const ownedIdentity = await handle.stat().then(toFileIdentity).catch(() => null);
|
|
171
|
+
await handle.close().catch(() => void 0);
|
|
172
|
+
if (ownedIdentity) await removeLockFileIfSame(lockFile, ownedIdentity);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var GrokAuthError = class extends Error {
|
|
177
|
+
code;
|
|
178
|
+
constructor(code, message) {
|
|
179
|
+
super(message);
|
|
180
|
+
this.code = code;
|
|
181
|
+
this.name = "GrokAuthError";
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
function defaultGrokHome() {
|
|
185
|
+
const fromEnv = process.env.GROK_HOME;
|
|
186
|
+
return fromEnv && fromEnv.trim() ? fromEnv : join(homedir(), ".grok");
|
|
187
|
+
}
|
|
188
|
+
function selectAuthEntry(file) {
|
|
189
|
+
const candidates = [];
|
|
190
|
+
for (const [entryKey, value] of Object.entries(file)) {
|
|
191
|
+
if (!isRecord(value)) continue;
|
|
192
|
+
const entry = value;
|
|
193
|
+
if (!nonEmptyString(entry.key)) continue;
|
|
194
|
+
let score = 0;
|
|
195
|
+
if (entry.auth_mode === "oidc") score += 4;
|
|
196
|
+
if (nonEmptyString(entry.refresh_token)) score += 2;
|
|
197
|
+
if (entryKey.includes("auth.x.ai") || entry.oidc_issuer === "https://auth.x.ai") score += 1;
|
|
198
|
+
candidates.push({
|
|
199
|
+
entryKey,
|
|
200
|
+
entry,
|
|
201
|
+
score
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
candidates.sort((a, b) => b.score - a.score || a.entryKey.localeCompare(b.entryKey));
|
|
205
|
+
return candidates[0] ?? null;
|
|
206
|
+
}
|
|
207
|
+
function selectAuthEntryByKey(file, entryKey) {
|
|
208
|
+
const value = file[entryKey];
|
|
209
|
+
if (!isRecord(value)) return null;
|
|
210
|
+
const entry = value;
|
|
211
|
+
if (!nonEmptyString(entry.key)) return null;
|
|
212
|
+
return {
|
|
213
|
+
entryKey,
|
|
214
|
+
entry
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async function refreshGrokOidcToken(input, signal) {
|
|
218
|
+
const body = new URLSearchParams({
|
|
219
|
+
grant_type: "refresh_token",
|
|
220
|
+
refresh_token: input.refreshToken,
|
|
221
|
+
client_id: input.clientId
|
|
222
|
+
});
|
|
223
|
+
const response = await fetch(input.tokenEndpoint, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
227
|
+
accept: "application/json"
|
|
228
|
+
},
|
|
229
|
+
body,
|
|
230
|
+
signal
|
|
231
|
+
});
|
|
232
|
+
if (!response.ok) throw new GrokAuthError("auth_refresh_failed", `Grok token refresh failed with HTTP ${response.status}`);
|
|
233
|
+
return await response.json();
|
|
234
|
+
}
|
|
235
|
+
function parseJwtExpiration(jwt) {
|
|
236
|
+
const exp = decodeJwtPayload(jwt)?.exp;
|
|
237
|
+
return typeof exp === "number" ? /* @__PURE__ */ new Date(exp * 1e3) : null;
|
|
238
|
+
}
|
|
239
|
+
function tokenEndpointForIssuer(issuer) {
|
|
240
|
+
if (!issuer) return DEFAULT_TOKEN_ENDPOINT;
|
|
241
|
+
if (issuer === "https://auth.x.ai" || issuer === "https://auth.x.ai/") return DEFAULT_TOKEN_ENDPOINT;
|
|
242
|
+
return `${issuer.replace(/\/$/, "")}/oauth2/token`;
|
|
243
|
+
}
|
|
244
|
+
function parseIssuerFromEntryKey(entryKey) {
|
|
245
|
+
const sep = entryKey.indexOf("::");
|
|
246
|
+
if (sep <= 0) return null;
|
|
247
|
+
return entryKey.slice(0, sep);
|
|
248
|
+
}
|
|
249
|
+
function parseClientIdFromEntryKey(entryKey) {
|
|
250
|
+
const sep = entryKey.indexOf("::");
|
|
251
|
+
if (sep < 0 || sep === entryKey.length - 2) return null;
|
|
252
|
+
return entryKey.slice(sep + 2) || null;
|
|
253
|
+
}
|
|
254
|
+
function parseExpiresAt(value) {
|
|
255
|
+
if (typeof value !== "string" || !value) return null;
|
|
256
|
+
const ms = Date.parse(value);
|
|
257
|
+
return Number.isFinite(ms) ? new Date(ms) : null;
|
|
258
|
+
}
|
|
259
|
+
function expiresWithin(expiresAt, now, skewMs) {
|
|
260
|
+
return expiresAt !== null && expiresAt.getTime() - now.getTime() <= skewMs;
|
|
261
|
+
}
|
|
262
|
+
async function writeAuthJsonAtomic(authFile, auth) {
|
|
263
|
+
await mkdir(dirname(authFile), { recursive: true });
|
|
264
|
+
const temp = `${authFile}.${process.pid}.${Date.now()}.tmp`;
|
|
265
|
+
await writeFile(temp, `${JSON.stringify(auth, null, 2)}\n`, { mode: 384 });
|
|
266
|
+
await chmod(temp, 384);
|
|
267
|
+
await rename(temp, authFile);
|
|
268
|
+
}
|
|
269
|
+
function decodeJwtPayload(jwt) {
|
|
270
|
+
const parts = jwt.split(".");
|
|
271
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
272
|
+
try {
|
|
273
|
+
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
274
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
275
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Grok CLI lock format is `pid:unix_seconds`. A valid live PID always owns its lock. */
|
|
281
|
+
async function isAbandonedGrokAuthLock(lockFile, now, maxAgeMs = 3e4) {
|
|
282
|
+
try {
|
|
283
|
+
const raw = (await readFile(lockFile, "utf8")).trim();
|
|
284
|
+
const match = /^(\d+):(\d+)$/.exec(raw);
|
|
285
|
+
if (match) {
|
|
286
|
+
const pid = Number(match[1]);
|
|
287
|
+
const tsSec = Number(match[2]);
|
|
288
|
+
if (Number.isFinite(pid) && pid > 0) return !isProcessAlive(pid);
|
|
289
|
+
if (Number.isFinite(tsSec) && now.getTime() - tsSec * 1e3 > maxAgeMs) return true;
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const info = await stat(lockFile);
|
|
293
|
+
return now.getTime() - info.mtimeMs > maxAgeMs;
|
|
294
|
+
} catch {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function toFileIdentity(info) {
|
|
299
|
+
return {
|
|
300
|
+
dev: info.dev,
|
|
301
|
+
ino: info.ino
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function fileIdentity(path) {
|
|
305
|
+
return stat(path).then(toFileIdentity).catch(() => null);
|
|
306
|
+
}
|
|
307
|
+
async function removeLockFileIfSame(lockFile, expected) {
|
|
308
|
+
const current = await fileIdentity(lockFile);
|
|
309
|
+
if (!current || current.dev !== expected.dev || current.ino !== expected.ino) return false;
|
|
310
|
+
return rm(lockFile).then(() => true).catch(() => false);
|
|
311
|
+
}
|
|
312
|
+
function isProcessAlive(pid) {
|
|
313
|
+
try {
|
|
314
|
+
process.kill(pid, 0);
|
|
315
|
+
return true;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (errorCode(error) === "EPERM") return true;
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/chat.ts
|
|
323
|
+
function buildGrokChatCompletionsBody(request) {
|
|
324
|
+
const body = {
|
|
325
|
+
model: request.modelId,
|
|
326
|
+
messages: inferenceItemsToMessages(request.systemPrompt, request.items),
|
|
327
|
+
stream: true,
|
|
328
|
+
stream_options: { include_usage: true }
|
|
329
|
+
};
|
|
330
|
+
if (request.tools.length > 0) {
|
|
331
|
+
body.tools = request.tools.map(toolToGrokTool);
|
|
332
|
+
body.tool_choice = "auto";
|
|
333
|
+
}
|
|
334
|
+
const reasoningEffort = thinkingToReasoningEffort(request);
|
|
335
|
+
if (reasoningEffort) body.reasoning_effort = reasoningEffort;
|
|
336
|
+
return body;
|
|
337
|
+
}
|
|
338
|
+
async function* mapGrokChatCompletionStream(events, signal) {
|
|
339
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
340
|
+
let thinkingStarted = false;
|
|
341
|
+
let usage = zeroUsage();
|
|
342
|
+
for await (const event of events) {
|
|
343
|
+
if (signal?.aborted) {
|
|
344
|
+
yield { type: "abort" };
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const data = event.data;
|
|
348
|
+
if (data === "[DONE]") {
|
|
349
|
+
yield* flushToolCalls(toolCalls);
|
|
350
|
+
yield {
|
|
351
|
+
type: "response",
|
|
352
|
+
usage
|
|
353
|
+
};
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const chunk = parseJsonObject(data);
|
|
357
|
+
if (!chunk) continue;
|
|
358
|
+
const error = isRecord(chunk.error) ? chunk.error : null;
|
|
359
|
+
if (error) {
|
|
360
|
+
const message = stringOrNull(error.message) ?? "Grok Build stream error";
|
|
361
|
+
yield {
|
|
362
|
+
type: "error",
|
|
363
|
+
message,
|
|
364
|
+
code: normalizeErrorCode(stringOrNull(error.code) ?? stringOrNull(error.type), message)
|
|
365
|
+
};
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (isRecord(chunk.usage)) usage = grokUsage(chunk.usage);
|
|
369
|
+
const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
|
|
370
|
+
for (const choice of choices) {
|
|
371
|
+
if (!isRecord(choice)) continue;
|
|
372
|
+
const delta = isRecord(choice.delta) ? choice.delta : null;
|
|
373
|
+
if (delta) {
|
|
374
|
+
const reasoning = stringOrNull(delta.reasoning_content);
|
|
375
|
+
if (reasoning) {
|
|
376
|
+
if (!thinkingStarted) {
|
|
377
|
+
thinkingStarted = true;
|
|
378
|
+
yield { type: "thinking_start" };
|
|
379
|
+
}
|
|
380
|
+
yield {
|
|
381
|
+
type: "thinking_delta",
|
|
382
|
+
text: reasoning
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
const content = stringOrNull(delta.content);
|
|
386
|
+
if (content) yield {
|
|
387
|
+
type: "text_delta",
|
|
388
|
+
text: content
|
|
389
|
+
};
|
|
390
|
+
if (Array.isArray(delta.tool_calls)) collectToolCalls(delta.tool_calls, toolCalls);
|
|
391
|
+
}
|
|
392
|
+
if (choice.finish_reason === "tool_calls") yield* flushToolCalls(toolCalls);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
yield* flushToolCalls(toolCalls);
|
|
396
|
+
yield {
|
|
397
|
+
type: "response",
|
|
398
|
+
usage
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function* readServerSentEvents(body, signal) {
|
|
402
|
+
if (!body) return;
|
|
403
|
+
const reader = body.getReader();
|
|
404
|
+
const decoder = new TextDecoder();
|
|
405
|
+
let buffer = "";
|
|
406
|
+
let eventName = null;
|
|
407
|
+
let dataLines = [];
|
|
408
|
+
const flush = function* () {
|
|
409
|
+
if (dataLines.length === 0) return;
|
|
410
|
+
yield {
|
|
411
|
+
event: eventName,
|
|
412
|
+
data: dataLines.join("\n")
|
|
413
|
+
};
|
|
414
|
+
eventName = null;
|
|
415
|
+
dataLines = [];
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
while (true) {
|
|
419
|
+
if (signal?.aborted) return;
|
|
420
|
+
const { value, done } = await reader.read();
|
|
421
|
+
if (done) break;
|
|
422
|
+
buffer += decoder.decode(value, { stream: true });
|
|
423
|
+
let newline = buffer.indexOf("\n");
|
|
424
|
+
while (newline !== -1) {
|
|
425
|
+
const raw = buffer.slice(0, newline);
|
|
426
|
+
buffer = buffer.slice(newline + 1);
|
|
427
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
428
|
+
if (line === "") yield* flush();
|
|
429
|
+
else if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
430
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
|
431
|
+
newline = buffer.indexOf("\n");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
buffer += decoder.decode();
|
|
435
|
+
if (buffer) {
|
|
436
|
+
const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
437
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
|
438
|
+
else if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
439
|
+
}
|
|
440
|
+
yield* flush();
|
|
441
|
+
} finally {
|
|
442
|
+
reader.releaseLock();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function inferenceItemsToMessages(systemPrompt, items) {
|
|
446
|
+
const messages = [];
|
|
447
|
+
let assistant = null;
|
|
448
|
+
const flushAssistant = () => {
|
|
449
|
+
if (!assistant) return;
|
|
450
|
+
messages.push({
|
|
451
|
+
role: "assistant",
|
|
452
|
+
content: assistant.content || null,
|
|
453
|
+
...assistant.toolCalls.length > 0 ? { tool_calls: assistant.toolCalls } : {}
|
|
454
|
+
});
|
|
455
|
+
assistant = null;
|
|
456
|
+
};
|
|
457
|
+
if (systemPrompt.trim()) messages.push({
|
|
458
|
+
role: "system",
|
|
459
|
+
content: systemPrompt
|
|
460
|
+
});
|
|
461
|
+
for (const item of items) switch (item.type) {
|
|
462
|
+
case "user_message":
|
|
463
|
+
case "user_steer":
|
|
464
|
+
flushAssistant();
|
|
465
|
+
messages.push({
|
|
466
|
+
role: "user",
|
|
467
|
+
content: userContentToParts(item.content)
|
|
468
|
+
});
|
|
469
|
+
break;
|
|
470
|
+
case "assistant_text":
|
|
471
|
+
assistant ??= {
|
|
472
|
+
content: "",
|
|
473
|
+
toolCalls: []
|
|
474
|
+
};
|
|
475
|
+
assistant.content += item.text;
|
|
476
|
+
break;
|
|
477
|
+
case "tool_use":
|
|
478
|
+
assistant ??= {
|
|
479
|
+
content: "",
|
|
480
|
+
toolCalls: []
|
|
481
|
+
};
|
|
482
|
+
assistant.toolCalls.push({
|
|
483
|
+
id: item.toolUseId,
|
|
484
|
+
type: "function",
|
|
485
|
+
function: {
|
|
486
|
+
name: item.toolName,
|
|
487
|
+
arguments: stringifyToolArguments(item.input)
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
break;
|
|
491
|
+
case "tool_result":
|
|
492
|
+
flushAssistant();
|
|
493
|
+
messages.push({
|
|
494
|
+
role: "tool",
|
|
495
|
+
tool_call_id: item.toolUseId,
|
|
496
|
+
content: toolResultContentToText(item.output)
|
|
497
|
+
});
|
|
498
|
+
break;
|
|
499
|
+
case "assistant_thinking":
|
|
500
|
+
case "assistant_redacted_thinking": break;
|
|
501
|
+
}
|
|
502
|
+
flushAssistant();
|
|
503
|
+
return messages;
|
|
504
|
+
}
|
|
505
|
+
function userContentToParts(content) {
|
|
506
|
+
const parts = [];
|
|
507
|
+
for (const block of content) if (block.type === "text") parts.push({
|
|
508
|
+
type: "text",
|
|
509
|
+
text: block.text
|
|
510
|
+
});
|
|
511
|
+
else if (block.type === "reference") parts.push({
|
|
512
|
+
type: "text",
|
|
513
|
+
text: block.reference
|
|
514
|
+
});
|
|
515
|
+
else if (block.type === "document") parts.push({
|
|
516
|
+
type: "text",
|
|
517
|
+
text: `[document:${block.source.fileName} ${block.source.mediaType}]`
|
|
518
|
+
});
|
|
519
|
+
else if (block.type === "video") parts.push({
|
|
520
|
+
type: "text",
|
|
521
|
+
text: `[video:${block.source.type === "url" ? block.source.url : block.source.mediaType}]`
|
|
522
|
+
});
|
|
523
|
+
else if (block.source.type === "url") parts.push({
|
|
524
|
+
type: "image_url",
|
|
525
|
+
image_url: {
|
|
526
|
+
url: block.source.url,
|
|
527
|
+
detail: "auto"
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
else parts.push({
|
|
531
|
+
type: "image_url",
|
|
532
|
+
image_url: {
|
|
533
|
+
url: `data:${block.source.mediaType};base64,${Buffer.from(block.source.data).toString("base64")}`,
|
|
534
|
+
detail: "auto"
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
if (parts.every((part) => part.type === "text")) return parts.map((part) => part.text).join("\n");
|
|
538
|
+
return parts;
|
|
539
|
+
}
|
|
540
|
+
function toolToGrokTool(tool) {
|
|
541
|
+
return {
|
|
542
|
+
type: "function",
|
|
543
|
+
function: {
|
|
544
|
+
name: tool.name,
|
|
545
|
+
description: tool.description,
|
|
546
|
+
parameters: tool.inputSchema
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function collectToolCalls(values, toolCalls) {
|
|
551
|
+
for (const value of values) {
|
|
552
|
+
if (!isRecord(value)) continue;
|
|
553
|
+
const index = typeof value.index === "number" ? value.index : toolCalls.size;
|
|
554
|
+
const existing = toolCalls.get(index) ?? {
|
|
555
|
+
id: "",
|
|
556
|
+
name: "",
|
|
557
|
+
arguments: ""
|
|
558
|
+
};
|
|
559
|
+
const fn = isRecord(value.function) ? value.function : null;
|
|
560
|
+
const id = stringOrNull(value.id);
|
|
561
|
+
if (id) existing.id = id;
|
|
562
|
+
const name = stringOrNull(fn?.name);
|
|
563
|
+
if (name) existing.name = name;
|
|
564
|
+
const delta = stringOrNull(fn?.arguments);
|
|
565
|
+
if (delta) existing.arguments += delta;
|
|
566
|
+
toolCalls.set(index, existing);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function* flushToolCalls(toolCalls) {
|
|
570
|
+
for (const [index, call] of [...toolCalls.entries()].sort(([a], [b]) => a - b)) {
|
|
571
|
+
if (!call.name) continue;
|
|
572
|
+
yield {
|
|
573
|
+
type: "tool_call_requested",
|
|
574
|
+
toolUseId: call.id || `tool_call_${index}`,
|
|
575
|
+
toolName: call.name,
|
|
576
|
+
input: parseJsonOrString(call.arguments || "{}")
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
toolCalls.clear();
|
|
580
|
+
}
|
|
581
|
+
function thinkingToReasoningEffort(request) {
|
|
582
|
+
const thinking = request.thinking;
|
|
583
|
+
if (!thinking || thinking.type === "disabled" || thinking.type === "budget") return void 0;
|
|
584
|
+
return thinking.effort;
|
|
585
|
+
}
|
|
586
|
+
function stringifyToolArguments(input) {
|
|
587
|
+
return typeof input === "string" ? input : JSON.stringify(input ?? {});
|
|
588
|
+
}
|
|
589
|
+
function grokUsage(usage) {
|
|
590
|
+
const inputTokens = numberOrZero(usage.prompt_tokens);
|
|
591
|
+
const outputTokens = numberOrZero(usage.completion_tokens);
|
|
592
|
+
const promptDetails = isRecord(usage.prompt_tokens_details) ? usage.prompt_tokens_details : null;
|
|
593
|
+
const cachedTokens = promptDetails ? numberOrZero(promptDetails.cached_tokens) : 0;
|
|
594
|
+
return {
|
|
595
|
+
inputTokens: Math.max(0, inputTokens - cachedTokens),
|
|
596
|
+
outputTokens,
|
|
597
|
+
cacheReadTokens: cachedTokens,
|
|
598
|
+
cacheWriteTokens: 0
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/credentials.ts
|
|
603
|
+
var PoolAwareGrokAuthStore = class {
|
|
604
|
+
pool;
|
|
605
|
+
vendorHome;
|
|
606
|
+
fileAuthOptions;
|
|
607
|
+
constructor(pool, options = {}) {
|
|
608
|
+
this.pool = pool;
|
|
609
|
+
this.vendorHome = options.grokHome ?? defaultGrokHome();
|
|
610
|
+
this.fileAuthOptions = options.fileAuthOptions ?? {};
|
|
611
|
+
}
|
|
612
|
+
async status() {
|
|
613
|
+
return this.currentStore().then((s) => s.status());
|
|
614
|
+
}
|
|
615
|
+
async resolveAuth(options) {
|
|
616
|
+
return this.currentStore().then((s) => s.resolveAuth(options));
|
|
617
|
+
}
|
|
618
|
+
async currentStore() {
|
|
619
|
+
await this.pool.ensureActivePointer();
|
|
620
|
+
const activeId = await this.pool.getActiveId();
|
|
621
|
+
if (activeId) {
|
|
622
|
+
const entryKey = (await this.pool.readMeta(activeId))?.identityKey ?? void 0;
|
|
623
|
+
return new FileGrokAuthStore({
|
|
624
|
+
...this.fileAuthOptions,
|
|
625
|
+
authFile: this.pool.secretPath(activeId),
|
|
626
|
+
grokHome: this.pool.entryDir(activeId),
|
|
627
|
+
entryKey
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
return new FileGrokAuthStore({
|
|
631
|
+
...this.fileAuthOptions,
|
|
632
|
+
grokHome: this.vendorHome
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
function openGrokCredentialPool(options = {}) {
|
|
637
|
+
return new FileCredentialPool({
|
|
638
|
+
stateDir: options.stateDir,
|
|
639
|
+
providerKey: "grok-build",
|
|
640
|
+
secretFileName: "auth.json"
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
function createGrokBuildCredentials(pool, authStore, options = {}) {
|
|
644
|
+
const vendorHome = options.grokHome ?? defaultGrokHome();
|
|
645
|
+
const loginCommand = options.loginCommand ?? "grok";
|
|
646
|
+
const loginArgs = options.loginArgs ?? ["login"];
|
|
647
|
+
const capability = () => ({
|
|
648
|
+
mode: "supported",
|
|
649
|
+
canBeginLogin: true,
|
|
650
|
+
canImportDefault: true,
|
|
651
|
+
canAdd: true,
|
|
652
|
+
multi: true
|
|
653
|
+
});
|
|
654
|
+
const getActive = async () => {
|
|
655
|
+
await pool.ensureActivePointer();
|
|
656
|
+
return {
|
|
657
|
+
credentialId: await pool.getActiveId(),
|
|
658
|
+
status: await authStore.status()
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
const setActive = async (credentialId) => {
|
|
662
|
+
await pool.setActiveId(credentialId);
|
|
663
|
+
options.quota?.clearLatest?.();
|
|
664
|
+
return getActive();
|
|
665
|
+
};
|
|
666
|
+
const importEntry = async (entryKey, entry, source) => {
|
|
667
|
+
const label = nonEmptyString(entry.email) ?? entryKey;
|
|
668
|
+
const identityKey = entryKey;
|
|
669
|
+
const id = (await pool.findByIdentityKey(identityKey))?.id ?? credentialIdFromIdentity(identityKey, label);
|
|
670
|
+
const file = { [entryKey]: entry };
|
|
671
|
+
const meta = {
|
|
672
|
+
id,
|
|
673
|
+
label,
|
|
674
|
+
detail: nonEmptyString(entry.auth_mode) ?? "oidc",
|
|
675
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
676
|
+
source,
|
|
677
|
+
identityKey
|
|
678
|
+
};
|
|
679
|
+
await pool.writeEntry(meta, `${JSON.stringify(file, null, 2)}\n`);
|
|
680
|
+
if (!await pool.getActiveId()) await pool.setActiveId(id);
|
|
681
|
+
options.quota?.clearLatest?.();
|
|
682
|
+
return {
|
|
683
|
+
id: meta.id,
|
|
684
|
+
label: meta.label,
|
|
685
|
+
detail: meta.detail,
|
|
686
|
+
updatedAt: meta.updatedAt
|
|
687
|
+
};
|
|
688
|
+
};
|
|
689
|
+
const importFromAuthJsonText = async (text, source) => {
|
|
690
|
+
let file;
|
|
691
|
+
try {
|
|
692
|
+
file = JSON.parse(text);
|
|
693
|
+
} catch {
|
|
694
|
+
throw new GrokAuthError("auth_invalid", "Grok auth material is not valid JSON");
|
|
695
|
+
}
|
|
696
|
+
if (!isRecord(file)) throw new GrokAuthError("auth_invalid", "Grok auth material is not an object");
|
|
697
|
+
const imported = [];
|
|
698
|
+
for (const [entryKey, value] of Object.entries(file)) {
|
|
699
|
+
if (!isRecord(value)) continue;
|
|
700
|
+
const entry = value;
|
|
701
|
+
if (!nonEmptyString(entry.key)) continue;
|
|
702
|
+
imported.push(await importEntry(entryKey, entry, source));
|
|
703
|
+
}
|
|
704
|
+
if (imported.length === 0) throw new GrokAuthError("auth_missing", "No Grok OAuth entries with access tokens found to import");
|
|
705
|
+
return imported;
|
|
706
|
+
};
|
|
707
|
+
return {
|
|
708
|
+
capability,
|
|
709
|
+
list: () => pool.list(),
|
|
710
|
+
getActive,
|
|
711
|
+
setActive,
|
|
712
|
+
beginLogin: async (loginOptions) => {
|
|
713
|
+
const result = await runVendorLoginCommand(loginCommand, loginArgs, { signal: loginOptions?.signal });
|
|
714
|
+
if (result.status === "completed") return { status: "completed" };
|
|
715
|
+
if (result.status === "cancelled") return { status: "cancelled" };
|
|
716
|
+
if (result.status === "unavailable") return {
|
|
717
|
+
status: "unavailable",
|
|
718
|
+
message: result.message ?? "Login unavailable"
|
|
719
|
+
};
|
|
720
|
+
return {
|
|
721
|
+
status: "failed",
|
|
722
|
+
message: result.message ?? "Login failed"
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
importDefault: async () => {
|
|
726
|
+
const authFile = join(vendorHome, "auth.json");
|
|
727
|
+
let text;
|
|
728
|
+
try {
|
|
729
|
+
text = await readFile(authFile, "utf8");
|
|
730
|
+
} catch {
|
|
731
|
+
throw new GrokAuthError("auth_missing", `No Grok auth at ${authFile}. Run grok login or beginLogin first.`);
|
|
732
|
+
}
|
|
733
|
+
const all = await importFromAuthJsonText(text, `vendor:${authFile}`);
|
|
734
|
+
const preferred = selectAuthEntry(JSON.parse(text));
|
|
735
|
+
if (preferred) {
|
|
736
|
+
const byKey = (await pool.listMeta()).find((m) => m.identityKey === preferred.entryKey);
|
|
737
|
+
if (byKey) {
|
|
738
|
+
await pool.setActiveId(byKey.id);
|
|
739
|
+
options.quota?.clearLatest?.();
|
|
740
|
+
return {
|
|
741
|
+
id: byKey.id,
|
|
742
|
+
label: byKey.label,
|
|
743
|
+
detail: byKey.detail,
|
|
744
|
+
updatedAt: byKey.updatedAt
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return all[0];
|
|
749
|
+
},
|
|
750
|
+
add: async (input) => {
|
|
751
|
+
if (typeof input.authJsonText === "string") return (await importFromAuthJsonText(input.authJsonText, "add:authJsonText"))[0];
|
|
752
|
+
if (typeof input.authFile === "string") {
|
|
753
|
+
const text = await readFile(input.authFile, "utf8");
|
|
754
|
+
return (await importFromAuthJsonText(text, `add:authFile:${input.authFile}`))[0];
|
|
755
|
+
}
|
|
756
|
+
if (typeof input.entryKey === "string" && isRecord(input.entry)) return importEntry(input.entryKey, input.entry, "add:entry");
|
|
757
|
+
throw new Error("Grok credentials.add expects authJsonText, authFile, or { entryKey, entry }");
|
|
758
|
+
},
|
|
759
|
+
remove: async (credentialId) => {
|
|
760
|
+
await pool.remove(credentialId);
|
|
761
|
+
options.quota?.clearLatest?.();
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const GROK_CLI_TOKEN_AUTH = "xai-grok-cli";
|
|
766
|
+
const GROK_CLIENT_SURFACE = "grok-build";
|
|
767
|
+
function buildGrokBuildHeaders(auth, request, options) {
|
|
768
|
+
const headers = new Headers(options?.extra);
|
|
769
|
+
headers.set("authorization", `Bearer ${auth.accessToken}`);
|
|
770
|
+
headers.set("X-XAI-Token-Auth", GROK_CLI_TOKEN_AUTH);
|
|
771
|
+
headers.set("x-grok-client-surface", GROK_CLIENT_SURFACE);
|
|
772
|
+
headers.set("x-grok-client-version", resolveGrokClientVersion(options?.clientVersion, options?.grokHome));
|
|
773
|
+
if (request?.modelId) headers.set("x-grok-model-override", request.modelId);
|
|
774
|
+
if (request?.sessionId) headers.set("x-grok-session-id", request.sessionId);
|
|
775
|
+
if (request?.requestId) headers.set("x-grok-req-id", request.requestId);
|
|
776
|
+
return headers;
|
|
777
|
+
}
|
|
778
|
+
function resolveGrokClientVersion(explicit, grokHome) {
|
|
779
|
+
const fromExplicit = nonEmptyString(explicit);
|
|
780
|
+
if (fromExplicit) return fromExplicit;
|
|
781
|
+
return readGrokCliVersion(grokHome ?? defaultGrokHome()) ?? "0.1.202";
|
|
782
|
+
}
|
|
783
|
+
function readGrokCliVersion(grokHome) {
|
|
784
|
+
try {
|
|
785
|
+
const parsed = JSON.parse(readFileSync(join(grokHome, "version.json"), "utf8"));
|
|
786
|
+
if (!isRecord(parsed)) return null;
|
|
787
|
+
return nonEmptyString(parsed.version) ?? null;
|
|
788
|
+
} catch {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/models.ts
|
|
794
|
+
const FALLBACK_SOURCE_FETCHED_AT = "1970-01-01T00:00:00.000Z";
|
|
795
|
+
function grokBuildFallbackModels(providerId = "grok-build") {
|
|
796
|
+
const sourceFetchedAt = FALLBACK_SOURCE_FETCHED_AT;
|
|
797
|
+
const model = {
|
|
798
|
+
providerId,
|
|
799
|
+
id: "grok-4.5",
|
|
800
|
+
displayName: "Grok 4.5",
|
|
801
|
+
description: "Grok Build frontier model",
|
|
802
|
+
contextWindow: 5e5,
|
|
803
|
+
outputLimit: null,
|
|
804
|
+
supportsTools: true,
|
|
805
|
+
supportsAttachments: capabilitiesForModel("grok-4.5").supportsAttachments,
|
|
806
|
+
supportsReasoning: true,
|
|
807
|
+
supportedThinkingEfforts: [
|
|
808
|
+
"low",
|
|
809
|
+
"medium",
|
|
810
|
+
"high"
|
|
811
|
+
],
|
|
812
|
+
defaultThinkingEffort: "high",
|
|
813
|
+
canDisableThinking: null,
|
|
814
|
+
serviceTiers: null,
|
|
815
|
+
defaultServiceTierId: null,
|
|
816
|
+
sourceFetchedAt,
|
|
817
|
+
stale: true
|
|
818
|
+
};
|
|
819
|
+
return {
|
|
820
|
+
providerId,
|
|
821
|
+
models: [model],
|
|
822
|
+
defaultModelId: model.id,
|
|
823
|
+
warnings: ["Using fallback Grok Build model catalog (live /v1/models unavailable)"],
|
|
824
|
+
sourceFetchedAt,
|
|
825
|
+
stale: true
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
async function listGrokBuildModels(options = {}) {
|
|
829
|
+
const providerId = options.providerId ?? "grok-build";
|
|
830
|
+
const authStore = options.authStore ?? new FileGrokAuthStore({ grokHome: options.grokHome });
|
|
831
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
832
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://cli-chat-proxy.grok.com/v1");
|
|
833
|
+
try {
|
|
834
|
+
const auth = await authStore.resolveAuth();
|
|
835
|
+
const response = await fetchImpl(modelsUrl(baseUrl), {
|
|
836
|
+
method: "GET",
|
|
837
|
+
headers: buildGrokBuildHeaders(auth, void 0, {
|
|
838
|
+
clientVersion: options.clientVersion,
|
|
839
|
+
grokHome: options.grokHome
|
|
840
|
+
})
|
|
841
|
+
});
|
|
842
|
+
if (!response.ok) return {
|
|
843
|
+
...grokBuildFallbackModels(providerId),
|
|
844
|
+
warnings: [`Grok Build /v1/models returned HTTP ${response.status}; using fallback catalog`]
|
|
845
|
+
};
|
|
846
|
+
return modelListFromGrokModelsPayload(await response.json(), providerId);
|
|
847
|
+
} catch {
|
|
848
|
+
return grokBuildFallbackModels(providerId);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function modelListFromGrokModelsPayload(payload, providerId) {
|
|
852
|
+
const sourceFetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
853
|
+
const data = isRecord(payload) && Array.isArray(payload.data) ? payload.data : Array.isArray(payload) ? payload : [];
|
|
854
|
+
const models = [];
|
|
855
|
+
for (const item of data) {
|
|
856
|
+
if (!isRecord(item)) continue;
|
|
857
|
+
const id = nonEmptyString(item.id) ?? nonEmptyString(item.model);
|
|
858
|
+
if (!id) continue;
|
|
859
|
+
const reasoningEfforts = parseReasoningEfforts(item);
|
|
860
|
+
models.push({
|
|
861
|
+
providerId,
|
|
862
|
+
id,
|
|
863
|
+
displayName: stringOrNull(item.name) ?? id,
|
|
864
|
+
description: stringOrNull(item.description) ?? void 0,
|
|
865
|
+
contextWindow: positiveOrDefault(numberOrNull(item.context_window), 2e5),
|
|
866
|
+
outputLimit: null,
|
|
867
|
+
supportsTools: true,
|
|
868
|
+
supportsAttachments: capabilitiesForModel(id).supportsAttachments,
|
|
869
|
+
supportsReasoning: item.supports_reasoning_effort === true || reasoningEfforts.length > 0 ? true : null,
|
|
870
|
+
supportedThinkingEfforts: reasoningEfforts.length > 0 ? reasoningEfforts : null,
|
|
871
|
+
defaultThinkingEffort: defaultReasoningEffort(item, reasoningEfforts),
|
|
872
|
+
canDisableThinking: null,
|
|
873
|
+
serviceTiers: null,
|
|
874
|
+
defaultServiceTierId: null,
|
|
875
|
+
sourceFetchedAt,
|
|
876
|
+
stale: false
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (models.length === 0) return grokBuildFallbackModels(providerId);
|
|
880
|
+
return {
|
|
881
|
+
providerId,
|
|
882
|
+
models,
|
|
883
|
+
defaultModelId: models[0]?.id ?? null,
|
|
884
|
+
warnings: [],
|
|
885
|
+
sourceFetchedAt,
|
|
886
|
+
stale: false
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
const DEFAULT_MODEL_CAPABILITIES = { supportsAttachments: false };
|
|
890
|
+
/**
|
|
891
|
+
* Known Grok Build models. Extend this table when the proxy adds ids;
|
|
892
|
+
* do not invent prefix heuristics here.
|
|
893
|
+
*/
|
|
894
|
+
const KNOWN_MODEL_CAPABILITIES = {
|
|
895
|
+
"grok-4.5": { supportsAttachments: true },
|
|
896
|
+
"grok-composer-2.5-fast": { supportsAttachments: false }
|
|
897
|
+
};
|
|
898
|
+
function capabilitiesForModel(modelId) {
|
|
899
|
+
return KNOWN_MODEL_CAPABILITIES[modelId] ?? DEFAULT_MODEL_CAPABILITIES;
|
|
900
|
+
}
|
|
901
|
+
function modelsUrl(baseUrl) {
|
|
902
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
903
|
+
return normalized.endsWith("/models") ? normalized : `${normalized}/models`;
|
|
904
|
+
}
|
|
905
|
+
function parseReasoningEfforts(item) {
|
|
906
|
+
if (!Array.isArray(item.reasoning_efforts)) return [];
|
|
907
|
+
const ids = [];
|
|
908
|
+
for (const entry of item.reasoning_efforts) {
|
|
909
|
+
if (!isRecord(entry)) continue;
|
|
910
|
+
const id = nonEmptyString(entry.id) ?? nonEmptyString(entry.value);
|
|
911
|
+
if (id) ids.push(id);
|
|
912
|
+
}
|
|
913
|
+
return ids;
|
|
914
|
+
}
|
|
915
|
+
function defaultReasoningEffort(item, efforts) {
|
|
916
|
+
if (Array.isArray(item.reasoning_efforts)) {
|
|
917
|
+
for (const entry of item.reasoning_efforts) if (isRecord(entry) && entry.default === true) return nonEmptyString(entry.id) ?? nonEmptyString(entry.value) ?? null;
|
|
918
|
+
}
|
|
919
|
+
const advertised = nonEmptyString(item.reasoning_effort);
|
|
920
|
+
if (advertised) return advertised;
|
|
921
|
+
return efforts[0] ?? null;
|
|
922
|
+
}
|
|
923
|
+
function positiveOrDefault(value, fallback) {
|
|
924
|
+
return value !== null && Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/quota.ts
|
|
928
|
+
/**
|
|
929
|
+
* Active probe against cli-chat-proxy:
|
|
930
|
+
* - GET /v1/user?include=subscription → tier
|
|
931
|
+
* - GET /v1/billing → monthly used/limit
|
|
932
|
+
*
|
|
933
|
+
* Optional observation of short-window x-ratelimit-* headers from chat responses
|
|
934
|
+
* (separate windows from monthly subscription quota).
|
|
935
|
+
*/
|
|
936
|
+
function createGrokBuildQuota(options = {}) {
|
|
937
|
+
const providerId = options.providerId ?? "grok-build";
|
|
938
|
+
const authStore = options.authStore ?? new FileGrokAuthStore({ grokHome: options.grokHome });
|
|
939
|
+
const baseUrl = (options.baseUrl ?? "https://cli-chat-proxy.grok.com/v1").replace(/\/+$/, "");
|
|
940
|
+
const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
941
|
+
const grokHome = options.grokHome;
|
|
942
|
+
const clientVersion = options.clientVersion;
|
|
943
|
+
return createProviderQuota({
|
|
944
|
+
providerId,
|
|
945
|
+
canProbe: true,
|
|
946
|
+
canObserve: true,
|
|
947
|
+
probeCost: "free",
|
|
948
|
+
staleAfterMs: 6e4,
|
|
949
|
+
probe: async ({ signal } = {}) => {
|
|
950
|
+
const auth = await authStore.resolveAuth();
|
|
951
|
+
const [user, billing] = await Promise.all([fetchJson(fetchImpl, `${baseUrl}/user?include=subscription`, auth, {
|
|
952
|
+
grokHome,
|
|
953
|
+
clientVersion
|
|
954
|
+
}, signal), fetchJson(fetchImpl, `${baseUrl}/billing`, auth, {
|
|
955
|
+
grokHome,
|
|
956
|
+
clientVersion
|
|
957
|
+
}, signal)]);
|
|
958
|
+
return mapGrokQuotaProbe(user, billing, auth);
|
|
959
|
+
},
|
|
960
|
+
observe: ({ headers }) => observeGrokRateLimitHeaders(headers)
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
function mapGrokQuotaProbe(user, billing, auth) {
|
|
964
|
+
const userRecord = isRecord(user) ? user : {};
|
|
965
|
+
const billingRecord = isRecord(billing) ? billing : {};
|
|
966
|
+
const config = isRecord(billingRecord.config) ? billingRecord.config : {};
|
|
967
|
+
const monthlyLimit = moneyVal(config.monthlyLimit);
|
|
968
|
+
const used = moneyVal(config.used);
|
|
969
|
+
const onDemandCap = moneyVal(config.onDemandCap);
|
|
970
|
+
const usedPercent = usedPercentFromRatio(used, monthlyLimit);
|
|
971
|
+
const windows = [{
|
|
972
|
+
id: "monthly",
|
|
973
|
+
label: "Monthly credits",
|
|
974
|
+
usedPercent,
|
|
975
|
+
used,
|
|
976
|
+
limit: monthlyLimit,
|
|
977
|
+
unit: "credits",
|
|
978
|
+
resetsAt: stringOrNull(config.billingPeriodEnd),
|
|
979
|
+
severity: severityFromUsedPercent(usedPercent)
|
|
980
|
+
}];
|
|
981
|
+
if (onDemandCap != null && onDemandCap > 0) windows.push({
|
|
982
|
+
id: "on_demand_cap",
|
|
983
|
+
label: "On-demand cap",
|
|
984
|
+
usedPercent: null,
|
|
985
|
+
used: null,
|
|
986
|
+
limit: onDemandCap,
|
|
987
|
+
unit: "credits",
|
|
988
|
+
resetsAt: stringOrNull(config.billingPeriodEnd)
|
|
989
|
+
});
|
|
990
|
+
const tier = stringOrNull(userRecord.subscriptionTier);
|
|
991
|
+
return {
|
|
992
|
+
plan: tier ? {
|
|
993
|
+
id: tier,
|
|
994
|
+
label: tier,
|
|
995
|
+
raw: tier
|
|
996
|
+
} : null,
|
|
997
|
+
accountLabel: auth?.email ?? stringOrNull(userRecord.email),
|
|
998
|
+
windows,
|
|
999
|
+
raw: {
|
|
1000
|
+
user,
|
|
1001
|
+
billing
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
/** Short-window chat ratelimits — not subscription monthly quota. */
|
|
1006
|
+
function observeGrokRateLimitHeaders(headers) {
|
|
1007
|
+
if (!headers) return null;
|
|
1008
|
+
const remReq = numberHeader(headers, "x-ratelimit-remaining-requests");
|
|
1009
|
+
const limReq = numberHeader(headers, "x-ratelimit-limit-requests");
|
|
1010
|
+
const remTok = numberHeader(headers, "x-ratelimit-remaining-tokens");
|
|
1011
|
+
const limTok = numberHeader(headers, "x-ratelimit-limit-tokens");
|
|
1012
|
+
if (remReq == null && limReq == null && remTok == null && limTok == null) return null;
|
|
1013
|
+
const windows = [];
|
|
1014
|
+
if (limReq != null) {
|
|
1015
|
+
const used = remReq != null ? limReq - remReq : null;
|
|
1016
|
+
const usedPercent = usedPercentFromRatio(used, limReq);
|
|
1017
|
+
windows.push({
|
|
1018
|
+
id: "rpm",
|
|
1019
|
+
label: "Requests (short window)",
|
|
1020
|
+
usedPercent,
|
|
1021
|
+
used,
|
|
1022
|
+
limit: limReq,
|
|
1023
|
+
unit: "requests",
|
|
1024
|
+
resetsAt: null,
|
|
1025
|
+
severity: severityFromUsedPercent(usedPercent)
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
if (limTok != null) {
|
|
1029
|
+
const used = remTok != null ? limTok - remTok : null;
|
|
1030
|
+
const usedPercent = usedPercentFromRatio(used, limTok);
|
|
1031
|
+
windows.push({
|
|
1032
|
+
id: "tpm",
|
|
1033
|
+
label: "Tokens (short window)",
|
|
1034
|
+
usedPercent,
|
|
1035
|
+
used,
|
|
1036
|
+
limit: limTok,
|
|
1037
|
+
unit: "tokens",
|
|
1038
|
+
resetsAt: null,
|
|
1039
|
+
severity: severityFromUsedPercent(usedPercent)
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
return windows.length > 0 ? { windows } : null;
|
|
1043
|
+
}
|
|
1044
|
+
async function fetchJson(fetchImpl, url, auth, opts, signal) {
|
|
1045
|
+
const headers = buildGrokBuildHeaders(auth, void 0, {
|
|
1046
|
+
clientVersion: opts.clientVersion ?? resolveGrokClientVersion(void 0, opts.grokHome),
|
|
1047
|
+
grokHome: opts.grokHome
|
|
1048
|
+
});
|
|
1049
|
+
headers.set("accept", "application/json");
|
|
1050
|
+
const response = await fetchImpl(url, {
|
|
1051
|
+
method: "GET",
|
|
1052
|
+
headers,
|
|
1053
|
+
signal
|
|
1054
|
+
});
|
|
1055
|
+
if (!response.ok) {
|
|
1056
|
+
const body = await response.text().catch(() => "");
|
|
1057
|
+
throw new Error(`Grok quota request failed (${response.status}): ${body.slice(0, 200)}`);
|
|
1058
|
+
}
|
|
1059
|
+
return response.json();
|
|
1060
|
+
}
|
|
1061
|
+
function moneyVal(value) {
|
|
1062
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1063
|
+
if (isRecord(value) && typeof value.val === "number" && Number.isFinite(value.val)) return value.val;
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/provider.ts
|
|
1068
|
+
var GrokBuildProvider = class {
|
|
1069
|
+
options;
|
|
1070
|
+
constructor(options) {
|
|
1071
|
+
this.options = options;
|
|
1072
|
+
}
|
|
1073
|
+
async *run(request) {
|
|
1074
|
+
if (request.cancel.aborted) {
|
|
1075
|
+
yield { type: "abort" };
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
let forceRefresh = false;
|
|
1079
|
+
let accessToken;
|
|
1080
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1081
|
+
let auth;
|
|
1082
|
+
try {
|
|
1083
|
+
auth = await this.options.authStore.resolveAuth({ forceRefresh });
|
|
1084
|
+
accessToken = auth.accessToken;
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
if (error instanceof GrokAuthError && error.code === "auth_missing") {
|
|
1087
|
+
yield {
|
|
1088
|
+
type: "error",
|
|
1089
|
+
message: error.message,
|
|
1090
|
+
code: "auth_missing"
|
|
1091
|
+
};
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
yield providerErrorFromUnknown(error, accessToken);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const headers = buildGrokBuildHeaders(auth, request, {
|
|
1099
|
+
extra: this.options.headers,
|
|
1100
|
+
clientVersion: this.options.clientVersion,
|
|
1101
|
+
grokHome: this.options.grokHome
|
|
1102
|
+
});
|
|
1103
|
+
headers.set("accept", "text/event-stream");
|
|
1104
|
+
headers.set("content-type", "application/json");
|
|
1105
|
+
const response = await this.options.fetch(chatCompletionsUrl(this.options.baseUrl), {
|
|
1106
|
+
method: "POST",
|
|
1107
|
+
headers,
|
|
1108
|
+
body: JSON.stringify(buildGrokChatCompletionsBody(request)),
|
|
1109
|
+
signal: request.cancel
|
|
1110
|
+
});
|
|
1111
|
+
try {
|
|
1112
|
+
this.options.quota?.observeResponse?.({
|
|
1113
|
+
headers: response.headers,
|
|
1114
|
+
status: response.status
|
|
1115
|
+
});
|
|
1116
|
+
} catch {}
|
|
1117
|
+
if (response.status === 401 && !forceRefresh) {
|
|
1118
|
+
await response.body?.cancel().catch(() => {});
|
|
1119
|
+
forceRefresh = true;
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
if (!response.ok) {
|
|
1123
|
+
yield await httpRequestFailedEvent(response, accessToken, "Grok Build");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
yield* mapGrokChatCompletionStream(readServerSentEvents(response.body, request.cancel), request.cancel);
|
|
1127
|
+
return;
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (request.cancel.aborted || isAbortError(error)) {
|
|
1130
|
+
yield { type: "abort" };
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
yield providerErrorFromUnknown(error, accessToken);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
function createGrokBuildProvider(options = {}) {
|
|
1140
|
+
const id = options.id ?? "grok-build";
|
|
1141
|
+
const displayName = options.displayName ?? "Grok Build";
|
|
1142
|
+
const enableCredentials = options.credentials ?? options.authStore === void 0;
|
|
1143
|
+
const pool = !options.authStore && enableCredentials ? openGrokCredentialPool({ stateDir: options.stateDir }) : null;
|
|
1144
|
+
const authStore = options.authStore ?? (pool ? new PoolAwareGrokAuthStore(pool, { grokHome: options.grokHome }) : new FileGrokAuthStore({ grokHome: options.grokHome }));
|
|
1145
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://cli-chat-proxy.grok.com/v1");
|
|
1146
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1147
|
+
const quota = createGrokBuildQuota({
|
|
1148
|
+
providerId: id,
|
|
1149
|
+
grokHome: options.grokHome,
|
|
1150
|
+
baseUrl,
|
|
1151
|
+
clientVersion: options.clientVersion,
|
|
1152
|
+
authStore,
|
|
1153
|
+
fetch: fetchImpl
|
|
1154
|
+
});
|
|
1155
|
+
const credentialsApi = pool ? createGrokBuildCredentials(pool, authStore, {
|
|
1156
|
+
grokHome: options.grokHome,
|
|
1157
|
+
quota
|
|
1158
|
+
}) : void 0;
|
|
1159
|
+
const runtimeOptions = {
|
|
1160
|
+
baseUrl,
|
|
1161
|
+
grokHome: options.grokHome,
|
|
1162
|
+
clientVersion: options.clientVersion,
|
|
1163
|
+
authStore,
|
|
1164
|
+
headers: options.headers,
|
|
1165
|
+
fetch: fetchImpl,
|
|
1166
|
+
quota
|
|
1167
|
+
};
|
|
1168
|
+
return defineProvider({
|
|
1169
|
+
id,
|
|
1170
|
+
displayName,
|
|
1171
|
+
auth: { status: () => authStore.status() },
|
|
1172
|
+
quota,
|
|
1173
|
+
...credentialsApi ? { credentials: credentialsApi } : {},
|
|
1174
|
+
state: () => ({
|
|
1175
|
+
status: "ready",
|
|
1176
|
+
message: credentialsApi ? "Uses Grok CLI OAuth + demi credential pool via cli-chat-proxy" : "Uses Grok CLI OAuth session (~/.grok/auth.json) via cli-chat-proxy"
|
|
1177
|
+
}),
|
|
1178
|
+
listModels: () => listGrokBuildModels({
|
|
1179
|
+
providerId: id,
|
|
1180
|
+
grokHome: options.grokHome,
|
|
1181
|
+
baseUrl,
|
|
1182
|
+
clientVersion: options.clientVersion,
|
|
1183
|
+
authStore,
|
|
1184
|
+
fetch: fetchImpl
|
|
1185
|
+
}),
|
|
1186
|
+
createRuntime: (_selection) => new GrokBuildProvider(runtimeOptions)
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
function parseGrokBuildProviderConfig(config) {
|
|
1190
|
+
if (config === void 0 || config === null) return {};
|
|
1191
|
+
if (!isRecord(config)) throw new Error("Grok Build provider config must be an object");
|
|
1192
|
+
const parsed = {};
|
|
1193
|
+
if (config.grokHome !== void 0) {
|
|
1194
|
+
if (typeof config.grokHome !== "string") throw new Error("Grok Build provider config field \"grokHome\" must be a string");
|
|
1195
|
+
parsed.grokHome = config.grokHome;
|
|
1196
|
+
}
|
|
1197
|
+
if (config.baseUrl !== void 0) {
|
|
1198
|
+
if (typeof config.baseUrl !== "string") throw new Error("Grok Build provider config field \"baseUrl\" must be a string");
|
|
1199
|
+
parsed.baseUrl = config.baseUrl;
|
|
1200
|
+
}
|
|
1201
|
+
if (config.headers !== void 0) {
|
|
1202
|
+
if (!isRecord(config.headers)) throw new Error("Grok Build provider config field \"headers\" must be an object");
|
|
1203
|
+
const headers = {};
|
|
1204
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
1205
|
+
if (typeof value !== "string") throw new Error(`Grok Build provider config headers.${key} must be a string`);
|
|
1206
|
+
headers[key] = value;
|
|
1207
|
+
}
|
|
1208
|
+
parsed.headers = headers;
|
|
1209
|
+
}
|
|
1210
|
+
return parsed;
|
|
1211
|
+
}
|
|
1212
|
+
function chatCompletionsUrl(baseUrl) {
|
|
1213
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
1214
|
+
return normalized.endsWith("/chat/completions") ? normalized : `${normalized}/chat/completions`;
|
|
1215
|
+
}
|
|
1216
|
+
//#endregion
|
|
1217
|
+
export { PoolAwareGrokAuthStore, createGrokBuildCredentials, createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, openGrokCredentialPool, parseGrokBuildProviderConfig };
|