@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 +3 -23
- package/dist/index.d.mts +3 -75
- package/dist/index.mjs +86 -403
- package/package.json +4 -4
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
2
|
-
|
|
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
|
-
|
|
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,
|
|
2
|
-
import { createProviderQuota, defineProvider, httpRequestFailedEvent, normalizeErrorCode,
|
|
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 =
|
|
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 ??
|
|
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:
|
|
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 =
|
|
60
|
-
if (!selected) throw new GrokAuthError("auth_missing",
|
|
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(
|
|
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(
|
|
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 (
|
|
145
|
-
throw new GrokAuthError("auth_invalid", `Failed to read Grok auth file ${this.authFile}: ${
|
|
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 (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
|
367
|
+
let data = [];
|
|
408
368
|
const flush = function* () {
|
|
409
|
-
if (
|
|
369
|
+
if (data.length === 0) return;
|
|
410
370
|
yield {
|
|
411
371
|
event: eventName,
|
|
412
|
-
data
|
|
372
|
+
data
|
|
413
373
|
};
|
|
414
374
|
eventName = null;
|
|
415
|
-
|
|
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:"))
|
|
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:"))
|
|
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
|
|
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:
|
|
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 {
|
|
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.
|
|
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.
|
|
15
|
-
"@demicodes/provider": "^0.10.
|
|
16
|
-
"@demicodes/utils": "^0.10.
|
|
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",
|