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,007 lines 52.5 kB
import { z } from "zod"; 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 // 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) { this.modelName = modelName || this.getDefaultModel(); this.providerName = providerName || this.getProviderName(); this.neurolink = neurolink; } /** * 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), }); // EVERY provider uses Vercel AI SDK - no exceptions const model = await this.getAISDKModel(); // This method is now REQUIRED // 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: { inputTokens: result.usage?.promptTokens || 0, outputTokens: result.usage?.completionTokens || 0, totalTokens: 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 || { promptTokens: 0, completionTokens: 0, totalTokens: 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 */ 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 || this.shouldSkipMiddleware(options)) { return baseModel; } try { // Create middleware context const context = MiddlewareFactory.createContext(this.providerName, this.modelName, options, { sessionId: this.sessionId, userId: this.userId, }); // Apply middleware to the model const wrappedModel = MiddlewareFactory.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 */ extractMiddlewareOptions(options) { // Check for middleware configuration in options const optionsRecord = options; const middlewareConfig = optionsRecord.middlewareConfig; const enabledMiddleware = optionsRecord.enabledMiddleware; const disabledMiddleware = optionsRecord.disabledMiddleware; const preset = optionsRecord.middlewarePreset; // If no middleware configuration is present, return null if (!middlewareConfig && !enabledMiddleware && !disabledMiddleware && !preset) { return null; } return { middlewareConfig, enabledMiddleware, disabledMiddleware, preset, global: { collectStats: true, continueOnError: true, }, }; } /** * Determine if middleware should be skipped for this request */ shouldSkipMiddleware(options) { // Skip middleware if explicitly disabled if (options.disableMiddleware === true) { return true; } // Skip middleware for tool-disabled requests to avoid conflicts if (options.disableTools === true) { return true; } return false; } // =================== // TOOL MANAGEMENT // =================== /** * 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)}`); // Add custom tools from setupToolExecutor if available if (this.customTools && this.customTools.size > 0) { 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, }); if (toolDef && typeof toolDef === "object" && "execute" in toolDef && typeof toolDef.execute === "function") { try { const { tool: createAISDKTool } = await import("ai"); const typedToolDef = toolDef; tools[toolName] = createAISDKTool({ description: typedToolDef.description || `Custom tool ${toolName}`, parameters: z.object({}), // Use empty schema for custom tools execute: async (params) => { logger.debug(`[BaseProvider] Executing custom tool: ${toolName}`, { params }); try { // Use the tool executor if available (from setupToolExecutor) let result; if (this.toolExecutor) { result = await this.toolExecutor(toolName, params); } else { result = await typedToolDef.execute(params); } // Log successful execution logger.debug(`[BaseProvider] Tool execution successful: ${toolName}`, { resultType: typeof result, hasResult: result !== null && result !== undefined, toolName, }); return result; } catch (error) { logger.warn(`[BaseProvider] Tool execution failed: ${toolName}`, { error: error instanceof Error ? error.message : String(error), params, toolName, }); // GENERIC ERROR HANDLING FOR ALL MCP TOOLS: // Return a generic error object that works with any MCP server // The AI can interpret this and try different approaches return { _neurolinkToolError: true, toolName: toolName, error: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString(), params: params, // Keep it simple - just indicate an error occurred message: `Error calling ${toolName}: ${error instanceof Error ? error.message : String(error)}`, }; } }, }); logger.debug(`[BaseProvider] Successfully added custom tool: ${toolName}`); } catch (error) { logger.error(`[BaseProvider] Failed to add custom tool: ${toolName}`, error); } } else { logger.warn(`[BaseProvider] Invalid custom tool format: ${toolName}`, { toolDef: typeof toolDef, hasExecute: toolDef && typeof toolDef === "object" && "execute" in toolDef, executeType: toolDef && typeof toolDef === "object" && "execute" in toolDef ? typeof toolDef.execute : "N/A", }); } } } // Add custom tools from NeuroLink if available logger.debug(`[BaseProvider] Checking NeuroLink: ${!!this.neurolink}, has getInMemoryServers: ${this.neurolink && typeof this.neurolink.getInMemoryServers}`); if (this.neurolink && typeof this.neurolink.getInMemoryServers === "function") { logger.debug(`[BaseProvider] NeuroLink check passed, loading custom tools`); try { const inMemoryServers = this.neurolink.getInMemoryServers(); logger.debug(`[BaseProvider] Got servers:`, inMemoryServers.size); logger.debug(`[BaseProvider] Loading custom tools from SDK, found ${inMemoryServers.size} servers`); if (inMemoryServers && inMemoryServers.size > 0) { // Convert in-memory server tools to AI SDK format for (const [_serverId, serverConfig] of inMemoryServers) { if (serverConfig && serverConfig.tools) { // Handle tools array from MCPServerInfo const toolEntries = serverConfig.tools.map((tool) => [ tool.name, tool, ]); for (const [toolName, toolInfo] of toolEntries) { if (toolInfo && typeof toolInfo.execute === "function") { logger.debug(`[BaseProvider] Converting custom tool: ${toolName}`); try { // Convert to AI SDK tool format const { tool: createAISDKTool } = await import("ai"); // Validate optional schemas if present (accept Zod or plain JSON schema objects) const isZodSchema = (s) => typeof s === "object" && s !== null && // Most Zod schemas have an internal _def and a parse method typeof s.parse === "function"; tools[toolName] = createAISDKTool({ description: toolInfo.description || `Tool ${toolName}`, parameters: isZodSchema(toolInfo.parameters) ? toolInfo.parameters : z.object({}), execute: async (params) => { const result = await toolInfo.execute(params); // Handle MCP-style results if (result && typeof result === "object" && "success" in result) { if (result.success) { return result.data; } else { const errorMsg = typeof result.error === "string" ? result.error : "Tool execution failed"; throw new Error(errorMsg); } } return result; }, }); } catch (toolCreationError) { logger.error(`Failed to create tool: ${toolName}`, toolCreationError); } } } } } } } catch (error) { logger.debug(`Failed to load custom tools for ${this.providerName}:`, error); // Not an error - custom tools are optional } } if (this.neurolink && typeof this.neurolink.getExternalMCPTools === "function") { try { logger.debug(`[BaseProvider] Loading external MCP tools from NeuroLink via direct tool access`); const externalTools = this.neurolink.getExternalMCPTools() || []; logger.debug(`[BaseProvider] Found ${externalTools.length} external MCP tools`); for (const tool of externalTools) { logger.debug(`[BaseProvider] Converting external MCP tool: ${tool.name}`); try { // Convert to AI SDK tool format const { tool: createAISDKTool } = await import("ai"); tools[tool.name] = 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`); } }, }); logger.debug(`[BaseProvider] Successfully added external MCP tool: ${tool.name}`); } catch (toolCreationError) { logger.error(`Failed to create external MCP tool: ${tool.name}`, toolCreationError); } } 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 } } else { logger.debug(`[BaseProvider] No external MCP tool interface available`, { hasNeuroLink: !!this.neurolink, hasGetExternalMCPTools: this.neurolink && typeof this.neurolink.getExternalMCPTools === "function", }); } // 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); } 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 evaluation = await evaluateResponse(result.content, options.prompt); 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.maxSte