@vpdeva/blackwall-llm-shield-js 0.1.2 → 0.1.5
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.
- package/README.md +107 -9
- package/package.json +9 -2
- package/src/index.js +390 -31
- package/src/providers.js +152 -0
package/README.md
CHANGED
|
@@ -12,7 +12,10 @@ JavaScript security middleware for LLM applications in Node.js and Next.js. Blac
|
|
|
12
12
|
- Supports shadow mode and side-by-side policy-pack evaluation
|
|
13
13
|
- Notifies webhooks or alert handlers when risky traffic appears
|
|
14
14
|
- Emits structured telemetry for prompt risk, masking volume, and output review outcomes
|
|
15
|
+
- Includes first-class provider adapters for OpenAI, Anthropic, Gemini, and OpenRouter
|
|
15
16
|
- Inspects model outputs for leaks, unsafe code, grounding drift, and tone violations
|
|
17
|
+
- Handles mixed text, image, and file message parts more gracefully in text-first multimodal flows
|
|
18
|
+
- Adds operator-friendly telemetry summaries and stronger presets for RAG and agent-tool workflows
|
|
16
19
|
- Ships Express, LangChain, and LlamaIndex integration helpers
|
|
17
20
|
- Enforces allowlists, denylists, validators, and approval-gated tools
|
|
18
21
|
- Sanitizes RAG documents before they are injected into context
|
|
@@ -74,6 +77,14 @@ console.log(guarded.report);
|
|
|
74
77
|
|
|
75
78
|
Use `shadowMode` with `shadowPolicyPacks` or `comparePolicyPacks` to record what would have been blocked without interrupting traffic.
|
|
76
79
|
|
|
80
|
+
### Provider adapters and stable wrappers
|
|
81
|
+
|
|
82
|
+
Use `createOpenAIAdapter()`, `createAnthropicAdapter()`, `createGeminiAdapter()`, or `createOpenRouterAdapter()` with `protectWithAdapter()` when you want Blackwall to wrap the provider call end to end.
|
|
83
|
+
|
|
84
|
+
### Observability and control-plane support
|
|
85
|
+
|
|
86
|
+
Use `summarizeOperationalTelemetry()` with emitted telemetry events when you want route-level summaries, blocked-event counts, and rollout visibility for operators.
|
|
87
|
+
|
|
77
88
|
### Output grounding and tone review
|
|
78
89
|
|
|
79
90
|
`OutputFirewall` can compare responses against retrieved documents and flag hallucination-style unsupported claims or unprofessional tone.
|
|
@@ -86,13 +97,15 @@ Use `createExpressMiddleware()`, `createLangChainCallbacks()`, or `createLlamaIn
|
|
|
86
97
|
|
|
87
98
|
Use `require('blackwall-llm-shield-js/integrations')` for callback wrappers and `require('blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
|
|
88
99
|
|
|
100
|
+
Use `require('blackwall-llm-shield-js/providers')` for provider adapter factories.
|
|
101
|
+
|
|
89
102
|
## Core Building Blocks
|
|
90
103
|
|
|
91
104
|
### `BlackwallShield`
|
|
92
105
|
|
|
93
106
|
Use it to sanitize inbound messages, mask sensitive data, score prompt-injection risk, and decide whether the request should continue to the model provider.
|
|
94
107
|
|
|
95
|
-
It also exposes `protectModelCall()` and `reviewModelResponse()` so you can enforce request checks before
|
|
108
|
+
It also exposes `protectModelCall()`, `protectWithAdapter()`, and `reviewModelResponse()` so you can enforce request checks before provider calls and review outputs before they go back to the user.
|
|
96
109
|
|
|
97
110
|
### `OutputFirewall`
|
|
98
111
|
|
|
@@ -106,6 +119,17 @@ Use it to allowlist tools, block disallowed tools, validate arguments, and requi
|
|
|
106
119
|
|
|
107
120
|
Use it before injecting retrieved documents into context so hostile instructions in your RAG data store do not quietly become model instructions.
|
|
108
121
|
|
|
122
|
+
### Contract Stability
|
|
123
|
+
|
|
124
|
+
The 0.1.x line treats `guardModelRequest()`, `protectWithAdapter()`, `reviewModelResponse()`, `ToolPermissionFirewall`, and `RetrievalSanitizer` as the long-term integration contracts. The exported `CORE_INTERFACES` map can be logged or asserted by applications that want to pin expected behavior.
|
|
125
|
+
|
|
126
|
+
Recommended presets:
|
|
127
|
+
|
|
128
|
+
- `shadowFirst` for low-friction rollout
|
|
129
|
+
- `strict` for high-sensitivity routes
|
|
130
|
+
- `ragSafe` for retrieval-heavy flows
|
|
131
|
+
- `agentTools` for tool-calling and approval-gated agent actions
|
|
132
|
+
|
|
109
133
|
### `AuditTrail`
|
|
110
134
|
|
|
111
135
|
Use it to record signed events, summarize security activity, and power dashboards or downstream analysis.
|
|
@@ -127,19 +151,22 @@ if (!guarded.allowed) {
|
|
|
127
151
|
### Wrap a provider call end to end
|
|
128
152
|
|
|
129
153
|
```js
|
|
154
|
+
const { BlackwallShield, createOpenAIAdapter } = require('blackwall-llm-shield-js');
|
|
155
|
+
|
|
130
156
|
const shield = new BlackwallShield({
|
|
131
|
-
|
|
157
|
+
preset: 'shadowFirst',
|
|
132
158
|
onTelemetry: async (event) => console.log(JSON.stringify(event)),
|
|
133
159
|
});
|
|
134
160
|
|
|
135
|
-
const
|
|
161
|
+
const adapter = createOpenAIAdapter({
|
|
162
|
+
client: openai,
|
|
163
|
+
model: 'gpt-4.1-mini',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await shield.protectWithAdapter({
|
|
167
|
+
adapter,
|
|
136
168
|
messages: [{ role: 'user', content: 'Summarize this shipment exception.' }],
|
|
137
169
|
metadata: { route: '/api/chat', tenantId: 'au-commerce', userId: 'ops-7' },
|
|
138
|
-
callModel: async ({ messages }) => openai.responses.create({
|
|
139
|
-
model: 'gpt-4.1-mini',
|
|
140
|
-
input: messages.map((msg) => `${msg.role}: ${msg.content}`).join('\n'),
|
|
141
|
-
}),
|
|
142
|
-
mapOutput: (response) => response.output_text,
|
|
143
170
|
firewallOptions: {
|
|
144
171
|
retrievalDocuments: [
|
|
145
172
|
{ id: 'kb-1', content: 'Shipment exceptions should include the parcel ID, lane, and next action.' },
|
|
@@ -150,6 +177,68 @@ const result = await shield.protectModelCall({
|
|
|
150
177
|
console.log(result.stage, result.allowed);
|
|
151
178
|
```
|
|
152
179
|
|
|
180
|
+
### Use presets and route-level policy overrides
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const shield = new BlackwallShield({
|
|
184
|
+
preset: 'shadowFirst',
|
|
185
|
+
routePolicies: [
|
|
186
|
+
{
|
|
187
|
+
route: '/api/admin/*',
|
|
188
|
+
options: {
|
|
189
|
+
preset: 'strict',
|
|
190
|
+
policyPack: 'finance',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
route: '/api/health',
|
|
195
|
+
options: {
|
|
196
|
+
shadowMode: true,
|
|
197
|
+
suppressPromptRules: ['ignore_instructions'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Route and domain examples
|
|
205
|
+
|
|
206
|
+
For RAG:
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
const shield = new BlackwallShield({
|
|
210
|
+
preset: 'shadowFirst',
|
|
211
|
+
routePolicies: [
|
|
212
|
+
{
|
|
213
|
+
route: '/api/rag/search',
|
|
214
|
+
options: {
|
|
215
|
+
policyPack: 'government',
|
|
216
|
+
outputFirewallDefaults: {
|
|
217
|
+
retrievalDocuments: kbDocs,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
For agent tool-calling:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
const toolFirewall = new ToolPermissionFirewall({
|
|
229
|
+
allowedTools: ['search', 'lookupCustomer', 'createRefund'],
|
|
230
|
+
requireHumanApprovalFor: ['createRefund'],
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Operational telemetry summaries
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
const summary = summarizeOperationalTelemetry(events);
|
|
238
|
+
console.log(summary.byRoute);
|
|
239
|
+
console.log(summary.highestSeverity);
|
|
240
|
+
```
|
|
241
|
+
|
|
153
242
|
### Inspect model output
|
|
154
243
|
|
|
155
244
|
```js
|
|
@@ -190,12 +279,21 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
|
|
|
190
279
|
- `npm run release:check` runs the JS test suite before release
|
|
191
280
|
- `npm run release:pack` creates the local npm tarball
|
|
192
281
|
- `npm run release:publish` publishes the package to npm
|
|
282
|
+
- `npm run changeset` creates a version/changelog entry for the next release
|
|
283
|
+
- `npm run version-packages` applies pending Changesets locally
|
|
284
|
+
|
|
285
|
+
## Migration and Benchmarks
|
|
286
|
+
|
|
287
|
+
- See [MIGRATING.md](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/MIGRATING.md) for compatibility notes and stable contract guidance
|
|
288
|
+
- See [BENCHMARKS.md](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/BENCHMARKS.md) for baseline latency numbers and regression coverage
|
|
193
289
|
|
|
194
290
|
## Rollout Notes
|
|
195
291
|
|
|
196
|
-
- Start with `shadowMode: true` and inspect `report.telemetry` plus `onTelemetry` events before enabling hard blocking.
|
|
292
|
+
- Start with `preset: 'shadowFirst'` or `shadowMode: true` and inspect `report.telemetry` plus `onTelemetry` events before enabling hard blocking.
|
|
197
293
|
- Use `RetrievalSanitizer` and `ToolPermissionFirewall` in front of RAG, search, admin actions, and tool-calling flows.
|
|
198
294
|
- Add regression prompts for instruction overrides, prompt leaks, token leaks, and Australian PII samples so upgrades stay safe.
|
|
295
|
+
- Expect some latency increase from grounding checks, output review, and custom detectors; benchmark with your real prompt and response sizes before enforcing globally.
|
|
296
|
+
- For agent workflows, keep approval-gated tools and route-specific presets separate from end-user chat routes so operators can see distinct risk patterns.
|
|
199
297
|
|
|
200
298
|
## Support
|
|
201
299
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vpdeva/blackwall-llm-shield-js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Open-source JavaScript enterprise LLM protection toolkit for Node.js and Next.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Vish <hello@vish.au> (https://vish.au)",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.js",
|
|
11
11
|
"./integrations": "./src/integrations.js",
|
|
12
|
+
"./providers": "./src/providers.js",
|
|
12
13
|
"./semantic": "./src/semantic.js"
|
|
13
14
|
},
|
|
14
15
|
"bin": {
|
|
@@ -16,6 +17,9 @@
|
|
|
16
17
|
},
|
|
17
18
|
"scripts": {
|
|
18
19
|
"test": "node --test tests/*.test.js",
|
|
20
|
+
"changeset": "changeset",
|
|
21
|
+
"version-packages": "changeset version",
|
|
22
|
+
"release": "changeset publish",
|
|
19
23
|
"release:check": "npm test",
|
|
20
24
|
"release:pack": "npm pack",
|
|
21
25
|
"release:publish": "npm publish --access public --provenance"
|
|
@@ -49,5 +53,8 @@
|
|
|
49
53
|
"enterprise",
|
|
50
54
|
"nextjs",
|
|
51
55
|
"node"
|
|
52
|
-
]
|
|
56
|
+
],
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@changesets/cli": "^2.29.6"
|
|
59
|
+
}
|
|
53
60
|
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const RED_TEAM_PROMPT_LIBRARY = require('./red_team_prompts.json');
|
|
3
|
+
const {
|
|
4
|
+
createOpenAIAdapter,
|
|
5
|
+
createAnthropicAdapter,
|
|
6
|
+
createGeminiAdapter,
|
|
7
|
+
createOpenRouterAdapter,
|
|
8
|
+
} = require('./providers');
|
|
3
9
|
|
|
4
10
|
const SENSITIVE_PATTERNS = {
|
|
5
11
|
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
@@ -100,6 +106,56 @@ const POLICY_PACKS = {
|
|
|
100
106
|
},
|
|
101
107
|
};
|
|
102
108
|
|
|
109
|
+
const SHIELD_PRESETS = {
|
|
110
|
+
balanced: {
|
|
111
|
+
blockOnPromptInjection: true,
|
|
112
|
+
promptInjectionThreshold: 'high',
|
|
113
|
+
notifyOnRiskLevel: 'medium',
|
|
114
|
+
shadowMode: false,
|
|
115
|
+
},
|
|
116
|
+
shadowFirst: {
|
|
117
|
+
blockOnPromptInjection: true,
|
|
118
|
+
promptInjectionThreshold: 'medium',
|
|
119
|
+
notifyOnRiskLevel: 'medium',
|
|
120
|
+
shadowMode: true,
|
|
121
|
+
},
|
|
122
|
+
strict: {
|
|
123
|
+
blockOnPromptInjection: true,
|
|
124
|
+
promptInjectionThreshold: 'medium',
|
|
125
|
+
notifyOnRiskLevel: 'medium',
|
|
126
|
+
shadowMode: false,
|
|
127
|
+
allowSystemMessages: false,
|
|
128
|
+
},
|
|
129
|
+
developerFriendly: {
|
|
130
|
+
blockOnPromptInjection: true,
|
|
131
|
+
promptInjectionThreshold: 'high',
|
|
132
|
+
notifyOnRiskLevel: 'high',
|
|
133
|
+
shadowMode: true,
|
|
134
|
+
allowSystemMessages: true,
|
|
135
|
+
},
|
|
136
|
+
ragSafe: {
|
|
137
|
+
blockOnPromptInjection: true,
|
|
138
|
+
promptInjectionThreshold: 'medium',
|
|
139
|
+
notifyOnRiskLevel: 'medium',
|
|
140
|
+
shadowMode: true,
|
|
141
|
+
},
|
|
142
|
+
agentTools: {
|
|
143
|
+
blockOnPromptInjection: true,
|
|
144
|
+
promptInjectionThreshold: 'medium',
|
|
145
|
+
notifyOnRiskLevel: 'medium',
|
|
146
|
+
shadowMode: false,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const CORE_INTERFACE_VERSION = '1.0';
|
|
151
|
+
const CORE_INTERFACES = Object.freeze({
|
|
152
|
+
guardModelRequest: CORE_INTERFACE_VERSION,
|
|
153
|
+
reviewModelResponse: CORE_INTERFACE_VERSION,
|
|
154
|
+
protectModelCall: CORE_INTERFACE_VERSION,
|
|
155
|
+
toolPermissionFirewall: CORE_INTERFACE_VERSION,
|
|
156
|
+
retrievalSanitizer: CORE_INTERFACE_VERSION,
|
|
157
|
+
});
|
|
158
|
+
|
|
103
159
|
const RISK_ORDER = ['low', 'medium', 'high', 'critical'];
|
|
104
160
|
const LEETSPEAK_MAP = { '0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '7': 't', '@': 'a', '$': 's' };
|
|
105
161
|
const TOXICITY_PATTERNS = [
|
|
@@ -165,6 +221,70 @@ function sanitizeText(input, maxLength = 5000) {
|
|
|
165
221
|
.slice(0, maxLength);
|
|
166
222
|
}
|
|
167
223
|
|
|
224
|
+
function stringifyMessageContent(content, maxLength = 5000) {
|
|
225
|
+
if (typeof content === 'string') return sanitizeText(content, maxLength);
|
|
226
|
+
if (Array.isArray(content)) {
|
|
227
|
+
return content
|
|
228
|
+
.map((item) => {
|
|
229
|
+
if (typeof item === 'string') return sanitizeText(item, maxLength);
|
|
230
|
+
if (item && typeof item.text === 'string') return sanitizeText(item.text, maxLength);
|
|
231
|
+
if (item && item.type === 'text' && typeof item.text === 'string') return sanitizeText(item.text, maxLength);
|
|
232
|
+
if (item && item.type === 'input_text' && typeof item.text === 'string') return sanitizeText(item.text, maxLength);
|
|
233
|
+
if (item && item.type === 'image_url') return '[IMAGE_CONTENT]';
|
|
234
|
+
if (item && item.type === 'file') return '[FILE_CONTENT]';
|
|
235
|
+
return '';
|
|
236
|
+
})
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.join('\n');
|
|
239
|
+
}
|
|
240
|
+
if (content && typeof content === 'object') {
|
|
241
|
+
if (typeof content.text === 'string') return sanitizeText(content.text, maxLength);
|
|
242
|
+
if (Array.isArray(content.parts)) return stringifyMessageContent(content.parts, maxLength);
|
|
243
|
+
return sanitizeText(JSON.stringify(content), maxLength);
|
|
244
|
+
}
|
|
245
|
+
return sanitizeText(String(content || ''), maxLength);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeContentParts(content, maxLength = 5000) {
|
|
249
|
+
if (typeof content === 'string') {
|
|
250
|
+
return [{ type: 'text', text: sanitizeText(content, maxLength) }].filter((item) => item.text);
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(content)) {
|
|
253
|
+
return content.map((item) => {
|
|
254
|
+
if (typeof item === 'string') return { type: 'text', text: sanitizeText(item, maxLength) };
|
|
255
|
+
if (!item || typeof item !== 'object') return null;
|
|
256
|
+
if ((item.type === 'text' || item.type === 'input_text') && typeof item.text === 'string') {
|
|
257
|
+
return { ...item, text: sanitizeText(item.text, maxLength) };
|
|
258
|
+
}
|
|
259
|
+
return { ...item };
|
|
260
|
+
}).filter(Boolean);
|
|
261
|
+
}
|
|
262
|
+
if (content && typeof content === 'object') {
|
|
263
|
+
if (Array.isArray(content.parts)) return normalizeContentParts(content.parts, maxLength);
|
|
264
|
+
if (typeof content.text === 'string') return [{ ...content, text: sanitizeText(content.text, maxLength) }];
|
|
265
|
+
return [{ type: 'json', value: sanitizeText(JSON.stringify(content), maxLength) }];
|
|
266
|
+
}
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function maskContentParts(parts = [], options = {}) {
|
|
271
|
+
const findings = [];
|
|
272
|
+
const vault = {};
|
|
273
|
+
const maskedParts = parts.map((part) => {
|
|
274
|
+
if (!part || typeof part !== 'object') return part;
|
|
275
|
+
const textValue = typeof part.text === 'string'
|
|
276
|
+
? part.text
|
|
277
|
+
: (part.type === 'json' && typeof part.value === 'string' ? part.value : null);
|
|
278
|
+
if (textValue == null) return { ...part };
|
|
279
|
+
const result = maskValue(textValue, options);
|
|
280
|
+
findings.push(...result.findings);
|
|
281
|
+
Object.assign(vault, result.vault);
|
|
282
|
+
if (typeof part.text === 'string') return { ...part, text: result.masked };
|
|
283
|
+
return { ...part, value: result.masked };
|
|
284
|
+
});
|
|
285
|
+
return { maskedParts, findings, vault };
|
|
286
|
+
}
|
|
287
|
+
|
|
168
288
|
function placeholder(type, index) {
|
|
169
289
|
return `[${String(type || 'SENSITIVE').toUpperCase()}_${index}]`;
|
|
170
290
|
}
|
|
@@ -223,6 +343,154 @@ function createTelemetryEvent(type, payload = {}) {
|
|
|
223
343
|
};
|
|
224
344
|
}
|
|
225
345
|
|
|
346
|
+
function summarizeOperationalTelemetry(events = []) {
|
|
347
|
+
const summary = {
|
|
348
|
+
totalEvents: 0,
|
|
349
|
+
blockedEvents: 0,
|
|
350
|
+
shadowModeEvents: 0,
|
|
351
|
+
byType: {},
|
|
352
|
+
byRoute: {},
|
|
353
|
+
highestSeverity: 'low',
|
|
354
|
+
};
|
|
355
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
356
|
+
const type = event && event.type ? event.type : 'unknown';
|
|
357
|
+
const route = event && event.metadata && (event.metadata.route || event.metadata.path) ? (event.metadata.route || event.metadata.path) : 'unknown';
|
|
358
|
+
const severity = event && event.report && event.report.outputReview
|
|
359
|
+
? event.report.outputReview.severity
|
|
360
|
+
: (event && event.report && event.report.promptInjection ? event.report.promptInjection.level : 'low');
|
|
361
|
+
summary.totalEvents += 1;
|
|
362
|
+
summary.byType[type] = (summary.byType[type] || 0) + 1;
|
|
363
|
+
summary.byRoute[route] = (summary.byRoute[route] || 0) + 1;
|
|
364
|
+
if (event && event.blocked) summary.blockedEvents += 1;
|
|
365
|
+
if (event && event.shadowMode) summary.shadowModeEvents += 1;
|
|
366
|
+
if (severityWeight(severity) > severityWeight(summary.highestSeverity)) summary.highestSeverity = severity;
|
|
367
|
+
}
|
|
368
|
+
return summary;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function resolveShieldPreset(name) {
|
|
372
|
+
if (!name) return {};
|
|
373
|
+
return SHIELD_PRESETS[name] ? { ...SHIELD_PRESETS[name] } : {};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function dedupeArray(values = []) {
|
|
377
|
+
return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function routePatternMatches(pattern, route = '', metadata = {}) {
|
|
381
|
+
if (!pattern) return false;
|
|
382
|
+
if (typeof pattern === 'function') return !!pattern(route, metadata);
|
|
383
|
+
if (pattern instanceof RegExp) return pattern.test(route);
|
|
384
|
+
if (typeof pattern === 'string') {
|
|
385
|
+
if (pattern === route) return true;
|
|
386
|
+
if (pattern.includes('*')) {
|
|
387
|
+
const regex = new RegExp(`^${pattern.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`);
|
|
388
|
+
return regex.test(route);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function resolveRoutePolicy(routePolicies = [], metadata = {}) {
|
|
395
|
+
const route = metadata.route || metadata.path || '';
|
|
396
|
+
const matched = (Array.isArray(routePolicies) ? routePolicies : []).filter((entry) => routePatternMatches(entry && entry.route, route, metadata));
|
|
397
|
+
if (!matched.length) return null;
|
|
398
|
+
return matched.reduce((acc, entry) => {
|
|
399
|
+
const options = entry && entry.options ? entry.options : {};
|
|
400
|
+
return {
|
|
401
|
+
...acc,
|
|
402
|
+
...options,
|
|
403
|
+
shadowPolicyPacks: dedupeArray([...(acc.shadowPolicyPacks || []), ...(options.shadowPolicyPacks || [])]),
|
|
404
|
+
entityDetectors: [...(acc.entityDetectors || []), ...(options.entityDetectors || [])],
|
|
405
|
+
customPromptDetectors: [...(acc.customPromptDetectors || []), ...(options.customPromptDetectors || [])],
|
|
406
|
+
suppressPromptRules: dedupeArray([...(acc.suppressPromptRules || []), ...(options.suppressPromptRules || [])]),
|
|
407
|
+
};
|
|
408
|
+
}, {});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function applyPromptRuleSuppressions(injection, suppressedIds = []) {
|
|
412
|
+
const suppressionSet = new Set(dedupeArray(suppressedIds));
|
|
413
|
+
if (!suppressionSet.size) return injection;
|
|
414
|
+
const matches = (injection.matches || []).filter((item) => !suppressionSet.has(item.id));
|
|
415
|
+
const score = Math.min(matches.reduce((sum, item) => sum + (item.score || 0), 0), 100);
|
|
416
|
+
return {
|
|
417
|
+
...injection,
|
|
418
|
+
matches,
|
|
419
|
+
score,
|
|
420
|
+
level: riskLevelFromScore(score),
|
|
421
|
+
blockedByDefault: score >= 45,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function applyCustomPromptDetectors(injection, text, options = {}, metadata = {}) {
|
|
426
|
+
const detectors = Array.isArray(options.customPromptDetectors) ? options.customPromptDetectors : [];
|
|
427
|
+
if (!detectors.length) return injection;
|
|
428
|
+
const matches = [...(injection.matches || [])];
|
|
429
|
+
const seen = new Set(matches.map((item) => item.id));
|
|
430
|
+
let score = injection.score || 0;
|
|
431
|
+
for (const detector of detectors) {
|
|
432
|
+
if (typeof detector !== 'function') continue;
|
|
433
|
+
const result = detector({ text, injection, metadata, options }) || [];
|
|
434
|
+
const findings = Array.isArray(result) ? result : [result];
|
|
435
|
+
for (const finding of findings) {
|
|
436
|
+
if (!finding || !finding.id || seen.has(finding.id)) continue;
|
|
437
|
+
seen.add(finding.id);
|
|
438
|
+
matches.push({
|
|
439
|
+
id: finding.id,
|
|
440
|
+
score: Math.max(0, Math.min(finding.score || 0, 40)),
|
|
441
|
+
reason: finding.reason || 'Custom prompt detector triggered',
|
|
442
|
+
source: finding.source || 'custom',
|
|
443
|
+
});
|
|
444
|
+
score += Math.max(0, Math.min(finding.score || 0, 40));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const cappedScore = Math.min(score, 100);
|
|
448
|
+
return {
|
|
449
|
+
...injection,
|
|
450
|
+
matches,
|
|
451
|
+
score: cappedScore,
|
|
452
|
+
level: riskLevelFromScore(cappedScore),
|
|
453
|
+
blockedByDefault: cappedScore >= 45,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function resolveEffectiveShieldOptions(baseOptions = {}, metadata = {}) {
|
|
458
|
+
const presetOptions = resolveShieldPreset(baseOptions.preset);
|
|
459
|
+
const routePolicy = resolveRoutePolicy(baseOptions.routePolicies, metadata);
|
|
460
|
+
const routePresetOptions = resolveShieldPreset(routePolicy && routePolicy.preset);
|
|
461
|
+
return {
|
|
462
|
+
...baseOptions,
|
|
463
|
+
...presetOptions,
|
|
464
|
+
...routePresetOptions,
|
|
465
|
+
...(routePolicy || {}),
|
|
466
|
+
shadowPolicyPacks: dedupeArray([
|
|
467
|
+
...((presetOptions && presetOptions.shadowPolicyPacks) || []),
|
|
468
|
+
...((routePresetOptions && routePresetOptions.shadowPolicyPacks) || []),
|
|
469
|
+
...(baseOptions.shadowPolicyPacks || []),
|
|
470
|
+
...((routePolicy && routePolicy.shadowPolicyPacks) || []),
|
|
471
|
+
]),
|
|
472
|
+
entityDetectors: [
|
|
473
|
+
...((presetOptions && presetOptions.entityDetectors) || []),
|
|
474
|
+
...((routePresetOptions && routePresetOptions.entityDetectors) || []),
|
|
475
|
+
...(baseOptions.entityDetectors || []),
|
|
476
|
+
...((routePolicy && routePolicy.entityDetectors) || []),
|
|
477
|
+
],
|
|
478
|
+
customPromptDetectors: [
|
|
479
|
+
...((presetOptions && presetOptions.customPromptDetectors) || []),
|
|
480
|
+
...((routePresetOptions && routePresetOptions.customPromptDetectors) || []),
|
|
481
|
+
...(baseOptions.customPromptDetectors || []),
|
|
482
|
+
...((routePolicy && routePolicy.customPromptDetectors) || []),
|
|
483
|
+
],
|
|
484
|
+
suppressPromptRules: dedupeArray([
|
|
485
|
+
...((presetOptions && presetOptions.suppressPromptRules) || []),
|
|
486
|
+
...((routePresetOptions && routePresetOptions.suppressPromptRules) || []),
|
|
487
|
+
...(baseOptions.suppressPromptRules || []),
|
|
488
|
+
...((routePolicy && routePolicy.suppressPromptRules) || []),
|
|
489
|
+
]),
|
|
490
|
+
routePolicy,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
226
494
|
function cloneRegex(regex) {
|
|
227
495
|
return new RegExp(regex.source, regex.flags);
|
|
228
496
|
}
|
|
@@ -575,11 +843,14 @@ function normalizeMessages(messages = [], options = {}) {
|
|
|
575
843
|
return (Array.isArray(messages) ? messages : [])
|
|
576
844
|
.slice(-maxMessages)
|
|
577
845
|
.map((message) => {
|
|
578
|
-
const
|
|
846
|
+
const originalContent = message && Object.prototype.hasOwnProperty.call(message, 'content') ? message.content : '';
|
|
847
|
+
const parts = typeof originalContent === 'string' ? [] : normalizeContentParts(originalContent, options.maxLength || 5000);
|
|
848
|
+
const content = stringifyMessageContent(originalContent, options.maxLength || 5000);
|
|
579
849
|
if (!content) return null;
|
|
580
850
|
return {
|
|
581
851
|
role: normalizeRole(message.role, allowSystemMessages, !!message.trusted),
|
|
582
852
|
content,
|
|
853
|
+
contentParts: parts.length ? parts : undefined,
|
|
583
854
|
};
|
|
584
855
|
})
|
|
585
856
|
.filter(Boolean);
|
|
@@ -590,16 +861,25 @@ function maskMessages(messages = [], options = {}) {
|
|
|
590
861
|
const vault = {};
|
|
591
862
|
const masked = (Array.isArray(messages) ? messages : []).map((message) => {
|
|
592
863
|
if (!message || typeof message !== 'object') return null;
|
|
864
|
+
const normalizedParts = Array.isArray(message.contentParts)
|
|
865
|
+
? message.contentParts
|
|
866
|
+
: (typeof message.content === 'string' ? [] : normalizeContentParts(message.content || '', options.maxLength || 5000));
|
|
593
867
|
const normalized = {
|
|
594
868
|
role: message.role === 'system' ? 'system' : normalizeRole(message.role, false, false),
|
|
595
|
-
content:
|
|
869
|
+
content: stringifyMessageContent(message.content || '', options.maxLength || 5000),
|
|
870
|
+
contentParts: normalizedParts.length ? normalizedParts : undefined,
|
|
596
871
|
};
|
|
597
872
|
if (!normalized.content) return null;
|
|
598
873
|
if (normalized.role === 'system') return normalized;
|
|
599
874
|
const result = maskValue(normalized.content, options);
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
875
|
+
const partsResult = maskContentParts(normalized.contentParts || [], options);
|
|
876
|
+
findings.push(...result.findings, ...partsResult.findings);
|
|
877
|
+
Object.assign(vault, result.vault, partsResult.vault);
|
|
878
|
+
return {
|
|
879
|
+
...normalized,
|
|
880
|
+
content: result.masked,
|
|
881
|
+
contentParts: partsResult.maskedParts.length ? partsResult.maskedParts : undefined,
|
|
882
|
+
};
|
|
603
883
|
}).filter(Boolean);
|
|
604
884
|
|
|
605
885
|
return {
|
|
@@ -748,14 +1028,19 @@ class BlackwallShield {
|
|
|
748
1028
|
maxLength: 5000,
|
|
749
1029
|
allowSystemMessages: false,
|
|
750
1030
|
shadowMode: false,
|
|
1031
|
+
preset: null,
|
|
751
1032
|
policyPack: null,
|
|
752
1033
|
shadowPolicyPacks: [],
|
|
753
1034
|
entityDetectors: [],
|
|
1035
|
+
customPromptDetectors: [],
|
|
1036
|
+
suppressPromptRules: [],
|
|
1037
|
+
routePolicies: [],
|
|
754
1038
|
detectNamedEntities: false,
|
|
755
1039
|
semanticScorer: null,
|
|
756
1040
|
sessionBuffer: null,
|
|
757
1041
|
tokenBudgetFirewall: null,
|
|
758
1042
|
systemPrompt: null,
|
|
1043
|
+
outputFirewallDefaults: {},
|
|
759
1044
|
onAlert: null,
|
|
760
1045
|
onTelemetry: null,
|
|
761
1046
|
webhookUrl: null,
|
|
@@ -764,10 +1049,13 @@ class BlackwallShield {
|
|
|
764
1049
|
}
|
|
765
1050
|
|
|
766
1051
|
inspectText(text) {
|
|
767
|
-
const
|
|
768
|
-
const
|
|
1052
|
+
const effectiveOptions = resolveEffectiveShieldOptions(this.options);
|
|
1053
|
+
const pii = maskValue(text, effectiveOptions);
|
|
1054
|
+
let injection = detectPromptInjection(text, effectiveOptions);
|
|
1055
|
+
injection = applyCustomPromptDetectors(injection, String(text || ''), effectiveOptions, {});
|
|
1056
|
+
injection = applyPromptRuleSuppressions(injection, effectiveOptions.suppressPromptRules);
|
|
769
1057
|
return {
|
|
770
|
-
sanitized: pii.original || sanitizeText(text,
|
|
1058
|
+
sanitized: pii.original || sanitizeText(text, effectiveOptions.maxLength),
|
|
771
1059
|
promptInjection: injection,
|
|
772
1060
|
sensitiveData: {
|
|
773
1061
|
findings: pii.findings,
|
|
@@ -792,35 +1080,43 @@ class BlackwallShield {
|
|
|
792
1080
|
}
|
|
793
1081
|
|
|
794
1082
|
async guardModelRequest({ messages = [], metadata = {}, allowSystemMessages = this.options.allowSystemMessages, comparePolicyPacks = [] } = {}) {
|
|
1083
|
+
const effectiveOptions = resolveEffectiveShieldOptions(this.options, metadata);
|
|
1084
|
+
const effectiveAllowSystemMessages = allowSystemMessages === this.options.allowSystemMessages
|
|
1085
|
+
? effectiveOptions.allowSystemMessages
|
|
1086
|
+
: allowSystemMessages;
|
|
795
1087
|
const normalizedMessages = normalizeMessages(messages, {
|
|
796
|
-
maxMessages:
|
|
797
|
-
allowSystemMessages,
|
|
1088
|
+
maxMessages: effectiveOptions.maxMessages,
|
|
1089
|
+
allowSystemMessages: effectiveAllowSystemMessages,
|
|
798
1090
|
});
|
|
799
1091
|
const masked = maskMessages(normalizedMessages, {
|
|
800
|
-
includeOriginals:
|
|
801
|
-
syntheticReplacement:
|
|
802
|
-
maxLength:
|
|
803
|
-
allowSystemMessages,
|
|
1092
|
+
includeOriginals: effectiveOptions.includeOriginals,
|
|
1093
|
+
syntheticReplacement: effectiveOptions.syntheticReplacement,
|
|
1094
|
+
maxLength: effectiveOptions.maxLength,
|
|
1095
|
+
allowSystemMessages: effectiveAllowSystemMessages,
|
|
1096
|
+
entityDetectors: effectiveOptions.entityDetectors,
|
|
1097
|
+
detectNamedEntities: effectiveOptions.detectNamedEntities,
|
|
804
1098
|
});
|
|
805
1099
|
const promptCandidate = normalizedMessages.filter((msg) => msg.role !== 'assistant');
|
|
806
|
-
const sessionBuffer =
|
|
1100
|
+
const sessionBuffer = effectiveOptions.sessionBuffer;
|
|
807
1101
|
if (sessionBuffer && typeof sessionBuffer.record === 'function') {
|
|
808
1102
|
promptCandidate.forEach((msg) => sessionBuffer.record(msg.content));
|
|
809
1103
|
}
|
|
810
1104
|
const sessionContext = sessionBuffer && typeof sessionBuffer.render === 'function'
|
|
811
1105
|
? sessionBuffer.render()
|
|
812
1106
|
: promptCandidate;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
const
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
|
|
1107
|
+
let injection = detectPromptInjection(sessionContext, effectiveOptions);
|
|
1108
|
+
injection = applyCustomPromptDetectors(injection, Array.isArray(sessionContext) ? JSON.stringify(sessionContext) : String(sessionContext || ''), effectiveOptions, metadata);
|
|
1109
|
+
injection = applyPromptRuleSuppressions(injection, effectiveOptions.suppressPromptRules);
|
|
1110
|
+
|
|
1111
|
+
const primaryPolicy = resolvePolicyPack(effectiveOptions.policyPack);
|
|
1112
|
+
const threshold = (primaryPolicy && primaryPolicy.promptInjectionThreshold) || effectiveOptions.promptInjectionThreshold;
|
|
1113
|
+
const wouldBlock = effectiveOptions.blockOnPromptInjection && compareRisk(injection.level, threshold);
|
|
1114
|
+
const shouldBlock = effectiveOptions.shadowMode ? false : wouldBlock;
|
|
1115
|
+
const shouldNotify = compareRisk(injection.level, effectiveOptions.notifyOnRiskLevel);
|
|
1116
|
+
const policyNames = [...new Set([...(effectiveOptions.shadowPolicyPacks || []), ...comparePolicyPacks].filter(Boolean))];
|
|
1117
|
+
const policyComparisons = policyNames.map((name) => evaluatePolicyPack(injection, name, effectiveOptions.promptInjectionThreshold));
|
|
1118
|
+
const budgetResult = effectiveOptions.tokenBudgetFirewall && typeof effectiveOptions.tokenBudgetFirewall.inspect === 'function'
|
|
1119
|
+
? effectiveOptions.tokenBudgetFirewall.inspect({
|
|
824
1120
|
userId: metadata.userId || metadata.user_id || 'anonymous',
|
|
825
1121
|
tenantId: metadata.tenantId || metadata.tenant_id || 'default',
|
|
826
1122
|
messages: normalizedMessages,
|
|
@@ -838,7 +1134,7 @@ class BlackwallShield {
|
|
|
838
1134
|
hasSensitiveData: masked.hasSensitiveData,
|
|
839
1135
|
},
|
|
840
1136
|
enforcement: {
|
|
841
|
-
shadowMode:
|
|
1137
|
+
shadowMode: effectiveOptions.shadowMode,
|
|
842
1138
|
wouldBlock: wouldBlock || !budgetResult.allowed,
|
|
843
1139
|
blocked: shouldBlock || !budgetResult.allowed,
|
|
844
1140
|
threshold,
|
|
@@ -846,6 +1142,13 @@ class BlackwallShield {
|
|
|
846
1142
|
policyPack: primaryPolicy ? primaryPolicy.name : null,
|
|
847
1143
|
policyComparisons,
|
|
848
1144
|
tokenBudget: budgetResult,
|
|
1145
|
+
coreInterfaces: CORE_INTERFACES,
|
|
1146
|
+
routePolicy: effectiveOptions.routePolicy ? {
|
|
1147
|
+
route: metadata.route || metadata.path || null,
|
|
1148
|
+
suppressPromptRules: effectiveOptions.routePolicy.suppressPromptRules || [],
|
|
1149
|
+
policyPack: effectiveOptions.routePolicy.policyPack || null,
|
|
1150
|
+
preset: effectiveOptions.routePolicy.preset || null,
|
|
1151
|
+
} : null,
|
|
849
1152
|
telemetry: {
|
|
850
1153
|
eventType: 'llm_request_reviewed',
|
|
851
1154
|
promptInjectionRuleHits: countFindingsByType(injection.matches),
|
|
@@ -861,7 +1164,7 @@ class BlackwallShield {
|
|
|
861
1164
|
await this.emitTelemetry(createTelemetryEvent('llm_request_reviewed', {
|
|
862
1165
|
metadata,
|
|
863
1166
|
blocked: shouldBlock || !budgetResult.allowed,
|
|
864
|
-
shadowMode:
|
|
1167
|
+
shadowMode: effectiveOptions.shadowMode,
|
|
865
1168
|
report,
|
|
866
1169
|
}));
|
|
867
1170
|
|
|
@@ -886,14 +1189,17 @@ class BlackwallShield {
|
|
|
886
1189
|
}
|
|
887
1190
|
|
|
888
1191
|
async reviewModelResponse({ output, metadata = {}, outputFirewall = null, firewallOptions = {} } = {}) {
|
|
889
|
-
const
|
|
1192
|
+
const effectiveOptions = resolveEffectiveShieldOptions(this.options, metadata);
|
|
1193
|
+
const primaryPolicy = resolvePolicyPack(effectiveOptions.policyPack);
|
|
890
1194
|
const firewall = outputFirewall || new OutputFirewall({
|
|
891
1195
|
riskThreshold: (primaryPolicy && primaryPolicy.outputRiskThreshold) || 'high',
|
|
892
|
-
systemPrompt:
|
|
1196
|
+
systemPrompt: effectiveOptions.systemPrompt,
|
|
1197
|
+
...effectiveOptions.outputFirewallDefaults,
|
|
893
1198
|
...firewallOptions,
|
|
894
1199
|
});
|
|
895
1200
|
const review = firewall.inspect(output, {
|
|
896
|
-
systemPrompt:
|
|
1201
|
+
systemPrompt: effectiveOptions.systemPrompt,
|
|
1202
|
+
...(effectiveOptions.outputFirewallDefaults || {}),
|
|
897
1203
|
...firewallOptions,
|
|
898
1204
|
});
|
|
899
1205
|
const report = {
|
|
@@ -902,6 +1208,7 @@ class BlackwallShield {
|
|
|
902
1208
|
metadata,
|
|
903
1209
|
outputReview: {
|
|
904
1210
|
...review,
|
|
1211
|
+
coreInterfaces: CORE_INTERFACES,
|
|
905
1212
|
telemetry: {
|
|
906
1213
|
eventType: 'llm_output_reviewed',
|
|
907
1214
|
findingCounts: countFindingsByType(review.findings),
|
|
@@ -988,6 +1295,38 @@ class BlackwallShield {
|
|
|
988
1295
|
review,
|
|
989
1296
|
};
|
|
990
1297
|
}
|
|
1298
|
+
|
|
1299
|
+
async protectWithAdapter({
|
|
1300
|
+
adapter,
|
|
1301
|
+
messages = [],
|
|
1302
|
+
metadata = {},
|
|
1303
|
+
allowSystemMessages = this.options.allowSystemMessages,
|
|
1304
|
+
comparePolicyPacks = [],
|
|
1305
|
+
outputFirewall = null,
|
|
1306
|
+
firewallOptions = {},
|
|
1307
|
+
} = {}) {
|
|
1308
|
+
if (!adapter || typeof adapter.invoke !== 'function') {
|
|
1309
|
+
throw new TypeError('adapter.invoke must be a function');
|
|
1310
|
+
}
|
|
1311
|
+
return this.protectModelCall({
|
|
1312
|
+
messages,
|
|
1313
|
+
metadata,
|
|
1314
|
+
allowSystemMessages,
|
|
1315
|
+
comparePolicyPacks,
|
|
1316
|
+
outputFirewall,
|
|
1317
|
+
firewallOptions,
|
|
1318
|
+
callModel: async (payload) => {
|
|
1319
|
+
const result = await adapter.invoke(payload);
|
|
1320
|
+
return result && Object.prototype.hasOwnProperty.call(result, 'response') ? result.response : result;
|
|
1321
|
+
},
|
|
1322
|
+
mapOutput: async (response, request) => {
|
|
1323
|
+
if (typeof adapter.extractOutput === 'function') {
|
|
1324
|
+
return adapter.extractOutput(response, request);
|
|
1325
|
+
}
|
|
1326
|
+
return response && Object.prototype.hasOwnProperty.call(response, 'output') ? response.output : response;
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
991
1330
|
}
|
|
992
1331
|
|
|
993
1332
|
function validateGrounding(text, documents = [], options = {}) {
|
|
@@ -1641,6 +1980,18 @@ function createLlamaIndexCallback({ shield, metadata = {} } = {}) {
|
|
|
1641
1980
|
};
|
|
1642
1981
|
}
|
|
1643
1982
|
|
|
1983
|
+
function buildShieldOptions(options = {}) {
|
|
1984
|
+
const presetOptions = resolveShieldPreset(options.preset);
|
|
1985
|
+
return {
|
|
1986
|
+
...presetOptions,
|
|
1987
|
+
...options,
|
|
1988
|
+
shadowPolicyPacks: dedupeArray([
|
|
1989
|
+
...(presetOptions.shadowPolicyPacks || []),
|
|
1990
|
+
...(options.shadowPolicyPacks || []),
|
|
1991
|
+
]),
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1644
1995
|
module.exports = {
|
|
1645
1996
|
AgenticCapabilityGater,
|
|
1646
1997
|
AgentIdentityRegistry,
|
|
@@ -1659,6 +2010,8 @@ module.exports = {
|
|
|
1659
2010
|
SENSITIVE_PATTERNS,
|
|
1660
2011
|
PROMPT_INJECTION_RULES,
|
|
1661
2012
|
POLICY_PACKS,
|
|
2013
|
+
SHIELD_PRESETS,
|
|
2014
|
+
CORE_INTERFACES,
|
|
1662
2015
|
sanitizeText,
|
|
1663
2016
|
deobfuscateText,
|
|
1664
2017
|
maskText,
|
|
@@ -1680,6 +2033,12 @@ module.exports = {
|
|
|
1680
2033
|
buildAdminDashboardModel,
|
|
1681
2034
|
getRedTeamPromptLibrary,
|
|
1682
2035
|
runRedTeamSuite,
|
|
2036
|
+
buildShieldOptions,
|
|
2037
|
+
summarizeOperationalTelemetry,
|
|
2038
|
+
createOpenAIAdapter,
|
|
2039
|
+
createAnthropicAdapter,
|
|
2040
|
+
createGeminiAdapter,
|
|
2041
|
+
createOpenRouterAdapter,
|
|
1683
2042
|
createExpressMiddleware,
|
|
1684
2043
|
createLangChainCallbacks,
|
|
1685
2044
|
createLlamaIndexCallback,
|
package/src/providers.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
function stringifyContent(content) {
|
|
2
|
+
if (typeof content === 'string') return content;
|
|
3
|
+
if (Array.isArray(content)) {
|
|
4
|
+
return content.map((item) => {
|
|
5
|
+
if (typeof item === 'string') return item;
|
|
6
|
+
if (item && typeof item.text === 'string') return item.text;
|
|
7
|
+
if (item && item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
8
|
+
return '';
|
|
9
|
+
}).filter(Boolean).join('\n');
|
|
10
|
+
}
|
|
11
|
+
if (content && typeof content.text === 'string') return content.text;
|
|
12
|
+
return String(content || '');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toOpenAIInput(messages = []) {
|
|
16
|
+
return messages.map((message) => ({
|
|
17
|
+
role: message.role,
|
|
18
|
+
content: stringifyContent(message.content),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toAnthropicMessages(messages = []) {
|
|
23
|
+
return messages
|
|
24
|
+
.filter((message) => message.role !== 'system')
|
|
25
|
+
.map((message) => ({
|
|
26
|
+
role: message.role === 'assistant' ? 'assistant' : 'user',
|
|
27
|
+
content: stringifyContent(message.content),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractSystemPrompt(messages = []) {
|
|
32
|
+
return messages.filter((message) => message.role === 'system').map((message) => stringifyContent(message.content)).join('\n\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function defaultAdapterResult(response, output) {
|
|
36
|
+
return { response, output };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createOpenAIAdapter({ client, model, request = {}, method = 'responses', extractOutput = null } = {}) {
|
|
40
|
+
if (!client) throw new TypeError('client is required');
|
|
41
|
+
return {
|
|
42
|
+
provider: 'openai',
|
|
43
|
+
async invoke({ messages, metadata = {} }) {
|
|
44
|
+
if (method === 'chat.completions') {
|
|
45
|
+
const response = await client.chat.completions.create({
|
|
46
|
+
model,
|
|
47
|
+
messages: toOpenAIInput(messages),
|
|
48
|
+
metadata,
|
|
49
|
+
...request,
|
|
50
|
+
});
|
|
51
|
+
return defaultAdapterResult(response, response && response.choices && response.choices[0] && response.choices[0].message
|
|
52
|
+
? stringifyContent(response.choices[0].message.content)
|
|
53
|
+
: '');
|
|
54
|
+
}
|
|
55
|
+
const response = await client.responses.create({
|
|
56
|
+
model,
|
|
57
|
+
input: toOpenAIInput(messages),
|
|
58
|
+
metadata,
|
|
59
|
+
...request,
|
|
60
|
+
});
|
|
61
|
+
return defaultAdapterResult(response, response && typeof response.output_text === 'string' ? response.output_text : '');
|
|
62
|
+
},
|
|
63
|
+
extractOutput(response) {
|
|
64
|
+
if (typeof extractOutput === 'function') return extractOutput(response);
|
|
65
|
+
if (response && typeof response.output_text === 'string') return response.output_text;
|
|
66
|
+
return response && response.choices && response.choices[0] && response.choices[0].message
|
|
67
|
+
? stringifyContent(response.choices[0].message.content)
|
|
68
|
+
: '';
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createAnthropicAdapter({ client, model, request = {}, extractOutput = null } = {}) {
|
|
74
|
+
if (!client) throw new TypeError('client is required');
|
|
75
|
+
return {
|
|
76
|
+
provider: 'anthropic',
|
|
77
|
+
async invoke({ messages, metadata = {} }) {
|
|
78
|
+
const response = await client.messages.create({
|
|
79
|
+
model,
|
|
80
|
+
system: extractSystemPrompt(messages) || undefined,
|
|
81
|
+
messages: toAnthropicMessages(messages),
|
|
82
|
+
metadata,
|
|
83
|
+
...request,
|
|
84
|
+
});
|
|
85
|
+
const output = Array.isArray(response && response.content)
|
|
86
|
+
? response.content.map((item) => stringifyContent(item)).filter(Boolean).join('\n')
|
|
87
|
+
: '';
|
|
88
|
+
return defaultAdapterResult(response, output);
|
|
89
|
+
},
|
|
90
|
+
extractOutput(response) {
|
|
91
|
+
if (typeof extractOutput === 'function') return extractOutput(response);
|
|
92
|
+
return Array.isArray(response && response.content)
|
|
93
|
+
? response.content.map((item) => stringifyContent(item)).filter(Boolean).join('\n')
|
|
94
|
+
: '';
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createGeminiAdapter({ client, model, request = {}, extractOutput = null } = {}) {
|
|
100
|
+
if (!client) throw new TypeError('client is required');
|
|
101
|
+
return {
|
|
102
|
+
provider: 'gemini',
|
|
103
|
+
async invoke({ messages }) {
|
|
104
|
+
const response = await client.models.generateContent({
|
|
105
|
+
model,
|
|
106
|
+
contents: messages.map((message) => ({
|
|
107
|
+
role: message.role === 'assistant' ? 'model' : 'user',
|
|
108
|
+
parts: [{ text: stringifyContent(message.content) }],
|
|
109
|
+
})),
|
|
110
|
+
...request,
|
|
111
|
+
});
|
|
112
|
+
return defaultAdapterResult(response, response && typeof response.text === 'string' ? response.text : '');
|
|
113
|
+
},
|
|
114
|
+
extractOutput(response) {
|
|
115
|
+
if (typeof extractOutput === 'function') return extractOutput(response);
|
|
116
|
+
if (response && typeof response.text === 'string') return response.text;
|
|
117
|
+
if (typeof response === 'string') return response;
|
|
118
|
+
return '';
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createOpenRouterAdapter({ client, model, request = {}, extractOutput = null } = {}) {
|
|
124
|
+
if (!client) throw new TypeError('client is required');
|
|
125
|
+
return {
|
|
126
|
+
provider: 'openrouter',
|
|
127
|
+
async invoke({ messages }) {
|
|
128
|
+
const response = await client.chat.completions.create({
|
|
129
|
+
model,
|
|
130
|
+
messages: toOpenAIInput(messages),
|
|
131
|
+
...request,
|
|
132
|
+
});
|
|
133
|
+
const output = response && response.choices && response.choices[0] && response.choices[0].message
|
|
134
|
+
? stringifyContent(response.choices[0].message.content)
|
|
135
|
+
: '';
|
|
136
|
+
return defaultAdapterResult(response, output);
|
|
137
|
+
},
|
|
138
|
+
extractOutput(response) {
|
|
139
|
+
if (typeof extractOutput === 'function') return extractOutput(response);
|
|
140
|
+
return response && response.choices && response.choices[0] && response.choices[0].message
|
|
141
|
+
? stringifyContent(response.choices[0].message.content)
|
|
142
|
+
: '';
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
createOpenAIAdapter,
|
|
149
|
+
createAnthropicAdapter,
|
|
150
|
+
createGeminiAdapter,
|
|
151
|
+
createOpenRouterAdapter,
|
|
152
|
+
};
|