UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

347 lines 15 kB
/** * Utilities Module * * Handles validation, normalization, schema conversion, and error handling utilities. * Extracted from BaseProvider to follow Single Responsibility Principle. * * Responsibilities: * - Options validation (text generation and stream options) * - Options normalization (string to object conversion) * - Provider information formatting * - Timeout parsing and calculation * - Schema utilities (Zod detection, permissive schema creation, OpenAI strict mode fixes) * - Tool result conversion * - Middleware options extraction * - Common error pattern handling * * @module core/modules/Utilities */ import { z } from "zod"; import { logger } from "../../utils/logger.js"; import { getSafeMaxTokens } from "../../utils/tokenLimits.js"; import { TimeoutError, getDefaultTimeout, parseTimeout, } from "../../utils/timeout.js"; import { validateStreamOptions as validateStreamOpts, validateTextGenerationOptions, ValidationError, createValidationSummary, } from "../../utils/parameterValidation.js"; import { STEP_LIMITS } from "../constants.js"; /** * Utilities class - Provides validation, normalization, and utility methods */ export class Utilities { providerName; modelName; defaultTimeout; middlewareOptions; constructor(providerName, modelName, defaultTimeout = 30000, middlewareOptions) { this.providerName = providerName; this.modelName = modelName; this.defaultTimeout = defaultTimeout; this.middlewareOptions = middlewareOptions; } /** * Validate text generation options */ 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`, ]); } } } /** * Validate stream options */ 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`, ]); } } } /** * Normalize text generation options from string or object */ 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); // CRITICAL FIX: Preserve the entire input object for multimodal support // This ensures images and content arrays are not lost during normalization const normalizedOptions = { ...optionsOrPrompt, prompt, provider: providerName, model: modelName, maxTokens: safeMaxTokens, }; // Ensure input object is preserved if it exists (for multimodal support) if (optionsOrPrompt.input) { normalizedOptions.input = { ...optionsOrPrompt.input, text: prompt, // Ensure text is consistent }; } return normalizedOptions; } /** * Normalize stream options from string or object */ 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, }; } /** * Get provider information */ getProviderInfo() { return { provider: this.providerName, model: this.modelName, }; } /** * Get timeout value in milliseconds from options * Supports number or string formats (e.g., '30s', '2m', '1h') */ getTimeout(options) { // If caller specified a timeout, use it (supports number ms and string formats) if (options.timeout !== undefined && options.timeout !== null) { const parsed = parseTimeout(options.timeout); if (parsed !== undefined) { return parsed; } } // Use per-provider default (e.g., vertex=60s, ollama=5m) instead of global 30s. // Always use "generate" operation here — streaming operations have their own // longer timeout (DEFAULT_TIMEOUTS.streaming = 2m) applied by the streaming // infrastructure in BaseProvider.stream(). Both TextGenerationOptions and // StreamOptions share the same `input` property, so there is no reliable // discriminator to detect streaming at this level. const providerDefault = parseTimeout(getDefaultTimeout(this.providerName, "generate")); return providerDefault ?? this.defaultTimeout; } /** * Get timeout scaled by estimated input token count. * For large contexts (>100K tokens), increase timeout proportionally. */ getContextAwareTimeout(options, estimatedTokens) { const baseTimeout = this.getTimeout(options); if (!estimatedTokens || estimatedTokens <= 100_000) { return baseTimeout; } // Scale: >100K → 1.5x, >200K → 2x, >300K → 2.5x (capped at 4x) // Use (estimatedTokens - 1) so exact multiples stay in the lower tier // (e.g., 100_000 → 1x, 100_001 → 1.5x) const scale = 1 + Math.floor((estimatedTokens - 1) / 100_000) * 0.5; return Math.round(baseTimeout * Math.min(scale, 4)); } /** * 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 * Handles tool failures gracefully to prevent stream termination */ async convertToolResult(result) { // Handle MCP-style results if (result && typeof result === "object" && "success" in result) { const mcpResult = result; if (mcpResult.success) { // If `data` field exists, return it (standard MCP format). // Otherwise fall back to the full result object so the LLM // receives the actual payload instead of `undefined`, which // would cause it to re-call the tool in a loop. return mcpResult.data !== undefined ? mcpResult.data : result; } else { // Instead of throwing, return a structured error result // This prevents tool failures from terminating streams const errorMsg = typeof mcpResult.error === "string" ? mcpResult.error : "Tool execution failed"; // Log the error for debugging but don't throw logger.warn(`Tool execution failed: ${errorMsg}`); // Return error as structured data that can be processed by the AI return { isError: true, error: errorMsg, content: [ { type: "text", text: `Tool execution failed: ${errorMsg}`, }, ], }; } } return result; } /** * Create a permissive Zod schema that accepts all parameters as-is */ createPermissiveZodSchema() { // Create a permissive record that accepts any object structure // This allows all parameters to pass through without validation issues return z .record(z.string(), z.unknown()) .transform((data) => { // Return the data as-is to preserve all parameter information return data; }); } /** * Recursively fix JSON Schema for OpenAI strict mode compatibility * OpenAI requires additionalProperties: false at ALL levels and preserves required array */ fixSchemaForOpenAIStrictMode(schema) { const fixedSchema = JSON.parse(JSON.stringify(schema)); if (fixedSchema.type === "object" && fixedSchema.properties && typeof fixedSchema.properties === "object") { const allPropertyNames = Object.keys(fixedSchema.properties); if (!fixedSchema.required || !Array.isArray(fixedSchema.required)) { fixedSchema.required = []; } fixedSchema.additionalProperties = false; for (const propName of allPropertyNames) { const propValue = fixedSchema.properties[propName]; if (propValue && typeof propValue === "object") { if (propValue.type === "object") { fixedSchema.properties[propName] = this.fixSchemaForOpenAIStrictMode(propValue); } else if (propValue.type === "array" && propValue.items && typeof propValue.items === "object") { fixedSchema.properties[propName].items = this.fixSchemaForOpenAIStrictMode(propValue.items); } } } } return fixedSchema; } /** * Extract middleware options from generation/stream 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 || middlewareOpts instanceof Date || middlewareOpts instanceof RegExp) { 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 || {}), }, }; } /** * Handle common error patterns across providers * Returns transformed error or null if not a common pattern */ 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 } } //# sourceMappingURL=Utilities.js.map