@ottocode/sdk 0.1.283 → 0.1.285
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/package.json
CHANGED
package/src/auth/src/index.ts
CHANGED
|
@@ -82,6 +82,16 @@ export {
|
|
|
82
82
|
type OpenAIOAuthResult,
|
|
83
83
|
} from './openai-oauth.ts';
|
|
84
84
|
|
|
85
|
+
export {
|
|
86
|
+
authorizeXai,
|
|
87
|
+
exchangeXai,
|
|
88
|
+
refreshXaiToken,
|
|
89
|
+
openXaiAuthUrl,
|
|
90
|
+
readGrokCliAuth,
|
|
91
|
+
type XaiOAuthResult,
|
|
92
|
+
type XaiOAuthTokens,
|
|
93
|
+
} from './xai-oauth.ts';
|
|
94
|
+
|
|
85
95
|
export {
|
|
86
96
|
generateWallet,
|
|
87
97
|
importWallet,
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const XAI_OAUTH_ISSUER = 'https://auth.x.ai';
|
|
9
|
+
const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
|
|
10
|
+
const XAI_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
|
|
11
|
+
const XAI_OAUTH_SCOPE =
|
|
12
|
+
'openid profile email offline_access grok-cli:access api:access';
|
|
13
|
+
const XAI_CALLBACK_HOST = '127.0.0.1';
|
|
14
|
+
const XAI_CALLBACK_PORT = 56121;
|
|
15
|
+
const XAI_CALLBACK_PATH = '/callback';
|
|
16
|
+
const XAI_REFRESH_SKEW_MS = 2 * 60 * 1000;
|
|
17
|
+
const XAI_GROK_CLI_AUTH_SCOPE_KEY = `${XAI_OAUTH_ISSUER}::${XAI_OAUTH_CLIENT_ID}`;
|
|
18
|
+
const XAI_GROK_CLI_LEGACY_AUTH_SCOPE_KEY = 'https://accounts.x.ai/sign-in';
|
|
19
|
+
|
|
20
|
+
type XaiDiscovery = {
|
|
21
|
+
authorization_endpoint: string;
|
|
22
|
+
token_endpoint: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type XaiTokenPayload = {
|
|
26
|
+
access_token?: string;
|
|
27
|
+
refresh_token?: string;
|
|
28
|
+
id_token?: string;
|
|
29
|
+
expires_in?: number;
|
|
30
|
+
scope?: string;
|
|
31
|
+
token_type?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type XaiOAuthTokens = {
|
|
35
|
+
access: string;
|
|
36
|
+
refresh: string;
|
|
37
|
+
expires: number;
|
|
38
|
+
idToken?: string;
|
|
39
|
+
scopes?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type XaiOAuthResult = {
|
|
43
|
+
url: string;
|
|
44
|
+
verifier: string;
|
|
45
|
+
redirectUri: string;
|
|
46
|
+
waitForCallback: () => Promise<string>;
|
|
47
|
+
close: () => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function generatePKCE() {
|
|
51
|
+
const verifier = randomBytes(32)
|
|
52
|
+
.toString('base64')
|
|
53
|
+
.replace(/\+/g, '-')
|
|
54
|
+
.replace(/\//g, '_')
|
|
55
|
+
.replace(/=/g, '');
|
|
56
|
+
|
|
57
|
+
const challenge = createHash('sha256')
|
|
58
|
+
.update(verifier)
|
|
59
|
+
.digest('base64')
|
|
60
|
+
.replace(/\+/g, '-')
|
|
61
|
+
.replace(/\//g, '_')
|
|
62
|
+
.replace(/=/g, '');
|
|
63
|
+
|
|
64
|
+
return { verifier, challenge };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function generateState() {
|
|
68
|
+
return randomBytes(32)
|
|
69
|
+
.toString('base64')
|
|
70
|
+
.replace(/\+/g, '-')
|
|
71
|
+
.replace(/\//g, '_')
|
|
72
|
+
.replace(/=/g, '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateXaiEndpoint(url: string): string {
|
|
76
|
+
const parsed = new URL(url);
|
|
77
|
+
const host = parsed.hostname.toLowerCase();
|
|
78
|
+
if (
|
|
79
|
+
parsed.protocol !== 'https:' ||
|
|
80
|
+
(host !== 'x.ai' && !host.endsWith('.x.ai'))
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(`xAI OAuth discovery returned unexpected endpoint: ${url}`);
|
|
83
|
+
}
|
|
84
|
+
return url;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function discoverXaiOAuth(): Promise<XaiDiscovery> {
|
|
88
|
+
const response = await fetch(XAI_OAUTH_DISCOVERY_URL, {
|
|
89
|
+
headers: { Accept: 'application/json' },
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`xAI OAuth discovery failed: ${response.status} ${await response.text()}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const data = (await response.json()) as Partial<XaiDiscovery>;
|
|
97
|
+
if (!data.authorization_endpoint || !data.token_endpoint) {
|
|
98
|
+
throw new Error('xAI OAuth discovery did not return auth/token endpoints');
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
authorization_endpoint: validateXaiEndpoint(data.authorization_endpoint),
|
|
102
|
+
token_endpoint: validateXaiEndpoint(data.token_endpoint),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function openBrowser(url: string) {
|
|
107
|
+
const platform = process.platform;
|
|
108
|
+
let command: string;
|
|
109
|
+
|
|
110
|
+
switch (platform) {
|
|
111
|
+
case 'darwin':
|
|
112
|
+
command = `open "${url}"`;
|
|
113
|
+
break;
|
|
114
|
+
case 'win32':
|
|
115
|
+
command = `start "${url}"`;
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
command = `xdg-open "${url}"`;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new Promise<void>((resolve, reject) => {
|
|
123
|
+
const child = spawn(command, [], { shell: true });
|
|
124
|
+
child.on('error', reject);
|
|
125
|
+
child.on('exit', (code) => {
|
|
126
|
+
if (code === 0) resolve();
|
|
127
|
+
else reject(new Error(`Failed to open browser (exit code ${code})`));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseExpiry(value: unknown): number | undefined {
|
|
133
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
134
|
+
if (typeof value !== 'string' || !value.trim()) return undefined;
|
|
135
|
+
const numeric = Number(value);
|
|
136
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
137
|
+
const parsed = Date.parse(value);
|
|
138
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function tokensFromPayload(
|
|
142
|
+
data: XaiTokenPayload,
|
|
143
|
+
fallbackRefresh = '',
|
|
144
|
+
): XaiOAuthTokens {
|
|
145
|
+
if (!data.access_token) {
|
|
146
|
+
throw new Error('xAI token response did not include an access token');
|
|
147
|
+
}
|
|
148
|
+
const refresh = data.refresh_token || fallbackRefresh;
|
|
149
|
+
if (!refresh) {
|
|
150
|
+
throw new Error('xAI token response did not include a refresh token');
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
access: data.access_token,
|
|
154
|
+
refresh,
|
|
155
|
+
expires:
|
|
156
|
+
Date.now() + (data.expires_in ?? 3600) * 1000 - XAI_REFRESH_SKEW_MS,
|
|
157
|
+
idToken: data.id_token,
|
|
158
|
+
scopes: data.scope,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function exchangeXaiToken(
|
|
163
|
+
body: Record<string, string>,
|
|
164
|
+
): Promise<XaiTokenPayload> {
|
|
165
|
+
const discovery = await discoverXaiOAuth();
|
|
166
|
+
const response = await fetch(discovery.token_endpoint, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
Accept: 'application/json',
|
|
170
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
171
|
+
},
|
|
172
|
+
body: new URLSearchParams(body).toString(),
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`xAI token request failed: ${response.status} ${await response.text()}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return (await response.json()) as XaiTokenPayload;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Start the xAI OAuth PKCE browser flow using a localhost callback. */
|
|
183
|
+
export async function authorizeXai(): Promise<XaiOAuthResult> {
|
|
184
|
+
const discovery = await discoverXaiOAuth();
|
|
185
|
+
const pkce = generatePKCE();
|
|
186
|
+
const state = generateState();
|
|
187
|
+
const nonce = generateState();
|
|
188
|
+
const redirectUri = `http://${XAI_CALLBACK_HOST}:${XAI_CALLBACK_PORT}${XAI_CALLBACK_PATH}`;
|
|
189
|
+
|
|
190
|
+
const params = new URLSearchParams({
|
|
191
|
+
response_type: 'code',
|
|
192
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
193
|
+
redirect_uri: redirectUri,
|
|
194
|
+
scope: XAI_OAUTH_SCOPE,
|
|
195
|
+
code_challenge: pkce.challenge,
|
|
196
|
+
code_challenge_method: 'S256',
|
|
197
|
+
state,
|
|
198
|
+
nonce,
|
|
199
|
+
});
|
|
200
|
+
const authUrl = `${discovery.authorization_endpoint}?${params.toString()}`;
|
|
201
|
+
|
|
202
|
+
let resolveCallback: (code: string) => void;
|
|
203
|
+
let rejectCallback: (error: Error) => void;
|
|
204
|
+
const callbackPromise = new Promise<string>((resolve, reject) => {
|
|
205
|
+
resolveCallback = resolve;
|
|
206
|
+
rejectCallback = reject;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const server = createServer((req, res) => {
|
|
210
|
+
const reqUrl = new URL(
|
|
211
|
+
req.url || '/',
|
|
212
|
+
`http://${XAI_CALLBACK_HOST}:${XAI_CALLBACK_PORT}`,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (reqUrl.pathname !== XAI_CALLBACK_PATH) {
|
|
216
|
+
res.writeHead(404);
|
|
217
|
+
res.end('Not found');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const code = reqUrl.searchParams.get('code');
|
|
222
|
+
const returnedState = reqUrl.searchParams.get('state');
|
|
223
|
+
const error = reqUrl.searchParams.get('error');
|
|
224
|
+
const errorDescription = reqUrl.searchParams.get('error_description');
|
|
225
|
+
|
|
226
|
+
if (error) {
|
|
227
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
228
|
+
res.end(
|
|
229
|
+
`<html><body><h1>xAI authentication failed</h1><p>${errorDescription || error}</p></body></html>`,
|
|
230
|
+
);
|
|
231
|
+
rejectCallback(
|
|
232
|
+
new Error(`xAI OAuth error: ${errorDescription || error}`),
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (returnedState !== state) {
|
|
238
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
239
|
+
res.end(
|
|
240
|
+
'<html><body><h1>Invalid State</h1><p>State mismatch. Please try again.</p></body></html>',
|
|
241
|
+
);
|
|
242
|
+
rejectCallback(new Error('xAI OAuth state mismatch'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!code) {
|
|
247
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
248
|
+
res.end('<html><body><h1>Missing Code</h1></body></html>');
|
|
249
|
+
rejectCallback(new Error('No xAI authorization code received'));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
254
|
+
res.end(`
|
|
255
|
+
<html>
|
|
256
|
+
<head>
|
|
257
|
+
<title>otto - xAI Connected</title>
|
|
258
|
+
<style>
|
|
259
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #111827 0%, #374151 100%); color: white; }
|
|
260
|
+
.container { text-align: center; padding: 2rem; background: rgba(255,255,255,0.1); border-radius: 16px; backdrop-filter: blur(10px); }
|
|
261
|
+
.checkmark { font-size: 4rem; margin-bottom: 1rem; }
|
|
262
|
+
h1 { margin: 0 0 0.5rem 0; }
|
|
263
|
+
p { margin: 0; opacity: 0.9; }
|
|
264
|
+
</style>
|
|
265
|
+
</head>
|
|
266
|
+
<body>
|
|
267
|
+
<div class="container">
|
|
268
|
+
<div class="checkmark">✓</div>
|
|
269
|
+
<h1>xAI connected!</h1>
|
|
270
|
+
<p>You can close this window.</p>
|
|
271
|
+
</div>
|
|
272
|
+
<script>setTimeout(() => window.close(), 1500);</script>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|
|
275
|
+
`);
|
|
276
|
+
resolveCallback(code);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await new Promise<void>((resolve, reject) => {
|
|
280
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
281
|
+
if (err.code === 'EADDRINUSE') {
|
|
282
|
+
reject(
|
|
283
|
+
new Error(
|
|
284
|
+
`Port ${XAI_CALLBACK_PORT} is already in use. Stop any running Grok/xAI OAuth flow and try again.`,
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
} else {
|
|
288
|
+
reject(err);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
server.listen(XAI_CALLBACK_PORT, XAI_CALLBACK_HOST, () => resolve());
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const timeoutMs = 5 * 60 * 1000;
|
|
295
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
296
|
+
const timeoutPromise = new Promise<string>((_resolve, reject) => {
|
|
297
|
+
timeoutId = setTimeout(() => {
|
|
298
|
+
server.close();
|
|
299
|
+
reject(new Error('xAI OAuth callback timeout'));
|
|
300
|
+
}, timeoutMs);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const waitForCallback = () =>
|
|
304
|
+
Promise.race([callbackPromise, timeoutPromise]).finally(() => {
|
|
305
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
url: authUrl,
|
|
310
|
+
verifier: pkce.verifier,
|
|
311
|
+
redirectUri,
|
|
312
|
+
waitForCallback,
|
|
313
|
+
close: () => {
|
|
314
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
315
|
+
server.close();
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Exchange an xAI OAuth authorization code for access and refresh tokens. */
|
|
321
|
+
export async function exchangeXai(
|
|
322
|
+
code: string,
|
|
323
|
+
verifier: string,
|
|
324
|
+
): Promise<XaiOAuthTokens> {
|
|
325
|
+
const redirectUri = `http://${XAI_CALLBACK_HOST}:${XAI_CALLBACK_PORT}${XAI_CALLBACK_PATH}`;
|
|
326
|
+
const data = await exchangeXaiToken({
|
|
327
|
+
grant_type: 'authorization_code',
|
|
328
|
+
code,
|
|
329
|
+
redirect_uri: redirectUri,
|
|
330
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
331
|
+
code_verifier: verifier,
|
|
332
|
+
});
|
|
333
|
+
return tokensFromPayload(data);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Refresh an xAI OAuth access token. */
|
|
337
|
+
export async function refreshXaiToken(
|
|
338
|
+
refreshToken: string,
|
|
339
|
+
): Promise<XaiOAuthTokens> {
|
|
340
|
+
const data = await exchangeXaiToken({
|
|
341
|
+
grant_type: 'refresh_token',
|
|
342
|
+
refresh_token: refreshToken,
|
|
343
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
344
|
+
});
|
|
345
|
+
return tokensFromPayload(data, refreshToken);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Open the xAI OAuth authorization URL in the user's default browser. */
|
|
349
|
+
export async function openXaiAuthUrl(url: string) {
|
|
350
|
+
try {
|
|
351
|
+
await openBrowser(url);
|
|
352
|
+
return true;
|
|
353
|
+
} catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Read reusable OAuth credentials created by the official Grok CLI, if present. */
|
|
359
|
+
export function readGrokCliAuth(): XaiOAuthTokens | undefined {
|
|
360
|
+
const authPath = join(homedir(), '.grok', 'auth.json');
|
|
361
|
+
if (!existsSync(authPath)) return undefined;
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const data = JSON.parse(readFileSync(authPath, 'utf8')) as Record<
|
|
365
|
+
string,
|
|
366
|
+
Record<string, unknown>
|
|
367
|
+
>;
|
|
368
|
+
const oidc = data[XAI_GROK_CLI_AUTH_SCOPE_KEY];
|
|
369
|
+
if (oidc) {
|
|
370
|
+
const access = String(oidc.key || oidc.access_token || oidc.token || '');
|
|
371
|
+
if (access) {
|
|
372
|
+
return {
|
|
373
|
+
access,
|
|
374
|
+
refresh: String(oidc.refresh_token || oidc.refresh || ''),
|
|
375
|
+
expires:
|
|
376
|
+
(parseExpiry(oidc.expires_at) || Date.now() + 6 * 60 * 60 * 1000) -
|
|
377
|
+
XAI_REFRESH_SKEW_MS,
|
|
378
|
+
idToken: String(oidc.id_token || '') || undefined,
|
|
379
|
+
scopes: String(oidc.scope || '') || undefined,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const legacy = data[XAI_GROK_CLI_LEGACY_AUTH_SCOPE_KEY];
|
|
385
|
+
const legacyAccess = legacy
|
|
386
|
+
? String(legacy.key || legacy.access_token || legacy.token || '')
|
|
387
|
+
: '';
|
|
388
|
+
if (legacyAccess) {
|
|
389
|
+
return {
|
|
390
|
+
access: legacyAccess,
|
|
391
|
+
refresh: String(legacy?.refresh_token || legacy?.refresh || ''),
|
|
392
|
+
expires:
|
|
393
|
+
parseExpiry(legacy?.expires_at || legacy?.expires) ||
|
|
394
|
+
Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
@@ -196,8 +196,9 @@ export async function resolveModel(
|
|
|
196
196
|
|
|
197
197
|
if (provider === 'xai') {
|
|
198
198
|
return createXaiModel(model, {
|
|
199
|
-
apiKey: config.apiKey,
|
|
199
|
+
apiKey: config.oauth?.access ?? config.apiKey,
|
|
200
200
|
baseURL: config.baseURL,
|
|
201
|
+
useResponses: !!config.oauth,
|
|
201
202
|
});
|
|
202
203
|
}
|
|
203
204
|
|
package/src/index.ts
CHANGED
|
@@ -191,6 +191,14 @@ export {
|
|
|
191
191
|
exchangeOpenAIWeb,
|
|
192
192
|
} from './auth/src/index.ts';
|
|
193
193
|
export type { OpenAIOAuthResult } from './auth/src/index.ts';
|
|
194
|
+
export {
|
|
195
|
+
authorizeXai,
|
|
196
|
+
exchangeXai,
|
|
197
|
+
refreshXaiToken,
|
|
198
|
+
openXaiAuthUrl,
|
|
199
|
+
readGrokCliAuth,
|
|
200
|
+
} from './auth/src/index.ts';
|
|
201
|
+
export type { XaiOAuthResult, XaiOAuthTokens } from './auth/src/index.ts';
|
|
194
202
|
export {
|
|
195
203
|
generateWallet,
|
|
196
204
|
importWallet,
|
|
@@ -4,12 +4,25 @@ import { catalog } from './catalog-merged.ts';
|
|
|
4
4
|
export type XaiProviderConfig = {
|
|
5
5
|
apiKey?: string;
|
|
6
6
|
baseURL?: string;
|
|
7
|
+
useResponses?: boolean;
|
|
7
8
|
};
|
|
8
9
|
|
|
10
|
+
function shouldUseXaiResponsesApi(model: string): boolean {
|
|
11
|
+
const normalized = model.toLowerCase().split('/').pop() || model;
|
|
12
|
+
return (
|
|
13
|
+
normalized === 'grok-4.3' ||
|
|
14
|
+
normalized === 'grok-build-0.1' ||
|
|
15
|
+
normalized.startsWith('grok-4.20-')
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export function createXaiModel(model: string, config?: XaiProviderConfig) {
|
|
10
20
|
const entry = catalog.xai;
|
|
11
21
|
const apiKey = config?.apiKey || process.env.XAI_API_KEY || '';
|
|
12
22
|
const baseURL = config?.baseURL || entry?.api;
|
|
13
23
|
const instance = createXai({ apiKey, baseURL });
|
|
24
|
+
if (config?.useResponses ?? shouldUseXaiResponsesApi(model)) {
|
|
25
|
+
return instance.responses(model);
|
|
26
|
+
}
|
|
14
27
|
return instance(model);
|
|
15
28
|
}
|