adaptive-memory-multi-model-router 1.9.5 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +37 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +44 -0
  5. package/.github/workflows/npm-stats-validation.yml +152 -0
  6. package/.github/workflows/pages.yml +37 -0
  7. package/CHANGELOG.md +122 -0
  8. package/CODE_OF_CONDUCT.md +128 -0
  9. package/CONTRIBUTING.md +110 -0
  10. package/LAUNCH-PAIN-DRIVEN.md +339 -0
  11. package/LAUNCH.md +575 -0
  12. package/POPULARITY_BOOSTERS.md +285 -0
  13. package/README.md +231 -300
  14. package/SECURITY.md +69 -0
  15. package/articles/CONTENT_STRUCTURE.md +292 -0
  16. package/articles/DEVTO_COST_GUIDE.md +473 -0
  17. package/articles/DEVTO_FINAL.md +416 -0
  18. package/articles/DEVTO_MULTI_PROVIDER.md +542 -0
  19. package/articles/DEVTO_V2_ANNOUNCEMENT.md +160 -0
  20. package/articles/HN_10X_BETTER.md +430 -0
  21. package/articles/HN_CHINESE_STYLE.md +308 -0
  22. package/articles/HN_FINAL.md +199 -0
  23. package/articles/HN_POSTED_VERSION.md +56 -0
  24. package/articles/HN_RESEARCH.md +364 -0
  25. package/articles/PAIN-DRIVEN-devto-v2.md +308 -0
  26. package/articles/PAIN-DRIVEN-devto-v3.md +268 -0
  27. package/articles/PAIN-DRIVEN-devto.md +242 -0
  28. package/articles/PAIN-DRIVEN-hackernews-v2.md +138 -0
  29. package/articles/PAIN-DRIVEN-hackernews-v3.md +151 -0
  30. package/articles/PAIN-DRIVEN-hackernews.md +131 -0
  31. package/articles/PAIN-DRIVEN-reddit-v2.md +301 -0
  32. package/articles/PAIN-DRIVEN-reddit-v3.md +236 -0
  33. package/articles/PAIN-DRIVEN-reddit.md +218 -0
  34. package/articles/PAIN-DRIVEN-twitter-v2.md +110 -0
  35. package/articles/PAIN-DRIVEN-twitter-v3.md +121 -0
  36. package/articles/PAIN-DRIVEN-twitter.md +120 -0
  37. package/articles/PORTKEY_VS_A3M.md +147 -0
  38. package/articles/REDDIT_FINAL.md +232 -0
  39. package/articles/TWITTER_FINAL.md +167 -0
  40. package/articles/WHY_10X_BETTER.md +261 -0
  41. package/articles/WHY_CHINESE_STYLE_BETTER.md +323 -0
  42. package/articles/hashnode-llm-cost-optimization.md +125 -0
  43. package/articles/medium-building-llm-router.md +205 -0
  44. package/articles/twitter-thread-cost-savings.md +98 -0
  45. package/articles/youtube-tutorial-script.md +262 -0
  46. package/assets/banner.svg +109 -0
  47. package/assets/logo.svg +68 -0
  48. package/assets/social-preview.svg +64 -0
  49. package/demo/demo-script.md +53 -0
  50. package/dist/analytics/costAnalytics.d.ts +77 -0
  51. package/dist/analytics/costAnalytics.d.ts.map +1 -0
  52. package/dist/analytics/costAnalytics.js +219 -0
  53. package/dist/analytics/costAnalytics.js.map +1 -0
  54. package/dist/cache/semanticCache.d.ts +62 -0
  55. package/dist/cache/semanticCache.d.ts.map +1 -0
  56. package/dist/cache/semanticCache.js +176 -0
  57. package/dist/cache/semanticCache.js.map +1 -0
  58. package/dist/cli.js +35 -0
  59. package/dist/index.d.ts +30 -722
  60. package/dist/index.js +76 -355
  61. package/dist/index.js.map +1 -1
  62. package/dist/integrations/langchainAdapter.d.ts +146 -0
  63. package/dist/integrations/langchainAdapter.d.ts.map +1 -0
  64. package/dist/integrations/langchainAdapter.js +731 -0
  65. package/dist/integrations/langchainAdapter.js.map +1 -0
  66. package/dist/integrations/oauth.d.ts +69 -0
  67. package/dist/integrations/oauth.d.ts.map +1 -0
  68. package/dist/integrations/oauth.js +225 -21
  69. package/dist/integrations/oauth.js.map +1 -0
  70. package/dist/memory/autoFetch.d.ts +39 -0
  71. package/dist/memory/autoFetch.d.ts.map +1 -0
  72. package/dist/memory/autoFetch.js +80 -88
  73. package/dist/memory/autoFetch.js.map +1 -0
  74. package/dist/memory/memoryTree.d.ts +76 -0
  75. package/dist/memory/memoryTree.d.ts.map +1 -0
  76. package/dist/memory/memoryTree.js +185 -130
  77. package/dist/memory/memoryTree.js.map +1 -0
  78. package/dist/memory/obsidianVault.d.ts +71 -0
  79. package/dist/memory/obsidianVault.d.ts.map +1 -0
  80. package/dist/memory/obsidianVault.js +207 -22
  81. package/dist/memory/obsidianVault.js.map +1 -0
  82. package/dist/providers/providerConfig.d.ts +49 -0
  83. package/dist/providers/providerConfig.d.ts.map +1 -0
  84. package/dist/providers/providerConfig.js +806 -401
  85. package/dist/providers/providerConfig.js.map +1 -0
  86. package/dist/security/guardrails.d.ts +76 -0
  87. package/dist/security/guardrails.d.ts.map +1 -0
  88. package/dist/security/guardrails.js +479 -0
  89. package/dist/security/guardrails.js.map +1 -0
  90. package/dist/server/dashboard.d.ts +58 -0
  91. package/dist/server/dashboard.d.ts.map +1 -0
  92. package/dist/server/dashboard.js +553 -0
  93. package/dist/server/dashboard.js.map +1 -0
  94. package/dist/server/modelMapper.d.ts +43 -0
  95. package/dist/server/modelMapper.d.ts.map +1 -0
  96. package/dist/server/modelMapper.js +154 -0
  97. package/dist/server/modelMapper.js.map +1 -0
  98. package/dist/server/proxyServer.d.ts +41 -0
  99. package/dist/server/proxyServer.d.ts.map +1 -0
  100. package/dist/server/proxyServer.js +932 -0
  101. package/dist/server/proxyServer.js.map +1 -0
  102. package/dist/skills/__tests__/skill_manager.test.d.ts +2 -0
  103. package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -0
  104. package/dist/skills/__tests__/skill_manager.test.js +268 -0
  105. package/dist/skills/__tests__/skill_manager.test.js.map +1 -0
  106. package/docs-site/index.html +347 -0
  107. package/package.json +53 -7
  108. package/playground/README.md +51 -0
  109. package/playground/codesandbox.json +12 -0
  110. package/playground/index.js +39 -0
  111. package/scripts/update-npm-badges.js +158 -0
  112. package/src/analytics/costAnalytics.ts +304 -0
  113. package/src/cache/semanticCache.ts +221 -0
  114. package/src/index.ts +99 -0
  115. package/src/integrations/langchainAdapter.ts +955 -0
  116. package/src/providers/providerConfig.ts +923 -0
  117. package/src/security/guardrails.ts +585 -0
  118. package/src/server/dashboard.ts +610 -0
  119. package/src/server/modelMapper.ts +182 -0
  120. package/src/server/proxyServer.ts +1105 -0
  121. package/src/types/langchain.d.ts +83 -0
  122. package/tsconfig.build.json +20 -0
@@ -0,0 +1,585 @@
1
+ /**
2
+ * A3M Router - Guardrails Engine
3
+ *
4
+ * Comprehensive input/output guardrail system for production LLM routing:
5
+ * - Prompt injection detection (score-based 0-100)
6
+ * - PII detection and redaction
7
+ * - Content filtering with configurable blocklist
8
+ * - Language detection for intelligent routing
9
+ * - Output validation and quality checks
10
+ * - Custom user-defined guardrails
11
+ */
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface GuardrailResult {
18
+ passed: boolean;
19
+ blocked: boolean;
20
+ reason?: string;
21
+ modified?: string;
22
+ metadata?: Record<string, any>;
23
+ }
24
+
25
+ export interface GuardrailConfig {
26
+ // Input guardrails
27
+ promptInjection: boolean;
28
+ piiDetection: boolean;
29
+ contentFilter: boolean;
30
+ maxLength: number;
31
+ languageDetection: boolean;
32
+
33
+ // Output guardrails
34
+ outputFilter: boolean;
35
+ outputPII: boolean;
36
+ hallucinationCheck: boolean;
37
+ }
38
+
39
+ export type GuardrailCheck = (content: string) => GuardrailResult;
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Default configuration
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const DEFAULT_CONFIG: GuardrailConfig = {
46
+ promptInjection: true,
47
+ piiDetection: true,
48
+ contentFilter: true,
49
+ maxLength: 100_000,
50
+ languageDetection: false,
51
+ outputFilter: true,
52
+ outputPII: true,
53
+ hallucinationCheck: true,
54
+ };
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Prompt-injection detection
58
+ // ---------------------------------------------------------------------------
59
+
60
+ const INJECTION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
61
+ { pattern: /ignore\s+(?:previous|above|earlier|all)\s+(?:instructions?|prompts?|rules?)/i, weight: 30, label: 'ignore-instructions' },
62
+ { pattern: /disregard\s+(?:previous|above|earlier|all)/i, weight: 25, label: 'disregard' },
63
+ { pattern: /forget\s+(?:previous|above|earlier|everything|all)/i, weight: 20, label: 'forget' },
64
+ { pattern: /^system\s*:/im, weight: 35, label: 'system-prefix' },
65
+ { pattern: /\[\s*system\s*\]/i, weight: 30, label: 'system-bracket' },
66
+ { pattern: /<\s*system\s*>/i, weight: 30, label: 'system-tag' },
67
+ { pattern: /\{\s*system\s*\}/i, weight: 25, label: 'system-brace' },
68
+ { pattern: /you\s+are\s+now\s+/i, weight: 25, label: 'you-are-now' },
69
+ { pattern: /new\s+instruction/i, weight: 20, label: 'new-instruction' },
70
+ { pattern: /override\s+(?:previous|settings|safety|filter)/i, weight: 30, label: 'override' },
71
+ { pattern: /bypass\s+(?:filter|restriction|safety|guard|security)/i, weight: 30, label: 'bypass' },
72
+ { pattern: /DAN\s*[\(\[]/i, weight: 35, label: 'dan-attack' },
73
+ { pattern: /jailbreak/i, weight: 35, label: 'jailbreak' },
74
+ { pattern: /act\s+as\s+(?:if\s+you|a\s+different)/i, weight: 15, label: 'act-as' },
75
+ { pattern: /pretend\s+(?:you\s+are|to\s+be)/i, weight: 15, label: 'pretend' },
76
+ { pattern: /simulate\s+(?:a\s+)?(?:different|new)\s+(?:persona|identity|character)/i, weight: 15, label: 'simulate-persona' },
77
+ { pattern: /reveal\s+(?:your|the|hidden|secret)/i, weight: 10, label: 'reveal-secrets' },
78
+ { pattern: /output\s+(?:your|the)\s+(?:system|initial|original)\s+(?:prompt|instructions?)/i, weight: 25, label: 'extract-prompt' },
79
+ ];
80
+
81
+ function scoreInjection(text: string): { score: number; triggers: string[] } {
82
+ let score = 0;
83
+ const triggers: string[] = [];
84
+
85
+ for (const { pattern, weight, label } of INJECTION_PATTERNS) {
86
+ if (pattern.test(text)) {
87
+ score += weight;
88
+ triggers.push(label);
89
+ }
90
+ }
91
+
92
+ // Clamp to 0-100
93
+ return { score: Math.min(score, 100), triggers };
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // PII detection & redaction
98
+ // ---------------------------------------------------------------------------
99
+
100
+ const PII_REDACTION_RULES: Array<{
101
+ type: string;
102
+ pattern: RegExp;
103
+ replacement: string;
104
+ }> = [
105
+ {
106
+ type: 'email',
107
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
108
+ replacement: '[EMAIL_REDACTED]',
109
+ },
110
+ {
111
+ type: 'phone',
112
+ pattern: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
113
+ replacement: '[PHONE_REDACTED]',
114
+ },
115
+ {
116
+ type: 'ssn',
117
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
118
+ replacement: '[SSN_REDACTED]',
119
+ },
120
+ {
121
+ type: 'creditCard',
122
+ pattern: /\b(?:\d[ -]*?){13,19}\b/g,
123
+ replacement: '[CC_REDACTED]',
124
+ },
125
+ {
126
+ type: 'apiKey',
127
+ pattern: /\b(?:sk-[a-zA-Z0-9]{20,}|key-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})\b/g,
128
+ replacement: '[API_KEY_REDACTED]',
129
+ },
130
+ {
131
+ type: 'ipAddress',
132
+ pattern: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|1?\d{1,2})\b/g,
133
+ replacement: '[IP_REDACTED]',
134
+ },
135
+ ];
136
+
137
+ function detectPII(text: string): { types: string[]; matches: Record<string, number> } {
138
+ const types: string[] = [];
139
+ const matches: Record<string, number> = {};
140
+
141
+ for (const rule of PII_REDACTION_RULES) {
142
+ // Clone regex because .test() advances lastIndex on global regexes
143
+ const re = new RegExp(rule.pattern.source, rule.pattern.flags);
144
+ const found = text.match(re);
145
+ if (found && found.length > 0) {
146
+ types.push(rule.type);
147
+ matches[rule.type] = found.length;
148
+ }
149
+ }
150
+
151
+ return { types, matches };
152
+ }
153
+
154
+ function redactPII(text: string): string {
155
+ let result = text;
156
+ for (const rule of PII_REDACTION_RULES) {
157
+ result = result.replace(rule.pattern, rule.replacement);
158
+ }
159
+ return result;
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Content filter
164
+ // ---------------------------------------------------------------------------
165
+
166
+ interface ContentRule {
167
+ category: string;
168
+ pattern: RegExp;
169
+ severity: 'warn' | 'block';
170
+ }
171
+
172
+ const DEFAULT_CONTENT_RULES: ContentRule[] = [
173
+ // Violence / hate
174
+ { category: 'hate', pattern: /\b(?:kill\s+(?:all|every|those)|ethnic\s+cleansing|genocide)\b/i, severity: 'block' },
175
+ { category: 'violence', pattern: /\b(?:how\s+to\s+(?:make|build|create)\s+(?:bomb|weapon|explosive))\b/i, severity: 'block' },
176
+ // Self-harm
177
+ { category: 'selfHarm', pattern: /\b(?:how\s+to\s+(?:commit|do)\s+(?:suicide|self\s*harm)|kill\s+myself)\b/i, severity: 'block' },
178
+ // Exploitation
179
+ { category: 'exploitation', pattern: /\b(?:child\s+(?:abuse|exploitation|pornography))\b/i, severity: 'block' },
180
+ // Illegal activity
181
+ { category: 'illegal', pattern: /\b(?:how\s+to\s+(?:hack|steal|commit\s+fraud))\b/i, severity: 'warn' },
182
+ ];
183
+
184
+ function filterContent(text: string, rules: ContentRule[]): { flagged: boolean; categories: string[]; action: 'pass' | 'warn' | 'block' } {
185
+ const categories: string[] = [];
186
+ let maxSeverity: 'pass' | 'warn' | 'block' = 'pass';
187
+
188
+ for (const rule of rules) {
189
+ if (rule.pattern.test(text)) {
190
+ categories.push(rule.category);
191
+ if (rule.severity === 'block') {
192
+ maxSeverity = 'block';
193
+ } else if (maxSeverity === 'pass') {
194
+ maxSeverity = 'warn';
195
+ }
196
+ }
197
+ }
198
+
199
+ return { flagged: categories.length > 0, categories, action: maxSeverity };
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Language detection (script-based)
204
+ // ---------------------------------------------------------------------------
205
+
206
+ export type DetectedLanguage = 'latin' | 'cjk' | 'cyrillic' | 'arabic' | 'devanagari' | 'mixed' | 'unknown';
207
+
208
+ interface LanguageDetectionResult {
209
+ primary: DetectedLanguage;
210
+ details: Record<DetectedLanguage, number>;
211
+ suggestedProviders: string[];
212
+ }
213
+
214
+ const LANGUAGE_PROVIDER_HINTS: Record<DetectedLanguage, string[]> = {
215
+ latin: ['openai', 'anthropic', 'google', 'groq', 'mistral'],
216
+ cjk: ['google', 'openai', 'anthropic'],
217
+ cyrillic: ['openai', 'anthropic', 'google', 'mistral'],
218
+ arabic: ['google', 'openai', 'anthropic'],
219
+ devanagari: ['google', 'openai', 'anthropic'],
220
+ mixed: ['google', 'openai', 'anthropic'],
221
+ unknown: ['openai', 'anthropic', 'google'],
222
+ };
223
+
224
+ function detectLanguage(text: string): LanguageDetectionResult {
225
+ const scripts: Record<DetectedLanguage, number> = {
226
+ latin: 0,
227
+ cjk: 0,
228
+ cyrillic: 0,
229
+ arabic: 0,
230
+ devanagari: 0,
231
+ mixed: 0,
232
+ unknown: 0,
233
+ };
234
+
235
+ for (const ch of text) {
236
+ const cp = ch.codePointAt(0)!;
237
+ if (cp >= 0x0041 && cp <= 0x024F) { scripts.latin++; }
238
+ else if ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3040 && cp <= 0x309F) || (cp >= 0x30A0 && cp <= 0x30FF) || (cp >= 0xAC00 && cp <= 0xD7AF)) { scripts.cjk++; }
239
+ else if (cp >= 0x0400 && cp <= 0x04FF) { scripts.cyrillic++; }
240
+ else if (cp >= 0x0600 && cp <= 0x06FF) { scripts.arabic++; }
241
+ else if (cp >= 0x0900 && cp <= 0x097F) { scripts.devanagari++; }
242
+ else if (cp > 0x7F && !/\s/.test(ch)) { scripts.unknown++; }
243
+ }
244
+
245
+ // Determine primary
246
+ const relevant = (['latin', 'cjk', 'cyrillic', 'arabic', 'devanagari'] as DetectedLanguage[])
247
+ .filter((s) => scripts[s] > 0);
248
+
249
+ let primary: DetectedLanguage = 'unknown';
250
+ if (relevant.length === 0) {
251
+ primary = 'latin'; // Default for pure ASCII
252
+ } else if (relevant.length === 1) {
253
+ primary = relevant[0];
254
+ } else {
255
+ primary = 'mixed';
256
+ }
257
+
258
+ return {
259
+ primary,
260
+ details: scripts,
261
+ suggestedProviders: LANGUAGE_PROVIDER_HINTS[primary],
262
+ };
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Output validation
267
+ // ---------------------------------------------------------------------------
268
+
269
+ interface OutputValidationResult {
270
+ valid: boolean;
271
+ issues: string[];
272
+ qualityScore: number; // 0-100
273
+ }
274
+
275
+ function validateOutput(input: string, output: string): OutputValidationResult {
276
+ const issues: string[] = [];
277
+ let qualityScore = 100;
278
+
279
+ // Empty output
280
+ if (!output || output.trim().length === 0) {
281
+ issues.push('empty_output');
282
+ qualityScore -= 50;
283
+ }
284
+
285
+ // Very short output for non-trivial input
286
+ if (input.length > 100 && output.trim().length < 10) {
287
+ issues.push('suspiciously_short');
288
+ qualityScore -= 20;
289
+ }
290
+
291
+ // Repetition detection
292
+ const words = output.split(/\s+/);
293
+ if (words.length > 20) {
294
+ const unique = new Set(words.map((w) => w.toLowerCase()));
295
+ const ratio = unique.size / words.length;
296
+ if (ratio < 0.3) {
297
+ issues.push('high_repetition');
298
+ qualityScore -= 25;
299
+ }
300
+ }
301
+
302
+ // GPT refusal patterns
303
+ const refusalPatterns = [
304
+ /I\s+(?:can'?t|cannot|won't|am\s+not\s+able\s+to)\s+(?:help|assist|do\s+that|provide)/i,
305
+ /As\s+an?\s+AI/i,
306
+ /I'?m\s+(?:sorry|unable)/i,
307
+ ];
308
+ for (const p of refusalPatterns) {
309
+ if (p.test(output)) {
310
+ issues.push('refusal_detected');
311
+ qualityScore -= 10;
312
+ break;
313
+ }
314
+ }
315
+
316
+ // Hallucination heuristic: output repeats the question verbatim without answering
317
+ const inputLower = input.toLowerCase().trim();
318
+ const outputLower = output.toLowerCase().trim();
319
+ if (outputLower === inputLower) {
320
+ issues.push('echo_response');
321
+ qualityScore -= 30;
322
+ }
323
+
324
+ return {
325
+ valid: qualityScore >= 20,
326
+ issues,
327
+ qualityScore: Math.max(0, qualityScore),
328
+ };
329
+ }
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // GuardrailEngine
333
+ // ---------------------------------------------------------------------------
334
+
335
+ export class GuardrailEngine {
336
+ private config: GuardrailConfig;
337
+ private customGuardrails: Map<string, GuardrailCheck> = new Map();
338
+ private contentRules: ContentRule[];
339
+ private blocklist: string[] = [];
340
+
341
+ constructor(config?: Partial<GuardrailConfig>) {
342
+ this.config = { ...DEFAULT_CONFIG, ...config };
343
+ this.contentRules = [...DEFAULT_CONTENT_RULES];
344
+ }
345
+
346
+ // ---- Public API ----
347
+
348
+ /**
349
+ * Check input content before routing to an LLM.
350
+ */
351
+ async checkInput(content: string): Promise<GuardrailResult> {
352
+ const metadata: Record<string, any> = {};
353
+
354
+ // Length check
355
+ if (content.length > this.config.maxLength) {
356
+ return {
357
+ passed: false,
358
+ blocked: true,
359
+ reason: `Input exceeds maximum length (${content.length} > ${this.config.maxLength})`,
360
+ modified: content.substring(0, this.config.maxLength),
361
+ metadata: { truncated: true, originalLength: content.length },
362
+ };
363
+ }
364
+
365
+ let modified = content;
366
+
367
+ // Prompt injection
368
+ if (this.config.promptInjection) {
369
+ const injection = scoreInjection(content);
370
+ metadata.injectionScore = injection.score;
371
+ metadata.injectionTriggers = injection.triggers;
372
+
373
+ if (injection.score >= 80) {
374
+ return {
375
+ passed: false,
376
+ blocked: true,
377
+ reason: `Prompt injection detected (score: ${injection.score})`,
378
+ metadata,
379
+ };
380
+ }
381
+ if (injection.score >= 50) {
382
+ metadata.injectionWarning = true;
383
+ }
384
+ }
385
+
386
+ // PII detection + redaction
387
+ if (this.config.piiDetection) {
388
+ const pii = detectPII(content);
389
+ metadata.piiTypes = pii.types;
390
+ metadata.piiCounts = pii.matches;
391
+
392
+ if (pii.types.length > 0) {
393
+ modified = redactPII(modified);
394
+ metadata.piiRedacted = true;
395
+ }
396
+ }
397
+
398
+ // Content filter
399
+ if (this.config.contentFilter) {
400
+ const contentResult = filterContent(modified, this.contentRules);
401
+ metadata.contentFlagged = contentResult.flagged;
402
+ metadata.contentCategories = contentResult.categories;
403
+
404
+ if (contentResult.action === 'block') {
405
+ return {
406
+ passed: false,
407
+ blocked: true,
408
+ reason: `Content blocked: ${contentResult.categories.join(', ')}`,
409
+ metadata,
410
+ };
411
+ }
412
+ if (contentResult.action === 'warn') {
413
+ metadata.contentWarning = true;
414
+ }
415
+ }
416
+
417
+ // Blocklist check
418
+ if (this.blocklist.length > 0) {
419
+ const lowerContent = modified.toLowerCase();
420
+ for (const term of this.blocklist) {
421
+ if (lowerContent.includes(term.toLowerCase())) {
422
+ return {
423
+ passed: false,
424
+ blocked: true,
425
+ reason: `Content blocked by blocklist: "${term}"`,
426
+ metadata,
427
+ };
428
+ }
429
+ }
430
+ }
431
+
432
+ // Language detection
433
+ if (this.config.languageDetection) {
434
+ const lang = detectLanguage(modified);
435
+ metadata.language = lang.primary;
436
+ metadata.suggestedProviders = lang.suggestedProviders;
437
+ }
438
+
439
+ // Custom guardrails
440
+ for (const [name, check] of this.customGuardrails) {
441
+ const result = check(modified);
442
+ if (result.blocked) {
443
+ return {
444
+ passed: false,
445
+ blocked: true,
446
+ reason: result.reason || `Blocked by custom guardrail: ${name}`,
447
+ metadata: { ...metadata, ...result.metadata, blockedBy: name },
448
+ };
449
+ }
450
+ if (result.modified) {
451
+ modified = result.modified;
452
+ }
453
+ }
454
+
455
+ return {
456
+ passed: true,
457
+ blocked: false,
458
+ modified: modified !== content ? modified : undefined,
459
+ metadata,
460
+ };
461
+ }
462
+
463
+ /**
464
+ * Check output content before returning to caller.
465
+ */
466
+ async checkOutput(input: string, output: string): Promise<GuardrailResult> {
467
+ const metadata: Record<string, any> = {};
468
+ let modified = output;
469
+
470
+ // PII in output
471
+ if (this.config.outputPII) {
472
+ const pii = detectPII(output);
473
+ metadata.piiTypes = pii.types;
474
+ if (pii.types.length > 0) {
475
+ modified = redactPII(modified);
476
+ metadata.piiRedacted = true;
477
+ }
478
+ }
479
+
480
+ // Content filter on output
481
+ if (this.config.outputFilter) {
482
+ const contentResult = filterContent(modified, this.contentRules);
483
+ metadata.contentFlagged = contentResult.flagged;
484
+ metadata.contentCategories = contentResult.categories;
485
+
486
+ if (contentResult.action === 'block') {
487
+ return {
488
+ passed: false,
489
+ blocked: true,
490
+ reason: `Output blocked: ${contentResult.categories.join(', ')}`,
491
+ metadata,
492
+ };
493
+ }
494
+ }
495
+
496
+ // Quality / hallucination checks
497
+ if (this.config.hallucinationCheck) {
498
+ const validation = validateOutput(input, modified);
499
+ metadata.qualityScore = validation.qualityScore;
500
+ metadata.issues = validation.issues;
501
+
502
+ if (!validation.valid) {
503
+ return {
504
+ passed: false,
505
+ blocked: true,
506
+ reason: `Output quality check failed: ${validation.issues.join(', ')}`,
507
+ metadata,
508
+ };
509
+ }
510
+ }
511
+
512
+ // Custom guardrails on output
513
+ for (const [name, check] of this.customGuardrails) {
514
+ const result = check(modified);
515
+ if (result.blocked) {
516
+ return {
517
+ passed: false,
518
+ blocked: true,
519
+ reason: result.reason || `Output blocked by custom guardrail: ${name}`,
520
+ metadata: { ...metadata, ...result.metadata, blockedBy: name },
521
+ };
522
+ }
523
+ if (result.modified) {
524
+ modified = result.modified;
525
+ }
526
+ }
527
+
528
+ return {
529
+ passed: true,
530
+ blocked: false,
531
+ modified: modified !== output ? modified : undefined,
532
+ metadata,
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Register a custom guardrail check function.
538
+ */
539
+ addGuardrail(name: string, check: GuardrailCheck): void {
540
+ this.customGuardrails.set(name, check);
541
+ }
542
+
543
+ /**
544
+ * Remove a custom guardrail.
545
+ */
546
+ removeGuardrail(name: string): boolean {
547
+ return this.customGuardrails.delete(name);
548
+ }
549
+
550
+ /**
551
+ * Add a term to the blocklist.
552
+ */
553
+ addBlocklistTerm(term: string): void {
554
+ this.blocklist.push(term);
555
+ }
556
+
557
+ /**
558
+ * Add a content filter rule.
559
+ */
560
+ addContentRule(rule: ContentRule): void {
561
+ this.contentRules.push(rule);
562
+ }
563
+
564
+ /**
565
+ * Get the current configuration.
566
+ */
567
+ getConfig(): Readonly<GuardrailConfig> {
568
+ return { ...this.config };
569
+ }
570
+
571
+ /**
572
+ * Update configuration.
573
+ */
574
+ updateConfig(partial: Partial<GuardrailConfig>): void {
575
+ Object.assign(this.config, partial);
576
+ }
577
+ }
578
+
579
+ // ---------------------------------------------------------------------------
580
+ // Convenience factory
581
+ // ---------------------------------------------------------------------------
582
+
583
+ export function createGuardrails(config?: Partial<GuardrailConfig>): GuardrailEngine {
584
+ return new GuardrailEngine(config);
585
+ }