-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathindex.ts
More file actions
315 lines (282 loc) · 11 KB
/
Copy pathindex.ts
File metadata and controls
315 lines (282 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/**
* pi-grok: xAI Grok OAuth provider for pi
*
* Brings SuperGrok / Premium subscription access (including Grok Build)
* into pi via the official xAI OAuth 2.0 + PKCE flow.
*
* Based on the Hermes agent xai-oauth implementation, rewritten for the pi SDK.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import {
type Api,
type AssistantMessageEventStream,
type Context,
type Model,
type OAuthCredentials,
type OAuthLoginCallbacks,
type SimpleStreamOptions,
streamSimpleOpenAIResponses,
} from "@earendil-works/pi-ai";
import * as oauth from "./oauth.js";
import { type XaiOAuthCredentials, getBaseUrl } from "./oauth.js";
import {
resolveModels,
rebuildModelsForOAuth,
thinkingLevelMapFor,
triggerDiscovery,
discoveryStatus,
CLI_PROXY_BASE_URL,
buildProxyHeaders,
type XaiModelConfig,
} from "./models.js";
import {
fetchUser,
formatStatusBlock,
parsePrivacyArg,
privacyLine,
privacyUsage,
setCodingDataRetention,
} from "./account.js";
import { runPrivacyPicker } from "./privacy.js";
import { sanitizePayload } from "./sanitize.js";
import { XaiOAuthError } from "./errors.js";
import { registerXSearchTool } from "./x-search-tool.js";
import { fetchUsage, formatUsageBlock, XaiUsageError } from "./usage.js";
// ─── Stream function ─────────────────────────────────────────────────────────
function streamGrok(
model: Model<Api>,
context: Context,
options?: SimpleStreamOptions,
): AssistantMessageEventStream {
const sessionId = options?.sessionId;
const headers = {
...options?.headers,
...(sessionId ? { "x-grok-conv-id": sessionId } : {}),
};
return streamSimpleOpenAIResponses(model as Model<"openai-responses">, context, {
...options,
headers,
});
}
/**
* Format a proxy/account error for display. A `reloginRequired` error means
* the access token is bad or expired, so skip the raw dump and point at /login.
*/
function formatProxyError(err: unknown, prefix: string): string {
if (err instanceof XaiOAuthError) {
if (err.reloginRequired) return "xAI session expired. Run /login to re-authenticate.";
return `${prefix}: ${err.message} (code: ${err.code})`;
}
const msg = err instanceof Error ? err.message : String(err);
return `${prefix}: ${msg}`;
}
// ─── Extension entry point ───────────────────────────────────────────────────
export default function (pi: ExtensionAPI) {
const baseUrl = getBaseUrl();
const models = resolveModels();
// `usesCallbackServer` selects the host's loopback-callback login path over
// manual paste. It is read at runtime but is absent from the published oauth
// config type, so the oauth block is built as a variable of this type (excess
// properties are allowed for variables, not literals) and handed to the host.
type XaiOAuthConfig = Parameters<ExtensionAPI["registerProvider"]>[1] extends
{ oauth?: infer O } ? O & { usesCallbackServer: boolean } : never;
const oauthConfig: XaiOAuthConfig = {
name: "xAI (SuperGrok Subscription)",
usesCallbackServer: true,
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
return oauth.login(callbacks);
},
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
return oauth.refresh(credentials);
},
getApiKey(credentials: OAuthCredentials): string {
return credentials.access;
},
modifyModels(models: Model<Api>[], credentials: OAuthCredentials) {
const creds = credentials as XaiOAuthCredentials;
// Kick off a background live-catalog fetch for enrichment (context
// windows, newly released ids). The OAuth session's model registry
// lives on the cli-chat-proxy, so discovery rides the proxy with
// the proxy identity headers, never api.x.ai. Routing of individual
// models is static and set in rebuildModelsForOAuth below.
if (creds.access) triggerDiscovery(creds.access, CLI_PROXY_BASE_URL);
// Full rebuild: append discovered ids, re-apply PI_XAI_OAUTH_MODELS,
// stamp api/provider, and route every model through the CLI proxy.
// rebuildModelsForOAuth is typed loosely (Record<string, unknown>[])
// because it rewrites provider entries generically; the result keeps
// the Model shape the host handed in, so cast back at the boundary.
return rebuildModelsForOAuth(
models as unknown as Array<Record<string, unknown>>,
"xai-oauth",
) as unknown as Model<Api>[];
},
};
// ── Register provider ─────────────────────────────────────────────────
pi.registerProvider("xai-oauth", {
name: "xAI (SuperGrok Subscription)",
baseUrl,
apiKey: "$XAI_OAUTH_TOKEN",
api: "openai-responses",
models: models.map((m: XaiModelConfig) => ({
id: m.id,
name: m.name,
reasoning: m.reasoning,
thinkingLevelMap: m.thinkingLevelMap ?? thinkingLevelMapFor(m.id, m.reasoning),
input: m.input,
cost: m.cost,
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
// Stamp proxy routing at registration so the XAI_OAUTH_TOKEN env
// bypass (which skips modifyModels) rides the proxy too. The OAuth
// path re-stamps this in rebuildModelsForOAuth, including discovered
// ids the registration map never saw.
baseUrl: CLI_PROXY_BASE_URL,
headers: buildProxyHeaders(m.id),
})),
oauth: oauthConfig,
streamSimple: streamGrok,
});
// ── Payload sanitization via event ────────────────────────────────────
pi.on("before_provider_request", (event, ctx) => {
if (ctx.model?.provider !== "xai-oauth") return;
const modelId = ctx.model?.id ?? "";
const sessionId = ctx.sessionManager?.getSessionId();
return sanitizePayload(event.payload as Record<string, unknown>, modelId, sessionId, ctx.model?.reasoning ?? false);
});
// ── X Search tool ─────────────────────────────────────────────────────
if ((process.env.PI_XAI_X_SEARCH ?? "true").toLowerCase() !== "false") {
registerXSearchTool(pi);
}
// ── /xai-status command ───────────────────────────────────────────────
pi.registerCommand("xai-status", {
description: "Show xAI Grok account, privacy, and model status",
handler: async (_args, ctx) => {
const grokModels = ctx.modelRegistry.getAll().filter((m: Model<Api>) => m.provider === "xai-oauth");
// Resolve the live access token. Handles both the OAuth path and the
// XAI_OAUTH_TOKEN env bypass; missing token means not logged in.
let token: string | undefined;
try {
token = await ctx.modelRegistry.getApiKeyForProvider("xai-oauth");
} catch {
token = undefined;
}
const tokenSource = process.env.XAI_OAUTH_TOKEN
? "env"
: token
? "oauth"
: "none";
// Fetch account enrichment best-effort: a failed lookup (offline,
// expired) still renders the model count so status stays useful.
let user = null;
if (token) {
try {
user = await fetchUser(token);
} catch (err) {
ctx.ui.notify(formatProxyError(err, "xAI account lookup failed"), "warning");
}
}
ctx.ui.notify(
formatStatusBlock({ user, modelCount: grokModels.length, tokenSource, discovery: discoveryStatus() }),
"info",
);
},
});
// ── /xai-privacy command ──────────────────────────────────────────────
pi.registerCommand("xai-privacy", {
description: "Show or set xAI coding data retention (privacy mode)",
handler: async (args, ctx) => {
let token: string | undefined;
try {
token = await ctx.modelRegistry.getApiKeyForProvider("xai-oauth");
} catch {
token = undefined;
}
if (!token) {
ctx.ui.notify("xAI: not logged in. Run /login, choose xAI (SuperGrok Subscription).", "warning");
return;
}
const parsed = parsePrivacyArg(args);
if (parsed.kind === "invalid") {
ctx.ui.notify(`Unknown argument \`${parsed.arg}\`. ${privacyUsage()}`, "warning");
return;
}
// Read current state. The /user fetch also surfaces isZdr when the
// org locks retention; if it does, the picker is moot.
let user;
try {
user = await fetchUser(token);
} catch (err) {
ctx.ui.notify(formatProxyError(err, "xAI privacy"), "warning");
return;
}
if (user.isZdr) {
ctx.ui.notify(`xAI privacy: ${privacyLine(user)}`, "info");
return;
}
// No argument: show an inline themed picker with both modes and a
// green tick on the current one (mirrors the login provider
// selector, rendered inline like /login, not as a popup). An explicit
// alias skips the picker and applies.
let target: boolean;
if (parsed.kind === "select") {
if (!ctx.hasUI) return; // non-interactive: nothing to pick
const picked = await runPrivacyPicker(ctx.ui, user.codingDataRetentionOptOut);
if (picked === undefined) return; // cancelled
target = picked;
} else {
target = parsed.optOut;
}
// Nothing to do if the account is already in the picked mode.
if (target === user.codingDataRetentionOptOut) {
ctx.ui.notify(`xAI privacy: ${privacyLine(user)} (no change)`, "info");
return;
}
try {
const applied = await setCodingDataRetention(token, target);
ctx.ui.notify(
`xAI privacy: ${privacyLine({ codingDataRetentionOptOut: applied })}`,
"info",
);
} catch (err) {
ctx.ui.notify(formatProxyError(err, "xAI privacy"), "warning");
}
},
});
// ── /xai-usage command ────────────────────────────────────────────────
pi.registerCommand("xai-usage", {
description: "Show xAI subscription credit usage",
handler: async (_args, ctx) => {
let token: string | undefined;
try {
token = await ctx.modelRegistry.getApiKeyForProvider("xai-oauth");
} catch {
token = undefined;
}
if (!token) {
ctx.ui.notify(
"xAI: not logged in. Run /login, choose xAI (SuperGrok Subscription).",
"warning",
);
return;
}
try {
const snapshot = await fetchUsage(token);
ctx.ui.notify(formatUsageBlock(snapshot), "info");
} catch (err) {
const message = err instanceof XaiUsageError
? err.message
: `xAI usage lookup failed: ${err instanceof Error ? err.message : String(err)}`;
ctx.ui.notify(message, "warning");
}
},
});
// ── Warn on env bypass ────────────────────────────────────────────────
if (process.env.XAI_OAUTH_TOKEN) {
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify(
"[pi-grok] Using XAI_OAUTH_TOKEN bypass: no auto-refresh, no model discovery",
"warning",
);
});
}
}