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