UNPKG

converse-mcp-server

Version:

Converse MCP Server - Converse with other LLMs with chat and consensus tools

565 lines (511 loc) 20.5 kB
/** * OpenAI Provider * * Provider implementation for OpenAI GPT models using the official OpenAI SDK v5. * Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse } */ import OpenAI from 'openai'; import { debugLog, debugError } from '../utils/console.js'; // Define supported models with their capabilities const SUPPORTED_MODELS = { 'gpt-5': { modelName: 'gpt-5', friendlyName: 'OpenAI (GPT-5)', contextWindow: 400000, maxOutputTokens: 128000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, // GPT-5 doesn't support temperature supportsWebSearch: true, supportsResponsesAPI: true, timeout: 300000, // 5 minutes description: 'Latest flagship model (400K context, 128K output) - Superior reasoning, code generation, and analysis', aliases: ['gpt5', 'gpt 5', 'gpt-5-2025-08-07'] }, 'gpt-5-mini': { modelName: 'gpt-5-mini', friendlyName: 'OpenAI (GPT-5-mini)', contextWindow: 400000, maxOutputTokens: 128000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, // GPT-5 models don't support temperature supportsWebSearch: true, supportsResponsesAPI: true, timeout: 180000, // 3 minutes description: 'Faster, cost-efficient GPT-5 (400K context, 128K output) - Well-defined tasks, precise prompts', aliases: ['gpt5-mini', 'gpt-5mini', 'gpt 5 mini', 'gpt-5-mini-2025-08-07'] }, 'gpt-5-nano': { modelName: 'gpt-5-nano', friendlyName: 'OpenAI (GPT-5-nano)', contextWindow: 400000, maxOutputTokens: 128000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, // GPT-5 models don't support temperature supportsWebSearch: false, // GPT-5-nano doesn't support web search supportsResponsesAPI: true, timeout: 120000, // 2 minutes description: 'Fastest, most cost-efficient GPT-5 (400K context, 128K output) - Summarization, classification', aliases: ['gpt5-nano', 'gpt-5nano', 'gpt 5 nano', 'gpt-5-nano-2025-08-07'] }, 'o3': { modelName: 'o3', friendlyName: 'OpenAI (O3)', contextWindow: 200000, maxOutputTokens: 100000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, supportsWebSearch: true, supportsResponsesAPI: true, timeout: 300000, // 5 minutes description: 'Strong reasoning (200K context) - Logical problems, code generation, systematic analysis', aliases: ['o3-2025-01-31'] }, 'o3-mini': { modelName: 'o3-mini', friendlyName: 'OpenAI (O3-mini)', contextWindow: 200000, maxOutputTokens: 100000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, supportsWebSearch: false, // o3-mini does not support web search supportsResponsesAPI: true, timeout: 300000, description: 'Fast O3 variant (200K context) - Balanced performance/speed, moderate complexity', aliases: ['o3mini', 'o3 mini', 'o3-mini-2025-01-31'] }, 'o3-pro-2025-06-10': { modelName: 'o3-pro-2025-06-10', friendlyName: 'OpenAI (O3-Pro)', contextWindow: 200000, maxOutputTokens: 100000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, supportsWebSearch: true, supportsResponsesAPI: true, timeout: 1800000, // 30 minutes description: 'Professional-grade reasoning (200K context) - EXTREMELY EXPENSIVE: Only for the most complex problems', aliases: ['o3-pro', 'o3pro', 'o3 pro'] }, 'o4-mini': { modelName: 'o4-mini', friendlyName: 'OpenAI (O4-mini)', contextWindow: 200000, maxOutputTokens: 100000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, supportsWebSearch: true, supportsResponsesAPI: true, timeout: 180000, // 3 minutes description: 'Latest reasoning model (200K context) - Optimized for shorter contexts, rapid reasoning', aliases: ['o4mini', 'o4', 'o4 mini', 'o4-mini-2025-01-30'] }, 'gpt-4.1-2025-04-14': { modelName: 'gpt-4.1-2025-04-14', friendlyName: 'OpenAI (GPT-4.1)', contextWindow: 1000000, maxOutputTokens: 32768, supportsStreaming: true, supportsImages: true, supportsTemperature: true, supportsWebSearch: true, supportsResponsesAPI: true, timeout: 300000, description: 'GPT-4.1 (1M context) - Advanced reasoning model with large context window', aliases: ['gpt4.1', 'gpt-4.1', 'gpt 4.1', 'gpt-4.1-latest'] }, 'gpt-4o': { modelName: 'gpt-4o', friendlyName: 'OpenAI (GPT-4o)', contextWindow: 128000, maxOutputTokens: 16384, supportsStreaming: true, supportsImages: true, supportsTemperature: true, supportsWebSearch: true, supportsResponsesAPI: true, timeout: 180000, description: 'GPT-4o (128K context) - Multimodal flagship model with vision capabilities', aliases: ['gpt4o', 'gpt 4o', '4o'] }, 'gpt-4o-mini': { modelName: 'gpt-4o-mini', friendlyName: 'OpenAI (GPT-4o-mini)', contextWindow: 128000, maxOutputTokens: 16384, supportsStreaming: true, supportsImages: true, supportsTemperature: true, supportsWebSearch: true, supportsResponsesAPI: true, timeout: 120000, description: 'GPT-4o-mini (128K context) - Fast and efficient multimodal model', aliases: ['gpt4o-mini', 'gpt 4o mini', '4o mini', '4o-mini'] }, 'o3-deep-research-2025-06-26': { modelName: 'o3-deep-research-2025-06-26', friendlyName: 'OpenAI (O3 Deep Research)', contextWindow: 200000, maxOutputTokens: 100000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, supportsWebSearch: true, supportsResponsesAPI: true, supportsDeepResearch: true, timeout: 5400000, // 90 minutes for deep research description: 'Deep research model (200K context) - In-depth synthesis, comprehensive reports, multi-source analysis (30-90 min runtime)', aliases: ['o3-deep-research', 'o3-research', 'o3 deep research', 'deep-research-o3'] }, 'o4-mini-deep-research-2025-06-26': { modelName: 'o4-mini-deep-research-2025-06-26', friendlyName: 'OpenAI (O4-mini Deep Research)', contextWindow: 200000, maxOutputTokens: 100000, supportsStreaming: true, supportsImages: true, supportsTemperature: false, supportsWebSearch: true, supportsResponsesAPI: true, supportsDeepResearch: true, timeout: 3600000, // 60 minutes for faster deep research description: 'Fast deep research model (200K context) - Lightweight research, faster results, latency-sensitive analysis (15-60 min runtime)', aliases: ['o4-mini-deep-research', 'o4-mini-research', 'o4-research', 'o4 mini deep research', 'deep-research-o4-mini', 'o4-deep-research'] } }; /** * Custom error class for OpenAI provider errors */ class OpenAIProviderError extends Error { constructor(message, code, originalError = null) { super(message); this.name = 'OpenAIProviderError'; this.code = code; this.originalError = originalError; } } /** * Resolve model name to canonical form, including aliases */ function resolveModelName(modelName) { const modelNameLower = modelName.toLowerCase(); // Check exact matches first for (const [supportedModel] of Object.entries(SUPPORTED_MODELS)) { if (supportedModel.toLowerCase() === modelNameLower) { return supportedModel; } } // Check aliases for (const [supportedModel, config] of Object.entries(SUPPORTED_MODELS)) { if (config.aliases) { for (const alias of config.aliases) { if (alias.toLowerCase() === modelNameLower) { return supportedModel; } } } } // Return as-is if not found (let OpenAI API handle unknown models) return modelName; } /** * Validate OpenAI API key format */ function validateApiKey(apiKey) { if (!apiKey || typeof apiKey !== 'string') { return false; } // OpenAI API keys typically start with 'sk-' and are at least 20 characters return apiKey.startsWith('sk-') && apiKey.length >= 20; } /** * Convert messages to OpenAI format, handling both Responses API and Chat Completions API */ function convertMessages(messages, useResponsesAPI = false) { if (!Array.isArray(messages)) { throw new OpenAIProviderError('Messages must be an array', 'INVALID_MESSAGES'); } return messages.map((msg, index) => { if (!msg || typeof msg !== 'object') { throw new OpenAIProviderError(`Message at index ${index} must be an object`, 'INVALID_MESSAGE'); } const { role, content } = msg; if (!role || !['system', 'user', 'assistant'].includes(role)) { throw new OpenAIProviderError(`Invalid role "${role}" at message index ${index}`, 'INVALID_ROLE'); } if (!content) { throw new OpenAIProviderError(`Message content is required at index ${index}`, 'MISSING_CONTENT'); } // Handle complex content structure (array with text and images) if (Array.isArray(content)) { debugLog(`[OpenAI] Processing complex content array with ${content.length} items for ${useResponsesAPI ? 'Responses API' : 'Chat Completions API'}`); if (useResponsesAPI) { // Convert to Responses API format const convertedContent = []; for (const item of content) { if (item.type === 'text') { convertedContent.push({ type: 'input_text', text: item.text }); } else if (item.type === 'image' && item.source) { // Convert Anthropic/Claude format to OpenAI Responses API format const imageUrl = `data:${item.source.media_type};base64,${item.source.data}`; debugLog(`[OpenAI] Converting image for Responses API: ${item.source.media_type}, data length: ${item.source.data.length}`); convertedContent.push({ type: 'input_image', image_url: imageUrl }); } } return { role, content: convertedContent }; } else { // Convert to Chat Completions API format const convertedContent = []; for (const item of content) { if (item.type === 'text') { convertedContent.push({ type: 'text', text: item.text }); } else if (item.type === 'image' && item.source) { // Convert Anthropic/Claude format to OpenAI Chat Completions format const imageUrl = `data:${item.source.media_type};base64,${item.source.data}`; debugLog(`[OpenAI] Converting image for Chat Completions API: ${item.source.media_type}, data length: ${item.source.data.length}`); convertedContent.push({ type: 'image_url', image_url: { url: imageUrl, detail: 'high' } }); } } return { role, content: convertedContent }; } } // Simple string content return { role, content }; }); } /** * Main OpenAI provider implementation */ export const openaiProvider = { /** * Unified provider interface: invoke messages with options * @param {Array} messages - Array of message objects with role and content * @param {Object} options - Configuration options * @returns {Object} - { content, stop_reason, rawResponse } */ async invoke(messages, options = {}) { const { model = 'gpt-4o-mini', temperature = 0.7, maxTokens = null, stream = false, reasoning_effort = 'medium', verbosity = 'medium', use_websearch = false, config, ...otherOptions } = options; // Validate API key if (!config?.apiKeys?.openai) { throw new OpenAIProviderError('OpenAI API key not configured', 'MISSING_API_KEY'); } if (!validateApiKey(config.apiKeys.openai)) { throw new OpenAIProviderError('Invalid OpenAI API key format', 'INVALID_API_KEY'); } // Initialize OpenAI client const openai = new OpenAI({ apiKey: config.apiKeys.openai, }); // Resolve model name const resolvedModel = resolveModelName(model); const modelConfig = SUPPORTED_MODELS[resolvedModel] || {}; // Always use Responses API since all OpenAI models support it // Only fallback to Chat Completions API if Responses API is explicitly not supported const shouldUseResponsesAPI = modelConfig.supportsResponsesAPI !== false; // Convert and validate messages const openaiMessages = convertMessages(messages, shouldUseResponsesAPI); // Build request payload based on API type let requestPayload; if (shouldUseResponsesAPI) { // Build Responses API payload requestPayload = { model: resolvedModel, input: openaiMessages, stream, ...otherOptions }; // Add web search tools only if requested and model supports it if (use_websearch && modelConfig.supportsWebSearch) { // Use web_search_preview tool for all models in Responses API requestPayload.tools = [{ type: 'web_search_preview' }]; } // Add temperature if model supports it if (modelConfig.supportsTemperature !== false && temperature !== undefined) { requestPayload.temperature = Math.max(0, Math.min(2, temperature)); } // Add reasoning effort for thinking models (o3 series and GPT-5 family) if ((resolvedModel.startsWith('o3') || resolvedModel.startsWith('gpt-5')) && reasoning_effort) { requestPayload.reasoning = { effort: reasoning_effort }; } // Add verbosity for GPT-5 models if (resolvedModel.startsWith('gpt-5') && verbosity) { requestPayload.text = { verbosity: verbosity }; } } else { // Build Chat Completions API payload const { reasoning_effort: _unused, verbosity: _unused2, ...cleanOptions } = otherOptions; requestPayload = { model: resolvedModel, messages: openaiMessages, stream, ...cleanOptions }; // Add temperature if model supports it if (modelConfig.supportsTemperature !== false && temperature !== undefined) { requestPayload.temperature = Math.max(0, Math.min(2, temperature)); } // Add reasoning effort for thinking models (o3 series and GPT-5 family) if ((resolvedModel.startsWith('o3') || resolvedModel.startsWith('gpt-5')) && reasoning_effort) { requestPayload.reasoning_effort = reasoning_effort; } // Add verbosity for GPT-5 models if (resolvedModel.startsWith('gpt-5') && verbosity) { requestPayload.verbosity = verbosity; } } // Add max tokens if specified (both APIs) if (maxTokens) { if (shouldUseResponsesAPI) { requestPayload.max_output_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 100000); } else { requestPayload.max_tokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 100000); } } try { const apiType = shouldUseResponsesAPI ? 'Responses API' : 'Chat Completions API'; const searchInfo = (use_websearch && modelConfig.supportsWebSearch) ? ' (with web search)' : ''; debugLog(`[OpenAI] Calling ${resolvedModel} via ${apiType} with ${openaiMessages.length} messages${searchInfo}`); const startTime = Date.now(); // Make the API call based on API type let response; if (shouldUseResponsesAPI) { response = await openai.responses.create(requestPayload); } else { response = await openai.chat.completions.create(requestPayload); } const responseTime = Date.now() - startTime; debugLog(`[OpenAI] Response received in ${responseTime}ms`); // Extract response data based on API type let content, stopReason, usage; if (shouldUseResponsesAPI) { // Handle Responses API response format if (!response.output_text) { throw new OpenAIProviderError('No output_text in Responses API response', 'NO_RESPONSE_CONTENT'); } content = response.output_text; stopReason = response.status || 'stop'; usage = response.usage || {}; } else { // Handle Chat Completions API response format const choice = response.choices[0]; if (!choice) { throw new OpenAIProviderError('No response choice received from OpenAI', 'NO_RESPONSE_CHOICE'); } content = choice.message?.content; if (!content) { throw new OpenAIProviderError('No content in response from OpenAI', 'NO_RESPONSE_CONTENT'); } stopReason = choice.finish_reason || 'stop'; usage = response.usage || {}; } // Determine web search usage const webSearchUsed = use_websearch && modelConfig.supportsWebSearch; const webSearchType = webSearchUsed ? 'web_search_preview' : null; // Return unified response format return { content, stop_reason: stopReason, rawResponse: response, metadata: { model: response.model || resolvedModel, usage: { input_tokens: usage.prompt_tokens || usage.input_tokens || 0, output_tokens: usage.completion_tokens || usage.output_tokens || 0, total_tokens: usage.total_tokens || 0 }, response_time_ms: responseTime, finish_reason: stopReason, provider: 'openai', api_type: apiType, web_search_used: webSearchUsed, web_search_type: webSearchType } }; } catch (error) { debugError('[OpenAI] Error during API call:', error); // Handle specific OpenAI errors if (error.code === 'insufficient_quota') { throw new OpenAIProviderError('OpenAI API quota exceeded', 'QUOTA_EXCEEDED', error); } else if (error.code === 'invalid_api_key') { throw new OpenAIProviderError('Invalid OpenAI API key', 'INVALID_API_KEY', error); } else if (error.code === 'model_not_found') { throw new OpenAIProviderError(`Model ${resolvedModel} not found`, 'MODEL_NOT_FOUND', error); } else if (error.code === 'context_length_exceeded') { throw new OpenAIProviderError('Context length exceeded for model', 'CONTEXT_LENGTH_EXCEEDED', error); } else if (error.type === 'invalid_request_error') { throw new OpenAIProviderError(`Invalid request: ${error.message}`, 'INVALID_REQUEST', error); } else if (error.type === 'rate_limit_error') { throw new OpenAIProviderError('OpenAI rate limit exceeded', 'RATE_LIMIT_EXCEEDED', error); } // Generic error handling throw new OpenAIProviderError( `OpenAI API error: ${error.message || 'Unknown error'}`, 'API_ERROR', error ); } }, /** * Validate configuration for OpenAI provider * @param {Object} config - Configuration object * @returns {boolean} - True if configuration is valid */ validateConfig(config) { return !!(config?.apiKeys?.openai && validateApiKey(config.apiKeys.openai)); }, /** * Check if provider is available with current configuration * @param {Object} config - Configuration object * @returns {boolean} - True if provider is available */ isAvailable(config) { return this.validateConfig(config); }, /** * Get supported models * @returns {Object} - Map of supported models and their configurations */ getSupportedModels() { return SUPPORTED_MODELS; }, /** * Get model configuration * @param {string} modelName - Model name * @returns {Object|null} - Model configuration or null if not found */ getModelConfig(modelName) { const resolved = resolveModelName(modelName); return SUPPORTED_MODELS[resolved] || null; } };