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