ai-sdk-guardrails
Version:
Input and output guardrails middleware for Vercel AI SDK.
655 lines (653 loc) • 21.4 kB
JavaScript
import {
createOutputGuardrail
} from "../chunk-HHQ3CIFN.js";
import "../chunk-LLCOPUS6.js";
// src/guardrails/output.ts
function extractContent(result) {
if ("content" in result && Array.isArray(result.content)) {
const content = result.content;
const textContent = content.filter((item) => item.type === "text" && item.text).map((item) => item.text).join("");
const usage = result.usage || {};
const mappedUsage = {
promptTokens: typeof usage.inputTokens === "number" ? usage.inputTokens : typeof usage.promptTokens === "number" ? usage.promptTokens : void 0,
completionTokens: typeof usage.outputTokens === "number" ? usage.outputTokens : typeof usage.completionTokens === "number" ? usage.completionTokens : void 0,
totalTokens: typeof usage.totalTokens === "number" ? usage.totalTokens : void 0
};
return {
text: textContent || "",
object: null,
usage: mappedUsage,
finishReason: result.finishReason,
generationTimeMs: result.experimental_providerMetadata?.generationTimeMs,
reasoningText: result.reasoningText || result.experimental_providerMetadata?.reasoningText
};
}
if ("object" in result && result.object != null) {
const objectResult = result;
return {
text: objectResult.text || "",
object: objectResult.object,
usage: objectResult.usage,
finishReason: objectResult.finishReason,
generationTimeMs: objectResult.experimental_providerMetadata?.generationTimeMs,
reasoningText: objectResult.reasoningText || objectResult.experimental_providerMetadata?.reasoningText
};
} else if ("text" in result) {
const textResult = result;
return {
text: textResult.text || "",
object: null,
usage: textResult.usage,
finishReason: textResult.finishReason,
generationTimeMs: textResult.experimental_providerMetadata?.generationTimeMs,
reasoningText: textResult.reasoningText || textResult.experimental_providerMetadata?.reasoningText
};
} else if ("textStream" in result) {
return {
text: "",
object: null,
usage: void 0,
finishReason: void 0,
generationTimeMs: void 0
};
} else if ("objectStream" in result) {
return {
text: "",
object: null,
usage: void 0,
finishReason: void 0,
generationTimeMs: void 0
};
} else if ("embeddings" in result || "then" in result) {
return {
text: "",
object: null,
usage: void 0,
finishReason: void 0,
generationTimeMs: void 0
};
}
return {
text: "",
object: null,
usage: void 0,
finishReason: void 0,
generationTimeMs: void 0
};
}
var lengthLimit = (maxLength) => createOutputGuardrail(
"output-length-limit",
(context, accumulatedText) => {
const { text, object, usage, finishReason, generationTimeMs } = extractContent(context.result);
const content = accumulatedText || text || (object ? JSON.stringify(object) : "");
return {
tripwireTriggered: content.length > maxLength,
message: `Output length ${content.length} exceeds limit of ${maxLength}`,
severity: "medium",
metadata: {
contentLength: content.length,
maxLength,
hasObject: !!object,
usage,
finishReason,
generationTimeMs,
tokensPerMs: usage?.totalTokens && generationTimeMs ? usage.totalTokens / generationTimeMs : void 0
}
};
}
);
var blockedContent = (words) => createOutputGuardrail(
"blocked-content",
(context) => {
const { text, object } = extractContent(context.result);
const content = (text || (object ? JSON.stringify(object) : "")).toLowerCase();
const blockedWord = words.find(
(word) => content.includes(word.toLowerCase())
);
return {
tripwireTriggered: !!blockedWord,
message: blockedWord ? `Blocked content detected: ${blockedWord}` : void 0,
severity: "high",
metadata: {
blockedWord,
allWords: words,
contentLength: content.length
}
};
}
);
var outputLengthLimit = (maxLength) => createOutputGuardrail(
"output-length-limit",
(context, accumulatedText) => {
const { text, object } = extractContent(context.result);
const content = accumulatedText || text || (object ? JSON.stringify(object) : "");
return {
tripwireTriggered: content.length > maxLength,
message: `Output length ${content.length} exceeds limit of ${maxLength}`,
severity: "medium",
metadata: {
contentLength: content.length,
maxLength,
hasObject: !!object
}
};
}
);
var blockedOutputContent = (words) => createOutputGuardrail(
"blocked-output-content",
(context) => {
const { text, object } = extractContent(context.result);
const content = (text || (object ? JSON.stringify(object) : "")).toLowerCase();
const blockedWord = words.find(
(word) => content.includes(word.toLowerCase())
);
return {
tripwireTriggered: !!blockedWord,
message: blockedWord ? `Blocked output content detected: ${blockedWord}` : void 0,
severity: "high",
metadata: {
blockedWord,
allWords: words,
contentLength: content.length
}
};
}
);
var jsonValidation = () => createOutputGuardrail(
"json-validation",
(context) => {
const { text, object } = extractContent(context.result);
if (object) return { tripwireTriggered: false };
try {
JSON.parse(text);
return { tripwireTriggered: false };
} catch (error) {
return {
tripwireTriggered: true,
message: "Output is not valid JSON",
severity: "medium",
metadata: {
error: error instanceof Error ? error.message : String(error),
textLength: text.length
}
};
}
}
);
var confidenceThreshold = (minConfidence) => createOutputGuardrail(
"confidence-threshold",
(context) => {
const {
text,
object,
usage,
finishReason,
generationTimeMs,
reasoningText
} = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const hasUncertainty = content.toLowerCase().includes("i think") || content.toLowerCase().includes("maybe") || content.toLowerCase().includes("probably") || content.toLowerCase().includes("uncertain") || content.toLowerCase().includes("not sure");
const finishReasonPenalty = finishReason === "length" ? 0.2 : 0;
const baseConfidence = hasUncertainty ? 0.5 : 0.9;
const confidence = Math.max(0, baseConfidence - finishReasonPenalty);
return {
tripwireTriggered: confidence < minConfidence,
message: `Output confidence ${confidence} below threshold ${minConfidence}`,
severity: "medium",
metadata: {
confidence,
minConfidence,
hasUncertainty,
textLength: content.length,
usage,
finishReason,
generationTimeMs,
finishReasonPenalty,
reasoningText
}
};
}
);
var toxicityFilter = (threshold = 0.7) => createOutputGuardrail(
"toxicity-filter",
(context) => {
const { text, object } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const toxicWords = ["toxic", "harmful", "offensive", "inappropriate"];
const detectedWords = toxicWords.filter(
(word) => content.toLowerCase().includes(word)
);
const toxicityScore = detectedWords.length * 0.3;
return {
tripwireTriggered: toxicityScore > threshold,
message: `Content toxicity score ${toxicityScore} exceeds threshold ${threshold}`,
severity: "high",
metadata: {
toxicityScore,
threshold,
detectedWords,
contentLength: content.length
}
};
}
);
var customValidation = (name, validator, message) => createOutputGuardrail(name, (context) => {
const { text, object, usage, finishReason, generationTimeMs } = extractContent(context.result);
const validatorInput = {
text,
object,
usage,
finishReason,
generationTimeMs
};
const blocked = validator(validatorInput);
return {
tripwireTriggered: blocked,
message: blocked ? message : void 0,
severity: "medium",
metadata: {
validatorName: name,
hasText: !!text,
hasObject: !!object,
usage,
finishReason,
generationTimeMs
}
};
});
var schemaValidation = (schema) => createOutputGuardrail(
"schema-validation",
(context) => {
const { object, usage, finishReason, generationTimeMs } = extractContent(
context.result
);
if (!object) {
return {
tripwireTriggered: true,
message: "No object to validate",
severity: "medium",
metadata: {
hasObject: false,
usage,
finishReason,
generationTimeMs
}
};
}
try {
schema.parse(object);
return {
tripwireTriggered: false,
metadata: {
hasObject: true,
validationPassed: true,
usage,
finishReason,
generationTimeMs
}
};
} catch (error) {
return {
tripwireTriggered: true,
message: `Schema validation failed: ${error instanceof Error ? error.message : String(error)}`,
severity: "high",
metadata: {
hasObject: true,
validationPassed: false,
error: error instanceof Error ? error.message : String(error),
usage,
finishReason,
generationTimeMs
}
};
}
}
);
var tokenUsageLimit = (maxTokens) => createOutputGuardrail(
"token-usage-limit",
(context) => {
const { text, object, usage, generationTimeMs } = extractContent(
context.result
);
const totalTokens = usage?.totalTokens || 0;
const content = text || (object ? JSON.stringify(object) : "");
return {
tripwireTriggered: totalTokens > maxTokens,
message: `Token usage ${totalTokens} exceeds limit of ${maxTokens}`,
severity: "medium",
metadata: {
totalTokens,
maxTokens,
inputTokens: usage?.inputTokens || usage?.promptTokens,
outputTokens: usage?.outputTokens || usage?.completionTokens,
contentLength: content.length,
generationTimeMs,
tokensPerMs: totalTokens && generationTimeMs ? totalTokens / generationTimeMs : void 0
}
};
}
);
var performanceMonitor = (maxGenerationTimeMs) => createOutputGuardrail(
"performance-monitor",
(context) => {
const { text, object, usage, generationTimeMs } = extractContent(
context.result
);
const actualGenerationTimeMs = generationTimeMs || 0;
const content = text || (object ? JSON.stringify(object) : "");
return {
tripwireTriggered: actualGenerationTimeMs > maxGenerationTimeMs,
message: `Generation time ${actualGenerationTimeMs}ms exceeds limit of ${maxGenerationTimeMs}ms`,
severity: "low",
metadata: {
generationTimeMs: actualGenerationTimeMs,
maxGenerationTimeMs,
contentLength: content.length,
usage,
tokensPerMs: usage?.totalTokens && actualGenerationTimeMs ? usage.totalTokens / actualGenerationTimeMs : void 0,
charactersPerMs: actualGenerationTimeMs ? content.length / actualGenerationTimeMs : void 0
}
};
}
);
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
var hallucinationDetector = (confidenceThreshold2 = 0.7) => createOutputGuardrail(
"hallucination-detector",
(context, accumulatedText) => {
const { text, object, usage, finishReason, generationTimeMs } = extractContent(context.result);
const content = accumulatedText || (object && isObject(object) ? JSON.stringify(object) : text || "");
const uncertaintyIndicators = [
"i think",
"i believe",
"probably",
"likely",
"might be",
"could be",
"not sure",
"uncertain",
"possibly",
"perhaps",
"maybe",
"seems like",
"appears to be",
"my understanding is",
"if i recall correctly"
];
const factualClaims = [
"according to",
"studies show",
"research indicates",
"data suggests",
"statistics show",
"proven fact",
"scientific evidence",
"documented",
"confirmed by",
"established that"
];
const uncertaintyCount = uncertaintyIndicators.filter(
(indicator) => content.toLowerCase().includes(indicator)
).length;
const factualClaimCount = factualClaims.filter(
(claim) => content.toLowerCase().includes(claim)
).length;
const hallucinationScore = uncertaintyCount * 0.3 + factualClaimCount * 0.2;
const isHallucination = hallucinationScore > confidenceThreshold2;
return {
tripwireTriggered: isHallucination,
message: isHallucination ? `Potential hallucination detected (score: ${hallucinationScore})` : void 0,
severity: hallucinationScore > 0.8 ? "high" : "medium",
metadata: {
hallucinationScore,
confidenceThreshold: confidenceThreshold2,
uncertaintyCount,
factualClaimCount,
contentLength: content.length,
usage,
finishReason,
generationTimeMs
},
suggestion: "Please verify factual claims and consider requesting sources"
};
}
);
var biasDetector = () => createOutputGuardrail("bias-detector", (context) => {
const { text, object } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const lowerContent = content.toLowerCase();
const biasPatterns = {
gender: [
"men are better at",
"women are better at",
"typical male",
"typical female",
"boys will be boys",
"women should",
"men should",
"ladies",
"gentlemen"
],
racial: [
"people of that race",
"those people",
"their culture",
"natural talent",
"genetic predisposition",
"inherent ability",
"cultural background"
],
age: [
"young people today",
"older people can't",
"millennials are",
"boomers are",
"too old to",
"too young to"
],
socioeconomic: [
"poor people are",
"rich people are",
"welfare recipients",
"privileged class",
"working class",
"upper class"
]
};
const detectedBias = [];
const matches = [];
for (const [category, patterns] of Object.entries(biasPatterns)) {
const found = patterns.filter(
(pattern) => lowerContent.includes(pattern)
);
if (found.length > 0) {
detectedBias.push(category);
matches.push(...found);
}
}
return {
tripwireTriggered: detectedBias.length > 0,
message: detectedBias.length > 0 ? `Potential bias detected in categories: ${detectedBias.join(", ")}` : void 0,
severity: "medium",
metadata: {
biasCategories: detectedBias,
biasPatterns: matches,
contentLength: content.length
},
suggestion: "Consider reviewing content for potential bias and using more inclusive language"
};
});
var factualAccuracyChecker = (requireSources = false) => createOutputGuardrail(
"factual-accuracy-checker",
(context) => {
const { text, object, generationTimeMs } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const factualClaims = [
"according to",
"studies show",
"research indicates",
"data suggests",
"statistics show",
"proven fact",
"scientific evidence",
"documented",
"confirmed by",
"established that",
"published in",
"survey found"
];
const sourceCitations = [
"source:",
"reference:",
"citation:",
"published in",
"journal of",
"university of",
"institute of",
"doi:",
"isbn:",
"url:",
"http"
];
const factualClaimCount = factualClaims.filter(
(claim) => content.toLowerCase().includes(claim)
).length;
const sourceCitationCount = sourceCitations.filter(
(source) => content.toLowerCase().includes(source)
).length;
const hasUnfoundedClaims = requireSources && factualClaimCount > 0 && sourceCitationCount === 0;
return {
tripwireTriggered: hasUnfoundedClaims,
message: hasUnfoundedClaims ? `Factual claims detected without sources (${factualClaimCount} claims, ${sourceCitationCount} sources)` : void 0,
severity: "medium",
metadata: {
factualClaimCount,
sourceCitationCount,
requireSources,
contentLength: content.length,
generationTimeMs
},
suggestion: "Please provide sources for factual claims or clarify that claims are general knowledge"
};
}
);
var privacyLeakageDetector = () => createOutputGuardrail(
"privacy-leakage-detector",
(context) => {
const { text, object } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const privacyPatterns = {
personal: /\b(john|jane|smith|doe|password|secret|private|confidential)\b/gi,
contact: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
phone: /\b\d{3}-\d{3}-\d{4}\b/g,
financial: /\b(credit card|ssn|social security|bank account|routing number)\b/gi,
location: /\b(address|street|apartment|zip code|postal code)\b/gi
};
const detectedLeakage = [];
const matches = [];
for (const [category, pattern] of Object.entries(privacyPatterns)) {
const found = content.match(pattern);
if (found) {
detectedLeakage.push(category);
matches.push(...found);
}
}
return {
tripwireTriggered: detectedLeakage.length > 0,
message: detectedLeakage.length > 0 ? `Potential privacy leakage detected: ${detectedLeakage.join(", ")}` : void 0,
severity: "critical",
metadata: {
leakageCategories: detectedLeakage,
matchCount: matches.length,
contentLength: content.length
},
suggestion: "Review output for any personal or sensitive information that should be removed"
};
}
);
var contentConsistencyChecker = (referenceContent) => createOutputGuardrail(
"content-consistency-checker",
(context) => {
const { text, object } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
if (!referenceContent) {
return { tripwireTriggered: false };
}
const contentWords = content.toLowerCase().split(/\s+/);
const referenceWords = referenceContent.toLowerCase().split(/\s+/);
const commonWords = contentWords.filter(
(word) => referenceWords.includes(word)
);
const consistencyScore = commonWords.length / Math.max(contentWords.length, referenceWords.length);
const isInconsistent = consistencyScore < 0.3;
return {
tripwireTriggered: isInconsistent,
message: isInconsistent ? `Content consistency score too low: ${consistencyScore.toFixed(2)}` : void 0,
severity: "medium",
metadata: {
consistencyScore,
contentLength: content.length,
referenceLength: referenceContent.length,
commonWordCount: commonWords.length
},
suggestion: "Ensure output maintains consistency with reference content"
};
}
);
var complianceChecker = (regulations = []) => createOutputGuardrail(
"compliance-checker",
(context) => {
const { text, object } = extractContent(context.result);
const content = text || (object ? JSON.stringify(object) : "");
const compliancePatterns = {
gdpr: ["personal data", "data processing", "consent", "data subject"],
hipaa: ["patient", "medical", "health information", "protected health"],
pci: ["credit card", "payment", "cardholder", "card number"],
sox: ["financial", "audit", "internal controls", "financial reporting"],
coppa: ["children", "under 13", "parental consent", "minor"]
};
const violations = [];
for (const regulation of regulations) {
const patterns = compliancePatterns[regulation.toLowerCase()];
if (patterns) {
const found = patterns.some(
(pattern) => content.toLowerCase().includes(pattern)
);
if (found) {
violations.push(regulation.toUpperCase());
}
}
}
return {
tripwireTriggered: violations.length > 0,
message: violations.length > 0 ? `Potential compliance violations detected: ${violations.join(", ")}` : void 0,
severity: "high",
metadata: {
violations,
regulations,
contentLength: content.length,
environment: context.input?.environment
},
suggestion: "Review output for compliance with applicable regulations"
};
}
);
export {
biasDetector,
blockedContent,
blockedOutputContent,
complianceChecker,
confidenceThreshold,
contentConsistencyChecker,
customValidation,
extractContent,
factualAccuracyChecker,
hallucinationDetector,
jsonValidation,
lengthLimit,
outputLengthLimit,
performanceMonitor,
privacyLeakageDetector,
schemaValidation,
tokenUsageLimit,
toxicityFilter
};