converse-mcp-server 1.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 +177 -0
- package/README.md +425 -0
- package/bin/converse.js +45 -0
- package/docs/API.md +897 -0
- package/docs/ARCHITECTURE.md +552 -0
- package/docs/EXAMPLES.md +736 -0
- package/package.json +101 -0
- package/src/config.js +521 -0
- package/src/continuationStore.js +340 -0
- package/src/index.js +216 -0
- package/src/providers/google.js +441 -0
- package/src/providers/index.js +87 -0
- package/src/providers/openai.js +348 -0
- package/src/providers/xai.js +305 -0
- package/src/router.js +497 -0
- package/src/systemPrompts.js +90 -0
- package/src/tools/chat.js +336 -0
- package/src/tools/consensus.js +478 -0
- package/src/tools/index.js +156 -0
- package/src/transport/httpTransport.js +548 -0
- package/src/utils/console.js +64 -0
- package/src/utils/contextProcessor.js +475 -0
- package/src/utils/errorHandler.js +555 -0
- package/src/utils/logger.js +450 -0
- package/src/utils/tokenLimiter.js +217 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Provider
|
|
3
|
+
*
|
|
4
|
+
* Provider implementation for OpenAI GPT models using the official OpenAI SDK v5.
|
|
5
|
+
* Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import OpenAI from 'openai';
|
|
9
|
+
import { debugLog, debugError } from '../utils/console.js';
|
|
10
|
+
|
|
11
|
+
// Define supported models with their capabilities
|
|
12
|
+
const SUPPORTED_MODELS = {
|
|
13
|
+
'o3': {
|
|
14
|
+
modelName: 'o3',
|
|
15
|
+
friendlyName: 'OpenAI (O3)',
|
|
16
|
+
contextWindow: 200000,
|
|
17
|
+
maxOutputTokens: 100000,
|
|
18
|
+
supportsStreaming: true,
|
|
19
|
+
supportsImages: true,
|
|
20
|
+
supportsTemperature: false,
|
|
21
|
+
timeout: 300000, // 5 minutes
|
|
22
|
+
description: 'Strong reasoning (200K context) - Logical problems, code generation, systematic analysis'
|
|
23
|
+
},
|
|
24
|
+
'o3-mini': {
|
|
25
|
+
modelName: 'o3-mini',
|
|
26
|
+
friendlyName: 'OpenAI (O3-mini)',
|
|
27
|
+
contextWindow: 200000,
|
|
28
|
+
maxOutputTokens: 100000,
|
|
29
|
+
supportsStreaming: true,
|
|
30
|
+
supportsImages: true,
|
|
31
|
+
supportsTemperature: false,
|
|
32
|
+
timeout: 300000,
|
|
33
|
+
description: 'Fast O3 variant (200K context) - Balanced performance/speed, moderate complexity',
|
|
34
|
+
aliases: ['o3mini']
|
|
35
|
+
},
|
|
36
|
+
'o3-pro-2025-06-10': {
|
|
37
|
+
modelName: 'o3-pro-2025-06-10',
|
|
38
|
+
friendlyName: 'OpenAI (O3-Pro)',
|
|
39
|
+
contextWindow: 200000,
|
|
40
|
+
maxOutputTokens: 100000,
|
|
41
|
+
supportsStreaming: true,
|
|
42
|
+
supportsImages: true,
|
|
43
|
+
supportsTemperature: false,
|
|
44
|
+
timeout: 1800000, // 30 minutes
|
|
45
|
+
description: 'Professional-grade reasoning (200K context) - EXTREMELY EXPENSIVE: Only for the most complex problems',
|
|
46
|
+
aliases: ['o3-pro']
|
|
47
|
+
},
|
|
48
|
+
'o4-mini': {
|
|
49
|
+
modelName: 'o4-mini',
|
|
50
|
+
friendlyName: 'OpenAI (O4-mini)',
|
|
51
|
+
contextWindow: 200000,
|
|
52
|
+
maxOutputTokens: 100000,
|
|
53
|
+
supportsStreaming: true,
|
|
54
|
+
supportsImages: true,
|
|
55
|
+
supportsTemperature: true,
|
|
56
|
+
timeout: 180000, // 3 minutes
|
|
57
|
+
description: 'Latest reasoning model (200K context) - Optimized for shorter contexts, rapid reasoning',
|
|
58
|
+
aliases: ['o4mini']
|
|
59
|
+
},
|
|
60
|
+
'gpt-4.1-2025-04-14': {
|
|
61
|
+
modelName: 'gpt-4.1-2025-04-14',
|
|
62
|
+
friendlyName: 'OpenAI (GPT-4.1)',
|
|
63
|
+
contextWindow: 1000000,
|
|
64
|
+
maxOutputTokens: 32768,
|
|
65
|
+
supportsStreaming: true,
|
|
66
|
+
supportsImages: true,
|
|
67
|
+
supportsTemperature: true,
|
|
68
|
+
timeout: 300000,
|
|
69
|
+
description: 'GPT-4.1 (1M context) - Advanced reasoning model with large context window',
|
|
70
|
+
aliases: ['gpt4.1']
|
|
71
|
+
},
|
|
72
|
+
'gpt-4o': {
|
|
73
|
+
modelName: 'gpt-4o',
|
|
74
|
+
friendlyName: 'OpenAI (GPT-4o)',
|
|
75
|
+
contextWindow: 128000,
|
|
76
|
+
maxOutputTokens: 16384,
|
|
77
|
+
supportsStreaming: true,
|
|
78
|
+
supportsImages: true,
|
|
79
|
+
supportsTemperature: true,
|
|
80
|
+
timeout: 180000,
|
|
81
|
+
description: 'GPT-4o (128K context) - Multimodal flagship model with vision capabilities'
|
|
82
|
+
},
|
|
83
|
+
'gpt-4o-mini': {
|
|
84
|
+
modelName: 'gpt-4o-mini',
|
|
85
|
+
friendlyName: 'OpenAI (GPT-4o-mini)',
|
|
86
|
+
contextWindow: 128000,
|
|
87
|
+
maxOutputTokens: 16384,
|
|
88
|
+
supportsStreaming: true,
|
|
89
|
+
supportsImages: true,
|
|
90
|
+
supportsTemperature: true,
|
|
91
|
+
timeout: 120000,
|
|
92
|
+
description: 'GPT-4o-mini (128K context) - Fast and efficient multimodal model'
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Custom error class for OpenAI provider errors
|
|
98
|
+
*/
|
|
99
|
+
class OpenAIProviderError extends Error {
|
|
100
|
+
constructor(message, code, originalError = null) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = 'OpenAIProviderError';
|
|
103
|
+
this.code = code;
|
|
104
|
+
this.originalError = originalError;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve model name to canonical form, including aliases
|
|
110
|
+
*/
|
|
111
|
+
function resolveModelName(modelName) {
|
|
112
|
+
const modelNameLower = modelName.toLowerCase();
|
|
113
|
+
|
|
114
|
+
// Check exact matches first
|
|
115
|
+
for (const [supportedModel] of Object.entries(SUPPORTED_MODELS)) {
|
|
116
|
+
if (supportedModel.toLowerCase() === modelNameLower) {
|
|
117
|
+
return supportedModel;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check aliases
|
|
122
|
+
for (const [supportedModel, config] of Object.entries(SUPPORTED_MODELS)) {
|
|
123
|
+
if (config.aliases) {
|
|
124
|
+
for (const alias of config.aliases) {
|
|
125
|
+
if (alias.toLowerCase() === modelNameLower) {
|
|
126
|
+
return supportedModel;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Return as-is if not found (let OpenAI API handle unknown models)
|
|
133
|
+
return modelName;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate OpenAI API key format
|
|
138
|
+
*/
|
|
139
|
+
function validateApiKey(apiKey) {
|
|
140
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// OpenAI API keys typically start with 'sk-' and are at least 20 characters
|
|
145
|
+
return apiKey.startsWith('sk-') && apiKey.length >= 20;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert messages to OpenAI format
|
|
150
|
+
*/
|
|
151
|
+
function convertMessages(messages) {
|
|
152
|
+
if (!Array.isArray(messages)) {
|
|
153
|
+
throw new OpenAIProviderError('Messages must be an array', 'INVALID_MESSAGES');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return messages.map((msg, index) => {
|
|
157
|
+
if (!msg || typeof msg !== 'object') {
|
|
158
|
+
throw new OpenAIProviderError(`Message at index ${index} must be an object`, 'INVALID_MESSAGE');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { role, content } = msg;
|
|
162
|
+
|
|
163
|
+
if (!role || !['system', 'user', 'assistant'].includes(role)) {
|
|
164
|
+
throw new OpenAIProviderError(`Invalid role "${role}" at message index ${index}`, 'INVALID_ROLE');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!content) {
|
|
168
|
+
throw new OpenAIProviderError(`Message content is required at index ${index}`, 'MISSING_CONTENT');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { role, content };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Main OpenAI provider implementation
|
|
177
|
+
*/
|
|
178
|
+
export const openaiProvider = {
|
|
179
|
+
/**
|
|
180
|
+
* Unified provider interface: invoke messages with options
|
|
181
|
+
* @param {Array} messages - Array of message objects with role and content
|
|
182
|
+
* @param {Object} options - Configuration options
|
|
183
|
+
* @returns {Object} - { content, stop_reason, rawResponse }
|
|
184
|
+
*/
|
|
185
|
+
async invoke(messages, options = {}) {
|
|
186
|
+
const {
|
|
187
|
+
model = 'gpt-4o-mini',
|
|
188
|
+
temperature = 0.7,
|
|
189
|
+
maxTokens = null,
|
|
190
|
+
stream = false,
|
|
191
|
+
reasoningEffort = 'medium',
|
|
192
|
+
config,
|
|
193
|
+
...otherOptions
|
|
194
|
+
} = options;
|
|
195
|
+
|
|
196
|
+
// Validate API key
|
|
197
|
+
if (!config?.apiKeys?.openai) {
|
|
198
|
+
throw new OpenAIProviderError('OpenAI API key not configured', 'MISSING_API_KEY');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!validateApiKey(config.apiKeys.openai)) {
|
|
202
|
+
throw new OpenAIProviderError('Invalid OpenAI API key format', 'INVALID_API_KEY');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Initialize OpenAI client
|
|
206
|
+
const openai = new OpenAI({
|
|
207
|
+
apiKey: config.apiKeys.openai,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Resolve model name
|
|
211
|
+
const resolvedModel = resolveModelName(model);
|
|
212
|
+
const modelConfig = SUPPORTED_MODELS[resolvedModel] || {};
|
|
213
|
+
|
|
214
|
+
// Convert and validate messages
|
|
215
|
+
const openaiMessages = convertMessages(messages);
|
|
216
|
+
|
|
217
|
+
// Build request payload (exclude reasoning_effort from otherOptions)
|
|
218
|
+
const { reasoning_effort: _unused, ...cleanOptions } = otherOptions;
|
|
219
|
+
const requestPayload = {
|
|
220
|
+
model: resolvedModel,
|
|
221
|
+
messages: openaiMessages,
|
|
222
|
+
stream,
|
|
223
|
+
...cleanOptions
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Add temperature if model supports it
|
|
227
|
+
if (modelConfig.supportsTemperature !== false && temperature !== undefined) {
|
|
228
|
+
requestPayload.temperature = Math.max(0, Math.min(2, temperature));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Add max tokens if specified
|
|
232
|
+
if (maxTokens) {
|
|
233
|
+
requestPayload.max_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 100000);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add reasoning effort for thinking models (o3 series only)
|
|
237
|
+
if (resolvedModel.startsWith('o3') && reasoningEffort) {
|
|
238
|
+
requestPayload.reasoning_effort = reasoningEffort;
|
|
239
|
+
}
|
|
240
|
+
// Note: GPT-4o and other models don't support reasoning_effort parameter
|
|
241
|
+
// Only O3 series models support this parameter
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
debugLog(`[OpenAI] Calling ${resolvedModel} with ${openaiMessages.length} messages`);
|
|
245
|
+
|
|
246
|
+
const startTime = Date.now();
|
|
247
|
+
|
|
248
|
+
// Make the API call
|
|
249
|
+
const response = await openai.chat.completions.create(requestPayload);
|
|
250
|
+
|
|
251
|
+
const responseTime = Date.now() - startTime;
|
|
252
|
+
debugLog(`[OpenAI] Response received in ${responseTime}ms`);
|
|
253
|
+
|
|
254
|
+
// Extract response data
|
|
255
|
+
const choice = response.choices[0];
|
|
256
|
+
if (!choice) {
|
|
257
|
+
throw new OpenAIProviderError('No response choice received from OpenAI', 'NO_RESPONSE_CHOICE');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const content = choice.message?.content;
|
|
261
|
+
if (!content) {
|
|
262
|
+
throw new OpenAIProviderError('No content in response from OpenAI', 'NO_RESPONSE_CONTENT');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Extract usage information
|
|
266
|
+
const usage = response.usage || {};
|
|
267
|
+
|
|
268
|
+
// Return unified response format
|
|
269
|
+
return {
|
|
270
|
+
content,
|
|
271
|
+
stop_reason: choice.finish_reason || 'stop',
|
|
272
|
+
rawResponse: response,
|
|
273
|
+
metadata: {
|
|
274
|
+
model: response.model || resolvedModel,
|
|
275
|
+
usage: {
|
|
276
|
+
input_tokens: usage.prompt_tokens || 0,
|
|
277
|
+
output_tokens: usage.completion_tokens || 0,
|
|
278
|
+
total_tokens: usage.total_tokens || 0
|
|
279
|
+
},
|
|
280
|
+
response_time_ms: responseTime,
|
|
281
|
+
finish_reason: choice.finish_reason,
|
|
282
|
+
provider: 'openai'
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
debugError('[OpenAI] Error during API call:', error);
|
|
288
|
+
|
|
289
|
+
// Handle specific OpenAI errors
|
|
290
|
+
if (error.code === 'insufficient_quota') {
|
|
291
|
+
throw new OpenAIProviderError('OpenAI API quota exceeded', 'QUOTA_EXCEEDED', error);
|
|
292
|
+
} else if (error.code === 'invalid_api_key') {
|
|
293
|
+
throw new OpenAIProviderError('Invalid OpenAI API key', 'INVALID_API_KEY', error);
|
|
294
|
+
} else if (error.code === 'model_not_found') {
|
|
295
|
+
throw new OpenAIProviderError(`Model ${resolvedModel} not found`, 'MODEL_NOT_FOUND', error);
|
|
296
|
+
} else if (error.code === 'context_length_exceeded') {
|
|
297
|
+
throw new OpenAIProviderError('Context length exceeded for model', 'CONTEXT_LENGTH_EXCEEDED', error);
|
|
298
|
+
} else if (error.type === 'invalid_request_error') {
|
|
299
|
+
throw new OpenAIProviderError(`Invalid request: ${error.message}`, 'INVALID_REQUEST', error);
|
|
300
|
+
} else if (error.type === 'rate_limit_error') {
|
|
301
|
+
throw new OpenAIProviderError('OpenAI rate limit exceeded', 'RATE_LIMIT_EXCEEDED', error);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Generic error handling
|
|
305
|
+
throw new OpenAIProviderError(
|
|
306
|
+
`OpenAI API error: ${error.message || 'Unknown error'}`,
|
|
307
|
+
'API_ERROR',
|
|
308
|
+
error
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validate configuration for OpenAI provider
|
|
315
|
+
* @param {Object} config - Configuration object
|
|
316
|
+
* @returns {boolean} - True if configuration is valid
|
|
317
|
+
*/
|
|
318
|
+
validateConfig(config) {
|
|
319
|
+
return !!(config?.apiKeys?.openai && validateApiKey(config.apiKeys.openai));
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if provider is available with current configuration
|
|
324
|
+
* @param {Object} config - Configuration object
|
|
325
|
+
* @returns {boolean} - True if provider is available
|
|
326
|
+
*/
|
|
327
|
+
isAvailable(config) {
|
|
328
|
+
return this.validateConfig(config);
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get supported models
|
|
333
|
+
* @returns {Object} - Map of supported models and their configurations
|
|
334
|
+
*/
|
|
335
|
+
getSupportedModels() {
|
|
336
|
+
return SUPPORTED_MODELS;
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get model configuration
|
|
341
|
+
* @param {string} modelName - Model name
|
|
342
|
+
* @returns {Object|null} - Model configuration or null if not found
|
|
343
|
+
*/
|
|
344
|
+
getModelConfig(modelName) {
|
|
345
|
+
const resolved = resolveModelName(modelName);
|
|
346
|
+
return SUPPORTED_MODELS[resolved] || null;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XAI (Grok) Provider
|
|
3
|
+
*
|
|
4
|
+
* Provider implementation for XAI Grok models using OpenAI-compatible API with custom baseURL.
|
|
5
|
+
* Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import OpenAI from 'openai';
|
|
9
|
+
import { debugLog, debugError } from '../utils/console.js';
|
|
10
|
+
|
|
11
|
+
// Define supported Grok models with their capabilities
|
|
12
|
+
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
|
+
timeout: 300000, // 5 minutes
|
|
22
|
+
description: 'GROK-4 (256K context) - Latest advanced model from X.AI with image support',
|
|
23
|
+
aliases: ['grok', 'grok4', 'grok-4', 'grok-4-latest']
|
|
24
|
+
},
|
|
25
|
+
'grok-3': {
|
|
26
|
+
modelName: 'grok-3',
|
|
27
|
+
friendlyName: 'X.AI (Grok 3)',
|
|
28
|
+
contextWindow: 131072,
|
|
29
|
+
maxOutputTokens: 131072,
|
|
30
|
+
supportsStreaming: true,
|
|
31
|
+
supportsImages: false,
|
|
32
|
+
supportsTemperature: true,
|
|
33
|
+
timeout: 300000,
|
|
34
|
+
description: 'GROK-3 (131K context) - Previous generation reasoning model from X.AI',
|
|
35
|
+
aliases: ['grok3']
|
|
36
|
+
},
|
|
37
|
+
'grok-3-fast': {
|
|
38
|
+
modelName: 'grok-3-fast',
|
|
39
|
+
friendlyName: 'X.AI (Grok 3 Fast)',
|
|
40
|
+
contextWindow: 131072,
|
|
41
|
+
maxOutputTokens: 131072,
|
|
42
|
+
supportsStreaming: true,
|
|
43
|
+
supportsImages: false,
|
|
44
|
+
supportsTemperature: true,
|
|
45
|
+
timeout: 300000,
|
|
46
|
+
description: 'GROK-3 Fast (131K context) - Higher performance variant, faster processing but more expensive',
|
|
47
|
+
aliases: ['grok3fast', 'grok3-fast']
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Custom error class for XAI provider errors
|
|
53
|
+
*/
|
|
54
|
+
class XAIProviderError extends Error {
|
|
55
|
+
constructor(message, code, originalError = null) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'XAIProviderError';
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.originalError = originalError;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve model name to canonical form, including aliases
|
|
65
|
+
*/
|
|
66
|
+
function resolveModelName(modelName) {
|
|
67
|
+
const modelNameLower = modelName.toLowerCase();
|
|
68
|
+
|
|
69
|
+
// Check exact matches first
|
|
70
|
+
for (const [supportedModel] of Object.entries(SUPPORTED_MODELS)) {
|
|
71
|
+
if (supportedModel.toLowerCase() === modelNameLower) {
|
|
72
|
+
return supportedModel;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check aliases
|
|
77
|
+
for (const [supportedModel, config] of Object.entries(SUPPORTED_MODELS)) {
|
|
78
|
+
if (config.aliases) {
|
|
79
|
+
for (const alias of config.aliases) {
|
|
80
|
+
if (alias.toLowerCase() === modelNameLower) {
|
|
81
|
+
return supportedModel;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Return as-is if not found (let XAI API handle unknown models)
|
|
88
|
+
return modelName;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate XAI API key format
|
|
93
|
+
*/
|
|
94
|
+
function validateApiKey(apiKey) {
|
|
95
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// XAI API keys typically start with 'xai-' and are at least 20 characters
|
|
100
|
+
return apiKey.startsWith('xai-') && apiKey.length >= 20;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert messages to XAI/OpenAI format
|
|
105
|
+
*/
|
|
106
|
+
function convertMessages(messages) {
|
|
107
|
+
if (!Array.isArray(messages)) {
|
|
108
|
+
throw new XAIProviderError('Messages must be an array', 'INVALID_MESSAGES');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return messages.map((msg, index) => {
|
|
112
|
+
if (!msg || typeof msg !== 'object') {
|
|
113
|
+
throw new XAIProviderError(`Message at index ${index} must be an object`, 'INVALID_MESSAGE');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { role, content } = msg;
|
|
117
|
+
|
|
118
|
+
if (!role || !['system', 'user', 'assistant'].includes(role)) {
|
|
119
|
+
throw new XAIProviderError(`Invalid role "${role}" at message index ${index}`, 'INVALID_ROLE');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!content) {
|
|
123
|
+
throw new XAIProviderError(`Message content is required at index ${index}`, 'MISSING_CONTENT');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { role, content };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Main XAI provider implementation
|
|
132
|
+
*/
|
|
133
|
+
export const xaiProvider = {
|
|
134
|
+
/**
|
|
135
|
+
* Unified provider interface: invoke messages with options
|
|
136
|
+
* @param {Array} messages - Array of message objects with role and content
|
|
137
|
+
* @param {Object} options - Configuration options
|
|
138
|
+
* @returns {Object} - { content, stop_reason, rawResponse }
|
|
139
|
+
*/
|
|
140
|
+
async invoke(messages, options = {}) {
|
|
141
|
+
const {
|
|
142
|
+
model = 'grok-4-0709',
|
|
143
|
+
temperature = 0.7,
|
|
144
|
+
maxTokens = null,
|
|
145
|
+
stream = false,
|
|
146
|
+
reasoningEffort = 'medium',
|
|
147
|
+
config,
|
|
148
|
+
...otherOptions
|
|
149
|
+
} = options;
|
|
150
|
+
|
|
151
|
+
// Validate API key
|
|
152
|
+
if (!config?.apiKeys?.xai) {
|
|
153
|
+
throw new XAIProviderError('XAI API key not configured', 'MISSING_API_KEY');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!validateApiKey(config.apiKeys.xai)) {
|
|
157
|
+
throw new XAIProviderError('Invalid XAI API key format', 'INVALID_API_KEY');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Get base URL from config or use default
|
|
161
|
+
const baseURL = config.providers?.xaiBaseUrl || 'https://api.x.ai/v1';
|
|
162
|
+
|
|
163
|
+
// Initialize OpenAI client with XAI base URL
|
|
164
|
+
const openai = new OpenAI({
|
|
165
|
+
apiKey: config.apiKeys.xai,
|
|
166
|
+
baseURL,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Resolve model name
|
|
170
|
+
const resolvedModel = resolveModelName(model);
|
|
171
|
+
const modelConfig = SUPPORTED_MODELS[resolvedModel] || {};
|
|
172
|
+
|
|
173
|
+
// Convert and validate messages
|
|
174
|
+
const xaiMessages = convertMessages(messages);
|
|
175
|
+
|
|
176
|
+
// Filter out unsupported parameters for XAI/Grok models
|
|
177
|
+
const { reasoning_effort, reasoningEffort: reasoningEffortAlias, ...supportedOptions } = otherOptions;
|
|
178
|
+
|
|
179
|
+
// Build request payload
|
|
180
|
+
const requestPayload = {
|
|
181
|
+
model: resolvedModel,
|
|
182
|
+
messages: xaiMessages,
|
|
183
|
+
stream,
|
|
184
|
+
...supportedOptions
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Add temperature (all Grok models support temperature)
|
|
188
|
+
if (temperature !== undefined) {
|
|
189
|
+
requestPayload.temperature = Math.max(0, Math.min(2, temperature));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add max tokens if specified
|
|
193
|
+
if (maxTokens) {
|
|
194
|
+
requestPayload.max_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 256000);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Note: XAI/Grok models don't currently support reasoning_effort parameter
|
|
198
|
+
// We silently ignore it for API consistency (no need to log warnings in tests)
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
debugLog(`[XAI] Calling ${resolvedModel} with ${xaiMessages.length} messages`);
|
|
202
|
+
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
|
|
205
|
+
// Make the API call
|
|
206
|
+
const response = await openai.chat.completions.create(requestPayload);
|
|
207
|
+
|
|
208
|
+
const responseTime = Date.now() - startTime;
|
|
209
|
+
debugLog(`[XAI] Response received in ${responseTime}ms`);
|
|
210
|
+
|
|
211
|
+
// Extract response data
|
|
212
|
+
const choice = response.choices[0];
|
|
213
|
+
if (!choice) {
|
|
214
|
+
throw new XAIProviderError('No response choice received from XAI', 'NO_RESPONSE_CHOICE');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const content = choice.message?.content;
|
|
218
|
+
if (!content) {
|
|
219
|
+
throw new XAIProviderError('No content in response from XAI', 'NO_RESPONSE_CONTENT');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Extract usage information
|
|
223
|
+
const usage = response.usage || {};
|
|
224
|
+
|
|
225
|
+
// Return unified response format
|
|
226
|
+
return {
|
|
227
|
+
content,
|
|
228
|
+
stop_reason: choice.finish_reason || 'stop',
|
|
229
|
+
rawResponse: response,
|
|
230
|
+
metadata: {
|
|
231
|
+
model: response.model || resolvedModel,
|
|
232
|
+
usage: {
|
|
233
|
+
input_tokens: usage.prompt_tokens || 0,
|
|
234
|
+
output_tokens: usage.completion_tokens || 0,
|
|
235
|
+
total_tokens: usage.total_tokens || 0
|
|
236
|
+
},
|
|
237
|
+
response_time_ms: responseTime,
|
|
238
|
+
finish_reason: choice.finish_reason,
|
|
239
|
+
provider: 'xai'
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
} catch (error) {
|
|
244
|
+
debugError('[XAI] Error during API call:', error);
|
|
245
|
+
|
|
246
|
+
// Handle specific XAI/OpenAI compatible errors
|
|
247
|
+
if (error.code === 'insufficient_quota') {
|
|
248
|
+
throw new XAIProviderError('XAI API quota exceeded', 'QUOTA_EXCEEDED', error);
|
|
249
|
+
} else if (error.code === 'invalid_api_key') {
|
|
250
|
+
throw new XAIProviderError('Invalid XAI API key', 'INVALID_API_KEY', error);
|
|
251
|
+
} else if (error.code === 'model_not_found') {
|
|
252
|
+
throw new XAIProviderError(`Model ${resolvedModel} not found`, 'MODEL_NOT_FOUND', error);
|
|
253
|
+
} else if (error.code === 'context_length_exceeded') {
|
|
254
|
+
throw new XAIProviderError('Context length exceeded for model', 'CONTEXT_LENGTH_EXCEEDED', error);
|
|
255
|
+
} else if (error.type === 'invalid_request_error') {
|
|
256
|
+
throw new XAIProviderError(`Invalid request: ${error.message}`, 'INVALID_REQUEST', error);
|
|
257
|
+
} else if (error.type === 'rate_limit_error') {
|
|
258
|
+
throw new XAIProviderError('XAI rate limit exceeded', 'RATE_LIMIT_EXCEEDED', error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Generic error handling
|
|
262
|
+
throw new XAIProviderError(
|
|
263
|
+
`XAI API error: ${error.message || 'Unknown error'}`,
|
|
264
|
+
'API_ERROR',
|
|
265
|
+
error
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validate configuration for XAI provider
|
|
272
|
+
* @param {Object} config - Configuration object
|
|
273
|
+
* @returns {boolean} - True if configuration is valid
|
|
274
|
+
*/
|
|
275
|
+
validateConfig(config) {
|
|
276
|
+
return !!(config?.apiKeys?.xai && validateApiKey(config.apiKeys.xai));
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if provider is available with current configuration
|
|
281
|
+
* @param {Object} config - Configuration object
|
|
282
|
+
* @returns {boolean} - True if provider is available
|
|
283
|
+
*/
|
|
284
|
+
isAvailable(config) {
|
|
285
|
+
return this.validateConfig(config);
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get supported models
|
|
290
|
+
* @returns {Object} - Map of supported models and their configurations
|
|
291
|
+
*/
|
|
292
|
+
getSupportedModels() {
|
|
293
|
+
return SUPPORTED_MODELS;
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get model configuration
|
|
298
|
+
* @param {string} modelName - Model name
|
|
299
|
+
* @returns {Object|null} - Model configuration or null if not found
|
|
300
|
+
*/
|
|
301
|
+
getModelConfig(modelName) {
|
|
302
|
+
const resolved = resolveModelName(modelName);
|
|
303
|
+
return SUPPORTED_MODELS[resolved] || null;
|
|
304
|
+
}
|
|
305
|
+
};
|