ai-sdk-guardrails
Version:
Input and output guardrails middleware for Vercel AI SDK.
1,756 lines • 56 kB
JavaScript
import {
extractContent
} from "./chunk-WC2PXTRI.js";
import {
GuardrailTimeoutError,
GuardrailsInputError,
GuardrailsOutputError
} from "./chunk-F7POYYOU.js";
// src/spec.ts
import { z } from "zod";
var GuardrailSpec = class {
constructor(name, description, mediaType, configSchema, checkFn, ctxRequirements, metadata) {
this.name = name;
this.description = description;
this.mediaType = mediaType;
this.configSchema = configSchema;
this.checkFn = checkFn;
this.ctxRequirements = ctxRequirements;
this.metadata = metadata;
}
name;
description;
mediaType;
configSchema;
checkFn;
ctxRequirements;
metadata;
/**
* Return a JSON schema-like representation for tooling/SDKs.
*/
schema() {
return this.configSchema._def;
}
/**
* Instantiate the guardrail with validated configuration.
*/
instantiate(config) {
const validated = this.configSchema instanceof z.ZodType ? this.configSchema.parse(config ?? {}) : config;
return new ConfiguredGuardrail(this, validated);
}
};
var ConfiguredGuardrail = class {
constructor(spec, config) {
this.spec = spec;
this.config = config;
}
spec;
config;
async ensureAsync(fn, ...args) {
const result = fn(...args);
if (result instanceof Promise) {
return await result;
}
return result;
}
async run(context, input) {
const startedAt = Date.now();
try {
const validatedContext = this.spec.ctxRequirements ? this.spec.ctxRequirements.parse(context) : context;
const result = await this.ensureAsync(
this.spec.checkFn,
validatedContext,
input,
this.config
);
const info = result.info || {};
if (!result.info) {
info.guardrailName = this.spec.name;
info.mediaType = this.spec.mediaType;
}
return {
...result,
info: {
...info,
guardrailName: this.spec.name,
mediaType: this.spec.mediaType
},
context: {
guardrailName: this.spec.name,
guardrailVersion: this.spec.metadata?.version,
executedAt: /* @__PURE__ */ new Date(),
executionTimeMs: Date.now() - startedAt,
...result.context
}
};
} catch (error) {
return {
tripwireTriggered: false,
executionFailed: true,
originalException: error instanceof Error ? error : new Error(String(error)),
info: {
guardrailName: this.spec.name,
mediaType: this.spec.mediaType,
error: error instanceof Error ? error.message : String(error)
},
message: error instanceof Error ? error.message : "Guardrail execution failed",
severity: "high",
context: {
guardrailName: this.spec.name,
executedAt: /* @__PURE__ */ new Date(),
executionTimeMs: Date.now() - startedAt
}
};
}
}
};
// src/registry.ts
import { z as z2 } from "zod";
var NO_CONFIG = z2.object({});
var NO_CONTEXT = z2.object({});
var GuardrailRegistry = class {
specs = /* @__PURE__ */ new Map();
registerSpec(spec) {
this.specs.set(spec.name, spec);
}
register(name, checkFn, description, mediaType = "text/plain", configSchema, ctxRequirements, metadata) {
const spec = new GuardrailSpec(
name,
description,
mediaType,
configSchema || NO_CONFIG,
checkFn,
ctxRequirements || NO_CONTEXT,
metadata
);
this.registerSpec(
spec
);
}
get(name) {
return this.specs.get(name);
}
has(name) {
return this.specs.has(name);
}
remove(name) {
return this.specs.delete(name);
}
size() {
return this.specs.size;
}
all() {
return [...this.specs.values()];
}
list() {
return this.all();
}
metadata() {
return this.all().map((spec) => ({
name: spec.name,
description: spec.description,
mediaType: spec.mediaType,
hasConfig: spec.configSchema !== NO_CONFIG,
hasContext: spec.ctxRequirements !== NO_CONTEXT,
metadata: spec.metadata
}));
}
};
var defaultRegistry = new GuardrailRegistry();
function createRegistry() {
return new GuardrailRegistry();
}
// src/enhanced-runtime.ts
async function runGuardrails(input, bundle, context = {}, options = {}) {
const {
raiseGuardrailErrors = false,
parallelExecution = true,
timeoutMs,
globalTimeoutMs,
signal
} = options;
const startTime = Date.now();
const guardrails = await instantiateGuardrails(bundle);
if (signal?.aborted) {
throw new Error("Guardrails execution aborted");
}
let results;
if (parallelExecution) {
const promises = guardrails.map(
(guardrail) => runSingleGuardrail(guardrail, context, input, timeoutMs, signal)
);
results = globalTimeoutMs ? await Promise.race([
Promise.all(promises),
rejectAfter(
globalTimeoutMs,
`Global timeout of ${globalTimeoutMs}ms exceeded`
)
]) : await Promise.all(promises);
} else {
results = [];
for (const guardrail of guardrails) {
if (globalTimeoutMs) {
const elapsed = Date.now() - startTime;
const remaining = globalTimeoutMs - elapsed;
if (remaining <= 0) {
throw new Error(`Global timeout of ${globalTimeoutMs}ms exceeded`);
}
}
if (signal?.aborted) {
throw new Error("Guardrails execution aborted");
}
const result = await runSingleGuardrail(
guardrail,
context,
input,
timeoutMs,
signal
);
results.push(result);
if (result.tripwireTriggered && result.severity === "critical") {
break;
}
}
}
if (raiseGuardrailErrors) {
const executionFailures = results.filter((r) => r.executionFailed);
if (executionFailures.length > 0) {
const firstFailure = executionFailures[0];
if (firstFailure.originalException) {
throw firstFailure.originalException;
}
throw new Error(firstFailure.message || "Guardrail execution failed");
}
}
const totalExecutionTimeMs = Date.now() - startTime;
const triggeredCount = results.filter((r) => r.tripwireTriggered).length;
const failedCount = results.filter((r) => r.executionFailed).length;
const successCount = results.length - failedCount;
return {
blocked: triggeredCount > 0,
results,
metadata: {
totalExecutionTimeMs,
triggeredCount,
failedCount,
successCount
}
};
}
async function runSingleGuardrail(guardrail, context, input, timeoutMs, signal) {
try {
if (signal?.aborted) {
throw new Error("Execution aborted");
}
if (timeoutMs) {
return await Promise.race([
guardrail.run(context, input),
rejectAfter(timeoutMs, `Guardrail timeout after ${timeoutMs}ms`)
]);
}
return await guardrail.run(context, input);
} catch (error) {
return {
tripwireTriggered: false,
executionFailed: true,
originalException: error instanceof Error ? error : new Error(String(error)),
info: {
guardrailName: guardrail.spec.name,
mediaType: guardrail.spec.mediaType,
error: error instanceof Error ? error.message : String(error)
},
message: `Guardrail execution failed: ${error instanceof Error ? error.message : String(error)}`,
severity: "high",
context: {
guardrailName: guardrail.spec.name,
executedAt: /* @__PURE__ */ new Date()
}
};
}
}
async function instantiateGuardrails(bundle) {
const guardrails = [];
for (const config of bundle.guardrails) {
const spec = defaultRegistry.get(config.name);
if (!spec) {
throw new Error(`Guardrail '${config.name}' not found in registry`);
}
try {
const guardrail = spec.instantiate(
config.config || {}
);
guardrails.push(guardrail);
} catch (error) {
throw new Error(
`Failed to instantiate guardrail '${config.name}': ${error instanceof Error ? error.message : String(error)}`,
{ cause: error }
);
}
}
return guardrails;
}
async function loadPipelineConfig(config) {
if (typeof config === "string") {
if (config.includes(".json") || config.includes(".yaml") || config.includes("/")) {
const fs = await import("fs/promises");
const content = await fs.readFile(config, "utf8");
if (config.endsWith(".yaml") || config.endsWith(".yml")) {
throw new Error("YAML support not yet implemented");
}
return JSON.parse(content);
}
return JSON.parse(config);
}
return config;
}
function loadGuardrailBundle(config) {
const guardrails = [];
const bundle = typeof config === "object" && config !== null && !Array.isArray(config) ? config : void 0;
if (Array.isArray(config)) {
guardrails.push(...config);
} else if (bundle && Array.isArray(bundle.guardrails)) {
guardrails.push(...bundle.guardrails);
} else {
throw new Error("Invalid guardrail bundle format");
}
for (const guardrail of guardrails) {
if (!guardrail.name || typeof guardrail.name !== "string") {
throw new Error("Invalid guardrail config: missing or invalid name");
}
if (guardrail.config !== void 0 && typeof guardrail.config !== "object") {
throw new Error(`Invalid config for guardrail '${guardrail.name}'`);
}
}
return {
version: bundle?.version || 1,
stageName: bundle?.stageName,
guardrails
};
}
async function checkPlainText(text, bundle, context, options) {
const result = await runGuardrails(text, bundle, context, options);
if (result.blocked) {
const triggeredGuardrails = result.results.filter((r) => r.tripwireTriggered).map((r) => r.message || "Guardrail triggered").join(", ");
const error = new Error(
`Content validation failed: ${result.metadata?.triggeredCount} violation(s) detected: ${triggeredGuardrails}`
);
throw Object.assign(error, {
guardrailResults: result.results.filter((r) => r.tripwireTriggered)
});
}
}
function rejectAfter(ms, message) {
return new Promise(
(_resolve, reject) => setTimeout(() => reject(new Error(message)), ms)
);
}
async function runStageGuardrails(input, pipeline, stage, context = {}, options) {
const bundle = pipeline[stage];
if (!bundle) {
return null;
}
return await runGuardrails(input, bundle, context, options);
}
function validatePipelineConfig(config) {
const errors = [];
if (config.version && typeof config.version !== "number") {
errors.push("Pipeline version must be a number");
}
const stages = ["pre_flight", "input", "output"];
for (const stage of stages) {
const bundle = config[stage];
if (bundle) {
try {
loadGuardrailBundle(bundle);
} catch (error) {
errors.push(
`Invalid ${stage} bundle: ${error instanceof Error ? error.message : String(error)}`
);
}
}
}
return errors;
}
var configUtils = {
loadPipelineConfig,
loadGuardrailBundle,
validatePipelineConfig
};
var runtimeUtils = {
runGuardrails,
runStageGuardrails,
checkPlainText,
instantiateGuardrails
};
// src/adapters/spec-adapter.ts
import { z as z3 } from "zod";
function guardrailToSpec(guardrail, options) {
return new GuardrailSpec(
guardrail.name,
guardrail.description || `Guardrail: ${guardrail.name}`,
"text/plain",
z3.unknown(),
// Legacy guardrails don't expose config schemas
async (context, input) => {
const result = await guardrail.execute(input);
return result;
},
void 0,
{
engine: "custom",
version: guardrail.version,
tags: guardrail.tags,
category: "security",
...options?.metadata
}
);
}
function outputGuardrailToSpec(guardrail, options) {
return new GuardrailSpec(
guardrail.name,
guardrail.description || `Output guardrail: ${guardrail.name}`,
"text/plain",
z3.unknown(),
// Placeholder schema for legacy guardrails
async (context, input) => {
const accumulatedText = context._accumulatedText;
const result = await guardrail.execute(
input,
accumulatedText
);
return result;
},
void 0,
{
engine: "custom",
version: guardrail.version,
tags: guardrail.tags,
category: "security",
...options?.metadata
}
);
}
function registerGuardrails(guardrails, options) {
const prefix = options?.prefix || "";
for (const guardrail of guardrails) {
const isInput = "execute" in guardrail && guardrail.execute.length === 1;
const spec = isInput ? guardrailToSpec(guardrail) : outputGuardrailToSpec(guardrail);
const name = prefix ? `${prefix}-${guardrail.name}` : guardrail.name;
const prefixedSpec = new GuardrailSpec(
name,
spec.description,
spec.mediaType,
spec.configSchema,
spec.checkFn,
spec.ctxRequirements,
spec.metadata
);
defaultRegistry.registerSpec(prefixedSpec);
}
}
// src/adapters/parallel-runtime-adapter.ts
async function executeInputGuardrailsWithEnhancedRuntime(guardrails, context, options = {}) {
const specs = guardrails.filter((g) => g.enabled !== false).map((g) => guardrailToSpec(g));
const bundle = {
version: 1,
stageName: "input",
guardrails: specs.map((spec) => ({
name: spec.name,
config: {}
}))
};
const tempRegistry = /* @__PURE__ */ new Map();
for (const spec of specs) tempRegistry.set(spec.name, spec);
const originalGet = defaultRegistry.get.bind(defaultRegistry);
defaultRegistry.get = (name) => tempRegistry.get(name) || originalGet(name);
try {
const result = await runGuardrails(
context,
bundle,
{},
{
parallelExecution: options.parallel ?? true,
timeoutMs: options.timeout,
raiseGuardrailErrors: !options.continueOnFailure,
signal: options.signal
}
);
defaultRegistry.get = originalGet;
return result.results.map((r, index) => {
const guardrail = guardrails[index];
const guardrailName = guardrail?.name || "unknown";
return {
tripwireTriggered: r.tripwireTriggered,
message: r.message,
severity: r.severity,
suggestion: r.suggestion,
metadata: r.metadata || {},
context: r.context?.guardrailName ? {
guardrailName: r.context.guardrailName,
guardrailVersion: r.context.guardrailVersion,
executedAt: r.context.executedAt || /* @__PURE__ */ new Date(),
executionTimeMs: r.context.executionTimeMs,
environment: r.context.environment
} : {
guardrailName,
executedAt: /* @__PURE__ */ new Date(),
executionTimeMs: r.context?.executionTimeMs,
environment: r.context?.environment
}
};
});
} catch (error) {
defaultRegistry.get = originalGet;
throw error;
}
}
async function executeOutputGuardrailsWithEnhancedRuntime(guardrails, context, options = {}) {
const specs = guardrails.filter((g) => g.enabled !== false).map(
(g) => outputGuardrailToSpec(g)
);
const bundle = {
version: 1,
stageName: "output",
guardrails: specs.map((spec) => ({
name: spec.name,
config: {}
}))
};
const tempRegistry = /* @__PURE__ */ new Map();
for (const spec of specs) tempRegistry.set(spec.name, spec);
const originalGet = defaultRegistry.get.bind(defaultRegistry);
defaultRegistry.get = (name) => tempRegistry.get(name) || originalGet(name);
try {
const runtimeContext = options.accumulatedText ? { _accumulatedText: options.accumulatedText } : {};
const result = await runGuardrails(context, bundle, runtimeContext, {
parallelExecution: options.parallel ?? true,
timeoutMs: options.timeout,
raiseGuardrailErrors: !options.continueOnFailure,
signal: options.signal
});
defaultRegistry.get = originalGet;
return result.results.map((r, index) => {
const guardrail = guardrails[index];
const guardrailName = guardrail?.name || "unknown";
return {
tripwireTriggered: r.tripwireTriggered,
message: r.message,
severity: r.severity,
suggestion: r.suggestion,
metadata: r.metadata || {},
context: r.context?.guardrailName ? {
guardrailName: r.context.guardrailName,
guardrailVersion: r.context.guardrailVersion,
executedAt: r.context.executedAt || /* @__PURE__ */ new Date(),
executionTimeMs: r.context.executionTimeMs,
environment: r.context.environment
} : {
guardrailName,
executedAt: /* @__PURE__ */ new Date(),
executionTimeMs: r.context?.executionTimeMs,
environment: r.context?.environment
}
};
});
} catch (error) {
defaultRegistry.get = originalGet;
throw error;
}
}
var V1_ENHANCED_RUNTIME_ENABLED = process.env.GUARDRAILS_ENHANCED_RUNTIME !== "false" && process.env.NODE_ENV !== "test";
// src/guardrails/internal.ts
var ENABLE_PERFORMANCE_TRACKING = process.env.NODE_ENV === "development" || process.env.GUARDRAILS_PERFORMANCE_TRACKING === "true";
var USE_ENHANCED_RUNTIME = process.env.GUARDRAILS_USE_ENHANCED_RUNTIME !== "false" && process.env.NODE_ENV !== "test";
function extractTextFromContent(content) {
return content.filter((part) => part.type === "text").map((part) => part.text || "").join("");
}
var normalizedContextCache = /* @__PURE__ */ new WeakMap();
var TimeoutControllerPool = class {
static controllers = [];
static MAX_POOL_SIZE = 20;
static acquire() {
const controller = this.controllers.pop();
if (controller && !controller.signal.aborted) {
return controller;
}
return new AbortController();
}
static release(controller) {
if (!controller.signal.aborted && this.controllers.length < this.MAX_POOL_SIZE) {
this.controllers.push(controller);
}
}
static clear() {
this.controllers = [];
}
};
function guardrailErrorResult(guardrailName, error, extra) {
const errMsg = error instanceof Error ? error.message : String(error);
const result = {
tripwireTriggered: true,
message: extra?.message ?? `Guardrail execution failed: ${errMsg}`,
severity: "critical",
metadata: extra?.metadata ?? { error: errMsg },
info: {
guardrailName,
executionFailed: true,
error: extra?.infoError ?? errMsg
}
};
if (extra?.context) {
result.context = extra.context;
}
return result;
}
var PRIORITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1 };
function prepareGuardrails(guardrails) {
return guardrails.filter((g) => g.enabled !== false).toSorted(
(a, b) => (PRIORITY_ORDER[b.priority || "medium"] || 2) - (PRIORITY_ORDER[a.priority || "medium"] || 2)
);
}
function createConditionalContext(guardrailName, guardrailVersion, executionTimeMs, existingContext) {
if (!ENABLE_PERFORMANCE_TRACKING) {
return {
guardrailName,
...existingContext
};
}
return {
guardrailName,
guardrailVersion,
executedAt: /* @__PURE__ */ new Date(),
executionTimeMs,
...existingContext
};
}
function checkStreamStopCondition(stopConfig, violations) {
if (stopConfig === true) {
const criticalViolations = violations.filter(
(v) => v.summary.blockedResults.some(
(r) => (r.severity ?? "medium") === "critical"
)
);
return violations.length >= 2 || criticalViolations.length > 0;
}
if (typeof stopConfig === "number") {
return violations.length >= stopConfig;
}
if (typeof stopConfig === "function") {
return stopConfig(violations);
}
return false;
}
async function executeWithOptimizedTimeout(execution, timeoutMs, errorMessage) {
const controller = TimeoutControllerPool.acquire();
let timeoutId;
try {
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
controller.abort();
reject(new Error(errorMessage));
}, timeoutMs);
});
const result = await Promise.race([
execution(controller.signal),
timeoutPromise
]);
return result;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
TimeoutControllerPool.release(controller);
}
}
function makeInvokeWithTimeout(invoke, timeoutMs) {
return (guardrail) => executeWithOptimizedTimeout(
(signal) => invoke(guardrail, signal),
timeoutMs,
`Guardrail ${guardrail.name} timed out after ${timeoutMs}ms`
).catch((error) => {
if (error.message.includes("timed out")) {
throw new GuardrailTimeoutError(guardrail.name, timeoutMs);
}
throw error;
});
}
async function executeSequential(guardrails, invokeWithTimeout, opts) {
const { continueOnFailure, logLevel, logger, label } = opts;
const results = [];
for (const guardrail of guardrails) {
try {
const result = await invokeWithTimeout(guardrail);
results.push(result);
if (result.tripwireTriggered) {
if (logLevel !== "none") {
logger.warn(
`${label} guardrail "${guardrail.name}" triggered: ${result.message}`
);
}
if (!continueOnFailure) {
break;
}
}
} catch (error) {
if (logLevel !== "none") {
logger.error(
`Error executing ${label.toLowerCase()} guardrail "${guardrail.name}":`,
error
);
}
results.push(guardrailErrorResult(guardrail.name, error));
if (!continueOnFailure) {
break;
}
}
}
return results;
}
async function executeParallelPerGuardrail(guardrails, invokeWithTimeout, label, logLevel, logger) {
return Promise.all(
guardrails.map(async (guardrail) => {
try {
const result = await invokeWithTimeout(guardrail);
if (result.tripwireTriggered && logLevel !== "none") {
logger.warn(
`${label} guardrail "${guardrail.name}" triggered: ${result.message}`
);
}
return result;
} catch (error) {
if (logLevel !== "none") {
logger.error(
`Error executing ${label.toLowerCase()} guardrail "${guardrail.name}":`,
error
);
}
return guardrailErrorResult(guardrail.name, error);
}
})
);
}
async function executeBatchInputGuardrails(guardrails, normalizedContext, timeoutMs, logLevel, logger) {
if (guardrails.length === 0) return [];
return executeWithOptimizedTimeout(
async (signal) => {
const results = await Promise.allSettled(
guardrails.map(async (guardrail) => {
try {
return await guardrail.execute(normalizedContext, { signal });
} catch (error) {
if (logLevel !== "none") {
logger.error(
`Error executing input guardrail "${guardrail.name}":`,
error
);
}
return guardrailErrorResult(guardrail.name, error);
}
})
);
return results.map((result, index) => {
const guardrail = guardrails[index];
if (result.status === "fulfilled") {
const guardResult = result.value;
if (guardResult.tripwireTriggered && logLevel !== "none") {
logger.warn(
`Input guardrail "${guardrail.name}" triggered: ${guardResult.message}`
);
}
return guardResult;
}
if (logLevel !== "none") {
logger.error(
`Input guardrail "${guardrail.name}" failed:`,
result.reason
);
}
return guardrailErrorResult(guardrail.name, result.reason);
});
},
timeoutMs,
`Batch guardrail execution timed out after ${timeoutMs}ms`
).catch((error) => {
if (error.message.includes("timed out")) {
return guardrails.map(
(guardrail) => guardrailErrorResult(guardrail.name, error, {
message: `Guardrail timed out after ${timeoutMs}ms`,
metadata: { timeout: true },
infoError: "Timeout"
})
);
}
throw error;
});
}
function normalizeGuardrailContext(params) {
const cached = normalizedContextCache.get(params);
if (cached) {
return cached;
}
const promptMessages = Array.isArray(params.prompt) ? params.prompt : [];
const systemMessage = promptMessages.find((msg) => msg.role === "system");
const system = systemMessage && Array.isArray(systemMessage.content) ? extractTextFromContent(systemMessage.content) : "";
const messages = promptMessages.filter((msg) => msg.role !== "system").map((msg) => ({
role: msg.role,
content: msg.content && Array.isArray(msg.content) ? extractTextFromContent(msg.content) : ""
}));
const prompt = messages.length === 1 && messages[0]?.role === "user" ? messages[0].content : messages.map((m) => m.content).join(" ");
const normalized = {
prompt,
messages,
system,
maxOutputTokens: params.maxOutputTokens,
temperature: params.temperature,
modelParams: {
topP: params.topP,
topK: params.topK,
frequencyPenalty: params.frequencyPenalty,
presencePenalty: params.presencePenalty,
seed: params.seed,
stopSequences: params.stopSequences
}
};
normalizedContextCache.set(params, normalized);
return normalized;
}
function toNormalizedGuardrailContext(params) {
const candidate = params;
if (typeof candidate.prompt === "string" && Array.isArray(candidate.messages)) {
return params;
}
return normalizeGuardrailContext(params);
}
function createExecutionSummary(results, startTime) {
const endTime = Date.now();
const totalExecutionTime = endTime - startTime;
const blockedResults = results.filter((r) => r.tripwireTriggered);
const execTimes = results.map((r) => r.context?.executionTimeMs).filter((t) => typeof t === "number");
const avgTime = execTimes.length > 0 ? execTimes.reduce((a, b) => a + b, 0) / execTimes.length : 0;
const stats = {
passed: results.filter((r) => !r.tripwireTriggered).length,
blocked: blockedResults.length,
failed: results.filter(
(r) => r.severity === "critical" && r.tripwireTriggered
).length,
averageExecutionTime: avgTime
};
return {
allResults: results,
blockedResults,
totalExecutionTime,
guardrailsExecuted: results.length,
stats
};
}
async function executeInputGuardrails(guardrails, params, options = {}) {
const {
parallel = true,
timeout = 3e4,
// 30 seconds
continueOnFailure = true,
logLevel = "warn",
logger = console
} = options;
const enabledGuardrails = prepareGuardrails(guardrails);
const invokeWithTimeout = makeInvokeWithTimeout(
(guardrail, signal) => Promise.resolve(
guardrail.execute(toNormalizedGuardrailContext(params), { signal })
),
timeout
);
if (!parallel) {
return executeSequential(enabledGuardrails, invokeWithTimeout, {
continueOnFailure,
logLevel,
logger,
label: "Input"
});
}
const runFallback = () => executeBatchInputGuardrails(
enabledGuardrails,
toNormalizedGuardrailContext(params),
timeout,
logLevel,
logger
);
if (USE_ENHANCED_RUNTIME) {
try {
return await executeInputGuardrailsWithEnhancedRuntime(
enabledGuardrails,
toNormalizedGuardrailContext(params),
{ parallel: true, timeout, continueOnFailure }
);
} catch {
return runFallback();
}
}
return runFallback();
}
async function executeOutputGuardrails(guardrails, params, options = {}) {
const {
parallel = true,
timeout = 3e4,
// 30 seconds
continueOnFailure = true,
logLevel = "warn",
logger = console,
accumulatedText
} = options;
const enabledGuardrails = prepareGuardrails(guardrails);
const invokeWithTimeout = makeInvokeWithTimeout(
(guardrail, signal) => Promise.resolve(guardrail.execute(params, accumulatedText, { signal })),
timeout
);
if (!parallel) {
return executeSequential(enabledGuardrails, invokeWithTimeout, {
continueOnFailure,
logLevel,
logger,
label: "Output"
});
}
const runFallback = () => executeParallelPerGuardrail(
enabledGuardrails,
invokeWithTimeout,
"Output",
logLevel,
logger
);
if (USE_ENHANCED_RUNTIME) {
try {
return await executeOutputGuardrailsWithEnhancedRuntime(
enabledGuardrails,
params,
{ parallel: true, timeout, continueOnFailure, accumulatedText }
);
} catch {
return runFallback();
}
}
return runFallback();
}
// src/guardrails/retry-helpers.ts
var SEVERITY_ORDER = {
low: 1,
medium: 2,
high: 3,
critical: 4
};
function findGuardrailForResult(result, guardrails) {
const name = result.context?.guardrailName ?? result.info?.guardrailName;
return guardrails.find((g) => g.name === name);
}
function getInstructionFromResult(result, guardrail, context) {
if (guardrail?.getRetryInstruction) {
const instruction = guardrail.getRetryInstruction({ ...context, result });
if (typeof instruction === "string") {
return { message: instruction };
}
return instruction ?? void 0;
}
if (result.message) {
const suggestionPart = result.suggestion ? `. ${result.suggestion}` : "";
return {
message: `Please try again. The previous response was blocked: ${result.message}${suggestionPart}`
};
}
return void 0;
}
function selectBlockedResults(summary, strategy) {
const { blockedResults } = summary;
if (blockedResults.length === 0) return [];
switch (strategy) {
case "first": {
return [blockedResults[0]];
}
case "all": {
return blockedResults;
}
default: {
const sorted = [...blockedResults].toSorted((a, b) => {
const severityA = SEVERITY_ORDER[a.severity ?? "medium"] ?? 2;
const severityB = SEVERITY_ORDER[b.severity ?? "medium"] ?? 2;
return severityB - severityA;
});
return [sorted[0]];
}
}
}
function combineInstructions(instructions) {
if (instructions.length === 0) {
return { message: "Please try again with a different approach." };
}
if (instructions.length === 1) {
return instructions[0];
}
const combinedMessage = instructions.map((inst, i) => `${i + 1}. ${inst.message}`).join("\n");
const tempAdjustments = instructions.map((i) => i.temperatureAdjustment).filter((t) => t !== void 0);
const avgTempAdjustment = tempAdjustments.length > 0 ? tempAdjustments.reduce((a, b) => a + b, 0) / tempAdjustments.length : void 0;
return {
message: `Please address the following issues:
${combinedMessage}`,
temperatureAdjustment: avgTempAdjustment,
context: { combinedFrom: instructions.length }
};
}
function createDefaultBuildRetryParams(options) {
const {
outputGuardrails,
multipleBlockedStrategy = "highest-severity",
attempt,
maxRetries
} = options;
return ({ summary, originalParams, lastParams }) => {
const selectedResults = selectBlockedResults(
summary,
multipleBlockedStrategy
);
const instructionContext = { attempt, maxRetries };
const instructions = [];
for (const result of selectedResults) {
const guardrail = findGuardrailForResult(result, outputGuardrails);
const instruction = getInstructionFromResult(
result,
guardrail,
instructionContext
);
if (instruction) {
instructions.push(instruction);
}
}
const finalInstruction = combineInstructions(instructions);
let newTemperature = lastParams.temperature ?? 0.7;
if (finalInstruction.temperatureAdjustment !== void 0) {
newTemperature = Math.max(
0,
Math.min(1, newTemperature + finalInstruction.temperatureAdjustment)
);
}
const existingPrompt = Array.isArray(lastParams.prompt) ? lastParams.prompt : Array.isArray(originalParams.prompt) ? originalParams.prompt : [];
return {
...lastParams,
temperature: newTemperature,
prompt: [
...existingPrompt,
{
role: "user",
content: [{ type: "text", text: finalInstruction.message }]
}
]
};
};
}
function resolveRetryConfig(globalRetry, blockedGuardrails) {
let maxRetries = globalRetry?.maxRetries ?? 0;
let backoffMs = globalRetry?.backoffMs ?? 0;
if (maxRetries === 0) {
for (const guardrail of blockedGuardrails) {
if (guardrail.retry?.maxRetries !== void 0) {
maxRetries = Math.max(maxRetries, guardrail.retry.maxRetries);
}
if (guardrail.retry?.backoffMs !== void 0 && backoffMs === 0) {
backoffMs = guardrail.retry.backoffMs;
}
}
}
return { maxRetries, backoffMs };
}
// src/guardrails/generate-result-sync.ts
function joinTextFromContent(content) {
if (!Array.isArray(content)) {
return void 0;
}
const parts = content.filter(
(part) => typeof part === "object" && part !== null && part.type === "text" && typeof part.text === "string"
).map((part) => part.text);
return parts.length > 0 ? parts.join("") : void 0;
}
function snapshotGenerateResultText(result) {
const record = result;
return {
text: typeof record.text === "string" ? record.text : void 0,
contentJoined: joinTextFromContent(record.content)
};
}
function syncGenerateResultTextAfterGuardrails(result, before) {
const record = result;
const afterContent = joinTextFromContent(record.content);
const afterText = typeof record.text === "string" ? record.text : void 0;
const textChanged = afterText !== void 0 && afterText !== before.text;
const contentChanged = afterContent !== void 0 && afterContent !== before.contentJoined;
if (textChanged && !contentChanged && afterText !== void 0) {
record.content = [{ type: "text", text: afterText }];
record.text = afterText;
return result;
}
if (contentChanged && afterContent !== void 0) {
record.text = afterContent;
return result;
}
if (afterContent !== void 0) {
record.text = afterContent;
} else if (afterText !== void 0) {
record.content = [{ type: "text", text: afterText }];
}
return result;
}
// src/guardrails/middleware-factories.ts
var emptyV4Usage = {
inputTokens: {
total: 0,
noCache: void 0,
cacheRead: void 0,
cacheWrite: void 0
},
outputTokens: {
total: 0,
text: void 0,
reasoning: void 0
}
};
var finishReasonStop = {
unified: "stop",
raw: void 0
};
var finishReasonOther = {
unified: "other",
raw: void 0
};
function inputGuardrailsMiddleware(config) {
const {
inputGuardrails,
context,
executionOptions = {},
onInputBlocked,
throwOnBlocked = false
} = config;
return {
specificationVersion: "v4",
transformParams: async ({
params
}) => {
const baseContext = normalizeGuardrailContext(params);
const guardrailContext = context ? { ...baseContext, requestContext: context } : baseContext;
const executionStartTime = Date.now();
const inputResults = await executeInputGuardrails(
inputGuardrails,
guardrailContext,
executionOptions
);
const blockedResults = inputResults.filter((r) => r.tripwireTriggered);
if (blockedResults.length > 0) {
if (onInputBlocked) {
const executionSummary = createExecutionSummary(
inputResults,
executionStartTime
);
onInputBlocked(executionSummary, guardrailContext);
}
if (throwOnBlocked) {
const blockedGuardrails = blockedResults.map((r) => ({
name: r.context?.guardrailName || "unknown",
message: r.message || "Blocked",
severity: r.severity || "medium"
}));
throw new GuardrailsInputError(blockedGuardrails);
}
const enhancedParams = params;
enhancedParams.guardrailsBlocked = blockedResults;
return enhancedParams;
}
return params;
},
wrapGenerate: async ({
doGenerate,
params
}) => {
const paramsWithGuardrails = params;
if (paramsWithGuardrails.guardrailsBlocked) {
const blockedResults = paramsWithGuardrails.guardrailsBlocked;
const blockedMessage = blockedResults.map((r) => r.message).join(", ");
const blockedText = `[Input blocked: ${blockedMessage}]`;
return {
text: blockedText,
content: [{ type: "text", text: blockedText }],
finishReason: finishReasonOther,
usage: emptyV4Usage,
warnings: [],
rawCall: { rawPrompt: params.prompt, rawSettings: {} },
response: { headers: {} }
};
}
return doGenerate();
},
wrapStream: async ({
doStream,
params
}) => {
const paramsWithGuardrails = params;
if (paramsWithGuardrails.guardrailsBlocked) {
const blockedResults = paramsWithGuardrails.guardrailsBlocked;
const blockedMessage = blockedResults.map((r) => r.message).join(", ");
const stream = new ReadableStream({
start(controller) {
controller.enqueue({
type: "text-delta",
id: "1",
delta: `[Input blocked: ${blockedMessage}]`
});
controller.enqueue({
type: "finish",
finishReason: finishReasonOther,
usage: emptyV4Usage
});
controller.close();
}
});
return { stream };
}
return doStream();
}
};
}
function outputGuardrailsMiddleware(config) {
const {
outputGuardrails,
context,
executionOptions = {},
onOutputBlocked,
throwOnBlocked = false,
replaceOnBlocked = true,
streamMode = "buffer",
retry,
stopOnGuardrailViolation
} = config;
return {
specificationVersion: "v4",
wrapGenerate: async ({
doGenerate,
params,
model
}) => {
const result = await doGenerate();
const resultTextBeforeGuardrails = snapshotGenerateResultText(result);
const baseContext = normalizeGuardrailContext(params);
const guardrailContext = context ? { ...baseContext, requestContext: context } : baseContext;
const aiResult = result;
const outputContext = {
input: guardrailContext,
result: aiResult
};
const startTime = Date.now();
const outputResults = await executeOutputGuardrails(
outputGuardrails,
outputContext,
executionOptions
);
const executionSummary = createExecutionSummary(
outputResults,
startTime
);
if (executionSummary.blockedResults.length > 0) {
const blockedGuardrailObjects = executionSummary.blockedResults.map(
(r) => outputGuardrails.find(
(g) => g.name === (r.context?.guardrailName ?? r.info?.guardrailName)
)
).filter((g) => g !== void 0);
const effectiveRetry = resolveRetryConfig(
retry,
blockedGuardrailObjects
);
if (effectiveRetry.maxRetries > 0 && (retry?.onlyWhen ? retry.onlyWhen(executionSummary) : true)) {
const maxRetries = effectiveRetry.maxRetries;
let lastParams = params;
let lastResult = result;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const wait = typeof effectiveRetry.backoffMs === "function" ? effectiveRetry.backoffMs(attempt) : effectiveRetry.backoffMs ?? 0;
if (wait && wait > 0) {
await new Promise((r) => setTimeout(r, wait));
}
const buildRetryParams = retry?.buildRetryParams ?? createDefaultBuildRetryParams({
outputGuardrails,
multipleBlockedStrategy: retry?.multipleBlockedStrategy ?? "highest-severity",
attempt,
maxRetries
});
const nextParams = buildRetryParams({
summary: executionSummary,
originalParams: params,
lastParams,
lastResult
});
const retryRaw = await model.doGenerate(nextParams);
const retryTextBeforeGuardrails = snapshotGenerateResultText(retryRaw);
const retryResult = retryRaw;
const retryContext = {
input: normalizeGuardrailContext(nextParams),
result: retryResult
};
const retryStart = Date.now();
const retryResults = await executeOutputGuardrails(
outputGuardrails,
retryContext,
executionOptions
);
const retrySummary = createExecutionSummary(
retryResults,
retryStart
);
if (retrySummary.blockedResults.length === 0) {
return syncGenerateResultTextAfterGuardrails(
retryRaw,
retryTextBeforeGuardrails
);
}
lastParams = nextParams;
lastResult = retryResult;
}
}
if (onOutputBlocked) {
onOutputBlocked(executionSummary, guardrailContext, result);
}
if (throwOnBlocked) {
const blockedGuardrails = executionSummary.blockedResults.map(
(r) => ({
name: r.context?.guardrailName || "unknown",
message: r.message || "Blocked",
severity: r.severity || "medium"
})
);
throw new GuardrailsOutputError(blockedGuardrails);
}
if (replaceOnBlocked) {
const blockedMessage = executionSummary.blockedResults.map((r) => r.message).join(", ");
const blockedText = `[Output blocked: ${blockedMessage}]`;
const replaced = {
...result,
text: blockedText,
content: [{ type: "text", text: blockedText }]
};
return replaced;
}
}
return syncGenerateResultTextAfterGuardrails(
result,
resultTextBeforeGuardrails
);
},
wrapStream: async ({
doStream,
params,
model
}) => {
const streamResult = await doStream();
if (streamMode === "buffer") {
let accumulatedText2 = "";
let streamUsage;
let streamFinishReason;
const blockedChunks = [];
const transformStream2 = new TransformStream({
transform(chunk) {
if (chunk.type === "text-delta") {
const anyChunk = chunk;
accumulatedText2 += anyChunk.delta ?? anyChunk.textDelta ?? "";
} else if (chunk.type === "finish") {
streamUsage = chunk.usage;
streamFinishReason = chunk.finishReason;
}
blockedChunks.push(chunk);
},
async flush(controller) {
const baseContext = normalizeGuardrailContext(params);
const guardrailContext = context ? { ...baseContext, requestContext: context } : baseContext;
const streamedResult = {
text: accumulatedText2,
content: [{ type: "text", text: accumulatedText2 }],
usage: streamUsage,
finishReason: streamFinishReason
};
const streamedTextBeforeGuardrails = snapshotGenerateResultText(streamedResult);
const outputContext = {
input: guardrailContext,
result: streamedResult
};
const startTime = Date.now();
const outputResults = await executeOutputGuardrails(
outputGuardrails,
outputContext,
{
...executionOptions,
accumulatedText: accumulatedText2
}
);
const executionSummary = createExecutionSummary(
outputResults,
startTime
);
if (executionSummary.blockedResults.length > 0) {
const blockedGuardrailObjects = executionSummary.blockedResults.map(
(r) => outputGuardrails.find(
(g) => g.name === (r.context?.guardrailName ?? r.info?.guardrailName)
)
).filter((g) => g !== void 0);
const effectiveRetry = resolveRetryConfig(
retry,
blockedGuardrailObjects
);
if (effectiveRetry.maxRetries > 0 && (retry?.onlyWhen ? retry.onlyWhen(executionSummary) : true)) {
const maxRetries = effectiveRetry.maxRetries;
let lastParams = params;
let lastResult = {
text: accumulatedText2
};
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const wait = typeof effectiveRetry.backoffMs === "function" ? effectiveRetry.backoffMs(attempt) : effectiveRetry.backoffMs ?? 0;
if (wait && wait > 0) {
await new Promise((r) => setTimeout(r, wait));
}
const buildRetryParams = retry?.buildRetryParams ?? createDefaultBuildRetryParams({
outputGuardrails,
multipleBlockedStrategy: retry?.multipleBlockedStrategy ?? "highest-severity",
attempt,
maxRetries
});
const nextParams = buildRetryParams({
summary: executionSummary,
originalParams: params,
lastParams,
lastResult
});
const retryResult = await model.doGenerate(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nextParams
);
const retryContext = {
input: normalizeGuardrailContext(nextParams),
result: retryResult
};
const retryStart = Date.now();
const retryResults = await executeOutputGuardrails(
outputGuardrails,
retryContext,
executionOptions
);
const retrySummary = createExecutionSummary(
retryResults,
retryStart
);
if (retrySummary.blockedResults.length === 0) {
const { text: repairedText } = extractContent(
retryResult
);
controller.enqueue({
type: "text-delta",
id: "1",
delta: repairedText
});
controller.enqueue({
type: "finish",
finishReason: finishReasonStop,
usage: emptyV4Usage
});
return;
}
lastParams = nextParams;
lastResult = retryResult;
}
}
if (onOutputBlocked) {
onOutputBlocked(
executionSummary,
guardrailContext,
streamedResult
);
}
if (throwOnBlocked) {
controller.error(
new Error(
`Output guardrails blocked response: ${executionSummary.blockedResults.map((r) => r.message).join(", ")}`
)
);
return;
}
if (replaceOnBlocked) {
const blockedMessage = executionSummary.blockedResults.map((r) => r.message).join(", ");
controller.enqueue({
type: "text-delta",
id: "1",
delta: `[Output blocked: ${blockedMessage}]`
});
controller.enqueue({
type: "finish",
finishReason: finishReasonOther,
usage: emptyV4Usage
});
} else {
for (const chunk of blockedChunks) {
controller.enqueue(chunk);
}
}
} else {
syncGenerateResultTextAfterGuardrails(
streamedResult,
streamedTextBeforeGuardrails
);
const finalText = typeof streamedResult.text === "string" ? streamedResult.text : accumulatedText2;
if (finalText === accumulatedText2) {
for (const chunk of blockedChunks) {
controller.enqueue(chunk);
}
} else {
controller.enqueue({
type: "text-delta",
id: "1",
delta: finalText
});
controller.enqueue({
type: "finish",
finishReason: streamFinishReason ?? finishReasonStop,
usage: streamUsage ?? emptyV4Usage
});
}
}
}
});
return {
stream: streamResult.stream.pipeThrough(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transformStream2
)
};
}
let accumulatedText = "";
let blocked = false;
let chunkIndex = 0;
const streamViolationHistory = [];
const transformStream = new TransformStream({
async transform(chunk, controller) {
if (blocked) {
return;
}
if (chunk.type === "text-delta") {
const anyChunk = chunk;
accumulatedText += anyChunk.delta ?? anyChunk.textDelta ?? "";
chunkIndex++;
const baseContext = normalizeGuardrailContext(params);
const guardrailContext = context ? { ...baseContext, requestContext: context } : baseContext;
const outputContext = {
input: guardrailContext,
result: { text: accumulatedText }
};
const startTime = Date.now();
const outputResults = await executeOutputGuardrails(
outputGuardrails,
outputContext,
{
...executionOptions,
accumulatedText
}
);
const executionSummary = createExecutionSummary(
outputResults,
startTime
);
if (executionSummary.blockedResults.length > 0) {
streamViolationHistory.push({
chunkIndex,
summary: executionSummary
});
const shouldStopEarly = stopOnGuardrailViolation && checkStreamStopCondition(
stopOnGuardrailViolation,
streamViolationHistory
);
if (shouldStopEarly) {
blocked = true;
}
if (blocked) {
if (onOutputBlocked) {
onOutputBlocked(executionSummary, guardrailContext, {
text: accumulatedText
});
}
if (throwOnBlocked) {
controller.error(
new Error(
`Output guardrails blocked response: ${executionSummary.blockedResults.map((r) => r.message).join(", ")}`
)
);
return;
}
if (replaceOnBlocked) {
const blockedMessage = executionSummary.blockedResults.map((r) => r.message).join(", ");
controller.enqueue({
type: "text-delta",
id: "1",
delta: `[Output blocked: ${blockedMessage}]`
});
controller.enqueue({
type: "finish",
finishReason: finishReasonOther,
usage: emptyV4Usage
});
return;
}
}
}
controller.enqueue(chunk);
} else {
controller.enqueue(chunk);
}
}
});
return {
stream: streamResult.stream.pipeThrough(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transformStream
)
};
}
};
}
// src/guardrails.ts
import { wrapLanguageModel } from "ai";
async function runWithEnvelope(name, version, run) {
const startTime = ENABLE_PERFORMANCE_TRACKING ? Date.now() : 0;
try {
const result = await run();
const executionTime = ENABLE_PERFORMANCE_TRACKING ? Date.now() - startTime : void 0;
return {
...result,
context: createConditionalContext(
name,
version,
executionTime,
result.context
)
};
} catch (error) {
const executionTime = ENABLE_PERFORMANCE_TRACKING ? Date.now() - startTime : void 0;
return guardrailErrorResult(name, error, {
context: createConditionalContext(name, version, executionTime)
});
}
}
function defineInputGuardrail(guardrail) {
const originalExecute = guardrail.execute;
return {
enabled: true,
priority: "medium",
version: "1.0.0",
tags: [],
...guardrail,
execute: (params, options) => runWithEnvelope(
guardrail.name,
guardrail.version,
() => options === void 0 ? originalExecute(params) : originalExecute(params, options)
)
};
}
function defineOutputGuardrail(guardrail) {
const originalExecute = guardrail.execute;
return {
enabled: true,
priority: "medium",
version: "1.0.0",
tags: [],
...guardrail,
execute: (params, options) => runWithEnvelope(
guardrail.name,
guardrail.version,
() => options === void 0 ? originalExecute(params) : originalExecute(params, options)
)
};
}
function withGuardrails(config) {
const {
model,
inputGuardrails = [],
outputGuardrails = [],
throwOnBlocked,
replaceOnBlocked,
streamMode,
stopOnGuardrailViolation,
executionOptions,
onInputBlocked,
onOutputBlocked,
retry
} = config;
const middlewares = [];
if (inputGuardrails.length > 0) {
middlewares.push(
inputGuardrailsMiddleware({
inputGuardrails,
throwOnBlocked,
executionOptions,
onInputBlocked
})
);
}
if (outputGuardrails.length > 0) {
middlewares.push(
outputGuardrailsMiddleware({
outputGuardrails,
throwOnBlocked,
replaceOnBlocked,
streamMode,
stopOnGuardrailViolation,
executionOptions,
onOutputBlocked,
retry
})
);
}
if (middlewares.length === 0) {
return model;
}
return wrapLanguageModel({
model,
middleware: middlewares
});
}
function createGuardrails(config) {
return (model) => withGuardrails({ model, ...config });
}
export {
GuardrailSpec,
ConfiguredGuardrail,
GuardrailRegistry,
defaultRegistry,
createRegistry,
runGuardrails,
instantiateGuardrails,
loadPipelineConfig,
loadGuardrailBundle,
checkPlainText,
runStageGuardrails,
validatePipelineConfig,
configUtils,
runtimeUtils,
registerGuardrails,
normalizeGuardrailContext,
executeInputGuardrails,
executeOutputGuardrails,
createDefaultBuildRetryParams,
resolveRetryConfig,
snapshotGenerateResultText,
syncGenerateResultTextAfterGuardrails,
inputGuardrailsMiddleware,
outputGuardrailsMiddleware,
defineInputGuardrail,
defineOutputGuardrail,
withGuardrails,
createGuardrails
};