ai-sdk-guardrails
Version:
Input and output guardrails middleware for Vercel AI SDK.
494 lines (490 loc) • 16 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/guardrails/input.ts
var input_exports = {};
__export(input_exports, {
blockedKeywords: () => blockedKeywords,
blockedWords: () => blockedWords,
codeGenerationLimiter: () => codeGenerationLimiter,
contentLengthLimit: () => contentLengthLimit,
customValidation: () => customValidation,
extractMetadata: () => extractMetadata,
extractTextContent: () => extractTextContent,
lengthLimit: () => lengthLimit,
mathHomeworkDetector: () => mathHomeworkDetector,
piiDetector: () => piiDetector,
profanityFilter: () => profanityFilter,
promptInjectionDetector: () => promptInjectionDetector,
rateLimiting: () => rateLimiting,
toxicityDetector: () => toxicityDetector
});
module.exports = __toCommonJS(input_exports);
// src/core.ts
function createInputGuardrail(name, description, execute) {
return { name, description, execute };
}
// src/guardrails/input.ts
function isEmbedParams(context) {
return "value" in context && !("prompt" in context);
}
function hasContextProperty(context) {
return "context" in context;
}
function extractTextContent(context) {
if (isEmbedParams(context)) {
const embedContext = context;
return {
prompt: String(embedContext.value || ""),
messages: [],
system: ""
};
}
const textContext = context;
return {
prompt: textContext.prompt || "",
messages: textContext.messages || [],
system: textContext.system || ""
};
}
function extractMetadata(context) {
const contextWithMetadata = context;
return {
model: contextWithMetadata.model,
temperature: contextWithMetadata.temperature,
maxOutputTokens: contextWithMetadata.maxOutputTokens
};
}
var lengthLimit = (maxLength) => createInputGuardrail(
"length-limit",
"Enforces maximum input length limit",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const { model, temperature, maxOutputTokens } = extractMetadata(context);
const totalLength = prompt.length + messages.reduce(
(sum, msg) => sum + String(msg?.content || "").length,
0
) + system.length;
return {
tripwireTriggered: totalLength > maxLength,
message: `Input length ${totalLength} exceeds limit of ${maxLength}`,
severity: "medium",
metadata: {
totalLength,
maxLength,
model: model ? String(model) : void 0,
temperature,
maxOutputTokens,
messageCount: messages.length
}
};
}
);
var blockedWords = (words) => createInputGuardrail(
"blocked-words",
"Blocks input containing specified words",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ").toLowerCase();
const blockedWord = words.find(
(word) => allText.includes(word.toLowerCase())
);
return {
tripwireTriggered: !!blockedWord,
message: blockedWord ? `Blocked word detected: ${blockedWord}` : void 0,
severity: "high",
metadata: {
blockedWord,
allWords: words
}
};
}
);
var contentLengthLimit = (maxLength) => createInputGuardrail(
"content-length-limit",
"Enforces maximum content length limit",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const totalLength = prompt.length + messages.reduce(
(sum, msg) => sum + String(msg?.content || "").length,
0
) + system.length;
return {
tripwireTriggered: totalLength > maxLength,
message: `Content length ${totalLength} exceeds limit of ${maxLength}`,
severity: "medium",
metadata: {
totalLength,
maxLength
}
};
}
);
var blockedKeywords = (keywords) => createInputGuardrail(
"blocked-keywords",
"Blocks input containing specified keywords",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ").toLowerCase();
const blockedWord = keywords.find(
(word) => allText.includes(word.toLowerCase())
);
return {
tripwireTriggered: !!blockedWord,
message: blockedWord ? `Blocked keyword detected: ${blockedWord}` : void 0,
severity: "high",
metadata: {
blockedWord,
allKeywords: keywords,
textLength: allText.length
}
};
}
);
var rateLimiting = (maxRequestsPerMinute) => {
const requestCounts = /* @__PURE__ */ new Map();
return createInputGuardrail(
"rate-limiting",
"Enforces rate limiting per minute",
(inputContext) => {
const { prompt } = extractTextContent(inputContext);
const { model, temperature, maxOutputTokens } = extractMetadata(inputContext);
let contextData;
if (hasContextProperty(inputContext)) {
contextData = inputContext.context;
}
const key = contextData?.user?.id || contextData?.request?.ip || "default";
const now = Date.now();
const windowMs = 6e4;
const current = requestCounts.get(key) || {
count: 0,
resetTime: now + windowMs
};
if (now > current.resetTime) {
current.count = 0;
current.resetTime = now + windowMs;
}
current.count++;
requestCounts.set(key, current);
return {
tripwireTriggered: current.count > maxRequestsPerMinute,
message: `Rate limit exceeded: ${current.count}/${maxRequestsPerMinute} requests per minute`,
severity: "medium",
metadata: {
currentCount: current.count,
maxRequests: maxRequestsPerMinute,
resetTime: current.resetTime,
userId: contextData?.user?.id,
userIp: contextData?.request?.ip,
model: model ? String(model) : void 0,
temperature,
maxOutputTokens,
promptLength: prompt.length
}
};
}
);
};
var profanityFilter = (customWords = []) => {
const defaultProfanity = ["profanity1", "profanity2"];
const allWords = [...defaultProfanity, ...customWords];
return createInputGuardrail(
"profanity-filter",
"Filters profanity and inappropriate language",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ").toLowerCase();
const profaneWord = allWords.find(
(word) => allText.includes(word.toLowerCase())
);
return {
tripwireTriggered: !!profaneWord,
message: profaneWord ? `Profanity detected: ${profaneWord}` : void 0,
severity: "high",
metadata: {
profaneWord,
allWords,
textLength: allText.length
}
};
}
);
};
var customValidation = (name, description, validator, message) => createInputGuardrail(name, description, (context) => {
const { prompt, messages, system } = extractTextContent(context);
const { model, temperature, maxOutputTokens } = extractMetadata(context);
const input = {
prompt,
messages,
system,
model,
temperature,
maxOutputTokens
};
const blocked = validator(input);
return {
tripwireTriggered: blocked,
message: blocked ? message : void 0,
severity: "medium",
metadata: {
validatorName: name,
inputKeys: Object.keys(input),
model: model ? String(model) : void 0,
temperature,
maxOutputTokens
}
};
});
var promptInjectionDetector = () => createInputGuardrail(
"prompt-injection-detector",
"Detects potential prompt injection attempts",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ");
const injectionPatterns = [
/ignore\s+previous\s+instructions/i,
/system\s*:\s*you\s+are\s+now/i,
/forget\s+everything\s+above/i,
/\bDAN\b.*mode/i,
/jailbreak/i,
/act\s+as\s+if\s+you\s+are/i,
/pretend\s+to\s+be/i,
/role\s*:\s*system/i
];
const detectedPatterns = injectionPatterns.filter(
(pattern) => pattern.test(allText)
);
return {
tripwireTriggered: detectedPatterns.length > 0,
message: detectedPatterns.length > 0 ? `Potential prompt injection detected: ${detectedPatterns.length} suspicious patterns found` : void 0,
severity: "critical",
metadata: {
patternsDetected: detectedPatterns.length,
textLength: allText.length,
suspiciousPatterns: detectedPatterns.map((p) => p.source)
},
suggestion: "Please rephrase your request without system instructions or role-playing elements"
};
}
);
var piiDetector = () => createInputGuardrail(
"pii-detector",
"Detects personally identifiable information in input",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ");
const piiPatterns = {
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
phone: /\b(?:\+?1[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})\b/g,
ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
creditCard: /\b(?:\d{4}[\s-]?){3}\d{4}\b/g,
ipAddress: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g
};
const detectedPII = [];
const matches = [];
for (const [type, pattern] of Object.entries(piiPatterns)) {
const found = allText.match(pattern);
if (found) {
detectedPII.push(type);
matches.push(...found);
}
}
return {
tripwireTriggered: detectedPII.length > 0,
message: detectedPII.length > 0 ? `PII detected: ${detectedPII.join(", ")}` : void 0,
severity: "critical",
metadata: {
piiTypes: detectedPII,
matchCount: matches.length,
textLength: allText.length
},
suggestion: "Please remove any personal information (emails, phone numbers, SSNs, etc.) from your input"
};
}
);
var toxicityDetector = (threshold = 0.7) => createInputGuardrail(
"toxicity-detector",
"Detects toxic and harmful content in input",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ").toLowerCase();
const toxicWords = [
"hate",
"kill",
"die",
"stupid",
"idiot",
"moron",
"toxic",
"harmful"
];
const detectedWords = toxicWords.filter((word) => allText.includes(word));
const toxicityScore = detectedWords.length * 0.3;
return {
tripwireTriggered: toxicityScore > threshold,
message: toxicityScore > threshold ? `Toxic content detected (score: ${toxicityScore})` : void 0,
severity: toxicityScore > 0.8 ? "critical" : "high",
metadata: {
toxicityScore,
threshold,
detectedWords,
textLength: allText.length
},
suggestion: "Please use respectful and constructive language"
};
}
);
var mathHomeworkDetector = () => createInputGuardrail(
"math-homework-detector",
"Detects potential math homework requests",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ");
const mathKeywords = [
"solve",
"calculate",
"equation",
"homework",
"assignment",
"problem set"
];
const mathPatterns = [
/\b\d+\s*[+\-*/]\s*\d+/g,
/\b[xy]\s*[+\-*/=]\s*\d+/g,
/\b(derivative|integral|limit|theorem|proof)/gi,
/find\s+the\s+(value|solution|answer)/i
];
const keywordMatches = mathKeywords.filter(
(keyword) => allText.toLowerCase().includes(keyword)
);
const patternMatches = mathPatterns.filter(
(pattern) => pattern.test(allText)
);
const isMathHomework = keywordMatches.length >= 2 || patternMatches.length > 0;
return {
tripwireTriggered: isMathHomework,
message: isMathHomework ? "Math homework request detected" : void 0,
severity: "high",
metadata: {
keywordMatches,
patternMatches: patternMatches.length,
textLength: allText.length,
confidence: isMathHomework ? 0.85 : 0.15
},
suggestion: "Try asking about learning concepts instead of solving specific problems"
};
}
);
var codeGenerationLimiter = (allowedLanguages = []) => createInputGuardrail(
"code-generation-limiter",
"Limits code generation to specified languages",
(context) => {
const { prompt, messages, system } = extractTextContent(context);
const allText = [
prompt,
...messages.map((msg) => String(msg?.content || "")),
system
].join(" ").toLowerCase();
const codeKeywords = [
"write code",
"generate code",
"create function",
"implement",
"script"
];
const languagePatterns = {
javascript: /\b(javascript|js|node|react|angular|vue)\b/gi,
python: /\b(python|py|django|flask|pandas)\b/gi,
java: /\b(java|spring|hibernate)\b/gi,
cpp: /\b(c\+\+|cpp|c plus plus)\b/gi,
csharp: /\b(c#|csharp|dotnet|asp\.net)\b/gi,
php: /\b(php|laravel|symfony)\b/gi,
ruby: /\b(ruby|rails|gem)\b/gi,
go: /\b(golang|go)\b/gi,
rust: /\b(rust|cargo)\b/gi,
sql: /\b(sql|mysql|postgresql|oracle)\b/gi
};
const hasCodeRequest = codeKeywords.some(
(keyword) => allText.includes(keyword)
);
const detectedLanguages = [];
for (const [lang, pattern] of Object.entries(languagePatterns)) {
if (pattern.test(allText)) {
detectedLanguages.push(lang);
}
}
const hasRestrictedLanguage = hasCodeRequest && detectedLanguages.length > 0 && !detectedLanguages.some((lang) => allowedLanguages.includes(lang));
return {
tripwireTriggered: hasRestrictedLanguage,
message: hasRestrictedLanguage ? `Code generation requested for restricted language(s): ${detectedLanguages.join(", ")}` : void 0,
severity: "medium",
metadata: {
hasCodeRequest,
detectedLanguages,
allowedLanguages,
textLength: allText.length
},
suggestion: allowedLanguages.length > 0 ? `Please request code only in allowed languages: ${allowedLanguages.join(", ")}` : "Code generation is not allowed"
};
}
);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
blockedKeywords,
blockedWords,
codeGenerationLimiter,
contentLengthLimit,
customValidation,
extractMetadata,
extractTextContent,
lengthLimit,
mathHomeworkDetector,
piiDetector,
profanityFilter,
promptInjectionDetector,
rateLimiting,
toxicityDetector
});