@vpdeva/blackwall-llm-shield-js 0.1.5 → 0.1.7

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 CHANGED
@@ -32,7 +32,7 @@ npm install @xenova/transformers
32
32
  ## Fast Start
33
33
 
34
34
  ```js
35
- const { BlackwallShield } = require('blackwall-llm-shield-js');
35
+ const { BlackwallShield } = require('@vpdeva/blackwall-llm-shield-js');
36
36
 
37
37
  const shield = new BlackwallShield({
38
38
  blockOnPromptInjection: true,
@@ -95,9 +95,9 @@ Use `createExpressMiddleware()`, `createLangChainCallbacks()`, or `createLlamaIn
95
95
 
96
96
  ### Subpath modules
97
97
 
98
- Use `require('blackwall-llm-shield-js/integrations')` for callback wrappers and `require('blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
98
+ Use `require('@vpdeva/blackwall-llm-shield-js/integrations')` for callback wrappers and `require('@vpdeva/blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
99
99
 
100
- Use `require('blackwall-llm-shield-js/providers')` for provider adapter factories.
100
+ Use `require('@vpdeva/blackwall-llm-shield-js/providers')` for provider adapter factories.
101
101
 
102
102
  ## Core Building Blocks
103
103
 
@@ -105,7 +105,7 @@ Use `require('blackwall-llm-shield-js/providers')` for provider adapter factorie
105
105
 
106
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.
107
107
 
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.
108
+ It also exposes `protectModelCall()`, `protectJsonModelCall()`, `protectWithAdapter()`, and `reviewModelResponse()` so you can enforce request checks before provider calls and review outputs before they go back to the user.
109
109
 
110
110
  ### `OutputFirewall`
111
111
 
@@ -129,6 +129,10 @@ Recommended presets:
129
129
  - `strict` for high-sensitivity routes
130
130
  - `ragSafe` for retrieval-heavy flows
131
131
  - `agentTools` for tool-calling and approval-gated agent actions
132
+ - `agentPlanner` for JSON-heavy planner and internal ops routes
133
+ - `documentReview` for classification and document-review pipelines
134
+ - `ragSearch` for search-heavy retrieval endpoints
135
+ - `toolCalling` for routes that broker external actions
132
136
 
133
137
  ### `AuditTrail`
134
138
 
@@ -151,7 +155,7 @@ if (!guarded.allowed) {
151
155
  ### Wrap a provider call end to end
152
156
 
153
157
  ```js
154
- const { BlackwallShield, createOpenAIAdapter } = require('blackwall-llm-shield-js');
158
+ const { BlackwallShield, createOpenAIAdapter } = require('@vpdeva/blackwall-llm-shield-js');
155
159
 
156
160
  const shield = new BlackwallShield({
157
161
  preset: 'shadowFirst',
@@ -177,6 +181,19 @@ const result = await shield.protectWithAdapter({
177
181
  console.log(result.stage, result.allowed);
178
182
  ```
179
183
 
184
+ ### Protect a strict JSON workflow
185
+
186
+ ```js
187
+ const result = await shield.protectJsonModelCall({
188
+ messages: [{ role: 'user', content: 'Return the shipment triage plan as JSON.' }],
189
+ metadata: { route: '/api/planner', feature: 'planner' },
190
+ requiredSchema: { steps: 'object' },
191
+ callModel: async () => JSON.stringify({ steps: ['triage', 'notify-ops'] }),
192
+ });
193
+
194
+ console.log(result.json.parsed);
195
+ ```
196
+
180
197
  ### Use presets and route-level policy overrides
181
198
 
182
199
  ```js
@@ -231,18 +248,51 @@ const toolFirewall = new ToolPermissionFirewall({
231
248
  });
232
249
  ```
233
250
 
251
+ For document review and verification:
252
+
253
+ ```js
254
+ const shield = new BlackwallShield({
255
+ preset: 'documentReview',
256
+ routePolicies: [
257
+ {
258
+ route: '/api/verify',
259
+ options: {
260
+ shadowMode: true,
261
+ outputFirewallDefaults: { requiredSchema: { verdict: 'string' } },
262
+ },
263
+ },
264
+ ],
265
+ });
266
+ ```
267
+
268
+ ### Choose your integration path
269
+
270
+ - Request-only guard: `guardModelRequest()`
271
+ - Request + output review: `protectModelCall()`
272
+ - Strict JSON planner/document workflows: `protectJsonModelCall()`
273
+ - Full provider wrapper: `protectWithAdapter()`
274
+ - Tool firewall + RAG sanitizer: `ToolPermissionFirewall` + `RetrievalSanitizer`
275
+
234
276
  ### Operational telemetry summaries
235
277
 
236
278
  ```js
279
+ const { summarizeOperationalTelemetry } = require('@vpdeva/blackwall-llm-shield-js');
237
280
  const summary = summarizeOperationalTelemetry(events);
238
281
  console.log(summary.byRoute);
282
+ console.log(summary.byFeature);
283
+ console.log(summary.noisiestRoutes);
284
+ console.log(summary.weeklyBlockEstimate);
239
285
  console.log(summary.highestSeverity);
240
286
  ```
241
287
 
288
+ ### TypeScript
289
+
290
+ The package now ships first-class declaration files for the main entry point plus `integrations`, `providers`, and `semantic` subpaths, so local declaration shims should no longer be necessary in TypeScript apps.
291
+
242
292
  ### Inspect model output
243
293
 
244
294
  ```js
245
- const { OutputFirewall } = require('blackwall-llm-shield-js');
295
+ const { OutputFirewall } = require('@vpdeva/blackwall-llm-shield-js');
246
296
 
247
297
  const firewall = new OutputFirewall({
248
298
  riskThreshold: 'high',
@@ -259,7 +309,7 @@ console.log(review.allowed);
259
309
  ### Gate tool execution
260
310
 
261
311
  ```js
262
- const { ToolPermissionFirewall } = require('blackwall-llm-shield-js');
312
+ const { ToolPermissionFirewall } = require('@vpdeva/blackwall-llm-shield-js');
263
313
 
264
314
  const tools = new ToolPermissionFirewall({
265
315
  allowedTools: ['search', 'lookupCustomer'],
@@ -274,6 +324,10 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
274
324
  - [`examples/nextjs-app-router/app/api/chat/route.js`](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/examples/nextjs-app-router/app/api/chat/route.js) shows guarded request handling in a Next.js route
275
325
  - [`examples/admin-dashboard/index.html`](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/examples/admin-dashboard/index.html) shows a polished security command center demo
276
326
 
327
+ For Next.js, the most production-real patterns are App Router route handlers, server actions for trusted internal mutations, and streaming endpoints that apply output review to assembled or final chunks instead of raw intermediate tokens.
328
+
329
+ For Gemini-heavy apps, the bundled adapter now preserves system instructions plus mixed text/image/file parts so App Router handlers can wrap direct `@google/generative-ai` calls with less translation glue.
330
+
277
331
  ## Release Commands
278
332
 
279
333
  - `npm run release:check` runs the JS test suite before release
package/index.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
2
+
3
+ export interface MessagePart {
4
+ type: string;
5
+ text?: string;
6
+ image_url?: string;
7
+ file_id?: string;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export interface ShieldMessage {
12
+ role: 'system' | 'user' | 'assistant';
13
+ content: string | MessagePart[] | Record<string, unknown>;
14
+ trusted?: boolean;
15
+ contentParts?: MessagePart[];
16
+ }
17
+
18
+ export interface GuardResult {
19
+ allowed: boolean;
20
+ blocked: boolean;
21
+ reason: string | null;
22
+ messages: Array<ShieldMessage & { content: string; contentParts?: MessagePart[] }>;
23
+ report: Record<string, unknown>;
24
+ vault: Record<string, string>;
25
+ }
26
+
27
+ export interface ReviewResult {
28
+ allowed: boolean;
29
+ severity: RiskLevel;
30
+ findings: Array<Record<string, unknown>>;
31
+ report: Record<string, unknown>;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export interface JsonProtectionResult extends Record<string, unknown> {
36
+ allowed: boolean;
37
+ blocked: boolean;
38
+ json?: {
39
+ parsed: unknown;
40
+ schemaValid: boolean;
41
+ parseError?: string;
42
+ };
43
+ }
44
+
45
+ export interface ProviderAdapter {
46
+ provider: string;
47
+ invoke(payload: { messages: ShieldMessage[]; metadata?: Record<string, unknown>; guard?: GuardResult }): Promise<unknown> | unknown;
48
+ extractOutput?(response: unknown, request?: GuardResult): unknown;
49
+ }
50
+
51
+ export interface ShieldOptions {
52
+ preset?: string | null;
53
+ policyPack?: string | null;
54
+ shadowMode?: boolean;
55
+ routePolicies?: Array<{ route: string | RegExp | ((route: string, metadata: Record<string, unknown>) => boolean); options: Record<string, unknown> }>;
56
+ customPromptDetectors?: Array<(payload: Record<string, unknown>) => Record<string, unknown> | Array<Record<string, unknown>> | null>;
57
+ onTelemetry?: (event: Record<string, unknown>) => void | Promise<void>;
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ export class BlackwallShield {
62
+ constructor(options?: ShieldOptions);
63
+ inspectText(text: unknown): Record<string, unknown>;
64
+ guardModelRequest(input?: { messages?: ShieldMessage[]; metadata?: Record<string, unknown>; allowSystemMessages?: boolean; comparePolicyPacks?: string[] }): Promise<GuardResult>;
65
+ reviewModelResponse(input?: { output: unknown; metadata?: Record<string, unknown>; outputFirewall?: OutputFirewall | null; firewallOptions?: Record<string, unknown> }): Promise<ReviewResult>;
66
+ protectModelCall(input: Record<string, unknown>): Promise<Record<string, unknown>>;
67
+ protectJsonModelCall(input: Record<string, unknown>): Promise<JsonProtectionResult>;
68
+ protectWithAdapter(input: { adapter: ProviderAdapter; messages?: ShieldMessage[]; metadata?: Record<string, unknown>; allowSystemMessages?: boolean; comparePolicyPacks?: string[]; outputFirewall?: OutputFirewall | null; firewallOptions?: Record<string, unknown> }): Promise<Record<string, unknown>>;
69
+ }
70
+
71
+ export class OutputFirewall {
72
+ constructor(options?: Record<string, unknown>);
73
+ inspect(output: unknown, options?: Record<string, unknown>): ReviewResult;
74
+ }
75
+
76
+ export class ToolPermissionFirewall {
77
+ constructor(options?: Record<string, unknown>);
78
+ inspectCall(input: Record<string, unknown>): Record<string, unknown>;
79
+ inspectCallAsync?(input: Record<string, unknown>): Promise<Record<string, unknown>>;
80
+ }
81
+
82
+ export class RetrievalSanitizer {
83
+ constructor(options?: Record<string, unknown>);
84
+ sanitizeDocuments(documents: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
85
+ }
86
+
87
+ export class AuditTrail {
88
+ constructor(options?: Record<string, unknown>);
89
+ record(event?: Record<string, unknown>): Record<string, unknown>;
90
+ summarize(): Record<string, unknown>;
91
+ }
92
+
93
+ export const SHIELD_PRESETS: Record<string, Record<string, unknown>>;
94
+ export const CORE_INTERFACES: Record<string, string>;
95
+ export const POLICY_PACKS: Record<string, Record<string, unknown>>;
96
+
97
+ export function buildShieldOptions(options?: Record<string, unknown>): Record<string, unknown>;
98
+ export function summarizeOperationalTelemetry(events?: Array<Record<string, unknown>>): Record<string, unknown>;
99
+ export function parseJsonOutput(output: unknown): unknown;
100
+
101
+ export function createOpenAIAdapter(input: Record<string, unknown>): ProviderAdapter;
102
+ export function createAnthropicAdapter(input: Record<string, unknown>): ProviderAdapter;
103
+ export function createGeminiAdapter(input: Record<string, unknown>): ProviderAdapter;
104
+ export function createOpenRouterAdapter(input: Record<string, unknown>): ProviderAdapter;
@@ -0,0 +1,18 @@
1
+ export { BlackwallShield, OutputFirewall, type ProviderAdapter } from './index';
2
+
3
+ export class BlackwallLangChainCallback {
4
+ constructor(options?: Record<string, unknown>);
5
+ handleLLMStart(llm: unknown, prompts?: string[]): Promise<unknown>;
6
+ guardMessages(messages: unknown, metadata?: Record<string, unknown>): Promise<unknown>;
7
+ handleLLMEnd(output: unknown): Promise<unknown>;
8
+ }
9
+
10
+ export class BlackwallLlamaIndexCallback {
11
+ constructor(options?: Record<string, unknown>);
12
+ onEventStart(event: unknown): Promise<unknown>;
13
+ onEventEnd(event: unknown): Promise<unknown>;
14
+ }
15
+
16
+ export function createExpressMiddleware(options?: Record<string, unknown>): (req: unknown, res: unknown, next: () => void) => Promise<void>;
17
+ export function createLangChainCallbacks(options?: Record<string, unknown>): Record<string, unknown>;
18
+ export function createLlamaIndexCallback(options?: Record<string, unknown>): Record<string, unknown>;
package/package.json CHANGED
@@ -1,16 +1,29 @@
1
1
  {
2
2
  "name": "@vpdeva/blackwall-llm-shield-js",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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)",
7
7
  "type": "commonjs",
8
8
  "main": "src/index.js",
9
+ "types": "./index.d.ts",
9
10
  "exports": {
10
- ".": "./src/index.js",
11
- "./integrations": "./src/integrations.js",
12
- "./providers": "./src/providers.js",
13
- "./semantic": "./src/semantic.js"
11
+ ".": {
12
+ "types": "./index.d.ts",
13
+ "default": "./src/index.js"
14
+ },
15
+ "./integrations": {
16
+ "types": "./integrations.d.ts",
17
+ "default": "./src/integrations.js"
18
+ },
19
+ "./providers": {
20
+ "types": "./providers.d.ts",
21
+ "default": "./src/providers.js"
22
+ },
23
+ "./semantic": {
24
+ "types": "./semantic.d.ts",
25
+ "default": "./src/semantic.js"
26
+ }
14
27
  },
15
28
  "bin": {
16
29
  "blackwall-scorecard": "src/scorecard.js"
@@ -26,6 +39,10 @@
26
39
  },
27
40
  "files": [
28
41
  "src",
42
+ "index.d.ts",
43
+ "integrations.d.ts",
44
+ "providers.d.ts",
45
+ "semantic.d.ts",
29
46
  "README.md",
30
47
  "LICENSE",
31
48
  "NOTICE"
package/providers.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type { ProviderAdapter } from './index';
2
+
3
+ export function createOpenAIAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
4
+ export function createAnthropicAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
5
+ export function createGeminiAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
6
+ export function createOpenRouterAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
package/semantic.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export class LightweightIntentScorer {
2
+ constructor(options?: Record<string, unknown>);
3
+ score(text: unknown, options?: Record<string, unknown>): Record<string, unknown>;
4
+ }
package/src/index.js CHANGED
@@ -145,6 +145,34 @@ const SHIELD_PRESETS = {
145
145
  notifyOnRiskLevel: 'medium',
146
146
  shadowMode: false,
147
147
  },
148
+ agentPlanner: {
149
+ blockOnPromptInjection: true,
150
+ promptInjectionThreshold: 'medium',
151
+ notifyOnRiskLevel: 'medium',
152
+ shadowMode: true,
153
+ shadowPolicyPacks: ['government'],
154
+ },
155
+ documentReview: {
156
+ blockOnPromptInjection: true,
157
+ promptInjectionThreshold: 'high',
158
+ notifyOnRiskLevel: 'medium',
159
+ shadowMode: true,
160
+ policyPack: 'healthcare',
161
+ },
162
+ ragSearch: {
163
+ blockOnPromptInjection: true,
164
+ promptInjectionThreshold: 'medium',
165
+ notifyOnRiskLevel: 'medium',
166
+ shadowMode: true,
167
+ shadowPolicyPacks: ['government'],
168
+ },
169
+ toolCalling: {
170
+ blockOnPromptInjection: true,
171
+ promptInjectionThreshold: 'medium',
172
+ notifyOnRiskLevel: 'medium',
173
+ shadowMode: false,
174
+ policyPack: 'finance',
175
+ },
148
176
  };
149
177
 
150
178
  const CORE_INTERFACE_VERSION = '1.0';
@@ -152,6 +180,7 @@ const CORE_INTERFACES = Object.freeze({
152
180
  guardModelRequest: CORE_INTERFACE_VERSION,
153
181
  reviewModelResponse: CORE_INTERFACE_VERSION,
154
182
  protectModelCall: CORE_INTERFACE_VERSION,
183
+ protectJsonModelCall: CORE_INTERFACE_VERSION,
155
184
  toolPermissionFirewall: CORE_INTERFACE_VERSION,
156
185
  retrievalSanitizer: CORE_INTERFACE_VERSION,
157
186
  });
@@ -350,24 +379,64 @@ function summarizeOperationalTelemetry(events = []) {
350
379
  shadowModeEvents: 0,
351
380
  byType: {},
352
381
  byRoute: {},
382
+ byFeature: {},
383
+ byTenant: {},
384
+ byModel: {},
385
+ byPolicyOutcome: {
386
+ blocked: 0,
387
+ shadowBlocked: 0,
388
+ allowed: 0,
389
+ },
390
+ topRules: {},
353
391
  highestSeverity: 'low',
392
+ noisiestRoutes: [],
393
+ weeklyBlockEstimate: 0,
354
394
  };
355
395
  for (const event of Array.isArray(events) ? events : []) {
356
396
  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';
397
+ const metadata = event && event.metadata ? event.metadata : {};
398
+ const route = metadata.route || metadata.path || 'unknown';
399
+ const feature = metadata.feature || metadata.capability || route;
400
+ const tenant = metadata.tenantId || metadata.tenant_id || 'unknown';
401
+ const model = metadata.model || metadata.modelName || 'unknown';
358
402
  const severity = event && event.report && event.report.outputReview
359
403
  ? event.report.outputReview.severity
360
404
  : (event && event.report && event.report.promptInjection ? event.report.promptInjection.level : 'low');
361
405
  summary.totalEvents += 1;
362
406
  summary.byType[type] = (summary.byType[type] || 0) + 1;
363
407
  summary.byRoute[route] = (summary.byRoute[route] || 0) + 1;
408
+ summary.byFeature[feature] = (summary.byFeature[feature] || 0) + 1;
409
+ summary.byTenant[tenant] = (summary.byTenant[tenant] || 0) + 1;
410
+ summary.byModel[model] = (summary.byModel[model] || 0) + 1;
364
411
  if (event && event.blocked) summary.blockedEvents += 1;
365
412
  if (event && event.shadowMode) summary.shadowModeEvents += 1;
413
+ if (event && event.blocked) summary.byPolicyOutcome.blocked += 1;
414
+ else if (event && event.shadowMode) summary.byPolicyOutcome.shadowBlocked += 1;
415
+ else summary.byPolicyOutcome.allowed += 1;
416
+ const rules = event && event.report && event.report.promptInjection && Array.isArray(event.report.promptInjection.matches)
417
+ ? event.report.promptInjection.matches.map((item) => item.id).filter(Boolean)
418
+ : [];
419
+ rules.forEach((rule) => {
420
+ summary.topRules[rule] = (summary.topRules[rule] || 0) + 1;
421
+ });
366
422
  if (severityWeight(severity) > severityWeight(summary.highestSeverity)) summary.highestSeverity = severity;
367
423
  }
424
+ summary.topRules = Object.fromEntries(
425
+ Object.entries(summary.topRules).sort((a, b) => b[1] - a[1]).slice(0, 10)
426
+ );
427
+ summary.noisiestRoutes = Object.entries(summary.byRoute)
428
+ .sort((a, b) => b[1] - a[1])
429
+ .slice(0, 5)
430
+ .map(([route, count]) => ({ route, count }));
431
+ summary.weeklyBlockEstimate = summary.byPolicyOutcome.blocked + summary.byPolicyOutcome.shadowBlocked;
368
432
  return summary;
369
433
  }
370
434
 
435
+ function parseJsonOutput(output) {
436
+ if (typeof output === 'string') return JSON.parse(output);
437
+ return output;
438
+ }
439
+
371
440
  function resolveShieldPreset(name) {
372
441
  if (!name) return {};
373
442
  return SHIELD_PRESETS[name] ? { ...SHIELD_PRESETS[name] } : {};
@@ -1327,6 +1396,69 @@ class BlackwallShield {
1327
1396
  },
1328
1397
  });
1329
1398
  }
1399
+
1400
+ async protectJsonModelCall({
1401
+ messages = [],
1402
+ metadata = {},
1403
+ allowSystemMessages = this.options.allowSystemMessages,
1404
+ comparePolicyPacks = [],
1405
+ callModel,
1406
+ mapMessages = null,
1407
+ mapOutput = null,
1408
+ outputFirewall = null,
1409
+ firewallOptions = {},
1410
+ requiredSchema = null,
1411
+ } = {}) {
1412
+ const result = await this.protectModelCall({
1413
+ messages,
1414
+ metadata,
1415
+ allowSystemMessages,
1416
+ comparePolicyPacks,
1417
+ callModel,
1418
+ mapMessages,
1419
+ mapOutput,
1420
+ outputFirewall,
1421
+ firewallOptions,
1422
+ });
1423
+ if (result.blocked) return result;
1424
+ try {
1425
+ const parsed = parseJsonOutput(result.review.maskedOutput != null ? result.review.maskedOutput : result.response);
1426
+ const schemaValid = validateRequiredSchema(parsed, requiredSchema);
1427
+ if (!schemaValid) {
1428
+ return {
1429
+ ...result,
1430
+ allowed: false,
1431
+ blocked: true,
1432
+ stage: 'output',
1433
+ reason: 'Model output failed JSON schema validation',
1434
+ json: {
1435
+ parsed,
1436
+ schemaValid: false,
1437
+ },
1438
+ };
1439
+ }
1440
+ return {
1441
+ ...result,
1442
+ json: {
1443
+ parsed,
1444
+ schemaValid: true,
1445
+ },
1446
+ };
1447
+ } catch (error) {
1448
+ return {
1449
+ ...result,
1450
+ allowed: false,
1451
+ blocked: true,
1452
+ stage: 'output',
1453
+ reason: 'Model output is not valid JSON',
1454
+ json: {
1455
+ parsed: null,
1456
+ schemaValid: false,
1457
+ parseError: error.message,
1458
+ },
1459
+ };
1460
+ }
1461
+ }
1330
1462
  }
1331
1463
 
1332
1464
  function validateGrounding(text, documents = [], options = {}) {
@@ -2035,6 +2167,7 @@ module.exports = {
2035
2167
  runRedTeamSuite,
2036
2168
  buildShieldOptions,
2037
2169
  summarizeOperationalTelemetry,
2170
+ parseJsonOutput,
2038
2171
  createOpenAIAdapter,
2039
2172
  createAnthropicAdapter,
2040
2173
  createGeminiAdapter,
package/src/providers.js CHANGED
@@ -12,6 +12,38 @@ function stringifyContent(content) {
12
12
  return String(content || '');
13
13
  }
14
14
 
15
+ function toGeminiPart(item) {
16
+ if (typeof item === 'string') return { text: item };
17
+ if (!item || typeof item !== 'object') return null;
18
+ if ((item.type === 'text' || item.type === 'input_text') && typeof item.text === 'string') {
19
+ return { text: item.text };
20
+ }
21
+ if (item.type === 'image_url' && typeof item.image_url === 'string') {
22
+ return { fileData: { fileUri: item.image_url } };
23
+ }
24
+ if (item.type === 'file') {
25
+ if (item.file_data && typeof item.file_data === 'object') return { inlineData: item.file_data };
26
+ if (typeof item.file_uri === 'string') return { fileData: { fileUri: item.file_uri } };
27
+ if (typeof item.file_id === 'string') return { fileData: { fileUri: item.file_id } };
28
+ }
29
+ if (item.type === 'json' && typeof item.value === 'string') {
30
+ return { text: item.value };
31
+ }
32
+ if (typeof item.text === 'string') return { text: item.text };
33
+ return null;
34
+ }
35
+
36
+ function toGeminiParts(content) {
37
+ if (typeof content === 'string') return [{ text: content }];
38
+ if (Array.isArray(content)) return content.map((item) => toGeminiPart(item)).filter(Boolean);
39
+ if (content && typeof content === 'object') {
40
+ if (Array.isArray(content.parts)) return toGeminiParts(content.parts);
41
+ const part = toGeminiPart(content);
42
+ return part ? [part] : [{ text: stringifyContent(content) }];
43
+ }
44
+ return [{ text: String(content || '') }];
45
+ }
46
+
15
47
  function toOpenAIInput(messages = []) {
16
48
  return messages.map((message) => ({
17
49
  role: message.role,
@@ -101,19 +133,30 @@ function createGeminiAdapter({ client, model, request = {}, extractOutput = null
101
133
  return {
102
134
  provider: 'gemini',
103
135
  async invoke({ messages }) {
136
+ const systemInstruction = extractSystemPrompt(messages);
104
137
  const response = await client.models.generateContent({
105
138
  model,
106
- contents: messages.map((message) => ({
107
- role: message.role === 'assistant' ? 'model' : 'user',
108
- parts: [{ text: stringifyContent(message.content) }],
109
- })),
139
+ contents: messages
140
+ .filter((message) => message.role !== 'system')
141
+ .map((message) => ({
142
+ role: message.role === 'assistant' ? 'model' : 'user',
143
+ parts: toGeminiParts(message.content),
144
+ })),
145
+ ...(systemInstruction ? { systemInstruction: { parts: [{ text: systemInstruction }] } } : {}),
110
146
  ...request,
111
147
  });
112
- return defaultAdapterResult(response, response && typeof response.text === 'string' ? response.text : '');
148
+ return defaultAdapterResult(response, this.extractOutput(response));
113
149
  },
114
150
  extractOutput(response) {
115
151
  if (typeof extractOutput === 'function') return extractOutput(response);
116
152
  if (response && typeof response.text === 'string') return response.text;
153
+ if (response && Array.isArray(response.candidates)) {
154
+ return response.candidates
155
+ .flatMap((candidate) => (((candidate || {}).content || {}).parts || []))
156
+ .map((part) => (part && typeof part.text === 'string' ? part.text : ''))
157
+ .filter(Boolean)
158
+ .join('\n');
159
+ }
117
160
  if (typeof response === 'string') return response;
118
161
  return '';
119
162
  },