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.
Files changed (42) hide show
  1. package/.env.example +6 -3
  2. package/README.md +96 -94
  3. package/docs/API.md +703 -1562
  4. package/docs/ARCHITECTURE.md +13 -11
  5. package/docs/EXAMPLES.md +241 -667
  6. package/docs/PROVIDERS.md +104 -79
  7. package/package.json +1 -1
  8. package/src/async/asyncJobStore.js +2 -2
  9. package/src/async/jobRunner.js +6 -1
  10. package/src/async/providerStreamNormalizer.js +59 -1
  11. package/src/config.js +4 -3
  12. package/src/prompts/helpPrompt.js +43 -61
  13. package/src/providers/anthropic.js +0 -31
  14. package/src/providers/claude.js +0 -14
  15. package/src/providers/codex.js +15 -21
  16. package/src/providers/copilot.js +36 -202
  17. package/src/providers/deepseek.js +73 -53
  18. package/src/providers/gemini-cli.js +0 -3
  19. package/src/providers/google.js +10 -27
  20. package/src/providers/interface.js +0 -3
  21. package/src/providers/mistral.js +169 -63
  22. package/src/providers/openai-compatible.js +113 -28
  23. package/src/providers/openai.js +14 -66
  24. package/src/providers/openrouter-discovery.js +308 -0
  25. package/src/providers/openrouter.js +452 -282
  26. package/src/providers/xai.js +298 -280
  27. package/src/services/summarizationService.js +4 -14
  28. package/src/systemPrompts.js +19 -2
  29. package/src/tools/cancelJob.js +7 -1
  30. package/src/tools/chat.js +957 -995
  31. package/src/tools/checkStatus.js +1 -0
  32. package/src/tools/index.js +2 -6
  33. package/src/tools/modes/parallel.js +488 -0
  34. package/src/tools/modes/roundtable.js +495 -0
  35. package/src/tools/modes/streamShared.js +31 -0
  36. package/src/utils/contextProcessor.js +1 -38
  37. package/src/utils/conversationExporter.js +6 -26
  38. package/src/utils/formatStatus.js +46 -19
  39. package/src/utils/modelRouting.js +207 -4
  40. package/src/providers/openrouter-endpoints-client.js +0 -220
  41. package/src/tools/consensus.js +0 -1813
  42. package/src/tools/conversation.js +0 -1233
@@ -1,93 +1,45 @@
1
1
  /**
2
2
  * XAI (Grok) Provider
3
3
  *
4
- * Provider implementation for XAI Grok models using OpenAI-compatible API with custom baseURL.
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
- // Define supported Grok models with their capabilities
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-0709': {
14
- modelName: 'grok-4-0709',
15
- friendlyName: 'X.AI (Grok 4)',
16
- contextWindow: 256000,
17
- maxOutputTokens: 256000,
18
- supportsStreaming: true,
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
- 'GROK Code Fast 1 (256K context) - Speedy and economical reasoning model that excels at agentic coding',
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 XAI/OpenAI format
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: 'text',
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 OpenAI format for XAI
166
+ // Convert Anthropic/Claude format to xAI Responses API format
190
167
  convertedContent.push({
191
- type: 'image_url',
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-0709',
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 xaiMessages = convertMessages(messages);
276
+ // Convert and validate messages to Responses API input format
277
+ const xaiInput = convertMessages(messages);
267
278
 
268
- // Filter out unsupported parameters for XAI/Grok models
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
- messages: xaiMessages,
282
+ input: xaiInput,
276
283
  stream,
277
- ...supportedOptions,
284
+ ...otherOptions,
278
285
  };
279
286
 
280
- // Add temperature (all Grok models support temperature)
281
- if (temperature !== undefined) {
282
- requestPayload.temperature = Math.max(0, Math.min(2, temperature));
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
- // Add web search parameters if requested and model supports it
294
- if (use_websearch && modelConfig.supportsWebSearch) {
295
- requestPayload.search_parameters = {
296
- mode: 'auto', // Let the model decide when to use web search
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 usage reporting for streaming mode
301
- if (stream) {
302
- requestPayload.stream_options = { include_usage: true };
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 ${xaiMessages.length} messages${use_websearch && modelConfig.supportsWebSearch ? ' (with live search)' : ''}`,
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 requestWithSignal = { ...requestPayload };
342
- if (signal) {
343
- requestWithSignal.signal = signal;
344
- }
345
- const response = await openai.chat.completions.create(requestWithSignal);
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 response data
351
- const choice = response.choices[0];
352
- if (!choice) {
353
- throw new XAIProviderError(
354
- 'No response choice received from XAI',
355
- 'NO_RESPONSE_CHOICE',
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
- const content = choice.message?.content;
360
- if (!content) {
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 content in response from XAI',
398
+ 'No output in Responses API response',
363
399
  'NO_RESPONSE_CONTENT',
364
400
  );
365
401
  }
366
402
 
367
- // Extract usage information
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: choice.finish_reason || 'stop',
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: choice.finish_reason,
420
+ finish_reason: stopReason,
384
421
  provider: 'xai',
385
- web_search_used: use_websearch && modelConfig.supportsWebSearch,
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 {boolean} use_websearch - Whether web search is enabled
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.messages?.length} messages${searchInfo}`,
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 OpenAI SDK with XAI base URL and abort signal support
489
- const requestWithSignal = { ...requestPayload };
490
- if (signal) {
491
- requestWithSignal.signal = signal;
492
- }
493
- const stream = await openai.chat.completions.create(requestWithSignal);
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
- // Handle Chat Completions API streaming format (XAI uses OpenAI-compatible format)
506
- const choice = chunk.choices?.[0];
507
- if (choice) {
508
- const content = choice.delta?.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
- if (choice.finish_reason) {
519
- finishReason = choice.finish_reason;
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
- // Handle usage information (typically in final chunk)
524
- if (chunk.usage) {
525
- lastUsage = chunk.usage;
526
- // Track search sources used for live search cost monitoring
527
- if (chunk.usage.num_sources_used) {
528
- searchSourcesUsed = chunk.usage.num_sources_used;
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
- const usageEvent = {
571
+ yield {
563
572
  type: 'usage',
564
573
  usage: {
565
- input_tokens: lastUsage.prompt_tokens || 0,
566
- output_tokens: lastUsage.completion_tokens || 0,
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: lastUsage?.completion_tokens || 0,
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: webSearchUsed,
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
- // Handle specific XAI/OpenAI compatible errors in streaming context
620
- let errorCode = 'STREAMING_ERROR';
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: errorMessage,
650
- code: errorCode,
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 new XAIProviderError(errorMessage, errorCode, error);
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