UNPKG

ai-sdk-guardrails

Version:

Input and output guardrails middleware for Vercel AI SDK.

494 lines (490 loc) 16 kB
"use strict"; 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 });