@demicodes/provider-grok-build 0.10.2 → 0.10.3

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/README.md CHANGED
@@ -5,28 +5,8 @@ stored by the official Grok CLI in `~/.grok/auth.json` (subscription login).
5
5
 
6
6
  ```ts
7
7
  import { createGrokBuildProvider } from '@demicodes/provider-grok-build'
8
-
9
- const provider = createGrokBuildProvider()
10
- // provider.auth, provider.quota, provider.credentials (multi-account pool by default)
11
8
  ```
12
9
 
13
- ## Auth and credentials
14
-
15
- - Default material: `~/.grok/auth.json` (or `$GROK_HOME`); multi-entry map supported.
16
- - Multi-credential pool under `$DEMI_HOME/credentials/grok-build/`; `importDefault`
17
- can snapshot each OIDC entry as its own pool credential.
18
- - Lifecycle: `beginLogin` → `grok login` → `importDefault` → `setActive`.
19
-
20
- Requires a prior login (or `beginLogin`) so vendor material exists before import.
21
- See [docs/provider-global-credentials.md](../../docs/provider-global-credentials.md).
22
-
23
- ## Quota
24
-
25
- - **probe** (cost: `free`): `/v1/billing` + `/v1/user?include=subscription` on
26
- cli-chat-proxy.
27
- - **observe**: short-window ratelimit headers on chat responses (separate from monthly).
28
-
29
- See [docs/provider-quota.md](../../docs/provider-quota.md).
30
-
31
- Implements the [`@demicodes/provider`](../provider/README.md) contract. Part of
32
- [Demi](../../README.md). Apache-2.0.
10
+ Requires a prior `grok login` (or equivalent) so `~/.grok/auth.json` contains an
11
+ OIDC session. Implements the [`@demicodes/provider`](../provider/README.md)
12
+ contract. Part of [Demi](../../README.md). Apache-2.0.
package/dist/index.d.mts CHANGED
@@ -1,20 +1,6 @@
1
- import { Provider, ProviderAuthState, ProviderCredentials, ProviderModelList, ProviderQuota, ProviderQuotaProbeResult } from "@demicodes/provider";
2
- import { FileCredentialPool } from "@demicodes/provider/credentials-pool";
1
+ import { Provider, ProviderAuthState, ProviderModelList, ProviderQuota, ProviderQuotaProbeResult } from "@demicodes/provider";
2
+
3
3
  //#region src/auth.d.ts
4
- /** One credential entry as written by the Grok CLI (`~/.grok/auth.json`). */
5
- interface GrokAuthEntry {
6
- key?: string;
7
- auth_mode?: string;
8
- refresh_token?: string;
9
- expires_at?: string;
10
- oidc_issuer?: string;
11
- oidc_client_id?: string;
12
- email?: string;
13
- first_name?: string;
14
- user_id?: string;
15
- team_id?: string;
16
- [key: string]: unknown;
17
- }
18
4
  interface GrokResolvedAuth {
19
5
  accessToken: string;
20
6
  refreshToken: string | null;
@@ -33,13 +19,6 @@ interface GrokAuthStore {
33
19
  }
34
20
  interface FileGrokAuthStoreOptions {
35
21
  grokHome?: string;
36
- /** Override auth.json path (e.g. demi credential pool entry). */
37
- authFile?: string;
38
- /**
39
- * Prefer this map key when the auth file has multiple OIDC entries.
40
- * When unset, uses {@link selectAuthEntry} scoring.
41
- */
42
- entryKey?: string;
43
22
  refresh?: GrokTokenRefresh;
44
23
  now?: () => Date;
45
24
  lockRetryDelayMs?: number;
@@ -69,10 +48,6 @@ interface GrokBuildProviderOptions {
69
48
  authStore?: GrokAuthStore;
70
49
  headers?: Record<string, string>;
71
50
  fetch?: GrokBuildFetch;
72
- /** Demi state root for credential pool (`$DEMI_HOME` / `~/.demi`). */
73
- stateDir?: string;
74
- /** When true (default if `authStore` unset), attach multi-credential pool. */
75
- credentials?: boolean;
76
51
  }
77
52
  declare function createGrokBuildProvider(options?: GrokBuildProviderOptions): Provider;
78
53
  declare function parseGrokBuildProviderConfig(config: unknown): Pick<GrokBuildProviderOptions, 'grokHome' | 'baseUrl' | 'headers'>;
@@ -111,51 +86,4 @@ declare function mapGrokQuotaProbe(user: unknown, billing: unknown, auth?: Pick<
111
86
  /** Short-window chat ratelimits — not subscription monthly quota. */
112
87
  declare function observeGrokRateLimitHeaders(headers: Headers | undefined): ProviderQuotaProbeResult | null;
113
88
  //#endregion
114
- //#region src/credentials.d.ts
115
- declare class PoolAwareGrokAuthStore implements GrokAuthStore {
116
- private readonly pool;
117
- private readonly vendorHome;
118
- private readonly fileAuthOptions;
119
- constructor(pool: FileCredentialPool, options?: {
120
- grokHome?: string;
121
- fileAuthOptions?: Omit<FileGrokAuthStoreOptions, 'grokHome' | 'authFile' | 'entryKey'>;
122
- });
123
- status(): Promise<import("@demicodes/provider").ProviderAuthState>;
124
- resolveAuth(options?: {
125
- forceRefresh?: boolean;
126
- }): Promise<GrokResolvedAuth>;
127
- private currentStore;
128
- }
129
- declare function openGrokCredentialPool(options?: {
130
- stateDir?: string;
131
- }): FileCredentialPool;
132
- declare function createGrokBuildCredentials(pool: FileCredentialPool, authStore: GrokAuthStore, options?: {
133
- grokHome?: string;
134
- quota?: ProviderQuota | null;
135
- /** Injectable fetch for the device-code login flow (tests). */
136
- loginFetch?: typeof fetch;
137
- }): ProviderCredentials;
138
- //#endregion
139
- //#region src/device-login.d.ts
140
- interface GrokDeviceLoginPending {
141
- verificationUrl: string;
142
- userCode: string;
143
- expiresAt: string;
144
- }
145
- interface GrokDeviceLoginOptions {
146
- signal?: AbortSignal;
147
- /** Fires once with the URL + one-time code the user needs. */
148
- onPending?: (pending: GrokDeviceLoginPending) => void;
149
- fetch?: typeof fetch;
150
- issuer?: string;
151
- clientId?: string;
152
- scope?: string;
153
- }
154
- interface GrokDeviceLoginResult {
155
- entryKey: string;
156
- entry: GrokAuthEntry;
157
- }
158
- /** Runs the full device flow and returns a vendor-shaped auth.json entry keyed like the Grok CLI. */
159
- declare function runGrokDeviceLogin(options?: GrokDeviceLoginOptions): Promise<GrokDeviceLoginResult>;
160
- //#endregion
161
- export { type GrokBuildFetch, type GrokBuildProviderOptions, type GrokBuildQuotaOptions, type GrokDeviceLoginOptions, type GrokDeviceLoginPending, type GrokDeviceLoginResult, PoolAwareGrokAuthStore, createGrokBuildCredentials, createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, openGrokCredentialPool, parseGrokBuildProviderConfig, runGrokDeviceLogin };
89
+ export { type GrokBuildFetch, type GrokBuildProviderOptions, type GrokBuildQuotaOptions, createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, parseGrokBuildProviderConfig };
package/dist/index.mjs CHANGED
@@ -1,19 +1,13 @@
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";
1
+ import { delay, errorMessage, isAbortError, isRecord, nonEmptyString, normalizeBaseUrl, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, stringOrNull } from "@demicodes/utils";
2
+ import { createProviderQuota, defineProvider, httpRequestFailedEvent, normalizeErrorCode, providerErrorFromUnknown, severityFromUsedPercent, usedPercentFromRatio } from "@demicodes/provider";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { chmod, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  import process from "node:process";
8
8
  import { zeroUsage } from "@demicodes/core";
9
- import { FileCredentialPool, credentialIdFromIdentity } from "@demicodes/provider/credentials-pool";
10
9
  import { readFileSync } from "node:fs";
11
10
  //#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
11
  const DEFAULT_TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token";
18
12
  const REFRESH_EXPIRY_SKEW_MS = 300 * 1e3;
19
13
  async function grokBuildAuthStatus(options = {}) {
@@ -22,19 +16,17 @@ async function grokBuildAuthStatus(options = {}) {
22
16
  var FileGrokAuthStore = class {
23
17
  grokHome;
24
18
  authFile;
25
- entryKey;
26
19
  refreshImpl;
27
20
  now;
28
21
  lockRetryDelayMs;
29
22
  lockTimeoutMs;
30
23
  constructor(options = {}) {
31
24
  this.grokHome = options.grokHome ?? defaultGrokHome();
32
- this.authFile = options.authFile ?? join(this.grokHome, "auth.json");
33
- this.entryKey = nonEmptyString(options.entryKey) ?? null;
25
+ this.authFile = join(this.grokHome, "auth.json");
34
26
  this.refreshImpl = options.refresh ?? refreshGrokOidcToken;
35
27
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
36
28
  this.lockRetryDelayMs = options.lockRetryDelayMs ?? 25;
37
- this.lockTimeoutMs = options.lockTimeoutMs ?? 3e4;
29
+ this.lockTimeoutMs = options.lockTimeoutMs ?? 5e3;
38
30
  }
39
31
  async status() {
40
32
  try {
@@ -50,14 +42,14 @@ var FileGrokAuthStore = class {
50
42
  };
51
43
  return {
52
44
  status: "error",
53
- message: redactGrokSecretText(error instanceof Error ? error.message : String(error))
45
+ message: redactSecretText(error instanceof Error ? error.message : String(error))
54
46
  };
55
47
  }
56
48
  }
57
49
  async resolveAuth(options = {}) {
58
50
  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.`);
51
+ const selected = selectAuthEntry(file);
52
+ if (!selected) throw new GrokAuthError("auth_missing", `No Grok OAuth session found in ${this.authFile}. Run \`grok login\` first.`);
61
53
  const { entryKey, entry } = selected;
62
54
  const accessToken = nonEmptyString(entry.key);
63
55
  if (!accessToken) throw new GrokAuthError("auth_missing", `Grok auth entry "${entryKey}" has no access token (key)`);
@@ -65,7 +57,7 @@ var FileGrokAuthStore = class {
65
57
  const clientId = nonEmptyString(entry.oidc_client_id) ?? parseClientIdFromEntryKey(entryKey);
66
58
  const issuer = nonEmptyString(entry.oidc_issuer) ?? parseIssuerFromEntryKey(entryKey);
67
59
  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, {
60
+ if ((options.forceRefresh === true || expiresWithin(expiresAt, this.now(), REFRESH_EXPIRY_SKEW_MS)) && refreshToken && clientId) return this.refreshAndResolve(file, entryKey, {
69
61
  refreshToken,
70
62
  clientId,
71
63
  tokenEndpoint: tokenEndpointForIssuer(issuer)
@@ -81,7 +73,7 @@ var FileGrokAuthStore = class {
81
73
  authFile: this.authFile
82
74
  };
83
75
  }
84
- async refreshAndResolve(staleAccessToken, entryKey, refreshInput) {
76
+ async refreshAndResolve(_file, entryKey, refreshInput) {
85
77
  return this.withAuthFileLock(async () => {
86
78
  const latest = await this.readAuthFile();
87
79
  const latestEntry = latest[entryKey];
@@ -89,20 +81,6 @@ var FileGrokAuthStore = class {
89
81
  const refreshToken = nonEmptyString(latestEntry.refresh_token) ?? refreshInput.refreshToken;
90
82
  const clientId = nonEmptyString(latestEntry.oidc_client_id) ?? refreshInput.clientId;
91
83
  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
84
  const response = await this.refreshImpl({
107
85
  refreshToken,
108
86
  clientId,
@@ -141,8 +119,8 @@ var FileGrokAuthStore = class {
141
119
  return parsed;
142
120
  } catch (error) {
143
121
  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))}`);
122
+ if (isNodeError(error) && error.code === "ENOENT") throw new GrokAuthError("auth_missing", `Grok auth file not found: ${this.authFile}. Run \`grok login\` first.`);
123
+ throw new GrokAuthError("auth_invalid", `Failed to read Grok auth file ${this.authFile}: ${redactSecretText(errorMessage(error))}`);
146
124
  }
147
125
  }
148
126
  async withAuthFileLock(fn) {
@@ -154,10 +132,10 @@ var FileGrokAuthStore = class {
154
132
  while (!handle) try {
155
133
  handle = await open(lockFile, "wx", 384);
156
134
  } 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;
135
+ if (!isNodeError(error) || error.code !== "EEXIST") throw new GrokAuthError("auth_lock_failed", `Failed to lock Grok auth file: ${redactSecretText(errorMessage(error))}`);
136
+ if (!brokeStaleLock && await isAbandonedGrokAuthLock(lockFile, this.now())) {
137
+ await rm(lockFile, { force: true }).catch(() => void 0);
138
+ brokeStaleLock = true;
161
139
  continue;
162
140
  }
163
141
  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.`);
@@ -167,9 +145,8 @@ var FileGrokAuthStore = class {
167
145
  await writeFile(lockFile, `${process.pid}:${Math.floor(this.now().getTime() / 1e3)}`, { mode: 384 });
168
146
  return await fn();
169
147
  } finally {
170
- const ownedIdentity = await handle.stat().then(toFileIdentity).catch(() => null);
171
148
  await handle.close().catch(() => void 0);
172
- if (ownedIdentity) await removeLockFileIfSame(lockFile, ownedIdentity);
149
+ await rm(lockFile, { force: true }).catch(() => void 0);
173
150
  }
174
151
  }
175
152
  };
@@ -204,16 +181,6 @@ function selectAuthEntry(file) {
204
181
  candidates.sort((a, b) => b.score - a.score || a.entryKey.localeCompare(b.entryKey));
205
182
  return candidates[0] ?? null;
206
183
  }
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
184
  async function refreshGrokOidcToken(input, signal) {
218
185
  const body = new URLSearchParams({
219
186
  grant_type: "refresh_token",
@@ -232,6 +199,9 @@ async function refreshGrokOidcToken(input, signal) {
232
199
  if (!response.ok) throw new GrokAuthError("auth_refresh_failed", `Grok token refresh failed with HTTP ${response.status}`);
233
200
  return await response.json();
234
201
  }
202
+ function redactSecretText(text) {
203
+ return text.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [REDACTED]").replace(/(access_token|refresh_token|id_token|\bkey\b)["'=:\s]+[A-Za-z0-9._~+/=-]+/gi, "$1=[REDACTED]");
204
+ }
235
205
  function parseJwtExpiration(jwt) {
236
206
  const exp = decodeJwtPayload(jwt)?.exp;
237
207
  return typeof exp === "number" ? /* @__PURE__ */ new Date(exp * 1e3) : null;
@@ -277,7 +247,10 @@ function decodeJwtPayload(jwt) {
277
247
  return null;
278
248
  }
279
249
  }
280
- /** Grok CLI lock format is `pid:unix_seconds`. A valid live PID always owns its lock. */
250
+ function isNodeError(error) {
251
+ return error instanceof Error && "code" in error;
252
+ }
253
+ /** Grok CLI lock format is `pid:unix_seconds`. Steal only when the owner is dead or very stale. */
281
254
  async function isAbandonedGrokAuthLock(lockFile, now, maxAgeMs = 3e4) {
282
255
  try {
283
256
  const raw = (await readFile(lockFile, "utf8")).trim();
@@ -285,7 +258,7 @@ async function isAbandonedGrokAuthLock(lockFile, now, maxAgeMs = 3e4) {
285
258
  if (match) {
286
259
  const pid = Number(match[1]);
287
260
  const tsSec = Number(match[2]);
288
- if (Number.isFinite(pid) && pid > 0) return !isProcessAlive(pid);
261
+ if (Number.isFinite(pid) && pid > 0 && !isProcessAlive(pid)) return true;
289
262
  if (Number.isFinite(tsSec) && now.getTime() - tsSec * 1e3 > maxAgeMs) return true;
290
263
  return false;
291
264
  }
@@ -295,26 +268,12 @@ async function isAbandonedGrokAuthLock(lockFile, now, maxAgeMs = 3e4) {
295
268
  return true;
296
269
  }
297
270
  }
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
271
  function isProcessAlive(pid) {
313
272
  try {
314
273
  process.kill(pid, 0);
315
274
  return true;
316
275
  } catch (error) {
317
- if (errorCode(error) === "EPERM") return true;
276
+ if (isNodeError(error) && error.code === "EPERM") return true;
318
277
  return false;
319
278
  }
320
279
  }
@@ -344,52 +303,53 @@ async function* mapGrokChatCompletionStream(events, signal) {
344
303
  yield { type: "abort" };
345
304
  return;
346
305
  }
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" };
306
+ for (const data of event.data) {
307
+ if (data === "[DONE]") {
308
+ yield* flushToolCalls(toolCalls);
309
+ yield {
310
+ type: "response",
311
+ usage
312
+ };
313
+ return;
314
+ }
315
+ const chunk = parseJsonObject(data);
316
+ if (!chunk) continue;
317
+ const error = isRecord(chunk.error) ? chunk.error : null;
318
+ if (error) {
319
+ const message = stringOrNull(error.message) ?? "Grok Build stream error";
320
+ yield {
321
+ type: "error",
322
+ message,
323
+ code: normalizeErrorCode(stringOrNull(error.code) ?? stringOrNull(error.type), message)
324
+ };
325
+ return;
326
+ }
327
+ if (isRecord(chunk.usage)) usage = grokUsage(chunk.usage);
328
+ const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
329
+ for (const choice of choices) {
330
+ if (!isRecord(choice)) continue;
331
+ const delta = isRecord(choice.delta) ? choice.delta : null;
332
+ if (delta) {
333
+ const reasoning = stringOrNull(delta.reasoning_content);
334
+ if (reasoning) {
335
+ if (!thinkingStarted) {
336
+ thinkingStarted = true;
337
+ yield { type: "thinking_start" };
338
+ }
339
+ yield {
340
+ type: "thinking_delta",
341
+ text: reasoning
342
+ };
379
343
  }
380
- yield {
381
- type: "thinking_delta",
382
- text: reasoning
344
+ const content = stringOrNull(delta.content);
345
+ if (content) yield {
346
+ type: "text_delta",
347
+ text: content
383
348
  };
349
+ if (Array.isArray(delta.tool_calls)) collectToolCalls(delta.tool_calls, toolCalls);
384
350
  }
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);
351
+ if (choice.finish_reason === "tool_calls") yield* flushToolCalls(toolCalls);
391
352
  }
392
- if (choice.finish_reason === "tool_calls") yield* flushToolCalls(toolCalls);
393
353
  }
394
354
  }
395
355
  yield* flushToolCalls(toolCalls);
@@ -404,15 +364,15 @@ async function* readServerSentEvents(body, signal) {
404
364
  const decoder = new TextDecoder();
405
365
  let buffer = "";
406
366
  let eventName = null;
407
- let dataLines = [];
367
+ let data = [];
408
368
  const flush = function* () {
409
- if (dataLines.length === 0) return;
369
+ if (data.length === 0) return;
410
370
  yield {
411
371
  event: eventName,
412
- data: dataLines.join("\n")
372
+ data
413
373
  };
414
374
  eventName = null;
415
- dataLines = [];
375
+ data = [];
416
376
  };
417
377
  try {
418
378
  while (true) {
@@ -427,14 +387,14 @@ async function* readServerSentEvents(body, signal) {
427
387
  const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
428
388
  if (line === "") yield* flush();
429
389
  else if (line.startsWith("event:")) eventName = line.slice(6).trim();
430
- else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
390
+ else if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
431
391
  newline = buffer.indexOf("\n");
432
392
  }
433
393
  }
434
394
  buffer += decoder.decode();
435
395
  if (buffer) {
436
396
  const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
437
- if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
397
+ if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
438
398
  else if (line.startsWith("event:")) eventName = line.slice(6).trim();
439
399
  }
440
400
  yield* flush();
@@ -516,10 +476,6 @@ function userContentToParts(content) {
516
476
  type: "text",
517
477
  text: `[document:${block.source.fileName} ${block.source.mediaType}]`
518
478
  });
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
479
  else if (block.source.type === "url") parts.push({
524
480
  type: "image_url",
525
481
  image_url: {
@@ -537,6 +493,9 @@ function userContentToParts(content) {
537
493
  if (parts.every((part) => part.type === "text")) return parts.map((part) => part.text).join("\n");
538
494
  return parts;
539
495
  }
496
+ function toolResultContentToText(output) {
497
+ return output.map((block) => block.type === "text" ? block.text : `[image:${block.source.mediaType}]`).join("\n");
498
+ }
540
499
  function toolToGrokTool(tool) {
541
500
  return {
542
501
  type: "function",
@@ -598,280 +557,6 @@ function grokUsage(usage) {
598
557
  cacheWriteTokens: 0
599
558
  };
600
559
  }
601
- //#endregion
602
- //#region src/device-login.ts
603
- const GROK_ISSUER = "https://auth.x.ai";
604
- const GROK_CLI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
605
- const GROK_LOGIN_SCOPE = "openid profile email offline_access grok-cli:access";
606
- const GROK_LOGIN_FALLBACK_INTERVAL_S = 5;
607
- const GROK_LOGIN_FALLBACK_EXPIRES_S = 900;
608
- async function postForm(fetchImpl, url, params, signal) {
609
- return fetchImpl(url, {
610
- method: "POST",
611
- headers: { "content-type": "application/x-www-form-urlencoded" },
612
- body: new URLSearchParams(params).toString(),
613
- signal
614
- });
615
- }
616
- async function jsonBody(response, what) {
617
- const body = await response.json().catch(() => null);
618
- if (!isRecord(body)) throw new GrokAuthError("auth_invalid", `${what} response is not a JSON object`);
619
- return body;
620
- }
621
- async function requestDeviceCode(fetchImpl, issuer, clientId, scope, signal) {
622
- const response = await postForm(fetchImpl, `${issuer}/oauth2/device/code`, {
623
- client_id: clientId,
624
- scope
625
- }, signal);
626
- if (!response.ok) throw new GrokAuthError("auth_invalid", `Grok device code request failed with HTTP ${response.status}`);
627
- const body = await jsonBody(response, "Grok device code");
628
- const deviceCode = nonEmptyString(body.device_code);
629
- const userCode = nonEmptyString(body.user_code);
630
- const verificationUrl = nonEmptyString(body.verification_uri_complete) ?? nonEmptyString(body.verification_uri);
631
- if (!deviceCode || !userCode || !verificationUrl) throw new GrokAuthError("auth_invalid", "Grok device code response is missing device_code, user_code, or verification_uri");
632
- const interval = Number(body.interval);
633
- const expiresIn = Number(body.expires_in);
634
- return {
635
- deviceCode,
636
- userCode,
637
- verificationUrl,
638
- intervalSeconds: Number.isFinite(interval) && interval >= 0 ? interval : GROK_LOGIN_FALLBACK_INTERVAL_S,
639
- expiresAt: Date.now() + (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : GROK_LOGIN_FALLBACK_EXPIRES_S) * 1e3
640
- };
641
- }
642
- async function pollForTokens(fetchImpl, issuer, clientId, device, signal) {
643
- let intervalSeconds = device.intervalSeconds;
644
- for (;;) {
645
- signal?.throwIfAborted();
646
- const response = await postForm(fetchImpl, `${issuer}/oauth2/token`, {
647
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
648
- device_code: device.deviceCode,
649
- client_id: clientId
650
- }, signal);
651
- const body = await jsonBody(response, "Grok device token");
652
- if (response.ok) {
653
- const accessToken = nonEmptyString(body.access_token);
654
- if (!accessToken) throw new GrokAuthError("auth_invalid", "Grok token response is missing access_token");
655
- const expiresIn = Number(body.expires_in);
656
- return {
657
- accessToken,
658
- refreshToken: nonEmptyString(body.refresh_token) ?? null,
659
- expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : null
660
- };
661
- }
662
- const error = nonEmptyString(body.error);
663
- if (error === "slow_down") intervalSeconds += 5;
664
- else if (error !== "authorization_pending") throw new GrokAuthError("auth_invalid", `Grok device login failed: ${error ?? `HTTP ${response.status}`}`);
665
- if (Date.now() >= device.expiresAt) throw new GrokAuthError("auth_invalid", "Grok device login timed out before the user confirmed");
666
- await delay(intervalSeconds * 1e3);
667
- }
668
- }
669
- async function fetchUserinfo(fetchImpl, issuer, accessToken, signal) {
670
- const response = await fetchImpl(`${issuer}/oauth2/userinfo`, {
671
- headers: { authorization: `Bearer ${accessToken}` },
672
- signal
673
- });
674
- if (!response.ok) return {};
675
- const body = await response.json().catch(() => null);
676
- return isRecord(body) ? body : {};
677
- }
678
- /** Runs the full device flow and returns a vendor-shaped auth.json entry keyed like the Grok CLI. */
679
- async function runGrokDeviceLogin(options = {}) {
680
- const fetchImpl = options.fetch ?? fetch;
681
- const issuer = (options.issuer ?? GROK_ISSUER).replace(/\/+$/, "");
682
- const clientId = options.clientId ?? GROK_CLI_CLIENT_ID;
683
- const device = await requestDeviceCode(fetchImpl, issuer, clientId, options.scope ?? GROK_LOGIN_SCOPE, options.signal);
684
- options.onPending?.({
685
- verificationUrl: device.verificationUrl,
686
- userCode: device.userCode,
687
- expiresAt: new Date(device.expiresAt).toISOString()
688
- });
689
- const tokens = await pollForTokens(fetchImpl, issuer, clientId, device, options.signal);
690
- const userinfo = await fetchUserinfo(fetchImpl, issuer, tokens.accessToken, options.signal);
691
- const entry = {
692
- key: tokens.accessToken,
693
- auth_mode: "oidc",
694
- ...tokens.refreshToken ? { refresh_token: tokens.refreshToken } : {},
695
- ...tokens.expiresIn ? { expires_at: new Date(Date.now() + tokens.expiresIn * 1e3).toISOString() } : {},
696
- oidc_issuer: issuer,
697
- oidc_client_id: clientId,
698
- ...nonEmptyString(userinfo.email) ? { email: nonEmptyString(userinfo.email) } : {},
699
- ...nonEmptyString(userinfo.given_name) ? { first_name: nonEmptyString(userinfo.given_name) } : {},
700
- ...nonEmptyString(userinfo.sub) ? { user_id: nonEmptyString(userinfo.sub) } : {}
701
- };
702
- return {
703
- entryKey: `${issuer}::${clientId}`,
704
- entry
705
- };
706
- }
707
- //#endregion
708
- //#region src/credentials.ts
709
- var PoolAwareGrokAuthStore = class {
710
- pool;
711
- vendorHome;
712
- fileAuthOptions;
713
- constructor(pool, options = {}) {
714
- this.pool = pool;
715
- this.vendorHome = options.grokHome ?? defaultGrokHome();
716
- this.fileAuthOptions = options.fileAuthOptions ?? {};
717
- }
718
- async status() {
719
- return this.currentStore().then((s) => s.status());
720
- }
721
- async resolveAuth(options) {
722
- return this.currentStore().then((s) => s.resolveAuth(options));
723
- }
724
- async currentStore() {
725
- await this.pool.ensureActivePointer();
726
- const activeId = await this.pool.getActiveId();
727
- if (activeId) {
728
- const entryKey = (await this.pool.readMeta(activeId))?.identityKey ?? void 0;
729
- return new FileGrokAuthStore({
730
- ...this.fileAuthOptions,
731
- authFile: this.pool.secretPath(activeId),
732
- grokHome: this.pool.entryDir(activeId),
733
- entryKey
734
- });
735
- }
736
- return new FileGrokAuthStore({
737
- ...this.fileAuthOptions,
738
- grokHome: this.vendorHome
739
- });
740
- }
741
- };
742
- function openGrokCredentialPool(options = {}) {
743
- return new FileCredentialPool({
744
- stateDir: options.stateDir,
745
- providerKey: "grok-build",
746
- secretFileName: "auth.json"
747
- });
748
- }
749
- function createGrokBuildCredentials(pool, authStore, options = {}) {
750
- const vendorHome = options.grokHome ?? defaultGrokHome();
751
- const capability = () => ({
752
- mode: "supported",
753
- canBeginLogin: true,
754
- canImportDefault: true,
755
- canAdd: true,
756
- multi: true
757
- });
758
- const getActive = async () => {
759
- await pool.ensureActivePointer();
760
- return {
761
- credentialId: await pool.getActiveId(),
762
- status: await authStore.status()
763
- };
764
- };
765
- const setActive = async (credentialId) => {
766
- await pool.setActiveId(credentialId);
767
- options.quota?.clearLatest?.();
768
- return getActive();
769
- };
770
- const importEntry = async (entryKey, entry, source) => {
771
- const label = nonEmptyString(entry.email) ?? entryKey;
772
- const identityKey = entryKey;
773
- const id = (await pool.findByIdentityKey(identityKey))?.id ?? credentialIdFromIdentity(identityKey, label);
774
- const file = { [entryKey]: entry };
775
- const meta = {
776
- id,
777
- label,
778
- detail: nonEmptyString(entry.auth_mode) ?? "oidc",
779
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
780
- source,
781
- identityKey
782
- };
783
- await pool.writeEntry(meta, `${JSON.stringify(file, null, 2)}\n`);
784
- if (!await pool.getActiveId()) await pool.setActiveId(id);
785
- options.quota?.clearLatest?.();
786
- return {
787
- id: meta.id,
788
- label: meta.label,
789
- detail: meta.detail,
790
- updatedAt: meta.updatedAt
791
- };
792
- };
793
- const importFromAuthJsonText = async (text, source) => {
794
- let file;
795
- try {
796
- file = JSON.parse(text);
797
- } catch {
798
- throw new GrokAuthError("auth_invalid", "Grok auth material is not valid JSON");
799
- }
800
- if (!isRecord(file)) throw new GrokAuthError("auth_invalid", "Grok auth material is not an object");
801
- const imported = [];
802
- for (const [entryKey, value] of Object.entries(file)) {
803
- if (!isRecord(value)) continue;
804
- const entry = value;
805
- if (!nonEmptyString(entry.key)) continue;
806
- imported.push(await importEntry(entryKey, entry, source));
807
- }
808
- if (imported.length === 0) throw new GrokAuthError("auth_missing", "No Grok OAuth entries with access tokens found to import");
809
- return imported;
810
- };
811
- return {
812
- capability,
813
- list: () => pool.list(),
814
- getActive,
815
- setActive,
816
- beginLogin: async (loginOptions) => {
817
- try {
818
- const { entryKey, entry } = await runGrokDeviceLogin({
819
- signal: loginOptions?.signal,
820
- onPending: loginOptions?.onPending,
821
- fetch: options.loginFetch
822
- });
823
- return {
824
- status: "completed",
825
- credentialId: (await importEntry(entryKey, entry, "login:device")).id
826
- };
827
- } catch (error) {
828
- if (loginOptions?.signal?.aborted) return { status: "cancelled" };
829
- return {
830
- status: "failed",
831
- message: errorMessage(error)
832
- };
833
- }
834
- },
835
- importDefault: async () => {
836
- const authFile = join(vendorHome, "auth.json");
837
- let text;
838
- try {
839
- text = await readFile(authFile, "utf8");
840
- } catch {
841
- throw new GrokAuthError("auth_missing", `No Grok auth at ${authFile}. Run grok login or beginLogin first.`);
842
- }
843
- const all = await importFromAuthJsonText(text, `vendor:${authFile}`);
844
- const preferred = selectAuthEntry(JSON.parse(text));
845
- if (preferred) {
846
- const byKey = (await pool.listMeta()).find((m) => m.identityKey === preferred.entryKey);
847
- if (byKey) {
848
- await pool.setActiveId(byKey.id);
849
- options.quota?.clearLatest?.();
850
- return {
851
- id: byKey.id,
852
- label: byKey.label,
853
- detail: byKey.detail,
854
- updatedAt: byKey.updatedAt
855
- };
856
- }
857
- }
858
- return all[0];
859
- },
860
- add: async (input) => {
861
- if (typeof input.authJsonText === "string") return (await importFromAuthJsonText(input.authJsonText, "add:authJsonText"))[0];
862
- if (typeof input.authFile === "string") {
863
- const text = await readFile(input.authFile, "utf8");
864
- return (await importFromAuthJsonText(text, `add:authFile:${input.authFile}`))[0];
865
- }
866
- if (typeof input.entryKey === "string" && isRecord(input.entry)) return importEntry(input.entryKey, input.entry, "add:entry");
867
- throw new Error("Grok credentials.add expects authJsonText, authFile, or { entryKey, entry }");
868
- },
869
- remove: async (credentialId) => {
870
- await pool.remove(credentialId);
871
- options.quota?.clearLatest?.();
872
- }
873
- };
874
- }
875
560
  const GROK_CLI_TOKEN_AUTH = "xai-grok-cli";
876
561
  const GROK_CLIENT_SURFACE = "grok-build";
877
562
  function buildGrokBuildHeaders(auth, request, options) {
@@ -1173,6 +858,12 @@ function moneyVal(value) {
1173
858
  if (isRecord(value) && typeof value.val === "number" && Number.isFinite(value.val)) return value.val;
1174
859
  return null;
1175
860
  }
861
+ function numberHeader(headers, name) {
862
+ const raw = headers.get(name);
863
+ if (raw == null || raw === "") return null;
864
+ const n = Number(raw);
865
+ return Number.isFinite(n) ? n : null;
866
+ }
1176
867
  //#endregion
1177
868
  //#region src/provider.ts
1178
869
  var GrokBuildProvider = class {
@@ -1225,7 +916,6 @@ var GrokBuildProvider = class {
1225
916
  });
1226
917
  } catch {}
1227
918
  if (response.status === 401 && !forceRefresh) {
1228
- await response.body?.cancel().catch(() => {});
1229
919
  forceRefresh = true;
1230
920
  continue;
1231
921
  }
@@ -1249,9 +939,7 @@ var GrokBuildProvider = class {
1249
939
  function createGrokBuildProvider(options = {}) {
1250
940
  const id = options.id ?? "grok-build";
1251
941
  const displayName = options.displayName ?? "Grok Build";
1252
- const enableCredentials = options.credentials ?? options.authStore === void 0;
1253
- const pool = !options.authStore && enableCredentials ? openGrokCredentialPool({ stateDir: options.stateDir }) : null;
1254
- const authStore = options.authStore ?? (pool ? new PoolAwareGrokAuthStore(pool, { grokHome: options.grokHome }) : new FileGrokAuthStore({ grokHome: options.grokHome }));
942
+ const authStore = options.authStore ?? new FileGrokAuthStore({ grokHome: options.grokHome });
1255
943
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://cli-chat-proxy.grok.com/v1");
1256
944
  const fetchImpl = options.fetch ?? fetch;
1257
945
  const quota = createGrokBuildQuota({
@@ -1262,10 +950,6 @@ function createGrokBuildProvider(options = {}) {
1262
950
  authStore,
1263
951
  fetch: fetchImpl
1264
952
  });
1265
- const credentialsApi = pool ? createGrokBuildCredentials(pool, authStore, {
1266
- grokHome: options.grokHome,
1267
- quota
1268
- }) : void 0;
1269
953
  const runtimeOptions = {
1270
954
  baseUrl,
1271
955
  grokHome: options.grokHome,
@@ -1280,10 +964,9 @@ function createGrokBuildProvider(options = {}) {
1280
964
  displayName,
1281
965
  auth: { status: () => authStore.status() },
1282
966
  quota,
1283
- ...credentialsApi ? { credentials: credentialsApi } : {},
1284
967
  state: () => ({
1285
968
  status: "ready",
1286
- 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"
969
+ message: "Uses Grok CLI OAuth session (~/.grok/auth.json) via cli-chat-proxy"
1287
970
  }),
1288
971
  listModels: () => listGrokBuildModels({
1289
972
  providerId: id,
@@ -1324,4 +1007,4 @@ function chatCompletionsUrl(baseUrl) {
1324
1007
  return normalized.endsWith("/chat/completions") ? normalized : `${normalized}/chat/completions`;
1325
1008
  }
1326
1009
  //#endregion
1327
- export { PoolAwareGrokAuthStore, createGrokBuildCredentials, createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, openGrokCredentialPool, parseGrokBuildProviderConfig, runGrokDeviceLogin };
1010
+ export { createGrokBuildProvider, createGrokBuildQuota, grokBuildAuthStatus, grokBuildFallbackModels, listGrokBuildModels, mapGrokQuotaProbe, observeGrokRateLimitHeaders, parseGrokBuildProviderConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/provider-grok-build",
3
3
  "description": "Grok Build provider adapter for Demi (reuses ~/.grok OAuth session).",
4
- "version": "0.10.2",
4
+ "version": "0.10.3",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -11,9 +11,9 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@demicodes/core": "^0.10.2",
15
- "@demicodes/provider": "^0.10.2",
16
- "@demicodes/utils": "^0.10.2"
14
+ "@demicodes/core": "^0.10.3",
15
+ "@demicodes/provider": "^0.10.3",
16
+ "@demicodes/utils": "^0.10.3"
17
17
  },
18
18
  "license": "Apache-2.0",
19
19
  "main": "./dist/index.mjs",