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,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google (Gemini) Provider
|
|
3
|
+
*
|
|
4
|
+
* Provider implementation for Google Gemini models using the official @google/genai SDK v1.11+.
|
|
5
|
+
* Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GoogleGenAI } from '@google/genai';
|
|
9
|
+
import { debugLog, debugError } from '../utils/console.js';
|
|
10
|
+
|
|
11
|
+
// Define supported Gemini models with their capabilities
|
|
12
|
+
const SUPPORTED_MODELS = {
|
|
13
|
+
'gemini-2.0-flash': {
|
|
14
|
+
modelName: 'gemini-2.0-flash',
|
|
15
|
+
friendlyName: 'Gemini (Flash 2.0)',
|
|
16
|
+
contextWindow: 1048576, // 1M tokens
|
|
17
|
+
maxOutputTokens: 65536,
|
|
18
|
+
supportsStreaming: true,
|
|
19
|
+
supportsImages: true,
|
|
20
|
+
supportsTemperature: true,
|
|
21
|
+
supportsThinking: true,
|
|
22
|
+
maxThinkingTokens: 24576,
|
|
23
|
+
timeout: 300000,
|
|
24
|
+
description: 'Gemini 2.0 Flash (1M context) - Latest fast model with experimental thinking, supports audio/video input',
|
|
25
|
+
aliases: ['flash-2.0', 'flash2']
|
|
26
|
+
},
|
|
27
|
+
'gemini-2.0-flash-lite': {
|
|
28
|
+
modelName: 'gemini-2.0-flash-lite',
|
|
29
|
+
friendlyName: 'Gemini (Flash Lite 2.0)',
|
|
30
|
+
contextWindow: 1048576, // 1M tokens
|
|
31
|
+
maxOutputTokens: 65536,
|
|
32
|
+
supportsStreaming: true,
|
|
33
|
+
supportsImages: false,
|
|
34
|
+
supportsTemperature: true,
|
|
35
|
+
supportsThinking: false,
|
|
36
|
+
maxThinkingTokens: 0,
|
|
37
|
+
timeout: 300000,
|
|
38
|
+
description: 'Gemini 2.0 Flash Lite (1M context) - Lightweight fast model, text-only',
|
|
39
|
+
aliases: ['flashlite', 'flash-lite']
|
|
40
|
+
},
|
|
41
|
+
'gemini-2.5-flash': {
|
|
42
|
+
modelName: 'gemini-2.5-flash',
|
|
43
|
+
friendlyName: 'Gemini (Flash 2.5)',
|
|
44
|
+
contextWindow: 1048576, // 1M tokens
|
|
45
|
+
maxOutputTokens: 65536,
|
|
46
|
+
supportsStreaming: true,
|
|
47
|
+
supportsImages: true,
|
|
48
|
+
supportsTemperature: true,
|
|
49
|
+
supportsThinking: true,
|
|
50
|
+
maxThinkingTokens: 24576,
|
|
51
|
+
timeout: 300000,
|
|
52
|
+
description: 'Ultra-fast (1M context) - Quick analysis, simple queries, rapid iterations',
|
|
53
|
+
aliases: ['flash', 'flash2.5', 'gemini-flash', 'gemini-flash-2.5']
|
|
54
|
+
},
|
|
55
|
+
'gemini-2.5-pro': {
|
|
56
|
+
modelName: 'gemini-2.5-pro',
|
|
57
|
+
friendlyName: 'Gemini (Pro 2.5)',
|
|
58
|
+
contextWindow: 1048576, // 1M tokens
|
|
59
|
+
maxOutputTokens: 65536,
|
|
60
|
+
supportsStreaming: true,
|
|
61
|
+
supportsImages: true,
|
|
62
|
+
supportsTemperature: true,
|
|
63
|
+
supportsThinking: true,
|
|
64
|
+
maxThinkingTokens: 32768,
|
|
65
|
+
timeout: 300000,
|
|
66
|
+
description: 'Deep reasoning + thinking mode (1M context) - Complex problems, architecture, deep analysis',
|
|
67
|
+
aliases: ['pro', 'gemini pro', 'gemini-pro', 'gemini']
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Thinking mode budget percentages
|
|
72
|
+
const THINKING_BUDGETS = {
|
|
73
|
+
minimal: 0.005, // 0.5% of max - minimal thinking for fast responses
|
|
74
|
+
low: 0.08, // 8% of max - light reasoning tasks
|
|
75
|
+
medium: 0.33, // 33% of max - balanced reasoning (default)
|
|
76
|
+
high: 0.67, // 67% of max - complex analysis
|
|
77
|
+
max: 1.0 // 100% of max - full thinking budget
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Custom error class for Google provider errors
|
|
82
|
+
*/
|
|
83
|
+
class GoogleProviderError extends Error {
|
|
84
|
+
constructor(message, code, originalError = null) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = 'GoogleProviderError';
|
|
87
|
+
this.code = code;
|
|
88
|
+
this.originalError = originalError;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve model name to canonical form, including aliases
|
|
94
|
+
*/
|
|
95
|
+
function resolveModelName(modelName) {
|
|
96
|
+
const modelNameLower = modelName.toLowerCase();
|
|
97
|
+
|
|
98
|
+
// Check exact matches first
|
|
99
|
+
for (const [supportedModel] of Object.entries(SUPPORTED_MODELS)) {
|
|
100
|
+
if (supportedModel.toLowerCase() === modelNameLower) {
|
|
101
|
+
return supportedModel;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check aliases
|
|
106
|
+
for (const [supportedModel, config] of Object.entries(SUPPORTED_MODELS)) {
|
|
107
|
+
if (config.aliases) {
|
|
108
|
+
for (const alias of config.aliases) {
|
|
109
|
+
if (alias.toLowerCase() === modelNameLower) {
|
|
110
|
+
return supportedModel;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Return as-is if not found (let Google API handle unknown models)
|
|
117
|
+
return modelName;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate Google API key format
|
|
122
|
+
*/
|
|
123
|
+
function validateApiKey(apiKey) {
|
|
124
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Google API keys are typically long strings, usually starting with specific patterns
|
|
129
|
+
// They are generally 39+ characters long
|
|
130
|
+
return apiKey.length >= 20;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convert messages to Google Gemini format
|
|
135
|
+
*/
|
|
136
|
+
function convertMessagesToGemini(messages) {
|
|
137
|
+
if (!Array.isArray(messages)) {
|
|
138
|
+
throw new GoogleProviderError('Messages must be an array', 'INVALID_MESSAGES');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const contents = [];
|
|
142
|
+
let systemPrompt = null;
|
|
143
|
+
|
|
144
|
+
for (const [index, msg] of messages.entries()) {
|
|
145
|
+
if (!msg || typeof msg !== 'object') {
|
|
146
|
+
throw new GoogleProviderError(`Message at index ${index} must be an object`, 'INVALID_MESSAGE');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { role, content } = msg;
|
|
150
|
+
|
|
151
|
+
if (!role || !['system', 'user', 'assistant'].includes(role)) {
|
|
152
|
+
throw new GoogleProviderError(`Invalid role "${role}" at message index ${index}`, 'INVALID_ROLE');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!content) {
|
|
156
|
+
throw new GoogleProviderError(`Message content is required at index ${index}`, 'MISSING_CONTENT');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (role === 'system') {
|
|
160
|
+
// Google Gemini handles system prompts differently - they are typically prepended to the first user message
|
|
161
|
+
systemPrompt = content;
|
|
162
|
+
} else if (role === 'user') {
|
|
163
|
+
// Combine system prompt with user message if present
|
|
164
|
+
const userContent = systemPrompt ? `${systemPrompt}\n\n${content}` : content;
|
|
165
|
+
contents.push({
|
|
166
|
+
role: 'user',
|
|
167
|
+
parts: [{ text: userContent }]
|
|
168
|
+
});
|
|
169
|
+
systemPrompt = null; // Only use system prompt once
|
|
170
|
+
} else if (role === 'assistant') {
|
|
171
|
+
contents.push({
|
|
172
|
+
role: 'model', // Google uses 'model' instead of 'assistant'
|
|
173
|
+
parts: [{ text: content }]
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return contents;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate thinking budget for models that support it
|
|
183
|
+
*/
|
|
184
|
+
function calculateThinkingBudget(modelConfig, reasoningEffort) {
|
|
185
|
+
if (!modelConfig.supportsThinking || !modelConfig.maxThinkingTokens) {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const budget = THINKING_BUDGETS[reasoningEffort] || THINKING_BUDGETS.medium;
|
|
190
|
+
return Math.floor(modelConfig.maxThinkingTokens * budget);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if error is retryable
|
|
195
|
+
*/
|
|
196
|
+
function isErrorRetryable(error) {
|
|
197
|
+
const errorStr = String(error).toLowerCase();
|
|
198
|
+
|
|
199
|
+
// Non-retryable errors
|
|
200
|
+
const nonRetryableIndicators = [
|
|
201
|
+
'quota exceeded',
|
|
202
|
+
'quota_exceeded',
|
|
203
|
+
'resource exhausted',
|
|
204
|
+
'resource_exhausted',
|
|
205
|
+
'context length',
|
|
206
|
+
'token limit',
|
|
207
|
+
'request too large',
|
|
208
|
+
'invalid request',
|
|
209
|
+
'invalid_request',
|
|
210
|
+
'read timeout',
|
|
211
|
+
'timeout error',
|
|
212
|
+
'408',
|
|
213
|
+
'deadline exceeded'
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
if (nonRetryableIndicators.some(indicator => errorStr.includes(indicator))) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Retryable errors
|
|
221
|
+
const retryableIndicators = [
|
|
222
|
+
'connection',
|
|
223
|
+
'network',
|
|
224
|
+
'temporary',
|
|
225
|
+
'unavailable',
|
|
226
|
+
'retry',
|
|
227
|
+
'internal error',
|
|
228
|
+
'429',
|
|
229
|
+
'500',
|
|
230
|
+
'502',
|
|
231
|
+
'503',
|
|
232
|
+
'504',
|
|
233
|
+
'ssl',
|
|
234
|
+
'handshake'
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
return retryableIndicators.some(indicator => errorStr.includes(indicator));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Retry with progressive delays
|
|
242
|
+
*/
|
|
243
|
+
async function retryWithBackoff(fn, maxRetries = 4) {
|
|
244
|
+
const retryDelays = [1000, 3000, 5000, 8000]; // Progressive delays in ms
|
|
245
|
+
let lastError;
|
|
246
|
+
|
|
247
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
248
|
+
try {
|
|
249
|
+
return await fn();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
lastError = error;
|
|
252
|
+
|
|
253
|
+
// If this is the last attempt or not retryable, give up
|
|
254
|
+
if (attempt === maxRetries - 1 || !isErrorRetryable(error)) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Wait before retrying
|
|
259
|
+
const delay = retryDelays[attempt];
|
|
260
|
+
debugLog(`[Google] Retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries}):`, error.message);
|
|
261
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw lastError;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Main Google provider implementation
|
|
270
|
+
*/
|
|
271
|
+
export const googleProvider = {
|
|
272
|
+
/**
|
|
273
|
+
* Unified provider interface: invoke messages with options
|
|
274
|
+
* @param {Array} messages - Array of message objects with role and content
|
|
275
|
+
* @param {Object} options - Configuration options
|
|
276
|
+
* @returns {Object} - { content, stop_reason, rawResponse }
|
|
277
|
+
*/
|
|
278
|
+
async invoke(messages, options = {}) {
|
|
279
|
+
const {
|
|
280
|
+
model = 'gemini-2.5-flash',
|
|
281
|
+
temperature = 0.7,
|
|
282
|
+
maxTokens = null,
|
|
283
|
+
stream: _unused_stream = false, // Acknowledged but not used yet
|
|
284
|
+
reasoningEffort = 'medium',
|
|
285
|
+
config,
|
|
286
|
+
...otherOptions
|
|
287
|
+
} = options;
|
|
288
|
+
|
|
289
|
+
// Validate API key
|
|
290
|
+
if (!config?.apiKeys?.google) {
|
|
291
|
+
throw new GoogleProviderError('Google API key not configured', 'MISSING_API_KEY');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!validateApiKey(config.apiKeys.google)) {
|
|
295
|
+
throw new GoogleProviderError('Invalid Google API key format', 'INVALID_API_KEY');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Initialize Google AI client
|
|
299
|
+
const genAI = new GoogleGenAI({apiKey: config.apiKeys.google});
|
|
300
|
+
|
|
301
|
+
// Resolve model name
|
|
302
|
+
const resolvedModel = resolveModelName(model);
|
|
303
|
+
const modelConfig = SUPPORTED_MODELS[resolvedModel] || {};
|
|
304
|
+
|
|
305
|
+
// Convert messages to Google format
|
|
306
|
+
const geminiContents = convertMessagesToGemini(messages);
|
|
307
|
+
|
|
308
|
+
// Note: No need to get model instance, we use genAI.models.generateContent directly
|
|
309
|
+
|
|
310
|
+
// Build generation config
|
|
311
|
+
const generationConfig = {};
|
|
312
|
+
|
|
313
|
+
// Add temperature if model supports it
|
|
314
|
+
if (modelConfig.supportsTemperature !== false && temperature !== undefined) {
|
|
315
|
+
generationConfig.temperature = Math.max(0, Math.min(2, temperature));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Add max tokens if specified
|
|
319
|
+
if (maxTokens) {
|
|
320
|
+
generationConfig.maxOutputTokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 65536);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add thinking configuration for models that support it
|
|
324
|
+
if (modelConfig.supportsThinking && reasoningEffort) {
|
|
325
|
+
const thinkingBudget = calculateThinkingBudget(modelConfig, reasoningEffort);
|
|
326
|
+
if (thinkingBudget > 0) {
|
|
327
|
+
generationConfig.thinkingConfig = { thinkingBudget };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
debugLog(`[Google] Calling ${resolvedModel} with ${messages.length} messages`);
|
|
333
|
+
|
|
334
|
+
const startTime = Date.now();
|
|
335
|
+
|
|
336
|
+
// Make the API call with retry logic
|
|
337
|
+
const response = await retryWithBackoff(async () => {
|
|
338
|
+
return await genAI.models.generateContent({
|
|
339
|
+
model: resolvedModel,
|
|
340
|
+
contents: geminiContents,
|
|
341
|
+
config: generationConfig
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const responseTime = Date.now() - startTime;
|
|
346
|
+
debugLog(`[Google] Response received in ${responseTime}ms`);
|
|
347
|
+
|
|
348
|
+
// Extract response data using the new SDK format
|
|
349
|
+
const content = response.text;
|
|
350
|
+
if (!content) {
|
|
351
|
+
throw new GoogleProviderError('No text content received from Google', 'NO_RESPONSE_CONTENT');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Extract usage information from the new SDK format
|
|
355
|
+
const usage = {
|
|
356
|
+
input_tokens: response.usageMetadata?.promptTokenCount || 0,
|
|
357
|
+
output_tokens: response.usageMetadata?.candidatesTokenCount || 0,
|
|
358
|
+
total_tokens: response.usageMetadata?.totalTokenCount || 0
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Extract finish reason from candidates
|
|
362
|
+
const finishReason = response.candidates?.[0]?.finishReason || 'STOP';
|
|
363
|
+
|
|
364
|
+
// Return unified response format
|
|
365
|
+
return {
|
|
366
|
+
content,
|
|
367
|
+
stop_reason: finishReason,
|
|
368
|
+
rawResponse: response,
|
|
369
|
+
metadata: {
|
|
370
|
+
model: resolvedModel,
|
|
371
|
+
usage,
|
|
372
|
+
response_time_ms: responseTime,
|
|
373
|
+
finish_reason: finishReason,
|
|
374
|
+
reasoning_effort: modelConfig.supportsThinking ? reasoningEffort : null,
|
|
375
|
+
provider: 'google'
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
} catch (error) {
|
|
380
|
+
debugError('[Google] Error during API call:', error);
|
|
381
|
+
|
|
382
|
+
// Handle specific Google errors
|
|
383
|
+
if (error.message?.includes('quota') || error.message?.includes('QUOTA_EXCEEDED')) {
|
|
384
|
+
throw new GoogleProviderError('Google API quota exceeded', 'QUOTA_EXCEEDED', error);
|
|
385
|
+
} else if (error.message?.includes('API_KEY_INVALID') || error.message?.includes('invalid api key')) {
|
|
386
|
+
throw new GoogleProviderError('Invalid Google API key', 'INVALID_API_KEY', error);
|
|
387
|
+
} else if (error.message?.includes('MODEL_NOT_FOUND')) {
|
|
388
|
+
throw new GoogleProviderError(`Model ${resolvedModel} not found`, 'MODEL_NOT_FOUND', error);
|
|
389
|
+
} else if (error.message?.includes('CONTEXT_LENGTH_EXCEEDED')) {
|
|
390
|
+
throw new GoogleProviderError('Context length exceeded for model', 'CONTEXT_LENGTH_EXCEEDED', error);
|
|
391
|
+
} else if (error.message?.includes('SAFETY')) {
|
|
392
|
+
throw new GoogleProviderError('Content blocked by safety filters', 'SAFETY_ERROR', error);
|
|
393
|
+
} else if (error.message?.includes('RATE_LIMIT_EXCEEDED')) {
|
|
394
|
+
throw new GoogleProviderError('Google rate limit exceeded', 'RATE_LIMIT_EXCEEDED', error);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Generic error handling
|
|
398
|
+
throw new GoogleProviderError(
|
|
399
|
+
`Google API error: ${error.message || 'Unknown error'}`,
|
|
400
|
+
'API_ERROR',
|
|
401
|
+
error
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Validate configuration for Google provider
|
|
408
|
+
* @param {Object} config - Configuration object
|
|
409
|
+
* @returns {boolean} - True if configuration is valid
|
|
410
|
+
*/
|
|
411
|
+
validateConfig(config) {
|
|
412
|
+
return !!(config?.apiKeys?.google && validateApiKey(config.apiKeys.google));
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Check if provider is available with current configuration
|
|
417
|
+
* @param {Object} config - Configuration object
|
|
418
|
+
* @returns {boolean} - True if provider is available
|
|
419
|
+
*/
|
|
420
|
+
isAvailable(config) {
|
|
421
|
+
return this.validateConfig(config);
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get supported models
|
|
426
|
+
* @returns {Object} - Map of supported models and their configurations
|
|
427
|
+
*/
|
|
428
|
+
getSupportedModels() {
|
|
429
|
+
return SUPPORTED_MODELS;
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get model configuration
|
|
434
|
+
* @param {string} modelName - Model name
|
|
435
|
+
* @returns {Object|null} - Model configuration or null if not found
|
|
436
|
+
*/
|
|
437
|
+
getModelConfig(modelName) {
|
|
438
|
+
const resolved = resolveModelName(modelName);
|
|
439
|
+
return SUPPORTED_MODELS[resolved] || null;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registry for all AI providers following functional architecture.
|
|
5
|
+
* Each provider implements: async invoke(messages, options) => { content, stop_reason, rawResponse }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Import individual providers (will be implemented in subsequent tasks)
|
|
9
|
+
import { openaiProvider } from './openai.js';
|
|
10
|
+
import { xaiProvider } from './xai.js';
|
|
11
|
+
import { googleProvider } from './google.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Provider registry map
|
|
15
|
+
* Each provider must implement the unified interface:
|
|
16
|
+
* - invoke(messages, options): Main invocation method
|
|
17
|
+
* - validateConfig(config): Configuration validation
|
|
18
|
+
* - isAvailable(config): Availability check
|
|
19
|
+
*/
|
|
20
|
+
const providers = {
|
|
21
|
+
// Will be populated by individual provider modules
|
|
22
|
+
openai: openaiProvider,
|
|
23
|
+
xai: xaiProvider,
|
|
24
|
+
google: googleProvider,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get all available providers
|
|
29
|
+
* @returns {object} Map of provider name to provider implementation
|
|
30
|
+
*/
|
|
31
|
+
export function getProviders() {
|
|
32
|
+
return providers;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a specific provider by name
|
|
37
|
+
* @param {string} name - Provider name
|
|
38
|
+
* @returns {object|null} Provider implementation or null if not found
|
|
39
|
+
*/
|
|
40
|
+
export function getProvider(name) {
|
|
41
|
+
return providers[name] || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register a new provider
|
|
46
|
+
* @param {string} name - Provider name
|
|
47
|
+
* @param {object} provider - Provider implementation
|
|
48
|
+
*/
|
|
49
|
+
export function registerProvider(name, provider) {
|
|
50
|
+
// Validate provider interface
|
|
51
|
+
if (!provider.invoke || typeof provider.invoke !== 'function') {
|
|
52
|
+
throw new Error(`Provider ${name} must implement invoke() method`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
providers[name] = provider;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get list of available provider names
|
|
60
|
+
* @param {object} config - Configuration object
|
|
61
|
+
* @returns {string[]} Array of available provider names
|
|
62
|
+
*/
|
|
63
|
+
export function getAvailableProviders(config) {
|
|
64
|
+
return Object.keys(providers).filter(name => {
|
|
65
|
+
const provider = providers[name];
|
|
66
|
+
return provider.isAvailable && provider.isAvailable(config);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unified provider interface validation
|
|
72
|
+
* @param {object} provider - Provider to validate
|
|
73
|
+
* @returns {boolean} True if provider implements required interface
|
|
74
|
+
*/
|
|
75
|
+
export function validateProviderInterface(provider) {
|
|
76
|
+
const requiredMethods = ['invoke'];
|
|
77
|
+
// const optionalMethods = ['validateConfig', 'isAvailable'];
|
|
78
|
+
|
|
79
|
+
// Check required methods
|
|
80
|
+
for (const method of requiredMethods) {
|
|
81
|
+
if (!provider[method] || typeof provider[method] !== 'function') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true;
|
|
87
|
+
}
|