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