converse-mcp-server 2.29.2 → 3.0.1
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/.env.example +6 -3
- package/README.md +96 -94
- package/docs/API.md +703 -1562
- package/docs/ARCHITECTURE.md +13 -11
- package/docs/EXAMPLES.md +241 -667
- package/docs/PROVIDERS.md +104 -79
- package/package.json +1 -1
- package/src/async/asyncJobStore.js +2 -2
- package/src/async/jobRunner.js +6 -1
- package/src/async/providerStreamNormalizer.js +59 -1
- package/src/config.js +4 -3
- package/src/prompts/helpPrompt.js +43 -61
- package/src/providers/anthropic.js +0 -31
- package/src/providers/claude.js +0 -14
- package/src/providers/codex.js +15 -21
- package/src/providers/copilot.js +36 -202
- package/src/providers/deepseek.js +73 -53
- package/src/providers/gemini-cli.js +0 -3
- package/src/providers/google.js +10 -27
- package/src/providers/interface.js +0 -3
- package/src/providers/mistral.js +169 -63
- package/src/providers/openai-compatible.js +113 -28
- package/src/providers/openai.js +14 -66
- package/src/providers/openrouter-discovery.js +308 -0
- package/src/providers/openrouter.js +452 -282
- package/src/providers/xai.js +298 -280
- package/src/services/summarizationService.js +4 -14
- package/src/systemPrompts.js +19 -2
- package/src/tools/cancelJob.js +7 -1
- package/src/tools/chat.js +957 -995
- package/src/tools/checkStatus.js +1 -0
- package/src/tools/index.js +2 -6
- package/src/tools/modes/parallel.js +488 -0
- package/src/tools/modes/roundtable.js +495 -0
- package/src/tools/modes/streamShared.js +31 -0
- package/src/utils/contextProcessor.js +1 -38
- package/src/utils/conversationExporter.js +6 -26
- package/src/utils/formatStatus.js +46 -19
- package/src/utils/modelRouting.js +207 -4
- package/src/providers/openrouter-endpoints-client.js +0 -220
- package/src/tools/consensus.js +0 -1813
- package/src/tools/conversation.js +0 -1233
package/src/providers/xai.js
CHANGED
|
@@ -1,93 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* XAI (Grok) Provider
|
|
3
3
|
*
|
|
4
|
-
* Provider implementation for XAI Grok models using
|
|
4
|
+
* Provider implementation for XAI Grok models using the xAI Responses API
|
|
5
|
+
* (`POST /v1/responses`) via the `openai` SDK pointed at `https://api.x.ai/v1`.
|
|
6
|
+
* The legacy Chat Completions endpoint is deprecated for xAI: it returns no
|
|
7
|
+
* reasoning content and only "function calling" for tools, so grok-4.5's
|
|
8
|
+
* headline capabilities (reasoning content, native web/X search via Agent
|
|
9
|
+
* Tools) are only available through the Responses API.
|
|
10
|
+
*
|
|
5
11
|
* Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
|
|
12
|
+
* (or an AsyncGenerator of start/delta/thinking/usage/end/error events when stream=true).
|
|
6
13
|
*/
|
|
7
14
|
|
|
8
15
|
import OpenAI from 'openai';
|
|
9
16
|
import { debugLog, debugError } from '../utils/console.js';
|
|
10
17
|
|
|
11
|
-
//
|
|
18
|
+
// Curated catalog: grok-4.5 only (verified live 2026-07-11 against
|
|
19
|
+
// GET https://api.x.ai/v1/models/grok-4.5 — id, aliases, 500k context).
|
|
20
|
+
//
|
|
21
|
+
// NOTE ON RETIRED IDS: xAI does NOT return a retirement error for old grok
|
|
22
|
+
// identifiers. Direct lookups on retired IDs return HTTP 200 but are silently
|
|
23
|
+
// server-remapped upstream (e.g. grok-4-0709 / grok-4-fast-* → grok-4.3,
|
|
24
|
+
// grok-code-fast-1 → grok-build-0.1). This contradicts the general
|
|
25
|
+
// "surface a clear retirement error" assumption for other providers. Explicit
|
|
26
|
+
// retired IDs are passed through unchanged (resolveModelName passthrough) and
|
|
27
|
+
// the xAI API remaps them itself — Converse never remaps client-side.
|
|
12
28
|
const SUPPORTED_MODELS = {
|
|
13
|
-
'grok-4
|
|
14
|
-
modelName: 'grok-4
|
|
15
|
-
friendlyName: 'X.AI (Grok 4)',
|
|
16
|
-
contextWindow:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
supportsImages: true,
|
|
20
|
-
supportsTemperature: true,
|
|
21
|
-
supportsWebSearch: true,
|
|
22
|
-
timeout: 300000, // 5 minutes
|
|
23
|
-
description:
|
|
24
|
-
'GROK-4 (256K context) - Latest advanced model from X.AI with image support and live search',
|
|
25
|
-
aliases: [
|
|
26
|
-
'grok',
|
|
27
|
-
'grok4',
|
|
28
|
-
'grok-4',
|
|
29
|
-
'grok-4-latest',
|
|
30
|
-
'grok 4',
|
|
31
|
-
'grok 4 latest',
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
'grok-4-fast-reasoning': {
|
|
35
|
-
modelName: 'grok-4-fast-reasoning',
|
|
36
|
-
friendlyName: 'X.AI (Grok 4 Fast Reasoning)',
|
|
37
|
-
contextWindow: 2000000, // 2M tokens
|
|
38
|
-
maxOutputTokens: 2000000,
|
|
29
|
+
'grok-4.5': {
|
|
30
|
+
modelName: 'grok-4.5',
|
|
31
|
+
friendlyName: 'X.AI (Grok 4.5)',
|
|
32
|
+
contextWindow: 500000,
|
|
33
|
+
// No documented output ceiling — reuse the context window as the cap.
|
|
34
|
+
maxOutputTokens: 500000,
|
|
39
35
|
supportsStreaming: true,
|
|
40
36
|
supportsImages: true,
|
|
41
|
-
supportsTemperature: true,
|
|
42
37
|
supportsWebSearch: true,
|
|
43
38
|
supportsReasoning: true,
|
|
44
|
-
supportsFunctionCalling: true,
|
|
45
|
-
supportsStructuredOutputs: true,
|
|
46
|
-
timeout: 300000, // 5 minutes
|
|
47
|
-
description:
|
|
48
|
-
'GROK-4 Fast Reasoning (2M context) - Cost-efficient reasoning model with function calling and structured outputs',
|
|
49
|
-
aliases: [
|
|
50
|
-
'grok-4-fast',
|
|
51
|
-
'grok-4-fast-reasoning-latest',
|
|
52
|
-
'grok 4 fast',
|
|
53
|
-
'grok 4 fast reasoning',
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
'grok-4-fast-non-reasoning': {
|
|
57
|
-
modelName: 'grok-4-fast-non-reasoning',
|
|
58
|
-
friendlyName: 'X.AI (Grok 4 Fast Non-Reasoning)',
|
|
59
|
-
contextWindow: 2000000, // 2M tokens
|
|
60
|
-
maxOutputTokens: 2000000,
|
|
61
|
-
supportsStreaming: true,
|
|
62
|
-
supportsImages: true,
|
|
63
|
-
supportsTemperature: true,
|
|
64
|
-
supportsWebSearch: true,
|
|
65
|
-
supportsReasoning: false,
|
|
66
|
-
supportsFunctionCalling: true,
|
|
67
|
-
supportsStructuredOutputs: true,
|
|
68
|
-
timeout: 300000, // 5 minutes
|
|
69
|
-
description:
|
|
70
|
-
'GROK-4 Fast Non-Reasoning (2M context) - Fast, cost-efficient model without reasoning for quick responses',
|
|
71
|
-
aliases: ['grok-4-fast-non-reasoning-latest', 'grok 4 fast non-reasoning'],
|
|
72
|
-
},
|
|
73
|
-
'grok-code-fast-1': {
|
|
74
|
-
modelName: 'grok-code-fast-1',
|
|
75
|
-
friendlyName: 'X.AI (Grok Code Fast 1)',
|
|
76
|
-
contextWindow: 256000,
|
|
77
|
-
maxOutputTokens: 256000,
|
|
78
|
-
supportsStreaming: true,
|
|
79
|
-
supportsImages: false,
|
|
80
|
-
supportsTemperature: true,
|
|
81
|
-
supportsWebSearch: false,
|
|
82
39
|
timeout: 300000, // 5 minutes
|
|
83
40
|
description:
|
|
84
|
-
'
|
|
85
|
-
aliases: [
|
|
86
|
-
'grok-code-fast',
|
|
87
|
-
'grok-code-fast-1-0825',
|
|
88
|
-
'grok code fast',
|
|
89
|
-
'grok code fast 1',
|
|
90
|
-
],
|
|
41
|
+
'Grok 4.5 (500K context) - Flagship X.AI model with image input, reasoning content, and native web/X search via Agent Tools',
|
|
42
|
+
aliases: ['grok', 'grok-4.5', 'grok-4.5-latest', 'grok-build-latest'],
|
|
91
43
|
},
|
|
92
44
|
};
|
|
93
45
|
|
|
@@ -127,10 +79,35 @@ function resolveModelName(modelName) {
|
|
|
127
79
|
}
|
|
128
80
|
}
|
|
129
81
|
|
|
130
|
-
// Return as-is if not found (let XAI API handle unknown models
|
|
82
|
+
// Return as-is if not found (let XAI API handle unknown models — retired
|
|
83
|
+
// grok IDs are silently server-remapped upstream, see SUPPORTED_MODELS note).
|
|
131
84
|
return modelName;
|
|
132
85
|
}
|
|
133
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Map Converse reasoning_effort to a value grok-4.5 accepts.
|
|
89
|
+
*
|
|
90
|
+
* grok-4.5 supports ONLY low/medium/high (default high) and cannot disable
|
|
91
|
+
* reasoning — there is no `none`/off value. Sending an unsupported value
|
|
92
|
+
* (e.g. `none`/`minimal`/`max`) returns HTTP 400, so unsupported Converse
|
|
93
|
+
* levels are clamped into {low, medium, high} rather than forwarded.
|
|
94
|
+
*/
|
|
95
|
+
function resolveReasoningEffort(reasoningEffort) {
|
|
96
|
+
switch (reasoningEffort) {
|
|
97
|
+
case 'none':
|
|
98
|
+
case 'minimal':
|
|
99
|
+
case 'low':
|
|
100
|
+
return 'low';
|
|
101
|
+
case 'medium':
|
|
102
|
+
return 'medium';
|
|
103
|
+
case 'high':
|
|
104
|
+
case 'max':
|
|
105
|
+
return 'high';
|
|
106
|
+
default:
|
|
107
|
+
return 'high';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
134
111
|
/**
|
|
135
112
|
* Validate XAI API key format
|
|
136
113
|
*/
|
|
@@ -144,7 +121,7 @@ function validateApiKey(apiKey) {
|
|
|
144
121
|
}
|
|
145
122
|
|
|
146
123
|
/**
|
|
147
|
-
* Convert messages to
|
|
124
|
+
* Convert messages to xAI Responses API format (input_text / input_image).
|
|
148
125
|
*/
|
|
149
126
|
function convertMessages(messages) {
|
|
150
127
|
if (!Array.isArray(messages)) {
|
|
@@ -182,17 +159,14 @@ function convertMessages(messages) {
|
|
|
182
159
|
for (const item of content) {
|
|
183
160
|
if (item.type === 'text') {
|
|
184
161
|
convertedContent.push({
|
|
185
|
-
type: '
|
|
162
|
+
type: 'input_text',
|
|
186
163
|
text: item.text,
|
|
187
164
|
});
|
|
188
165
|
} else if (item.type === 'image' && item.source) {
|
|
189
|
-
// Convert Anthropic/Claude format to
|
|
166
|
+
// Convert Anthropic/Claude format to xAI Responses API format
|
|
190
167
|
convertedContent.push({
|
|
191
|
-
type: '
|
|
192
|
-
image_url: {
|
|
193
|
-
url: `data:${item.source.media_type};base64,${item.source.data}`,
|
|
194
|
-
detail: 'high',
|
|
195
|
-
},
|
|
168
|
+
type: 'input_image',
|
|
169
|
+
image_url: `data:${item.source.media_type};base64,${item.source.data}`,
|
|
196
170
|
});
|
|
197
171
|
debugLog(
|
|
198
172
|
`[XAI] Converting image: ${item.source.media_type}, data length: ${item.source.data.length}`,
|
|
@@ -208,6 +182,44 @@ function convertMessages(messages) {
|
|
|
208
182
|
});
|
|
209
183
|
}
|
|
210
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Extract url citations from a Responses API output (web/X search results).
|
|
187
|
+
* Walks message output_text annotations for `url_citation` entries.
|
|
188
|
+
*/
|
|
189
|
+
function extractCitations(response) {
|
|
190
|
+
if (!response?.output || !Array.isArray(response.output)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const citations = [];
|
|
195
|
+
for (const item of response.output) {
|
|
196
|
+
if (item.type !== 'message' || !Array.isArray(item.content)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
for (const part of item.content) {
|
|
200
|
+
if (!Array.isArray(part.annotations)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const annotation of part.annotations) {
|
|
204
|
+
if (annotation.type === 'url_citation') {
|
|
205
|
+
citations.push({
|
|
206
|
+
url: annotation.url,
|
|
207
|
+
title: annotation.title,
|
|
208
|
+
...(annotation.start_index != null && {
|
|
209
|
+
start_index: annotation.start_index,
|
|
210
|
+
}),
|
|
211
|
+
...(annotation.end_index != null && {
|
|
212
|
+
end_index: annotation.end_index,
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return citations.length > 0 ? citations : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
211
223
|
/**
|
|
212
224
|
* Main XAI provider implementation
|
|
213
225
|
*/
|
|
@@ -220,17 +232,16 @@ export const xaiProvider = {
|
|
|
220
232
|
*/
|
|
221
233
|
async invoke(messages, options = {}) {
|
|
222
234
|
const {
|
|
223
|
-
model = 'grok-4
|
|
224
|
-
temperature = 0.7,
|
|
235
|
+
model = 'grok-4.5',
|
|
225
236
|
maxTokens = null,
|
|
226
237
|
stream = false,
|
|
227
238
|
reasoning_effort = 'medium',
|
|
228
|
-
use_websearch = false,
|
|
229
239
|
signal,
|
|
230
240
|
config,
|
|
231
241
|
// Filter out options not meant for the API
|
|
232
242
|
continuation_id, // eslint-disable-line no-unused-vars
|
|
233
243
|
continuationStore, // eslint-disable-line no-unused-vars
|
|
244
|
+
web_search, // eslint-disable-line no-unused-vars -- xAI gates search on modelConfig, not this flag
|
|
234
245
|
...otherOptions
|
|
235
246
|
} = options;
|
|
236
247
|
|
|
@@ -252,7 +263,7 @@ export const xaiProvider = {
|
|
|
252
263
|
// Get base URL from config or use default
|
|
253
264
|
const baseURL = config.providers?.xaiBaseUrl || 'https://api.x.ai/v1';
|
|
254
265
|
|
|
255
|
-
// Initialize OpenAI client with XAI base URL
|
|
266
|
+
// Initialize OpenAI client with XAI base URL (drives the Responses API)
|
|
256
267
|
const openai = new OpenAI({
|
|
257
268
|
apiKey: config.apiKeys.xai,
|
|
258
269
|
baseURL,
|
|
@@ -262,44 +273,40 @@ export const xaiProvider = {
|
|
|
262
273
|
const resolvedModel = resolveModelName(model);
|
|
263
274
|
const modelConfig = SUPPORTED_MODELS[resolvedModel] || {};
|
|
264
275
|
|
|
265
|
-
// Convert and validate messages
|
|
266
|
-
const
|
|
276
|
+
// Convert and validate messages to Responses API input format
|
|
277
|
+
const xaiInput = convertMessages(messages);
|
|
267
278
|
|
|
268
|
-
//
|
|
269
|
-
const { reasoning_effort: _unused_reasoning_effort, ...supportedOptions } =
|
|
270
|
-
otherOptions;
|
|
271
|
-
|
|
272
|
-
// Build request payload
|
|
279
|
+
// Build Responses API request payload
|
|
273
280
|
const requestPayload = {
|
|
274
281
|
model: resolvedModel,
|
|
275
|
-
|
|
282
|
+
input: xaiInput,
|
|
276
283
|
stream,
|
|
277
|
-
...
|
|
284
|
+
...otherOptions,
|
|
278
285
|
};
|
|
279
286
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// Add max tokens if specified
|
|
286
|
-
if (maxTokens) {
|
|
287
|
-
requestPayload.max_tokens = Math.min(
|
|
288
|
-
maxTokens,
|
|
289
|
-
modelConfig.maxOutputTokens || 256000,
|
|
290
|
-
);
|
|
287
|
+
// Web search via Agent Tools — attached whenever the model supports it
|
|
288
|
+
// (always-on capability gate, no per-request arg). The model decides
|
|
289
|
+
// per-request whether to actually search.
|
|
290
|
+
if (modelConfig.supportsWebSearch) {
|
|
291
|
+
requestPayload.tools = [{ type: 'web_search' }];
|
|
291
292
|
}
|
|
292
293
|
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
// Reasoning effort — capability-gated. Only attached when the resolved
|
|
295
|
+
// model supports reasoning, so unknown/retired pass-through IDs (which
|
|
296
|
+
// would HTTP-400 on grok-4.5 reasoning params) receive no reasoning field.
|
|
297
|
+
if (modelConfig.supportsReasoning && reasoning_effort) {
|
|
298
|
+
requestPayload.reasoning = {
|
|
299
|
+
effort: resolveReasoningEffort(reasoning_effort),
|
|
300
|
+
summary: 'auto',
|
|
297
301
|
};
|
|
298
302
|
}
|
|
299
303
|
|
|
300
|
-
// Add
|
|
301
|
-
if (
|
|
302
|
-
requestPayload.
|
|
304
|
+
// Add max output tokens if specified
|
|
305
|
+
if (maxTokens) {
|
|
306
|
+
requestPayload.max_output_tokens = Math.min(
|
|
307
|
+
maxTokens,
|
|
308
|
+
modelConfig.maxOutputTokens || 500000,
|
|
309
|
+
);
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
// If streaming is requested and model doesn't support it, fall back to non-streaming
|
|
@@ -317,17 +324,13 @@ export const xaiProvider = {
|
|
|
317
324
|
requestPayload,
|
|
318
325
|
resolvedModel,
|
|
319
326
|
modelConfig,
|
|
320
|
-
use_websearch,
|
|
321
327
|
signal,
|
|
322
328
|
);
|
|
323
329
|
}
|
|
324
330
|
|
|
325
|
-
// Note: XAI/Grok models don't currently support reasoning_effort parameter
|
|
326
|
-
// We silently ignore it for API consistency (no need to log warnings in tests)
|
|
327
|
-
|
|
328
331
|
try {
|
|
329
332
|
debugLog(
|
|
330
|
-
`[XAI] Calling ${resolvedModel} with ${
|
|
333
|
+
`[XAI] Calling ${resolvedModel} via Responses API with ${xaiInput.length} messages${modelConfig.supportsWebSearch ? ' (with web search)' : ''}`,
|
|
331
334
|
);
|
|
332
335
|
|
|
333
336
|
// Check if already aborted before making request
|
|
@@ -338,112 +341,103 @@ export const xaiProvider = {
|
|
|
338
341
|
const startTime = Date.now();
|
|
339
342
|
|
|
340
343
|
// Make the API call with abort signal support
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
const requestOptions = signal ? { signal } : {};
|
|
345
|
+
const response = await openai.responses.create(
|
|
346
|
+
requestPayload,
|
|
347
|
+
requestOptions,
|
|
348
|
+
);
|
|
346
349
|
|
|
347
350
|
const responseTime = Date.now() - startTime;
|
|
348
351
|
debugLog(`[XAI] Response received in ${responseTime}ms`);
|
|
349
352
|
|
|
350
|
-
// Extract
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
// Extract content + reasoning from the Responses API output items
|
|
354
|
+
let content;
|
|
355
|
+
let reasoningSummary = null;
|
|
356
|
+
|
|
357
|
+
if (response.output && Array.isArray(response.output)) {
|
|
358
|
+
const messageOutput = response.output.find(
|
|
359
|
+
(item) => item.type === 'message',
|
|
360
|
+
);
|
|
361
|
+
const reasoningOutput = response.output.find(
|
|
362
|
+
(item) => item.type === 'reasoning',
|
|
356
363
|
);
|
|
357
|
-
}
|
|
358
364
|
|
|
359
|
-
|
|
360
|
-
|
|
365
|
+
if (!messageOutput || !messageOutput.content) {
|
|
366
|
+
throw new XAIProviderError(
|
|
367
|
+
'No message content in Responses API response',
|
|
368
|
+
'NO_RESPONSE_CONTENT',
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const textContent = messageOutput.content.find(
|
|
373
|
+
(item) => item.type === 'output_text',
|
|
374
|
+
);
|
|
375
|
+
if (!textContent) {
|
|
376
|
+
throw new XAIProviderError(
|
|
377
|
+
'No text content in message output',
|
|
378
|
+
'NO_RESPONSE_CONTENT',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
content = textContent.text;
|
|
382
|
+
|
|
383
|
+
// Extract reasoning summary if present (grok-4.5 returns encrypted
|
|
384
|
+
// reasoning content — only the summary is rendered).
|
|
385
|
+
if (reasoningOutput && Array.isArray(reasoningOutput.summary)) {
|
|
386
|
+
const summaryText = reasoningOutput.summary.find(
|
|
387
|
+
(item) => item.type === 'summary_text',
|
|
388
|
+
);
|
|
389
|
+
if (summaryText) {
|
|
390
|
+
reasoningSummary = summaryText.text;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} else if (response.output_text) {
|
|
394
|
+
// Legacy/simple format
|
|
395
|
+
content = response.output_text;
|
|
396
|
+
} else {
|
|
361
397
|
throw new XAIProviderError(
|
|
362
|
-
'No
|
|
398
|
+
'No output in Responses API response',
|
|
363
399
|
'NO_RESPONSE_CONTENT',
|
|
364
400
|
);
|
|
365
401
|
}
|
|
366
402
|
|
|
367
|
-
|
|
403
|
+
const stopReason = response.status || 'stop';
|
|
368
404
|
const usage = response.usage || {};
|
|
405
|
+
const citations = extractCitations(response);
|
|
369
406
|
|
|
370
407
|
// Return unified response format
|
|
371
408
|
return {
|
|
372
409
|
content,
|
|
373
|
-
stop_reason:
|
|
410
|
+
stop_reason: stopReason,
|
|
374
411
|
rawResponse: response,
|
|
375
412
|
metadata: {
|
|
376
413
|
model: response.model || resolvedModel,
|
|
377
414
|
usage: {
|
|
378
|
-
input_tokens: usage.prompt_tokens || 0,
|
|
379
|
-
output_tokens: usage.completion_tokens || 0,
|
|
415
|
+
input_tokens: usage.input_tokens || usage.prompt_tokens || 0,
|
|
416
|
+
output_tokens: usage.output_tokens || usage.completion_tokens || 0,
|
|
380
417
|
total_tokens: usage.total_tokens || 0,
|
|
381
418
|
},
|
|
382
419
|
response_time_ms: responseTime,
|
|
383
|
-
finish_reason:
|
|
420
|
+
finish_reason: stopReason,
|
|
384
421
|
provider: 'xai',
|
|
385
|
-
web_search_used:
|
|
422
|
+
web_search_used: !!modelConfig.supportsWebSearch,
|
|
423
|
+
...(reasoningSummary && { reasoning: reasoningSummary }),
|
|
424
|
+
...(citations && { citations }),
|
|
386
425
|
},
|
|
387
426
|
};
|
|
388
427
|
} catch (error) {
|
|
389
428
|
debugError('[XAI] Error during API call:', error);
|
|
390
|
-
|
|
391
|
-
// Handle specific XAI/OpenAI compatible errors
|
|
392
|
-
if (error.code === 'insufficient_quota') {
|
|
393
|
-
throw new XAIProviderError(
|
|
394
|
-
'XAI API quota exceeded',
|
|
395
|
-
'QUOTA_EXCEEDED',
|
|
396
|
-
error,
|
|
397
|
-
);
|
|
398
|
-
} else if (error.code === 'invalid_api_key') {
|
|
399
|
-
throw new XAIProviderError(
|
|
400
|
-
'Invalid XAI API key',
|
|
401
|
-
'INVALID_API_KEY',
|
|
402
|
-
error,
|
|
403
|
-
);
|
|
404
|
-
} else if (error.code === 'model_not_found') {
|
|
405
|
-
throw new XAIProviderError(
|
|
406
|
-
`Model ${resolvedModel} not found`,
|
|
407
|
-
'MODEL_NOT_FOUND',
|
|
408
|
-
error,
|
|
409
|
-
);
|
|
410
|
-
} else if (error.code === 'context_length_exceeded') {
|
|
411
|
-
throw new XAIProviderError(
|
|
412
|
-
'Context length exceeded for model',
|
|
413
|
-
'CONTEXT_LENGTH_EXCEEDED',
|
|
414
|
-
error,
|
|
415
|
-
);
|
|
416
|
-
} else if (error.type === 'invalid_request_error') {
|
|
417
|
-
throw new XAIProviderError(
|
|
418
|
-
`Invalid request: ${error.message}`,
|
|
419
|
-
'INVALID_REQUEST',
|
|
420
|
-
error,
|
|
421
|
-
);
|
|
422
|
-
} else if (error.type === 'rate_limit_error') {
|
|
423
|
-
throw new XAIProviderError(
|
|
424
|
-
'XAI rate limit exceeded',
|
|
425
|
-
'RATE_LIMIT_EXCEEDED',
|
|
426
|
-
error,
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Generic error handling
|
|
431
|
-
throw new XAIProviderError(
|
|
432
|
-
`XAI API error: ${error.message || 'Unknown error'}`,
|
|
433
|
-
'API_ERROR',
|
|
434
|
-
error,
|
|
435
|
-
);
|
|
429
|
+
throw this._normalizeError(error, resolvedModel);
|
|
436
430
|
}
|
|
437
431
|
},
|
|
438
432
|
|
|
439
433
|
/**
|
|
440
|
-
* Create streaming generator for XAI responses
|
|
434
|
+
* Create streaming generator for XAI Responses API responses
|
|
441
435
|
* @private
|
|
442
436
|
* @param {OpenAI} openai - OpenAI client instance configured for XAI
|
|
443
437
|
* @param {Object} requestPayload - Request payload
|
|
444
438
|
* @param {string} resolvedModel - Resolved model name
|
|
445
439
|
* @param {Object} modelConfig - Model configuration
|
|
446
|
-
* @param {
|
|
440
|
+
* @param {AbortSignal} signal - Abort signal
|
|
447
441
|
* @returns {AsyncGenerator} - Streaming generator yielding events
|
|
448
442
|
*/
|
|
449
443
|
async *_createStreamingGenerator(
|
|
@@ -451,25 +445,22 @@ export const xaiProvider = {
|
|
|
451
445
|
requestPayload,
|
|
452
446
|
resolvedModel,
|
|
453
447
|
modelConfig,
|
|
454
|
-
use_websearch,
|
|
455
448
|
signal,
|
|
456
449
|
) {
|
|
457
|
-
const searchInfo =
|
|
458
|
-
use_websearch && modelConfig.supportsWebSearch
|
|
459
|
-
? ' (with live search)'
|
|
460
|
-
: '';
|
|
450
|
+
const searchInfo = modelConfig.supportsWebSearch ? ' (with web search)' : '';
|
|
461
451
|
|
|
462
452
|
debugLog(
|
|
463
|
-
`[XAI] Starting streaming for ${resolvedModel} with ${requestPayload.
|
|
453
|
+
`[XAI] Starting streaming for ${resolvedModel} via Responses API with ${requestPayload.input?.length} messages${searchInfo}`,
|
|
464
454
|
);
|
|
465
455
|
|
|
466
456
|
const startTime = Date.now();
|
|
467
457
|
let totalContent = '';
|
|
458
|
+
let totalReasoning = '';
|
|
459
|
+
let reasoningStreamed = false;
|
|
468
460
|
let lastUsage = null;
|
|
469
461
|
let finishReason = null;
|
|
470
462
|
let finalModel = resolvedModel;
|
|
471
463
|
let citations = null;
|
|
472
|
-
let searchSourcesUsed = 0;
|
|
473
464
|
|
|
474
465
|
try {
|
|
475
466
|
// Check if already aborted before starting
|
|
@@ -485,12 +476,12 @@ export const xaiProvider = {
|
|
|
485
476
|
provider: 'xai',
|
|
486
477
|
};
|
|
487
478
|
|
|
488
|
-
// Create stream using
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
479
|
+
// Create stream using the Responses API with abort signal support
|
|
480
|
+
const requestOptions = signal ? { signal } : {};
|
|
481
|
+
const stream = await openai.responses.create(
|
|
482
|
+
requestPayload,
|
|
483
|
+
requestOptions,
|
|
484
|
+
);
|
|
494
485
|
|
|
495
486
|
// Process stream chunks
|
|
496
487
|
for await (const chunk of stream) {
|
|
@@ -502,10 +493,10 @@ export const xaiProvider = {
|
|
|
502
493
|
);
|
|
503
494
|
break;
|
|
504
495
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
const content =
|
|
496
|
+
|
|
497
|
+
// Answer-text deltas
|
|
498
|
+
if (chunk.type === 'response.output_text.delta') {
|
|
499
|
+
const content = chunk.delta || '';
|
|
509
500
|
if (content) {
|
|
510
501
|
totalContent += content;
|
|
511
502
|
yield {
|
|
@@ -514,29 +505,49 @@ export const xaiProvider = {
|
|
|
514
505
|
timestamp: new Date().toISOString(),
|
|
515
506
|
};
|
|
516
507
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
508
|
+
} else if (
|
|
509
|
+
chunk.type === 'response.reasoning_summary_text.delta'
|
|
510
|
+
) {
|
|
511
|
+
// Reasoning summary streamed incrementally — emit as thinking
|
|
512
|
+
// deltas so the normalizer accumulates them separately from text.
|
|
513
|
+
const summaryDelta = chunk.delta || '';
|
|
514
|
+
if (summaryDelta) {
|
|
515
|
+
totalReasoning += summaryDelta;
|
|
516
|
+
reasoningStreamed = true;
|
|
517
|
+
yield {
|
|
518
|
+
type: 'thinking',
|
|
519
|
+
content: summaryDelta,
|
|
520
|
+
timestamp: new Date().toISOString(),
|
|
521
|
+
};
|
|
520
522
|
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
if
|
|
528
|
-
|
|
523
|
+
} else if (
|
|
524
|
+
chunk.type === 'response.reasoning_summary_part.done' ||
|
|
525
|
+
chunk.type === 'response.reasoning_summary_text.done'
|
|
526
|
+
) {
|
|
527
|
+
// Some responses deliver the summary only as a terminal "done"
|
|
528
|
+
// event (no deltas). Emit a single thinking event in that case;
|
|
529
|
+
// if deltas already streamed, skip to avoid double-counting.
|
|
530
|
+
if (!reasoningStreamed) {
|
|
531
|
+
const summaryText = chunk.part?.text || chunk.text || '';
|
|
532
|
+
if (summaryText) {
|
|
533
|
+
totalReasoning = summaryText;
|
|
534
|
+
yield {
|
|
535
|
+
type: 'thinking',
|
|
536
|
+
content: summaryText,
|
|
537
|
+
timestamp: new Date().toISOString(),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} else if (chunk.type === 'response.completed') {
|
|
542
|
+
finishReason = chunk.response?.status || 'stop';
|
|
543
|
+
finalModel = chunk.response?.model || resolvedModel;
|
|
544
|
+
if (chunk.response?.usage) {
|
|
545
|
+
lastUsage = chunk.response.usage;
|
|
546
|
+
}
|
|
547
|
+
const chunkCitations = extractCitations(chunk.response);
|
|
548
|
+
if (chunkCitations) {
|
|
549
|
+
citations = chunkCitations;
|
|
529
550
|
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Handle citations for live search (XAI-specific feature)
|
|
533
|
-
if (chunk.citations) {
|
|
534
|
-
citations = chunk.citations;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Update model if provided
|
|
538
|
-
if (chunk.model) {
|
|
539
|
-
finalModel = chunk.model;
|
|
540
551
|
}
|
|
541
552
|
} catch (chunkError) {
|
|
542
553
|
debugError('[XAI] Error processing stream chunk:', chunkError);
|
|
@@ -553,54 +564,38 @@ export const xaiProvider = {
|
|
|
553
564
|
}
|
|
554
565
|
|
|
555
566
|
const responseTime = Date.now() - startTime;
|
|
556
|
-
debugLog(
|
|
557
|
-
`[XAI] Streaming completed in ${responseTime}ms${searchSourcesUsed > 0 ? ` (used ${searchSourcesUsed} search sources)` : ''}`,
|
|
558
|
-
);
|
|
567
|
+
debugLog(`[XAI] Streaming completed in ${responseTime}ms`);
|
|
559
568
|
|
|
560
569
|
// Yield usage information if available
|
|
561
570
|
if (lastUsage) {
|
|
562
|
-
|
|
571
|
+
yield {
|
|
563
572
|
type: 'usage',
|
|
564
573
|
usage: {
|
|
565
|
-
input_tokens: lastUsage.prompt_tokens || 0,
|
|
566
|
-
output_tokens:
|
|
574
|
+
input_tokens: lastUsage.input_tokens || lastUsage.prompt_tokens || 0,
|
|
575
|
+
output_tokens:
|
|
576
|
+
lastUsage.output_tokens || lastUsage.completion_tokens || 0,
|
|
567
577
|
total_tokens: lastUsage.total_tokens || 0,
|
|
568
578
|
},
|
|
569
579
|
timestamp: new Date().toISOString(),
|
|
570
580
|
};
|
|
571
|
-
|
|
572
|
-
// Add search-specific usage information
|
|
573
|
-
if (searchSourcesUsed > 0) {
|
|
574
|
-
usageEvent.usage.search_sources_used = searchSourcesUsed;
|
|
575
|
-
usageEvent.usage.search_cost_estimate = searchSourcesUsed * 0.025; // $0.025 per source
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
yield usageEvent;
|
|
579
581
|
}
|
|
580
582
|
|
|
581
|
-
// Determine web search usage
|
|
582
|
-
const webSearchUsed = use_websearch && modelConfig.supportsWebSearch;
|
|
583
|
-
|
|
584
583
|
// Build final metadata
|
|
585
584
|
const metadata = {
|
|
586
585
|
model: finalModel,
|
|
587
586
|
usage: {
|
|
588
|
-
input_tokens: lastUsage?.prompt_tokens || 0,
|
|
589
|
-
output_tokens:
|
|
587
|
+
input_tokens: lastUsage?.input_tokens || lastUsage?.prompt_tokens || 0,
|
|
588
|
+
output_tokens:
|
|
589
|
+
lastUsage?.output_tokens || lastUsage?.completion_tokens || 0,
|
|
590
590
|
total_tokens: lastUsage?.total_tokens || 0,
|
|
591
591
|
},
|
|
592
592
|
response_time_ms: responseTime,
|
|
593
593
|
finish_reason: finishReason || 'stop',
|
|
594
594
|
provider: 'xai',
|
|
595
|
-
web_search_used:
|
|
595
|
+
web_search_used: !!modelConfig.supportsWebSearch,
|
|
596
|
+
...(totalReasoning && { reasoning: totalReasoning }),
|
|
596
597
|
};
|
|
597
598
|
|
|
598
|
-
// Add search-specific metadata
|
|
599
|
-
if (searchSourcesUsed > 0) {
|
|
600
|
-
metadata.search_sources_used = searchSourcesUsed;
|
|
601
|
-
metadata.search_cost_estimate = searchSourcesUsed * 0.025;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
599
|
if (citations) {
|
|
605
600
|
metadata.citations = citations;
|
|
606
601
|
}
|
|
@@ -616,38 +611,15 @@ export const xaiProvider = {
|
|
|
616
611
|
} catch (error) {
|
|
617
612
|
debugError('[XAI] Streaming error:', error);
|
|
618
613
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
let errorMessage = `XAI streaming error: ${error.message || 'Unknown error'}`;
|
|
622
|
-
let recoverable = false;
|
|
623
|
-
|
|
624
|
-
if (error.code === 'insufficient_quota') {
|
|
625
|
-
errorCode = 'QUOTA_EXCEEDED';
|
|
626
|
-
errorMessage = 'XAI API quota exceeded';
|
|
627
|
-
} else if (error.code === 'invalid_api_key') {
|
|
628
|
-
errorCode = 'INVALID_API_KEY';
|
|
629
|
-
errorMessage = 'Invalid XAI API key';
|
|
630
|
-
} else if (error.code === 'model_not_found') {
|
|
631
|
-
errorCode = 'MODEL_NOT_FOUND';
|
|
632
|
-
errorMessage = `Model ${resolvedModel} not found`;
|
|
633
|
-
} else if (error.code === 'context_length_exceeded') {
|
|
634
|
-
errorCode = 'CONTEXT_LENGTH_EXCEEDED';
|
|
635
|
-
errorMessage = 'Context length exceeded for model';
|
|
636
|
-
} else if (error.type === 'invalid_request_error') {
|
|
637
|
-
errorCode = 'INVALID_REQUEST';
|
|
638
|
-
errorMessage = `Invalid request: ${error.message}`;
|
|
639
|
-
} else if (error.type === 'rate_limit_error') {
|
|
640
|
-
errorCode = 'RATE_LIMIT_EXCEEDED';
|
|
641
|
-
errorMessage = 'XAI rate limit exceeded';
|
|
642
|
-
recoverable = true;
|
|
643
|
-
}
|
|
614
|
+
const normalized = this._normalizeError(error, resolvedModel);
|
|
615
|
+
const recoverable = normalized.code === 'RATE_LIMIT_EXCEEDED';
|
|
644
616
|
|
|
645
617
|
// Yield final error event
|
|
646
618
|
yield {
|
|
647
619
|
type: 'error',
|
|
648
620
|
error: {
|
|
649
|
-
message:
|
|
650
|
-
code:
|
|
621
|
+
message: normalized.message,
|
|
622
|
+
code: normalized.code,
|
|
651
623
|
recoverable,
|
|
652
624
|
originalError: error.message,
|
|
653
625
|
},
|
|
@@ -655,10 +627,56 @@ export const xaiProvider = {
|
|
|
655
627
|
};
|
|
656
628
|
|
|
657
629
|
// Re-throw to maintain error propagation
|
|
658
|
-
throw
|
|
630
|
+
throw normalized;
|
|
659
631
|
}
|
|
660
632
|
},
|
|
661
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Normalize an SDK/API error into an XAIProviderError with a stable code.
|
|
636
|
+
* @private
|
|
637
|
+
*/
|
|
638
|
+
_normalizeError(error, resolvedModel) {
|
|
639
|
+
if (error instanceof XAIProviderError) {
|
|
640
|
+
return error;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (error.code === 'insufficient_quota') {
|
|
644
|
+
return new XAIProviderError('XAI API quota exceeded', 'QUOTA_EXCEEDED', error);
|
|
645
|
+
} else if (error.code === 'invalid_api_key') {
|
|
646
|
+
return new XAIProviderError('Invalid XAI API key', 'INVALID_API_KEY', error);
|
|
647
|
+
} else if (error.code === 'model_not_found') {
|
|
648
|
+
return new XAIProviderError(
|
|
649
|
+
`Model ${resolvedModel} not found`,
|
|
650
|
+
'MODEL_NOT_FOUND',
|
|
651
|
+
error,
|
|
652
|
+
);
|
|
653
|
+
} else if (error.code === 'context_length_exceeded') {
|
|
654
|
+
return new XAIProviderError(
|
|
655
|
+
'Context length exceeded for model',
|
|
656
|
+
'CONTEXT_LENGTH_EXCEEDED',
|
|
657
|
+
error,
|
|
658
|
+
);
|
|
659
|
+
} else if (error.type === 'invalid_request_error') {
|
|
660
|
+
return new XAIProviderError(
|
|
661
|
+
`Invalid request: ${error.message}`,
|
|
662
|
+
'INVALID_REQUEST',
|
|
663
|
+
error,
|
|
664
|
+
);
|
|
665
|
+
} else if (error.type === 'rate_limit_error') {
|
|
666
|
+
return new XAIProviderError(
|
|
667
|
+
'XAI rate limit exceeded',
|
|
668
|
+
'RATE_LIMIT_EXCEEDED',
|
|
669
|
+
error,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return new XAIProviderError(
|
|
674
|
+
`XAI API error: ${error.message || 'Unknown error'}`,
|
|
675
|
+
'API_ERROR',
|
|
676
|
+
error,
|
|
677
|
+
);
|
|
678
|
+
},
|
|
679
|
+
|
|
662
680
|
/**
|
|
663
681
|
* Validate configuration for XAI provider
|
|
664
682
|
* @param {Object} config - Configuration object
|