UNPKG

ai-sdk-guardrails

Version:

Input and output guardrails middleware for Vercel AI SDK.

407 lines (404 loc) 11.5 kB
// src/errors.ts import { AISDKError } from "@ai-sdk/provider"; var marker = "ai-sdk-guardrails.error"; var symbol = Symbol.for(marker); var GuardrailsError = class extends AISDKError { [symbol] = true; timestamp; metadata; constructor(name, message, metadata = {}, cause) { super({ name, message, cause }); this.timestamp = /* @__PURE__ */ new Date(); this.metadata = metadata; } /** * Convert the error to a serializable object for logging/reporting */ toJSON() { return { name: this.name, code: this.code, message: this.message, timestamp: this.timestamp.toISOString(), metadata: this.metadata, stack: this.stack }; } /** * Check if this error is of a specific guardrails error subclass. */ is(errorClass) { return this instanceof errorClass; } /** * Checks whether the given value is a guardrails error, across package * versions (marker-based, like `AISDKError.isInstance`). */ static isInstance(error) { return super.hasMarker(error, marker); } }; var GuardrailValidationError = class extends GuardrailsError { code = "GUARDRAIL_VALIDATION_FAILED"; guardrailName; validationErrors; constructor(guardrailName, validationErrors, metadata = {}) { const message = `Guardrail "${guardrailName}" validation failed: ${validationErrors.map((e) => e.message).join(", ")}`; super("GuardrailValidationError", message, { ...metadata, guardrailName, validationErrors }); this.guardrailName = guardrailName; this.validationErrors = validationErrors; } }; var GuardrailExecutionError = class extends GuardrailsError { code = "GUARDRAIL_EXECUTION_FAILED"; guardrailName; originalError; constructor(guardrailName, originalError, metadata = {}) { const message = originalError ? `Guardrail "${guardrailName}" execution failed: ${originalError.message}` : `Guardrail "${guardrailName}" execution failed`; super( "GuardrailExecutionError", message, { ...metadata, guardrailName, originalError: originalError?.message }, originalError ); this.guardrailName = guardrailName; this.originalError = originalError; } }; var GuardrailTimeoutError = class extends GuardrailsError { code = "GUARDRAIL_TIMEOUT"; guardrailName; timeoutMs; constructor(guardrailName, timeoutMs, metadata = {}) { const message = `Guardrail "${guardrailName}" timed out after ${timeoutMs}ms`; super("GuardrailTimeoutError", message, { ...metadata, guardrailName, timeoutMs }); this.guardrailName = guardrailName; this.timeoutMs = timeoutMs; } }; var GuardrailConfigurationError = class extends GuardrailsError { code = "GUARDRAIL_CONFIG_INVALID"; configPath; configErrors; constructor(configErrors, configPath, metadata = {}) { const message = `Guardrail configuration error${configPath ? ` in ${configPath}` : ""}: ${configErrors.join(", ")}`; super("GuardrailConfigurationError", message, { ...metadata, configPath, configErrors }); this.configPath = configPath; this.configErrors = configErrors; } }; var GuardrailsInputError = class extends GuardrailsError { code = "INPUT_BLOCKED"; blockedGuardrails; constructor(blockedGuardrails, metadata = {}) { const guardrailNames = blockedGuardrails.map((g) => g.name).join(", "); const message = `Input blocked by guardrail${blockedGuardrails.length > 1 ? "s" : ""}: ${guardrailNames}`; super("GuardrailsInputError", message, { ...metadata, blockedGuardrails }); this.blockedGuardrails = blockedGuardrails; } }; var GuardrailsOutputError = class extends GuardrailsError { code = "OUTPUT_BLOCKED"; blockedGuardrails; constructor(blockedGuardrails, metadata = {}) { const guardrailNames = blockedGuardrails.map((g) => g.name).join(", "); const message = `Output blocked by guardrail${blockedGuardrails.length > 1 ? "s" : ""}: ${guardrailNames}`; super("GuardrailsOutputError", message, { ...metadata, blockedGuardrails }); this.blockedGuardrails = blockedGuardrails; } }; var MiddlewareError = class extends GuardrailsError { code = "MIDDLEWARE_ERROR"; middlewareType; phase; originalError; constructor(middlewareType, phase, originalError, metadata = {}) { const message = originalError ? `${middlewareType} middleware ${phase} error: ${originalError.message}` : `${middlewareType} middleware ${phase} error`; super( "MiddlewareError", message, { ...metadata, middlewareType, phase, originalError: originalError?.message }, originalError ); this.middlewareType = middlewareType; this.phase = phase; this.originalError = originalError; } }; function isGuardrailsError(error) { return GuardrailsError.isInstance(error); } function extractErrorInfo(error) { if (isGuardrailsError(error)) { return { name: error.name, message: error.message, code: error.code, metadata: error.metadata }; } if (error instanceof Error) { return { name: error.name, message: error.message }; } return { name: "UnknownError", message: String(error) }; } // src/core.ts function createInputGuardrail(name, description, execute) { return { name, description, execute }; } function createOutputGuardrail(name, execute) { return { name, execute }; } function createGenerateWithErrorHandling(generate, signal, onError, retryOnError, maxRetries) { return async (params, attemptNum) => { try { return signal ? await generate(params, signal) : await generate(params); } catch (error) { onError?.(error, attemptNum); if (retryOnError?.(error, attemptNum) && attemptNum <= maxRetries) { throw error; } throw error; } }; } function buildRetrySummary(attemptHistory, validationResult, isUsingEnhancedFeatures, maxRetries) { const blockedResults = attemptHistory.filter((historyAttempt) => historyAttempt.blocked).map(() => ({ message: validationResult.message, metadata: validationResult.metadata })); const summary = { blockedResults }; if (isUsingEnhancedFeatures) { summary.totalAttempts = maxRetries + 1; summary.attempts = [...attemptHistory]; } return summary; } async function performInitialAttempt(params, generateFn, validate, onAttempt, maxRetries, retryOnError) { const attemptHistory = []; onAttempt?.({ attempt: 0, totalAttempts: maxRetries + 1, isRetry: false }); try { const result = await generateFn(params, 0); const validationResult = await Promise.resolve(validate(result)); attemptHistory.push({ attempt: 0, result, blocked: validationResult.blocked }); return { result, validationResult, attemptHistory }; } catch (error) { if (!retryOnError?.(error, 0)) { throw error; } return { result: void 0, validationResult: { blocked: true, message: `Generation error: ${error}` }, attemptHistory }; } } async function performBackoffWait(backoffMs, attempt, signal) { const wait = typeof backoffMs === "function" ? backoffMs(attempt) : backoffMs ?? 0; if (wait && wait > 0) { await new Promise((resolve, reject) => { const timeout = setTimeout(resolve, wait); signal?.addEventListener("abort", () => { clearTimeout(timeout); reject(new Error("Aborted during retry backoff")); }); }); } return; } async function retry(options) { const { generate, params, validate, buildRetryParams, maxRetries = 1, backoffMs, signal, onAttempt, retryOnError, onError, onExhausted = "return-last" } = options; signal?.throwIfAborted(); const generateFn = createGenerateWithErrorHandling( generate, signal, onError, retryOnError, maxRetries ); const { result: initialResult, validationResult, attemptHistory } = await performInitialAttempt( params, generateFn, validate, onAttempt, maxRetries, retryOnError ); let result = initialResult; let v = validationResult; let lastParams = params; let attempt = 0; while (v.blocked && attempt < maxRetries) { attempt++; signal?.throwIfAborted(); const enhancedFeatures = !!(signal || onAttempt || retryOnError || onError || onExhausted !== "return-last"); const summary = buildRetrySummary( attemptHistory, v, enhancedFeatures, maxRetries ); const nextParams = buildRetryParams({ summary, originalParams: params, lastParams, lastResult: result }); const wait = typeof backoffMs === "function" ? backoffMs(attempt) : backoffMs ?? 0; onAttempt?.({ attempt, totalAttempts: maxRetries + 1, lastResult: result, waitMs: wait, isRetry: true }); await performBackoffWait(backoffMs, attempt, signal); lastParams = nextParams; try { result = await generateFn(lastParams, attempt); v = await Promise.resolve(validate(result)); attemptHistory.push({ attempt, result, blocked: v.blocked, waitMs: wait }); } catch (error) { if (!retryOnError?.(error, attempt)) { throw error; } v = { blocked: true, message: `Generation error: ${error}` }; } } if (v.blocked && onExhausted === "throw") { throw new Error( `Retry exhausted after ${maxRetries} attempts: ${v.message}` ); } return result; } var retryHelpers = { /** * Increases max output tokens for retry attempts */ increaseTokens: (increase = 200) => ({ lastParams }) => ({ ...lastParams, maxOutputTokens: Math.max( 400, (lastParams.maxOutputTokens ?? 400) + increase ) }), /** * Adds encouraging prompt for retry attempts */ addEncouragingPrompt: (encouragement = "Please provide a more detailed and comprehensive response.") => ({ lastParams, summary }) => { const basePrompt = Array.isArray(lastParams.prompt) ? lastParams.prompt : [ { role: "user", content: [ { type: "text", text: String(lastParams.prompt || "") } ] } ]; return { ...lastParams, prompt: [ ...basePrompt, { role: "user", content: [ { type: "text", text: `${summary.blockedResults[0]?.message ? `Note: ${summary.blockedResults[0].message}.` : ""} ${encouragement}` } ] } ] }; }, /** * Combines token increase with encouraging prompt */ improveResponse: (tokenIncrease = 200, encouragement) => (args) => { const withTokens = retryHelpers.increaseTokens(tokenIncrease)(args); const withEncouragement = retryHelpers.addEncouragingPrompt( encouragement )({ ...args, lastParams: withTokens }); return { ...withTokens, ...withEncouragement }; }, /** * Simple parameter passthrough (no changes) */ noChange: () => ({ lastParams }) => lastParams }; export { GuardrailsError, GuardrailValidationError, GuardrailExecutionError, GuardrailTimeoutError, GuardrailConfigurationError, GuardrailsInputError, GuardrailsOutputError, MiddlewareError, isGuardrailsError, extractErrorInfo, createInputGuardrail, createOutputGuardrail, retry, retryHelpers };