@rise-so/pi-grok-build 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/extension.d.ts +2 -0
- package/dist/extension.js +63 -0
- package/dist/provider.d.ts +4 -0
- package/dist/provider.js +44 -0
- package/dist/xai-grok-build.d.ts +161 -0
- package/dist/xai-grok-build.js +391 -0
- package/package.json +61 -0
- package/src/extension.ts +82 -0
- package/src/provider.ts +74 -0
- package/src/xai-grok-build.ts +549 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// Core of the xAI Grok Build integration: model metadata, PKCE, OIDC
|
|
2
|
+
// discovery, loopback callback server, token exchange/refresh, and credential
|
|
3
|
+
// narrowing. OAuth only — API keys are deliberately unsupported in this
|
|
4
|
+
// project (the subscription token is the whole point). This module has ZERO
|
|
5
|
+
// pi-ai runtime
|
|
6
|
+
// imports so it can be shared by both the pi-ai adapter (provider.ts) and the
|
|
7
|
+
// pi extension adapter (extension.ts) — the extension loader's jiti alias
|
|
8
|
+
// mangles pi-ai subpath imports, so anything the extension reaches must stay
|
|
9
|
+
// free of them.
|
|
10
|
+
//
|
|
11
|
+
// The OAuth flow borrows the Grok CLI's public client id, scope, and fixed
|
|
12
|
+
// loopback port (56121) because xAI has no public client registration. xAI can
|
|
13
|
+
// break this flow at any time, and a concurrent Grok CLI login on the same
|
|
14
|
+
// machine collides on the port — accepted for a personal tool.
|
|
15
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
16
|
+
import { createServer } from "node:http";
|
|
17
|
+
export const XAI_PROVIDER_ID = "xai-grok-build";
|
|
18
|
+
export const XAI_MODEL_ID = "grok-build-0.1";
|
|
19
|
+
export const XAI_BASE_URL = "https://api.x.ai/v1";
|
|
20
|
+
const XAI_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
21
|
+
const XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
22
|
+
const XAI_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
23
|
+
const XAI_REDIRECT_HOST = "127.0.0.1";
|
|
24
|
+
const XAI_REDIRECT_PORT = 56_121;
|
|
25
|
+
const XAI_REDIRECT_PATH = "/callback";
|
|
26
|
+
const XAI_REFRESH_LEAD_MS = 5 * 60 * 1000;
|
|
27
|
+
const MODEL_COMPAT = {
|
|
28
|
+
sendSessionIdHeader: false,
|
|
29
|
+
supportsLongCacheRetention: false,
|
|
30
|
+
};
|
|
31
|
+
// Catalog notes:
|
|
32
|
+
// - Context windows and per-Mtok pricing for grok-build-0.1 and grok-4.3 are
|
|
33
|
+
// confirmed by xAI's live GET /v1/models (see `pnpm smoke:models`). Live
|
|
34
|
+
// pricing doubles past a 200k-token threshold; pi's cost model has no
|
|
35
|
+
// long-context tier, so the base tier is recorded.
|
|
36
|
+
// - maxTokens values are third-party registry metadata, unverified against
|
|
37
|
+
// live output caps: pi-ai sends max_output_tokens only when stream options
|
|
38
|
+
// set maxTokens, which they normally don't.
|
|
39
|
+
// - grok-composer-2.5-fast does NOT appear in the live /models catalog but
|
|
40
|
+
// accepts /responses calls with a subscription OAuth token. Context/output
|
|
41
|
+
// metadata comes from third-party registries; pricing was supplied by the
|
|
42
|
+
// maintainer (not machine-verifiable against /models since it's unlisted).
|
|
43
|
+
//
|
|
44
|
+
// The credential also sees media models (grok-imagine-image,
|
|
45
|
+
// grok-imagine-image-quality, grok-imagine-video, grok-imagine-video-1.5).
|
|
46
|
+
// They use xAI's /images and /videos endpoints, not /responses, so they are
|
|
47
|
+
// deliberately NOT registered here — see README "Media models".
|
|
48
|
+
export const GROK_BUILD_MODEL = {
|
|
49
|
+
id: XAI_MODEL_ID,
|
|
50
|
+
name: "Grok Build 0.1",
|
|
51
|
+
api: "openai-responses",
|
|
52
|
+
provider: XAI_PROVIDER_ID,
|
|
53
|
+
baseUrl: XAI_BASE_URL,
|
|
54
|
+
reasoning: false,
|
|
55
|
+
input: ["text", "image"],
|
|
56
|
+
cost: {
|
|
57
|
+
input: 1,
|
|
58
|
+
output: 2,
|
|
59
|
+
cacheRead: 0.2,
|
|
60
|
+
cacheWrite: 0,
|
|
61
|
+
},
|
|
62
|
+
contextWindow: 256_000,
|
|
63
|
+
maxTokens: 256_000,
|
|
64
|
+
compat: MODEL_COMPAT,
|
|
65
|
+
};
|
|
66
|
+
export const GROK_43_MODEL = {
|
|
67
|
+
id: "grok-4.3",
|
|
68
|
+
name: "Grok 4.3",
|
|
69
|
+
api: "openai-responses",
|
|
70
|
+
provider: XAI_PROVIDER_ID,
|
|
71
|
+
baseUrl: XAI_BASE_URL,
|
|
72
|
+
reasoning: true,
|
|
73
|
+
input: ["text", "image"],
|
|
74
|
+
cost: {
|
|
75
|
+
input: 1.25,
|
|
76
|
+
output: 2.5,
|
|
77
|
+
cacheRead: 0.2,
|
|
78
|
+
cacheWrite: 0,
|
|
79
|
+
},
|
|
80
|
+
contextWindow: 1_000_000,
|
|
81
|
+
maxTokens: 65_536,
|
|
82
|
+
compat: MODEL_COMPAT,
|
|
83
|
+
};
|
|
84
|
+
export const GROK_COMPOSER_MODEL = {
|
|
85
|
+
id: "grok-composer-2.5-fast",
|
|
86
|
+
name: "Grok Composer 2.5 Fast",
|
|
87
|
+
api: "openai-responses",
|
|
88
|
+
provider: XAI_PROVIDER_ID,
|
|
89
|
+
baseUrl: XAI_BASE_URL,
|
|
90
|
+
reasoning: false,
|
|
91
|
+
input: ["text"],
|
|
92
|
+
cost: {
|
|
93
|
+
input: 3,
|
|
94
|
+
output: 15,
|
|
95
|
+
cacheRead: 0.5,
|
|
96
|
+
cacheWrite: 0,
|
|
97
|
+
},
|
|
98
|
+
contextWindow: 200_000,
|
|
99
|
+
maxTokens: 32_768,
|
|
100
|
+
compat: MODEL_COMPAT,
|
|
101
|
+
};
|
|
102
|
+
export const GROK_MODELS = [GROK_BUILD_MODEL, GROK_43_MODEL, GROK_COMPOSER_MODEL];
|
|
103
|
+
/** pi hands back `Record<string, unknown>`. Narrow at the boundary; fail fast. */
|
|
104
|
+
export function requireXaiOAuthCredential(value) {
|
|
105
|
+
const access = stringValue(value["access"]);
|
|
106
|
+
const refresh = stringValue(value["refresh"]);
|
|
107
|
+
const tokenEndpoint = stringValue(value["tokenEndpoint"]);
|
|
108
|
+
const expires = value["expires"];
|
|
109
|
+
if (access === undefined ||
|
|
110
|
+
refresh === undefined ||
|
|
111
|
+
tokenEndpoint === undefined ||
|
|
112
|
+
typeof expires !== "number") {
|
|
113
|
+
throw new Error("Stored xAI credential is incomplete; log in again");
|
|
114
|
+
}
|
|
115
|
+
return { access, refresh, expires, tokenEndpoint };
|
|
116
|
+
}
|
|
117
|
+
export async function loginXai(host, opts = {}) {
|
|
118
|
+
const pkce = generatePkce();
|
|
119
|
+
const state = randomToken();
|
|
120
|
+
const nonce = randomToken();
|
|
121
|
+
const discovery = await discoverXaiOAuth(host.signal);
|
|
122
|
+
const callbackServer = await startXaiCallbackServer(state);
|
|
123
|
+
try {
|
|
124
|
+
const authUrl = buildAuthorizeUrl({
|
|
125
|
+
authorizationEndpoint: discovery.authorizationEndpoint,
|
|
126
|
+
redirectUri: callbackServer.redirectUri,
|
|
127
|
+
codeChallenge: pkce.challenge,
|
|
128
|
+
state,
|
|
129
|
+
nonce,
|
|
130
|
+
referrer: opts.referrer ?? "pi-grok-build",
|
|
131
|
+
});
|
|
132
|
+
host.authUrl(authUrl, "Complete xAI login in your browser. This local process is waiting for the OAuth callback.");
|
|
133
|
+
host.progress("Waiting for xAI authentication callback...");
|
|
134
|
+
// The callback server only settles on requests carrying our state, so no
|
|
135
|
+
// re-check is needed here.
|
|
136
|
+
const result = await waitForCallbackOrAbort(callbackServer.waitForCallback(), host.signal);
|
|
137
|
+
if (result.type === "error") {
|
|
138
|
+
throw new Error(`xAI authentication failed: ${result.error}`);
|
|
139
|
+
}
|
|
140
|
+
host.progress("Exchanging xAI authorization code...");
|
|
141
|
+
const token = await exchangeXaiCode(result.code, callbackServer.redirectUri, pkce.verifier, discovery.tokenEndpoint, host.signal);
|
|
142
|
+
return tokenToCredential(token, discovery.tokenEndpoint);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
callbackServer.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function refreshXai(credential) {
|
|
149
|
+
const token = await postTokenForm(credential.tokenEndpoint, {
|
|
150
|
+
grant_type: "refresh_token",
|
|
151
|
+
client_id: XAI_CLIENT_ID,
|
|
152
|
+
refresh_token: credential.refresh,
|
|
153
|
+
});
|
|
154
|
+
return tokenToCredential(token, credential.tokenEndpoint, credential.refresh);
|
|
155
|
+
}
|
|
156
|
+
async function discoverXaiOAuth(signal) {
|
|
157
|
+
const response = await fetch(XAI_DISCOVERY_URL, {
|
|
158
|
+
headers: { Accept: "application/json" },
|
|
159
|
+
...(signal !== undefined ? { signal } : {}),
|
|
160
|
+
});
|
|
161
|
+
const text = await response.text();
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`xAI discovery failed (${response.status}): ${text || response.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
const body = parseJsonObject(text, "xAI discovery response");
|
|
166
|
+
const authorizationEndpoint = validateXaiOAuthEndpoint(body["authorization_endpoint"], "authorization_endpoint");
|
|
167
|
+
const tokenEndpoint = validateXaiOAuthEndpoint(body["token_endpoint"], "token_endpoint");
|
|
168
|
+
return { authorizationEndpoint, tokenEndpoint };
|
|
169
|
+
}
|
|
170
|
+
function buildAuthorizeUrl(input) {
|
|
171
|
+
const url = new URL(input.authorizationEndpoint);
|
|
172
|
+
url.searchParams.set("response_type", "code");
|
|
173
|
+
url.searchParams.set("client_id", XAI_CLIENT_ID);
|
|
174
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
175
|
+
url.searchParams.set("scope", XAI_SCOPE);
|
|
176
|
+
url.searchParams.set("code_challenge", input.codeChallenge);
|
|
177
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
178
|
+
url.searchParams.set("state", input.state);
|
|
179
|
+
url.searchParams.set("nonce", input.nonce);
|
|
180
|
+
url.searchParams.set("plan", "generic");
|
|
181
|
+
url.searchParams.set("referrer", input.referrer);
|
|
182
|
+
return url.toString();
|
|
183
|
+
}
|
|
184
|
+
async function startXaiCallbackServer(expectedState) {
|
|
185
|
+
const redirectUri = `http://${XAI_REDIRECT_HOST}:${XAI_REDIRECT_PORT}${XAI_REDIRECT_PATH}`;
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
let settled = false;
|
|
188
|
+
let settleWait;
|
|
189
|
+
const waitPromise = new Promise((resolveWait) => {
|
|
190
|
+
settleWait = (value) => {
|
|
191
|
+
if (settled)
|
|
192
|
+
return;
|
|
193
|
+
settled = true;
|
|
194
|
+
resolveWait(value);
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
const server = createServer((req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const url = new URL(req.url ?? "", redirectUri);
|
|
200
|
+
if (url.pathname !== XAI_REDIRECT_PATH) {
|
|
201
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
202
|
+
res.end(oauthPage("Authentication failed", "Callback route not found."));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const error = stringValue(url.searchParams.get("error"));
|
|
206
|
+
const state = stringValue(url.searchParams.get("state"));
|
|
207
|
+
const code = stringValue(url.searchParams.get("code"));
|
|
208
|
+
// Only a request carrying the expected state may settle the wait: the
|
|
209
|
+
// fixed loopback port is shared with the Grok CLI, so a stray request
|
|
210
|
+
// or stale browser tab must not kill a legitimate login in flight.
|
|
211
|
+
// OAuth requires xAI to echo our state on both success and error
|
|
212
|
+
// redirects, so the real callback always passes this gate.
|
|
213
|
+
if (state !== expectedState) {
|
|
214
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
215
|
+
res.end(oauthPage("Authentication failed", "OAuth state mismatch."));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (error !== undefined) {
|
|
219
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
220
|
+
res.end(oauthPage("Authentication failed", `xAI returned: ${error}`));
|
|
221
|
+
settleWait?.({ type: "error", error, state });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (code === undefined) {
|
|
225
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
226
|
+
res.end(oauthPage("Authentication failed", "Missing authorization code."));
|
|
227
|
+
settleWait?.({ type: "error", error: "missing authorization code", state });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
231
|
+
res.end(oauthPage("Authentication successful", "xAI authentication completed. You can close this window."));
|
|
232
|
+
settleWait?.({ type: "success", code, state });
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// An unparseable request cannot be our callback; answer it and keep
|
|
236
|
+
// waiting. The login timeout still bounds the wait.
|
|
237
|
+
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
238
|
+
res.end(oauthPage("Authentication failed", "Internal error while processing OAuth callback."));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const onListenError = (error) => {
|
|
242
|
+
if (error.code === "EADDRINUSE") {
|
|
243
|
+
reject(new Error(`Port ${XAI_REDIRECT_PORT} is already in use — likely a concurrent Grok CLI ` +
|
|
244
|
+
"login (the OAuth client shares its fixed loopback port). Close it and retry."));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
reject(error);
|
|
248
|
+
};
|
|
249
|
+
server.once("error", onListenError);
|
|
250
|
+
server.listen(XAI_REDIRECT_PORT, XAI_REDIRECT_HOST, () => {
|
|
251
|
+
server.off("error", onListenError);
|
|
252
|
+
resolve({
|
|
253
|
+
redirectUri,
|
|
254
|
+
close: () => closeServer(server),
|
|
255
|
+
waitForCallback: () => waitPromise,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async function waitForCallbackOrAbort(promise, signal) {
|
|
261
|
+
const timeoutSignal = AbortSignal.timeout(5 * 60 * 1000);
|
|
262
|
+
const combinedSignal = signal !== undefined ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
263
|
+
if (combinedSignal.aborted)
|
|
264
|
+
throw new Error("xAI OAuth login cancelled");
|
|
265
|
+
return Promise.race([
|
|
266
|
+
promise,
|
|
267
|
+
new Promise((_, reject) => {
|
|
268
|
+
combinedSignal.addEventListener("abort", () => reject(new Error("xAI OAuth login cancelled or timed out")), { once: true });
|
|
269
|
+
}),
|
|
270
|
+
]);
|
|
271
|
+
}
|
|
272
|
+
async function exchangeXaiCode(code, redirectUri, verifier, tokenEndpoint, signal) {
|
|
273
|
+
return postTokenForm(tokenEndpoint, {
|
|
274
|
+
grant_type: "authorization_code",
|
|
275
|
+
code,
|
|
276
|
+
redirect_uri: redirectUri,
|
|
277
|
+
client_id: XAI_CLIENT_ID,
|
|
278
|
+
code_verifier: verifier,
|
|
279
|
+
}, signal);
|
|
280
|
+
}
|
|
281
|
+
async function postTokenForm(tokenEndpoint, fields, signal) {
|
|
282
|
+
const endpoint = validateXaiOAuthEndpoint(tokenEndpoint, "token_endpoint");
|
|
283
|
+
const response = await fetch(endpoint, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
286
|
+
body: new URLSearchParams(fields),
|
|
287
|
+
...(signal !== undefined ? { signal } : {}),
|
|
288
|
+
});
|
|
289
|
+
const text = await response.text();
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
throw new Error(`xAI token request failed (${response.status}): ${text || response.statusText}`);
|
|
292
|
+
}
|
|
293
|
+
return parseTokenResponse(text);
|
|
294
|
+
}
|
|
295
|
+
function parseTokenResponse(text) {
|
|
296
|
+
const body = parseJsonObject(text, "xAI token response");
|
|
297
|
+
const accessToken = requiredString(body["access_token"], "access_token");
|
|
298
|
+
const refreshToken = stringValue(body["refresh_token"]);
|
|
299
|
+
const expiresIn = numberValue(body["expires_in"]);
|
|
300
|
+
if (expiresIn === undefined) {
|
|
301
|
+
throw new Error("xAI token response missing expires_in");
|
|
302
|
+
}
|
|
303
|
+
assertBearer(stringValue(body["token_type"]));
|
|
304
|
+
return {
|
|
305
|
+
accessToken,
|
|
306
|
+
...(refreshToken !== undefined ? { refreshToken } : {}),
|
|
307
|
+
expiresIn,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// RFC 6749 §7.1: token_type values are case-insensitive; xAI sends "Bearer".
|
|
311
|
+
function assertBearer(tokenType) {
|
|
312
|
+
if (tokenType !== undefined && tokenType.toLowerCase() !== "bearer") {
|
|
313
|
+
throw new Error(`xAI returned unsupported token_type: ${tokenType}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function tokenToCredential(token, tokenEndpoint, fallbackRefreshToken) {
|
|
317
|
+
const refresh = token.refreshToken ?? fallbackRefreshToken;
|
|
318
|
+
if (refresh === undefined) {
|
|
319
|
+
throw new Error("xAI token response missing refresh_token");
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
access: token.accessToken,
|
|
323
|
+
refresh,
|
|
324
|
+
expires: Date.now() + token.expiresIn * 1000 - XAI_REFRESH_LEAD_MS,
|
|
325
|
+
tokenEndpoint,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function generatePkce() {
|
|
329
|
+
const verifier = randomBytes(96).toString("base64url");
|
|
330
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
331
|
+
return { verifier, challenge };
|
|
332
|
+
}
|
|
333
|
+
function randomToken() {
|
|
334
|
+
return randomBytes(16).toString("hex");
|
|
335
|
+
}
|
|
336
|
+
function validateXaiOAuthEndpoint(value, field) {
|
|
337
|
+
const endpoint = requiredString(value, field);
|
|
338
|
+
const url = new URL(endpoint);
|
|
339
|
+
if (url.protocol !== "https:") {
|
|
340
|
+
throw new Error(`xAI discovery ${field} must use https`);
|
|
341
|
+
}
|
|
342
|
+
const host = url.hostname.toLowerCase();
|
|
343
|
+
if (host !== "x.ai" && !host.endsWith(".x.ai")) {
|
|
344
|
+
throw new Error(`xAI discovery ${field} host is not on x.ai`);
|
|
345
|
+
}
|
|
346
|
+
return endpoint;
|
|
347
|
+
}
|
|
348
|
+
function parseJsonObject(text, label) {
|
|
349
|
+
const parsed = JSON.parse(text);
|
|
350
|
+
if (!isRecord(parsed))
|
|
351
|
+
throw new Error(`${label} must be a JSON object`);
|
|
352
|
+
return parsed;
|
|
353
|
+
}
|
|
354
|
+
function requiredString(value, field) {
|
|
355
|
+
const result = stringValue(value);
|
|
356
|
+
if (result === undefined)
|
|
357
|
+
throw new Error(`xAI response missing ${field}`);
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
function numberValue(value) {
|
|
361
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
362
|
+
}
|
|
363
|
+
function stringValue(value) {
|
|
364
|
+
if (typeof value !== "string")
|
|
365
|
+
return undefined;
|
|
366
|
+
const trimmed = value.trim();
|
|
367
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
368
|
+
}
|
|
369
|
+
function oauthPage(title, message) {
|
|
370
|
+
const escapedTitle = escapeHtml(title);
|
|
371
|
+
const escapedMessage = escapeHtml(message);
|
|
372
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escapedTitle}</title></head><body><main><h1>${escapedTitle}</h1><p>${escapedMessage}</p></main></body></html>`;
|
|
373
|
+
}
|
|
374
|
+
function escapeHtml(value) {
|
|
375
|
+
return value
|
|
376
|
+
.replaceAll("&", "&")
|
|
377
|
+
.replaceAll("<", "<")
|
|
378
|
+
.replaceAll(">", ">")
|
|
379
|
+
.replaceAll('"', """)
|
|
380
|
+
.replaceAll("'", "'");
|
|
381
|
+
}
|
|
382
|
+
function closeServer(server) {
|
|
383
|
+
server.close((error) => {
|
|
384
|
+
if (error) {
|
|
385
|
+
// Nothing actionable for the caller: the login flow has already settled.
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function isRecord(value) {
|
|
390
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
391
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rise-so/pi-grok-build",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "xAI Grok Build provider for pi: OAuth login (borrowed Grok CLI client) plus Grok chat models over the OpenAI Responses API. OAuth only. Usable as a pi extension or as a pi-ai library provider.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/rise-so/pi-grok-build.git"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/provider.d.ts",
|
|
14
|
+
"default": "./dist/provider.js"
|
|
15
|
+
},
|
|
16
|
+
"./extension": {
|
|
17
|
+
"types": "./dist/extension.d.ts",
|
|
18
|
+
"default": "./dist/extension.js"
|
|
19
|
+
},
|
|
20
|
+
"./core": {
|
|
21
|
+
"types": "./dist/xai-grok-build.d.ts",
|
|
22
|
+
"default": "./dist/xai-grok-build.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"!src/*.test.ts"
|
|
29
|
+
],
|
|
30
|
+
"keywords": [
|
|
31
|
+
"pi-package"
|
|
32
|
+
],
|
|
33
|
+
"pi": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./src/extension.ts"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=24.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-ai": ">=0.80.0 <0.81.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@earendil-works/pi-ai": "0.80.3",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.80.3",
|
|
47
|
+
"@types/node": "24.12.4",
|
|
48
|
+
"oxfmt": "^0.58.0",
|
|
49
|
+
"typescript": "5.9.3"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/fix-dts-extensions.ts",
|
|
53
|
+
"test": "node --test \"src/**/*.test.ts\"",
|
|
54
|
+
"test:live": "node --test \"live/**/*.test.ts\"",
|
|
55
|
+
"smoke:models": "node scripts/live-models.ts",
|
|
56
|
+
"smoke:extension": "scripts/smoke-extension.sh",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"check": "pnpm typecheck && pnpm test",
|
|
59
|
+
"fmt": "oxfmt ."
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// pi extension adapter: registers the Grok Build provider with pi's coding
|
|
2
|
+
// agent so /login offers "xAI (Grok subscription)" and the model lists.
|
|
3
|
+
//
|
|
4
|
+
// LOAD-BEARING CONSTRAINT: this module must never import anything under
|
|
5
|
+
// `@earendil-works/pi-ai/(api|providers)/` — pi's extension loader aliases the
|
|
6
|
+
// bare `@earendil-works/pi-ai` prefix to its own compat bundle, which mangles
|
|
7
|
+
// subpath specifiers into paths that do not exist. That failure typechecks
|
|
8
|
+
// clean and only surfaces at extension load. It must not import provider.ts
|
|
9
|
+
// either (which needs a pi-ai subpath at runtime). extension.test.ts guards
|
|
10
|
+
// this with an import grep.
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
OAuthCredentials,
|
|
14
|
+
OAuthLoginCallbacks,
|
|
15
|
+
OAuthProviderInterface,
|
|
16
|
+
} from "@earendil-works/pi-ai"; // type-only: erased, no runtime resolution
|
|
17
|
+
import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import {
|
|
19
|
+
GROK_MODELS,
|
|
20
|
+
XAI_BASE_URL,
|
|
21
|
+
XAI_PROVIDER_ID,
|
|
22
|
+
loginXai,
|
|
23
|
+
refreshXai,
|
|
24
|
+
requireXaiOAuthCredential,
|
|
25
|
+
type XaiLoginHost,
|
|
26
|
+
} from "./xai-grok-build.ts";
|
|
27
|
+
|
|
28
|
+
// Identify as pi in the authorize URL.
|
|
29
|
+
const EXTENSION_OPTS = { referrer: "pi" } as const;
|
|
30
|
+
|
|
31
|
+
// `usesCallbackServer` lives on OAuthProviderInterface and survives pi's
|
|
32
|
+
// registration spread, but ProviderConfig's inline `oauth` type omits it.
|
|
33
|
+
// Typing the const as Omit<…, "id"> gets it checked and dodges excess-property
|
|
34
|
+
// checking at the use site.
|
|
35
|
+
const oauth: Omit<OAuthProviderInterface, "id"> = {
|
|
36
|
+
name: "xAI (Grok subscription)",
|
|
37
|
+
usesCallbackServer: true,
|
|
38
|
+
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
39
|
+
return loginXai(toHost(callbacks), EXTENSION_OPTS);
|
|
40
|
+
},
|
|
41
|
+
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
42
|
+
return refreshXai(requireXaiOAuthCredential(credentials));
|
|
43
|
+
},
|
|
44
|
+
getApiKey(credentials: OAuthCredentials): string {
|
|
45
|
+
// Sync and string-only, so the provider-level baseUrl below carries what
|
|
46
|
+
// the library adapter's toAuth() returns alongside the key.
|
|
47
|
+
return requireXaiOAuthCredential(credentials).access;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Register synchronously inside the factory: pi drains pending registrations
|
|
52
|
+
// before any session exists, which is what makes `pi --list-models` and
|
|
53
|
+
// /login both see the provider.
|
|
54
|
+
export default function xaiGrokBuild(pi: ExtensionAPI): void {
|
|
55
|
+
pi.registerProvider(XAI_PROVIDER_ID, {
|
|
56
|
+
name: "xAI Grok Build",
|
|
57
|
+
baseUrl: XAI_BASE_URL,
|
|
58
|
+
// OAuth only, on purpose: no `apiKey` entry means pi never offers an
|
|
59
|
+
// API-key row for this provider and env keys are ignored. The Grok
|
|
60
|
+
// subscription token is the sole credential in this project.
|
|
61
|
+
api: "openai-responses",
|
|
62
|
+
models: GROK_MODELS.map((model) => ({
|
|
63
|
+
id: model.id,
|
|
64
|
+
name: model.name,
|
|
65
|
+
reasoning: model.reasoning,
|
|
66
|
+
input: [...model.input],
|
|
67
|
+
cost: { ...model.cost },
|
|
68
|
+
contextWindow: model.contextWindow,
|
|
69
|
+
maxTokens: model.maxTokens,
|
|
70
|
+
compat: { ...model.compat },
|
|
71
|
+
})),
|
|
72
|
+
oauth,
|
|
73
|
+
} satisfies ProviderConfig);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toHost(cb: OAuthLoginCallbacks): XaiLoginHost {
|
|
77
|
+
return {
|
|
78
|
+
authUrl: (url, instructions) => cb.onAuth({ url, instructions }),
|
|
79
|
+
progress: (message) => cb.onProgress?.(message),
|
|
80
|
+
...(cb.signal !== undefined ? { signal: cb.signal } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// pi-ai library adapter for the Grok Build core: import this from server-side
|
|
2
|
+
// code that composes a pi-ai / pi-agent Models registry. Keep it free of app
|
|
3
|
+
// imports and side effects, and never import it from client code. The
|
|
4
|
+
// extension adapter (extension.ts) must NOT import this module — the pi-ai
|
|
5
|
+
// subpath import below is fatal under pi's extension loader.
|
|
6
|
+
//
|
|
7
|
+
// Auth is OAuth-only by design: API keys are not supported anywhere in this
|
|
8
|
+
// project. The Grok subscription token is the sole credential.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createProvider,
|
|
12
|
+
type AuthLoginCallbacks,
|
|
13
|
+
type OAuthAuth,
|
|
14
|
+
type Provider,
|
|
15
|
+
} from "@earendil-works/pi-ai";
|
|
16
|
+
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
17
|
+
import {
|
|
18
|
+
GROK_MODELS,
|
|
19
|
+
XAI_BASE_URL,
|
|
20
|
+
XAI_PROVIDER_ID,
|
|
21
|
+
loginXai,
|
|
22
|
+
refreshXai,
|
|
23
|
+
requireXaiOAuthCredential,
|
|
24
|
+
type XaiLoginHost,
|
|
25
|
+
type XaiOptions,
|
|
26
|
+
} from "./xai-grok-build.ts";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
GROK_43_MODEL,
|
|
30
|
+
GROK_BUILD_MODEL,
|
|
31
|
+
GROK_COMPOSER_MODEL,
|
|
32
|
+
GROK_MODELS,
|
|
33
|
+
XAI_BASE_URL,
|
|
34
|
+
XAI_MODEL_ID,
|
|
35
|
+
XAI_PROVIDER_ID,
|
|
36
|
+
type XaiOptions,
|
|
37
|
+
} from "./xai-grok-build.ts";
|
|
38
|
+
|
|
39
|
+
export function xaiGrokBuildResponsesProvider(
|
|
40
|
+
options: XaiOptions = {},
|
|
41
|
+
): Provider<"openai-responses"> {
|
|
42
|
+
const opts: XaiOptions = { ...options };
|
|
43
|
+
return createProvider({
|
|
44
|
+
id: XAI_PROVIDER_ID,
|
|
45
|
+
name: "xAI Grok Build",
|
|
46
|
+
baseUrl: XAI_BASE_URL,
|
|
47
|
+
auth: { oauth: xaiOAuthAuth(opts) },
|
|
48
|
+
models: [...GROK_MODELS],
|
|
49
|
+
api: openAIResponsesApi(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function xaiOAuthAuth(opts: XaiOptions): OAuthAuth {
|
|
54
|
+
return {
|
|
55
|
+
name: "xAI (Grok subscription)",
|
|
56
|
+
async login(callbacks) {
|
|
57
|
+
return { type: "oauth", ...(await loginXai(toHost(callbacks), opts)) };
|
|
58
|
+
},
|
|
59
|
+
async refresh(credential) {
|
|
60
|
+
return { type: "oauth", ...(await refreshXai(requireXaiOAuthCredential(credential))) };
|
|
61
|
+
},
|
|
62
|
+
async toAuth(credential) {
|
|
63
|
+
return { apiKey: requireXaiOAuthCredential(credential).access, baseUrl: XAI_BASE_URL };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toHost(cb: AuthLoginCallbacks): XaiLoginHost {
|
|
69
|
+
return {
|
|
70
|
+
authUrl: (url, instructions) => cb.notify({ type: "auth_url", url, instructions }),
|
|
71
|
+
progress: (message) => cb.notify({ type: "progress", message }),
|
|
72
|
+
...(cb.signal !== undefined ? { signal: cb.signal } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|