UNPKG

ai-sdk-guardrails

Version:

Input and output guardrails middleware for Vercel AI SDK.

699 lines (695 loc) 23.3 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/output.ts var output_exports = {}; __export(output_exports, { biasDetector: () => biasDetector, blockedContent: () => blockedContent, blockedOutputContent: () => blockedOutputContent, complianceChecker: () => complianceChecker, confidenceThreshold: () => confidenceThreshold, contentConsistencyChecker: () => contentConsistencyChecker, customValidation: () => customValidation, extractContent: () => extractContent, factualAccuracyChecker: () => factualAccuracyChecker, hallucinationDetector: () => hallucinationDetector, jsonValidation: () => jsonValidation, lengthLimit: () => lengthLimit, outputLengthLimit: () => outputLengthLimit, performanceMonitor: () => performanceMonitor, privacyLeakageDetector: () => privacyLeakageDetector, schemaValidation: () => schemaValidation, tokenUsageLimit: () => tokenUsageLimit, toxicityFilter: () => toxicityFilter }); module.exports = __toCommonJS(output_exports); // src/core.ts function createOutputGuardrail(name, execute) { return { name, execute }; } // 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" }; } ); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { biasDetector, blockedContent, blockedOutputContent, complianceChecker, confidenceThreshold, contentConsistencyChecker, customValidation, extractContent, factualAccuracyChecker, hallucinationDetector, jsonValidation, lengthLimit, outputLengthLimit, performanceMonitor, privacyLeakageDetector, schemaValidation, tokenUsageLimit, toxicityFilter });