ai-sdk-guardrails
Version:
Input and output guardrails middleware for Vercel AI SDK.
407 lines (404 loc) • 11.5 kB
JavaScript
// 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
};