@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
JavaScript
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