UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and

1,059 lines (1,058 loc) 47.8 kB
import { MiddlewareFactory } from "../middleware/factory.js"; import { logger } from "../utils/logger.js"; import { DEFAULT_MAX_STEPS, STEP_LIMITS } from "../core/constants.js"; import { directAgentTools } from "../agent/directTools.js"; import { getSafeMaxTokens } from "../utils/tokenLimits.js"; import { createTimeoutController, TimeoutError } from "../utils/timeout.js"; import { shouldDisableBuiltinTools } from "../utils/toolUtils.js"; import { buildMessagesArray } from "../utils/messageBuilder.js"; import { getKeysAsString, getKeyCount } from "../utils/transformationUtils.js"; import { validateStreamOptions as validateStreamOpts, validateTextGenerationOptions, ValidationError, createValidationSummary, } from "../utils/parameterValidation.js"; import { recordProviderPerformanceFromMetrics, getPerformanceOptimizedProvider, } from "./evaluationProviders.js"; import { modelConfig } from "./modelConfiguration.js"; /** * Abstract base class for all AI providers * Tools are integrated as first-class citizens - always available by default */ export class BaseProvider { modelName; providerName; defaultTimeout = 30000; // 30 seconds middlewareOptions; // TODO: Implement global level middlewares that can be used // Tools are conditionally included based on centralized configuration directTools = shouldDisableBuiltinTools() ? {} : directAgentTools; mcpTools; // MCP tools loaded dynamically when available customTools; // Custom tools from registerTool() toolExecutor; // Tool executor from setupToolExecutor sessionId; userId; neurolink; // Reference to actual NeuroLink instance for MCP tools constructor(modelName, providerName, neurolink, middleware) { this.modelName = modelName || this.getDefaultModel(); this.providerName = providerName || this.getProviderName(); this.neurolink = neurolink; this.middlewareOptions = middleware; } /** * Check if this provider supports tool/function calling * Override in subclasses to disable tools for specific providers or models * @returns true by default, providers can override to return false */ supportsTools() { return true; } // =================== // PUBLIC API METHODS // =================== /** * Primary streaming method - implements AIProvider interface * When tools are involved, falls back to generate() with synthetic streaming */ async stream(optionsOrPrompt, analysisSchema) { const options = this.normalizeStreamOptions(optionsOrPrompt); // CRITICAL FIX: Always prefer real streaming over fake streaming // Try real streaming first, use fake streaming only as fallback try { const realStreamResult = await this.executeStream(options, analysisSchema); // If real streaming succeeds, return it (with tools support via Vercel AI SDK) return realStreamResult; } catch (realStreamError) { logger.warn(`Real streaming failed for ${this.providerName}, falling back to fake streaming:`, realStreamError); // Fallback to fake streaming only if real streaming fails AND tools are enabled if (!options.disableTools && this.supportsTools()) { try { // Convert stream options to text generation options const textOptions = { prompt: options.input?.text || "", systemPrompt: options.systemPrompt, temperature: options.temperature, maxTokens: options.maxTokens, disableTools: false, maxSteps: options.maxSteps || 5, provider: options.provider, model: options.model, // 🔧 FIX: Include analytics and evaluation options from stream options enableAnalytics: options.enableAnalytics, enableEvaluation: options.enableEvaluation, evaluationDomain: options.evaluationDomain, toolUsageContext: options.toolUsageContext, context: options.context, }; const result = await this.generate(textOptions, analysisSchema); // Create a synthetic stream from the generate result that simulates progressive delivery return { stream: (async function* () { if (result?.content) { // Split content into words for more natural streaming const words = result.content.split(/(\s+)/); // Keep whitespace let buffer = ""; for (let i = 0; i < words.length; i++) { buffer += words[i]; // Yield chunks of roughly 5-10 words or at punctuation const shouldYield = i === words.length - 1 || // Last word buffer.length > 50 || // Buffer getting long /[.!?;,]\s*$/.test(buffer); // End of sentence/clause if (shouldYield && buffer.trim()) { yield { content: buffer }; buffer = ""; // Small delay to simulate streaming (1-10ms) await new Promise((resolve) => setTimeout(resolve, Math.random() * 9 + 1)); } } // Yield all remaining content if (buffer.trim()) { yield { content: buffer }; } } })(), usage: result?.usage, provider: result?.provider, model: result?.model, toolCalls: result?.toolCalls?.map((call) => ({ toolName: call.toolName, parameters: call.args, id: call.toolCallId, })), toolResults: result?.toolResults ? result.toolResults.map((tr) => ({ toolName: tr.toolName || "unknown", status: (tr.status === "error" ? "failure" : "success"), result: tr.result, error: tr.error, })) : undefined, // 🔧 FIX: Include analytics and evaluation from generate result analytics: result?.analytics, evaluation: result?.evaluation, }; } catch (error) { logger.error(`Fake streaming fallback failed for ${this.providerName}:`, error); throw this.handleProviderError(error); } } else { // If real streaming failed and no tools are enabled, re-throw the original error logger.error(`Real streaming failed for ${this.providerName}:`, realStreamError); throw this.handleProviderError(realStreamError); } } } /** * Text generation method - implements AIProvider interface * Tools are always available unless explicitly disabled */ async generate(optionsOrPrompt, _analysisSchema) { const options = this.normalizeTextOptions(optionsOrPrompt); // Validate options before proceeding this.validateOptions(options); const startTime = Date.now(); try { // Import generateText dynamically to avoid circular dependencies const { generateText } = await import("ai"); // Get ALL available tools (direct + MCP + external from options) const shouldUseTools = !options.disableTools && this.supportsTools(); const baseTools = shouldUseTools ? await this.getAllTools() : {}; const tools = shouldUseTools ? { ...baseTools, ...(options.tools || {}), // Include external tools passed from NeuroLink } : {}; logger.debug(`[BaseProvider.generate] Tools for ${this.providerName}:`, { directTools: getKeyCount(baseTools), directToolNames: getKeysAsString(baseTools), externalTools: getKeyCount(options.tools || {}), externalToolNames: getKeysAsString(options.tools || {}), totalTools: getKeyCount(tools), totalToolNames: getKeysAsString(tools), }); const model = await this.getAISDKModelWithMiddleware(options); // Build proper message array with conversation history const messages = buildMessagesArray(options); const result = await generateText({ model, messages: messages, tools, maxSteps: options.maxSteps || DEFAULT_MAX_STEPS, toolChoice: shouldUseTools ? "auto" : "none", temperature: options.temperature, maxTokens: options.maxTokens || 8192, }); const responseTime = Date.now() - startTime; try { // Calculate actual cost based on token usage and provider configuration const calculateActualCost = () => { try { const costInfo = modelConfig.getCostInfo(this.providerName, this.modelName); if (!costInfo) { return 0; // No cost info available } const promptTokens = result.usage?.promptTokens || 0; const completionTokens = result.usage?.completionTokens || 0; // Calculate cost per 1K tokens const inputCost = (promptTokens / 1000) * costInfo.input; const outputCost = (completionTokens / 1000) * costInfo.output; return inputCost + outputCost; } catch (error) { logger.debug(`Cost calculation failed for ${this.providerName}:`, error); return 0; // Fallback to 0 on any error } }; const actualCost = calculateActualCost(); recordProviderPerformanceFromMetrics(this.providerName, { responseTime, tokensGenerated: result.usage?.totalTokens || 0, cost: actualCost, success: true, }); // Show what the system learned (updated to include cost) const optimizedProvider = getPerformanceOptimizedProvider("speed"); logger.debug(`🚀 Performance recorded for ${this.providerName}:`, { responseTime: `${responseTime}ms`, tokens: result.usage?.totalTokens || 0, estimatedCost: `$${actualCost.toFixed(6)}`, recommendedSpeedProvider: optimizedProvider?.provider || "none", }); } catch (perfError) { logger.warn("⚠️ Performance recording failed:", perfError); } // Extract tool names from tool calls for tracking // AI SDK puts tool calls in steps array for multi-step generation const toolsUsed = []; // First check direct tool calls (fallback) if (result.toolCalls && result.toolCalls.length > 0) { toolsUsed.push(...result.toolCalls.map((tc) => { return (tc.toolName || tc.name || "unknown"); })); } // Then check steps for tool calls (primary source for multi-step) if (result.steps && Array.isArray(result.steps)) { for (const step of result.steps || []) { if (step?.toolCalls && Array.isArray(step.toolCalls)) { toolsUsed.push(...step.toolCalls.map((tc) => { return tc.toolName || tc.name || "unknown"; })); } } } // Remove duplicates const uniqueToolsUsed = [...new Set(toolsUsed)]; // ✅ Extract tool executions from AI SDK result const toolExecutions = []; // Create a map of tool calls to their arguments for matching with results const toolCallArgsMap = new Map(); // Extract tool executions from AI SDK result steps if (result.steps && Array.isArray(result.steps)) { for (const step of result.steps || []) { // First, collect tool calls and their arguments if (step?.toolCalls && Array.isArray(step.toolCalls)) { for (const toolCall of step.toolCalls) { const tcRecord = toolCall; const toolName = tcRecord.toolName || tcRecord.name || "unknown"; const toolId = tcRecord.toolCallId || tcRecord.id || toolName; // Extract arguments from tool call let callArgs = {}; if (tcRecord.args) { callArgs = tcRecord.args; } else if (tcRecord.arguments) { callArgs = tcRecord.arguments; } else if (tcRecord.parameters) { callArgs = tcRecord.parameters; } toolCallArgsMap.set(toolId, callArgs); toolCallArgsMap.set(toolName, callArgs); // Also map by name as fallback } } // Then, process tool results and match with call arguments if (step?.toolResults && Array.isArray(step.toolResults)) { for (const toolResult of step.toolResults) { const trRecord = toolResult; const toolName = trRecord.toolName || "unknown"; const toolId = trRecord.toolCallId || trRecord.id; // Try to get arguments from the tool result first let toolArgs = {}; if (trRecord.args) { toolArgs = trRecord.args; } else if (trRecord.arguments) { toolArgs = trRecord.arguments; } else if (trRecord.parameters) { toolArgs = trRecord.parameters; } else if (trRecord.input) { toolArgs = trRecord.input; } else { // Fallback: get arguments from the corresponding tool call toolArgs = toolCallArgsMap.get(toolId || toolName) || {}; } toolExecutions.push({ name: toolName, input: toolArgs, output: trRecord.result || "success", }); } } } } // Format the result with tool executions included const enhancedResult = { content: result.text, usage: { input: result.usage?.promptTokens || 0, output: result.usage?.completionTokens || 0, total: result.usage?.totalTokens || 0, }, provider: this.providerName, model: this.modelName, toolCalls: result.toolCalls ? result.toolCalls.map((tc) => ({ toolCallId: tc.toolCallId || tc.id || "unknown", toolName: tc.toolName || tc.name || "unknown", args: tc.args || tc.parameters || {}, })) : [], toolResults: result.toolResults, toolsUsed: uniqueToolsUsed, toolExecutions, // ✅ Add extracted tool executions availableTools: Object.keys(tools).map((name) => { const tool = tools[name]; return { name, description: tool.description || "No description available", parameters: tool.parameters || {}, server: tool.serverId || "direct", }; }), }; // Enhanced result with analytics and evaluation return await this.enhanceResult(enhancedResult, options, startTime); } catch (error) { logger.error(`Generate failed for ${this.providerName}:`, error); throw this.handleProviderError(error); } } /** * Alias for generate method - implements AIProvider interface */ async gen(optionsOrPrompt, analysisSchema) { return this.generate(optionsOrPrompt, analysisSchema); } /** * BACKWARD COMPATIBILITY: Legacy generateText method * Converts EnhancedGenerateResult to TextGenerationResult format * Ensures existing scripts using createAIProvider().generateText() continue to work */ async generateText(options) { // Validate required parameters for backward compatibility if (!options.prompt || typeof options.prompt !== "string" || options.prompt.trim() === "") { throw new Error("GenerateText options must include prompt as a non-empty string"); } // Call the main generate method const result = await this.generate(options); if (!result) { throw new Error("Generation failed: No result returned"); } // Convert EnhancedGenerateResult to TextGenerationResult format return { content: result.content || "", provider: result.provider || this.providerName, model: result.model || this.modelName, usage: result.usage || { input: 0, output: 0, total: 0, }, responseTime: 0, // BaseProvider doesn't track response time directly toolsUsed: result.toolsUsed || [], enhancedWithTools: !!(result.toolsUsed && result.toolsUsed.length > 0), analytics: result.analytics, evaluation: result.evaluation, }; } /** * Get AI SDK model with middleware applied * This method wraps the base model with any configured middleware * TODO: Implement global level middlewares that can be used */ async getAISDKModelWithMiddleware(options = {}) { // Get the base model const baseModel = await this.getAISDKModel(); // Check if middleware should be applied const middlewareOptions = this.extractMiddlewareOptions(options); if (!middlewareOptions) { return baseModel; } try { // Create a new factory instance with the specified options const factory = new MiddlewareFactory(middlewareOptions); // Create middleware context const context = factory.createContext(this.providerName, this.modelName, options, { sessionId: this.sessionId, userId: this.userId, }); // Apply middleware to the model const wrappedModel = factory.applyMiddleware(baseModel, context, middlewareOptions); logger.debug(`Applied middleware to ${this.providerName} model`, { provider: this.providerName, model: this.modelName, hasMiddleware: true, }); return wrappedModel; } catch (error) { logger.warn(`Failed to apply middleware to ${this.providerName}, using base model`, { error: error instanceof Error ? error.message : String(error), }); // Return base model on middleware failure to maintain functionality return baseModel; } } /** * Extract middleware options from generation options. This is the single * source of truth for deciding if middleware should be applied. */ extractMiddlewareOptions(options) { // 1. Determine effective middleware config: per-request overrides global. const middlewareOpts = options.middleware ?? this.middlewareOptions; if (!middlewareOpts) { return null; } // 2. The middleware property must be an object with configuration. if (typeof middlewareOpts !== "object" || middlewareOpts === null) { return null; } // 3. Check if the middleware object has any actual configuration keys. const fullOpts = middlewareOpts; const hasArray = (arr) => Array.isArray(arr) && arr.length > 0; const hasConfig = !!fullOpts.middlewareConfig || hasArray(fullOpts.enabledMiddleware) || hasArray(fullOpts.disabledMiddleware) || !!fullOpts.preset || hasArray(fullOpts.middleware); if (!hasConfig) { return null; } // 4. Return the formatted options if configuration is present. return { ...fullOpts, global: { collectStats: true, continueOnError: true, ...(fullOpts.global || {}), }, }; } // =================== // TOOL MANAGEMENT // =================== /** * Check if a schema is a Zod schema */ isZodSchema(schema) { return (typeof schema === "object" && schema !== null && // Most Zod schemas have an internal _def and a parse method typeof schema.parse === "function"); } /** * Convert tool execution result from MCP format to standard format */ async convertToolResult(result) { // Handle MCP-style results if (result && typeof result === "object" && "success" in result) { const mcpResult = result; if (mcpResult.success) { return mcpResult.data; } else { const errorMsg = typeof mcpResult.error === "string" ? mcpResult.error : "Tool execution failed"; throw new Error(errorMsg); } } return result; } /** * Create a custom tool from tool definition */ async createCustomToolFromDefinition(toolName, toolInfo) { try { logger.debug(`[BaseProvider] Converting custom tool: ${toolName}`); // Convert to AI SDK tool format const { tool: createAISDKTool } = await import("ai"); const { z } = await import("zod"); return createAISDKTool({ description: toolInfo.description || `Tool ${toolName}`, parameters: this.isZodSchema(toolInfo.parameters) ? toolInfo.parameters : z.object({}), execute: async (params) => { const result = await toolInfo.execute(params); return await this.convertToolResult(result); }, }); } catch (toolCreationError) { logger.error(`Failed to create tool: ${toolName}`, toolCreationError); return null; } } /** * Process custom tools from setupToolExecutor */ async processCustomTools(tools) { if (!this.customTools || this.customTools.size === 0) { return; } logger.debug(`[BaseProvider] Loading ${this.customTools.size} custom tools from setupToolExecutor`); for (const [toolName, toolDef] of this.customTools.entries()) { logger.debug(`[BaseProvider] Processing custom tool: ${toolName}`, { toolDef: typeof toolDef, hasExecute: toolDef && typeof toolDef === "object" && "execute" in toolDef, hasName: toolDef && typeof toolDef === "object" && "name" in toolDef, }); // Validate tool definition has required execute function const toolInfo = toolDef || {}; if (toolInfo && typeof toolInfo.execute === "function") { const tool = await this.createCustomToolFromDefinition(toolName, toolInfo); if (tool) { tools[toolName] = tool; } } } logger.debug(`[BaseProvider] Custom tools processing complete`, { customToolsProcessed: this.customTools.size, }); } /** * Create an external MCP tool */ async createExternalMCPTool(tool) { try { logger.debug(`[BaseProvider] Converting external MCP tool: ${tool.name}`); // Convert to AI SDK tool format const { tool: createAISDKTool } = await import("ai"); return createAISDKTool({ description: tool.description || `External MCP tool ${tool.name}`, parameters: await this.convertMCPSchemaToZod(tool.inputSchema), execute: async (params) => { logger.debug(`[BaseProvider] Executing external MCP tool: ${tool.name}`, { params }); // Execute via NeuroLink's direct tool execution if (this.neurolink && typeof this.neurolink.executeExternalMCPTool === "function") { return await this.neurolink.executeExternalMCPTool(tool.serverId || "unknown", tool.name, params); } else { throw new Error(`Cannot execute external MCP tool: NeuroLink executeExternalMCPTool not available`); } }, }); } catch (toolCreationError) { logger.error(`Failed to create external MCP tool: ${tool.name}`, toolCreationError); return null; } } /** * Process external MCP tools */ async processExternalMCPTools(tools) { if (!this.neurolink || typeof this.neurolink.getExternalMCPTools !== "function") { logger.debug(`[BaseProvider] No external MCP tool interface available`, { hasNeuroLink: !!this.neurolink, hasGetExternalMCPTools: this.neurolink && typeof this.neurolink.getExternalMCPTools === "function", }); return; } try { logger.debug(`[BaseProvider] Loading external MCP tools for ${this.providerName}`); const externalTools = await this.neurolink.getExternalMCPTools(); logger.debug(`[BaseProvider] Found ${externalTools.length} external MCP tools`); for (const tool of externalTools) { const mcpTool = await this.createExternalMCPTool(tool); if (mcpTool) { tools[tool.name] = mcpTool; logger.debug(`[BaseProvider] Successfully added external MCP tool: ${tool.name}`); } } logger.debug(`[BaseProvider] External MCP tools loading complete`, { totalToolsAdded: externalTools.length, }); } catch (error) { logger.error(`[BaseProvider] Failed to load external MCP tools for ${this.providerName}:`, error); // Not an error - external tools are optional } } /** * Process MCP tools integration */ async processMCPTools(tools) { // MCP tools loading simplified - removed functionCalling dependency if (!this.mcpTools) { // Set empty tools object - MCP tools are handled at a higher level this.mcpTools = {}; } // Add MCP tools if available if (this.mcpTools) { Object.assign(tools, this.mcpTools); } } /** * Get all available tools - direct tools are ALWAYS available * MCP tools are added when available (without blocking) */ async getAllTools() { const tools = { ...this.directTools, // Always include direct tools }; logger.debug(`[BaseProvider] getAllTools called for ${this.providerName}`, { neurolinkAvailable: !!this.neurolink, neurolinkType: typeof this.neurolink, directToolsCount: getKeyCount(this.directTools), }); logger.debug(`[BaseProvider] Direct tools: ${getKeysAsString(this.directTools)}`); // Process all tool types using dedicated helper methods await this.processCustomTools(tools); await this.processExternalMCPTools(tools); await this.processMCPTools(tools); logger.debug(`[BaseProvider] getAllTools returning tools: ${getKeysAsString(tools)}`); return tools; } /** * Convert MCP JSON Schema to Zod schema for AI SDK tools * Handles common MCP schema patterns safely */ async convertMCPSchemaToZod(inputSchema) { const { z } = await import("zod"); if (!inputSchema || typeof inputSchema !== "object") { return z.object({}); } try { const schema = inputSchema; const zodFields = {}; // Handle JSON Schema properties if (schema.properties && typeof schema.properties === "object") { const required = new Set(Array.isArray(schema.required) ? schema.required : []); for (const [propName, propDef] of Object.entries(schema.properties)) { const prop = propDef; let zodType; // Convert based on JSON Schema type switch (prop.type) { case "string": zodType = z.string(); if (prop.description && typeof prop.description === "string") { zodType = zodType.describe(prop.description); } break; case "number": case "integer": zodType = z.number(); if (prop.description && typeof prop.description === "string") { zodType = zodType.describe(prop.description); } break; case "boolean": zodType = z.boolean(); if (prop.description && typeof prop.description === "string") { zodType = zodType.describe(prop.description); } break; case "array": zodType = z.array(z.unknown()); if (prop.description && typeof prop.description === "string") { zodType = zodType.describe(prop.description); } break; case "object": zodType = z.object({}); if (prop.description && typeof prop.description === "string") { zodType = zodType.describe(prop.description); } break; default: // Unknown type, use string as fallback zodType = z.string(); if (prop.description && typeof prop.description === "string") { zodType = zodType.describe(prop.description); } } // Make optional if not required if (!required.has(propName)) { zodType = zodType.optional(); } zodFields[propName] = zodType; } } return getKeyCount(zodFields) > 0 ? z.object(zodFields) : z.object({}); } catch (error) { logger.warn(`Failed to convert MCP schema to Zod, using empty schema:`, error); return z.object({}); } } /** * Set session context for MCP tools */ setSessionContext(sessionId, userId) { this.sessionId = sessionId; this.userId = userId; } // =================== // CONSOLIDATED PROVIDER METHODS - MOVED FROM INDIVIDUAL PROVIDERS // =================== /** * Execute operation with timeout and proper cleanup * Consolidates identical timeout handling from 8/10 providers */ async executeWithTimeout(operation, options) { const timeout = this.getTimeout(options); const timeoutController = createTimeoutController(timeout, this.providerName, options.operationType || "generate"); try { if (timeoutController) { return await Promise.race([ operation(), new Promise((_, reject) => { timeoutController.controller.signal.addEventListener("abort", () => { reject(new TimeoutError(`${this.providerName} operation timed out`, timeoutController.timeoutMs, this.providerName, options.operationType || "generate")); }); }), ]); } else { return await operation(); } } finally { timeoutController?.cleanup(); } } /** * Validate stream options - consolidates validation from 7/10 providers */ validateStreamOptions(options) { const validation = validateStreamOpts(options); if (!validation.isValid) { const summary = createValidationSummary(validation); throw new ValidationError(`Stream options validation failed: ${summary}`, "options", "VALIDATION_FAILED", validation.suggestions); } // Log warnings if any if (validation.warnings.length > 0) { logger.warn("Stream options validation warnings:", validation.warnings); } // Additional BaseProvider-specific validation if (options.maxSteps !== undefined) { if (options.maxSteps < STEP_LIMITS.min || options.maxSteps > STEP_LIMITS.max) { throw new ValidationError(`maxSteps must be between ${STEP_LIMITS.min} and ${STEP_LIMITS.max}`, "maxSteps", "OUT_OF_RANGE", [ `Use a value between ${STEP_LIMITS.min} and ${STEP_LIMITS.max} for optimal performance`, ]); } } } /** * Create text stream transformation - consolidates identical logic from 7/10 providers */ createTextStream(result) { return (async function* () { for await (const chunk of result.textStream) { yield { content: chunk }; } })(); } /** * Create standardized stream result - consolidates result structure */ createStreamResult(stream, additionalProps = {}) { return { stream, provider: this.providerName, model: this.modelName, ...additionalProps, }; } /** * Create stream analytics - consolidates analytics from 4/10 providers */ async createStreamAnalytics(result, startTime, options) { try { const { createAnalytics } = await import("./analytics.js"); const analytics = createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, { requestId: `${this.providerName}-stream-${Date.now()}`, streamingMode: true, ...options.context, }); return analytics; } catch (error) { logger.warn(`Analytics creation failed for ${this.providerName}:`, error); return undefined; } } /** * Handle common error patterns - consolidates error handling from multiple providers */ handleCommonErrors(error) { if (error instanceof TimeoutError) { return new Error(`${this.providerName} request timed out after ${error.timeout}ms. Consider increasing timeout or using a lighter model.`); } const message = error instanceof Error ? error.message : String(error); // Common API key errors if (message.includes("API_KEY_INVALID") || message.includes("Invalid API key") || message.includes("authentication") || message.includes("unauthorized")) { return new Error(`Invalid API key for ${this.providerName}. Please check your API key environment variable.`); } // Common rate limit errors if (message.includes("rate limit") || message.includes("quota") || message.includes("429")) { return new Error(`Rate limit exceeded for ${this.providerName}. Please wait before making more requests.`); } return null; // Not a common error, let provider handle it } /** * Set up tool executor for a provider to enable actual tool execution * Consolidates identical setupToolExecutor logic from neurolink.ts (used in 4 places) * @param sdk - The NeuroLinkSDK instance for tool execution * @param functionTag - Function name for logging */ setupToolExecutor(sdk, functionTag) { // Store custom tools for use in getAllTools() this.customTools = sdk.customTools; this.toolExecutor = sdk.executeTool; logger.debug(`[${functionTag}] Setting up tool executor for provider`, { providerType: this.constructor.name, availableCustomTools: sdk.customTools.size, customToolsStored: !!this.customTools, toolExecutorStored: !!this.toolExecutor, }); // Note: Tool execution will be handled through getAllTools() -> AI SDK tools // The custom tools are converted to AI SDK format in getAllTools() method } // =================== // TEMPLATE METHODS - COMMON FUNCTIONALITY // =================== normalizeTextOptions(optionsOrPrompt) { if (typeof optionsOrPrompt === "string") { const safeMaxTokens = getSafeMaxTokens(this.providerName, this.modelName); return { prompt: optionsOrPrompt, provider: this.providerName, model: this.modelName, maxTokens: safeMaxTokens, }; } // Handle both prompt and input.text formats const prompt = optionsOrPrompt.prompt || optionsOrPrompt.input?.text || ""; const modelName = optionsOrPrompt.model || this.modelName; const providerName = optionsOrPrompt.provider || this.providerName; // Apply safe maxTokens based on provider and model const safeMaxTokens = getSafeMaxTokens(providerName, modelName, optionsOrPrompt.maxTokens); return { ...optionsOrPrompt, prompt, provider: providerName, model: modelName, maxTokens: safeMaxTokens, }; } normalizeStreamOptions(optionsOrPrompt) { if (typeof optionsOrPrompt === "string") { const safeMaxTokens = getSafeMaxTokens(this.providerName, this.modelName); return { input: { text: optionsOrPrompt }, provider: this.providerName, model: this.modelName, maxTokens: safeMaxTokens, }; } const modelName = optionsOrPrompt.model || this.modelName; const providerName = optionsOrPrompt.provider || this.providerName; // Apply safe maxTokens based on provider and model const safeMaxTokens = getSafeMaxTokens(providerName, modelName, optionsOrPrompt.maxTokens); return { ...optionsOrPrompt, provider: providerName, model: modelName, maxTokens: safeMaxTokens, }; } async enhanceResult(result, options, startTime) { const responseTime = Date.now() - startTime; let enhancedResult = { ...result }; if (options.enableAnalytics) { try { logger.debug(`Creating analytics for ${this.providerName}...`); const analytics = await this.createAnalytics(result, responseTime, options); logger.debug(`Analytics created:`, analytics); enhancedResult = { ...enhancedResult, analytics }; } catch (error) { logger.warn(`Analytics creation failed for ${this.providerName}:`, error); } } if (options.enableEvaluation) { try { const evaluation = await this.createEvaluation(result, options); enhancedResult = { ...enhancedResult, evaluation }; } catch (error) { logger.warn(`Evaluation creation failed for ${this.providerName}:`, error); } } return enhancedResult; } async createAnalytics(result, responseTime, options) { const { createAnalytics } = await import("./analytics.js"); return createAnalytics(this.providerName, this.modelName, result, responseTime, options.context); } async createEvaluation(result, options) { const { evaluateResponse } = await import("../core/evaluation.js"); const context = { userQuery: options.prompt || options.input?.text || "Generated response", aiResponse: result.content, context: options.context, primaryDomain: options.evaluationDomain, assistantRole: "AI assistant", conversationHistory: options.conversationHistory?.map((msg) => ({ role: msg.role, content: msg.content, })), toolUsage: options.toolUsageContext ? [ { toolName: options.toolUsageContext, input: {}, output: {}, executionTime: 0, }, ] : undefined, expectedOutcome: options.expectedOutcome, evaluationCriteria: options.evaluationCriteria, }; const evaluation = await evaluateResponse(context); return evaluation; } validateOptions(options) { const validation = validateTextGenerationOptions(options); if (!validation.isValid) { const summary = createValidationSummary(validation); throw new ValidationError(`Text generation options validation failed: ${summary}`, "options", "VALIDATION_FAILED", validation.suggestions); } // Log warnings if any if (validation.warnings.length > 0) { logger.warn("Text generation options validation warnings:", validation.warnings); } // Additional BaseProvider-specific validation if (options.maxSteps !== undefined) { if (options.maxSteps < STEP_LIMITS.min || options.maxSteps > STEP_LIMITS.max) { throw new ValidationError(`maxSteps must be between ${STEP_LIMITS.min} and ${STEP_LIMITS.max}`, "maxSteps", "OUT_OF_RANGE", [ `Use a value between ${STEP_LIMITS.min} and ${STEP_LIMITS.max} for optimal performance`, ]); } } } getProviderInfo() { return { provider: this.providerName, model: this.modelName, }; } /** * Get timeout value in milliseconds */ getTimeout(options) { if (!options.timeout) { return this.defaultTimeout; } if (typeof options.timeout === "number") { return options.timeout; } // Parse string timeout (e.g., '30s', '2m', '1h') const timeoutStr = options.timeout.toLowerCase(); const value = parseInt(timeoutStr); if (timeoutStr.includes("h")) { return value * 60 * 60 * 1000; } else if (timeoutStr.includes("m")) { return value * 60 * 1000; } else if (timeoutStr.includes("s")) { return value * 1000; } return this.defaultTimeout; } /** * Utility method to chunk large prompts into smaller pieces * @param prompt The prompt to chunk * @param maxChunkSize Maximum size per chunk (default: 900,000 characters) * @param overlap Overlap between chunks to maintain context (default: 100 characters) * @returns Array of prompt chunks */ static chunkPrompt(prompt, maxChunkSize = 900000, overlap = 100) { if (prompt.length <= maxChunkSize) { return [prompt]; } const chunks = []; let start = 0; while (start < prompt.length) { const end = Math.min(start + maxChunkSize, prompt.length); chunks.push(prompt.slice(start, end)); // Break if we've reached the end if (end >= prompt.length) { break; } // Move start forward, accounting for overlap const nextStart = end - overlap; // Ensure we make progress (avoid infinite loops) if (nextStart <= start) { start = end; } else { start = Math.max(nextStart, 0); } } return chunks; } }