@intflows/genkit-guard 0.0.14 → 0.1.0

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.
@@ -1,3 +1,4 @@
1
+ import type { GuardDecisionStore } from '../core/decision-storage.js';
1
2
  import { type GuardDecision, type ToolGuardConfig } from '../core/decision.js';
2
3
  import { type PiiLabelMappings } from '../guard.config.js';
3
4
  import { z } from 'genkit';
@@ -17,6 +18,11 @@ export type GuardConfig = {
17
18
  reversible?: boolean;
18
19
  model?: string;
19
20
  mode?: 'ner' | 'classifier';
21
+ fallback?: {
22
+ model: string;
23
+ mode?: 'ner' | 'classifier';
24
+ labelMappings?: PiiLabelMappings;
25
+ };
20
26
  labelMappings?: PiiLabelMappings;
21
27
  vault?: {
22
28
  storage?: PiiVaultStorage;
@@ -29,9 +35,12 @@ export type GuardConfig = {
29
35
  serviceName?: string;
30
36
  /** Awaited audit callback, independent of console logging level/enabled. Failure stops execution. */
31
37
  onDecision?: (decision: GuardDecision) => void | Promise<void>;
38
+ /** Durable audit delivery. Failure stops execution; called before onDecision. */
39
+ store?: GuardDecisionStore;
32
40
  };
33
41
  models?: {
34
42
  extractor?: string;
43
+ extractorFallback?: string;
35
44
  };
36
45
  [key: string]: any;
37
46
  };
@@ -83,6 +92,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
83
92
  reversible: z.ZodOptional<z.ZodBoolean>;
84
93
  model: z.ZodOptional<z.ZodString>;
85
94
  mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
95
+ fallback: z.ZodOptional<z.ZodObject<{
96
+ model: z.ZodString;
97
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
98
+ labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
99
+ }, "strip", z.ZodTypeAny, {
100
+ model: string;
101
+ mode?: "ner" | "classifier" | undefined;
102
+ labelMappings?: Record<string, string | null> | undefined;
103
+ }, {
104
+ model: string;
105
+ mode?: "ner" | "classifier" | undefined;
106
+ labelMappings?: Record<string, string | null> | undefined;
107
+ }>>;
86
108
  labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
87
109
  vault: z.ZodOptional<z.ZodObject<{
88
110
  storage: z.ZodOptional<z.ZodAny>;
@@ -96,18 +118,28 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
96
118
  }>>;
97
119
  }, "strip", z.ZodTypeAny, {
98
120
  model?: string | undefined;
99
- labelMappings?: Record<string, string | null> | undefined;
121
+ fallback?: {
122
+ model: string;
123
+ mode?: "ner" | "classifier" | undefined;
124
+ labelMappings?: Record<string, string | null> | undefined;
125
+ } | undefined;
100
126
  mode?: "ner" | "classifier" | undefined;
101
127
  reversible?: boolean | undefined;
128
+ labelMappings?: Record<string, string | null> | undefined;
102
129
  vault?: {
103
130
  storage?: any;
104
131
  scopeId?: any;
105
132
  } | undefined;
106
133
  }, {
107
134
  model?: string | undefined;
108
- labelMappings?: Record<string, string | null> | undefined;
135
+ fallback?: {
136
+ model: string;
137
+ mode?: "ner" | "classifier" | undefined;
138
+ labelMappings?: Record<string, string | null> | undefined;
139
+ } | undefined;
109
140
  mode?: "ner" | "classifier" | undefined;
110
141
  reversible?: boolean | undefined;
142
+ labelMappings?: Record<string, string | null> | undefined;
111
143
  vault?: {
112
144
  storage?: any;
113
145
  scopeId?: any;
@@ -118,23 +150,29 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
118
150
  level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
119
151
  serviceName: z.ZodOptional<z.ZodString>;
120
152
  onDecision: z.ZodOptional<z.ZodAny>;
153
+ store: z.ZodOptional<z.ZodAny>;
121
154
  }, "strip", z.ZodTypeAny, {
122
155
  enabled?: boolean | undefined;
123
156
  level?: "debug" | "info" | "warn" | "error" | undefined;
124
157
  serviceName?: string | undefined;
125
158
  onDecision?: any;
159
+ store?: any;
126
160
  }, {
127
161
  enabled?: boolean | undefined;
128
162
  level?: "debug" | "info" | "warn" | "error" | undefined;
129
163
  serviceName?: string | undefined;
130
164
  onDecision?: any;
165
+ store?: any;
131
166
  }>>;
132
167
  models: z.ZodOptional<z.ZodObject<{
133
168
  extractor: z.ZodOptional<z.ZodString>;
169
+ extractorFallback: z.ZodOptional<z.ZodString>;
134
170
  }, "strip", z.ZodTypeAny, {
135
171
  extractor?: string | undefined;
172
+ extractorFallback?: string | undefined;
136
173
  }, {
137
174
  extractor?: string | undefined;
175
+ extractorFallback?: string | undefined;
138
176
  }>>;
139
177
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
140
178
  policyVersion: z.ZodOptional<z.ZodString>;
@@ -183,6 +221,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
183
221
  reversible: z.ZodOptional<z.ZodBoolean>;
184
222
  model: z.ZodOptional<z.ZodString>;
185
223
  mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
224
+ fallback: z.ZodOptional<z.ZodObject<{
225
+ model: z.ZodString;
226
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
227
+ labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
228
+ }, "strip", z.ZodTypeAny, {
229
+ model: string;
230
+ mode?: "ner" | "classifier" | undefined;
231
+ labelMappings?: Record<string, string | null> | undefined;
232
+ }, {
233
+ model: string;
234
+ mode?: "ner" | "classifier" | undefined;
235
+ labelMappings?: Record<string, string | null> | undefined;
236
+ }>>;
186
237
  labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
187
238
  vault: z.ZodOptional<z.ZodObject<{
188
239
  storage: z.ZodOptional<z.ZodAny>;
@@ -196,18 +247,28 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
196
247
  }>>;
197
248
  }, "strip", z.ZodTypeAny, {
198
249
  model?: string | undefined;
199
- labelMappings?: Record<string, string | null> | undefined;
250
+ fallback?: {
251
+ model: string;
252
+ mode?: "ner" | "classifier" | undefined;
253
+ labelMappings?: Record<string, string | null> | undefined;
254
+ } | undefined;
200
255
  mode?: "ner" | "classifier" | undefined;
201
256
  reversible?: boolean | undefined;
257
+ labelMappings?: Record<string, string | null> | undefined;
202
258
  vault?: {
203
259
  storage?: any;
204
260
  scopeId?: any;
205
261
  } | undefined;
206
262
  }, {
207
263
  model?: string | undefined;
208
- labelMappings?: Record<string, string | null> | undefined;
264
+ fallback?: {
265
+ model: string;
266
+ mode?: "ner" | "classifier" | undefined;
267
+ labelMappings?: Record<string, string | null> | undefined;
268
+ } | undefined;
209
269
  mode?: "ner" | "classifier" | undefined;
210
270
  reversible?: boolean | undefined;
271
+ labelMappings?: Record<string, string | null> | undefined;
211
272
  vault?: {
212
273
  storage?: any;
213
274
  scopeId?: any;
@@ -218,23 +279,29 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
218
279
  level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
219
280
  serviceName: z.ZodOptional<z.ZodString>;
220
281
  onDecision: z.ZodOptional<z.ZodAny>;
282
+ store: z.ZodOptional<z.ZodAny>;
221
283
  }, "strip", z.ZodTypeAny, {
222
284
  enabled?: boolean | undefined;
223
285
  level?: "debug" | "info" | "warn" | "error" | undefined;
224
286
  serviceName?: string | undefined;
225
287
  onDecision?: any;
288
+ store?: any;
226
289
  }, {
227
290
  enabled?: boolean | undefined;
228
291
  level?: "debug" | "info" | "warn" | "error" | undefined;
229
292
  serviceName?: string | undefined;
230
293
  onDecision?: any;
294
+ store?: any;
231
295
  }>>;
232
296
  models: z.ZodOptional<z.ZodObject<{
233
297
  extractor: z.ZodOptional<z.ZodString>;
298
+ extractorFallback: z.ZodOptional<z.ZodString>;
234
299
  }, "strip", z.ZodTypeAny, {
235
300
  extractor?: string | undefined;
301
+ extractorFallback?: string | undefined;
236
302
  }, {
237
303
  extractor?: string | undefined;
304
+ extractorFallback?: string | undefined;
238
305
  }>>;
239
306
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
240
307
  policyVersion: z.ZodOptional<z.ZodString>;
@@ -283,6 +350,19 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
283
350
  reversible: z.ZodOptional<z.ZodBoolean>;
284
351
  model: z.ZodOptional<z.ZodString>;
285
352
  mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
353
+ fallback: z.ZodOptional<z.ZodObject<{
354
+ model: z.ZodString;
355
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
356
+ labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
357
+ }, "strip", z.ZodTypeAny, {
358
+ model: string;
359
+ mode?: "ner" | "classifier" | undefined;
360
+ labelMappings?: Record<string, string | null> | undefined;
361
+ }, {
362
+ model: string;
363
+ mode?: "ner" | "classifier" | undefined;
364
+ labelMappings?: Record<string, string | null> | undefined;
365
+ }>>;
286
366
  labelMappings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
287
367
  vault: z.ZodOptional<z.ZodObject<{
288
368
  storage: z.ZodOptional<z.ZodAny>;
@@ -296,18 +376,28 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
296
376
  }>>;
297
377
  }, "strip", z.ZodTypeAny, {
298
378
  model?: string | undefined;
299
- labelMappings?: Record<string, string | null> | undefined;
379
+ fallback?: {
380
+ model: string;
381
+ mode?: "ner" | "classifier" | undefined;
382
+ labelMappings?: Record<string, string | null> | undefined;
383
+ } | undefined;
300
384
  mode?: "ner" | "classifier" | undefined;
301
385
  reversible?: boolean | undefined;
386
+ labelMappings?: Record<string, string | null> | undefined;
302
387
  vault?: {
303
388
  storage?: any;
304
389
  scopeId?: any;
305
390
  } | undefined;
306
391
  }, {
307
392
  model?: string | undefined;
308
- labelMappings?: Record<string, string | null> | undefined;
393
+ fallback?: {
394
+ model: string;
395
+ mode?: "ner" | "classifier" | undefined;
396
+ labelMappings?: Record<string, string | null> | undefined;
397
+ } | undefined;
309
398
  mode?: "ner" | "classifier" | undefined;
310
399
  reversible?: boolean | undefined;
400
+ labelMappings?: Record<string, string | null> | undefined;
311
401
  vault?: {
312
402
  storage?: any;
313
403
  scopeId?: any;
@@ -318,23 +408,29 @@ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodO
318
408
  level: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>;
319
409
  serviceName: z.ZodOptional<z.ZodString>;
320
410
  onDecision: z.ZodOptional<z.ZodAny>;
411
+ store: z.ZodOptional<z.ZodAny>;
321
412
  }, "strip", z.ZodTypeAny, {
322
413
  enabled?: boolean | undefined;
323
414
  level?: "debug" | "info" | "warn" | "error" | undefined;
324
415
  serviceName?: string | undefined;
325
416
  onDecision?: any;
417
+ store?: any;
326
418
  }, {
327
419
  enabled?: boolean | undefined;
328
420
  level?: "debug" | "info" | "warn" | "error" | undefined;
329
421
  serviceName?: string | undefined;
330
422
  onDecision?: any;
423
+ store?: any;
331
424
  }>>;
332
425
  models: z.ZodOptional<z.ZodObject<{
333
426
  extractor: z.ZodOptional<z.ZodString>;
427
+ extractorFallback: z.ZodOptional<z.ZodString>;
334
428
  }, "strip", z.ZodTypeAny, {
335
429
  extractor?: string | undefined;
430
+ extractorFallback?: string | undefined;
336
431
  }, {
337
432
  extractor?: string | undefined;
433
+ extractorFallback?: string | undefined;
338
434
  }>>;
339
435
  }, z.ZodTypeAny, "passthrough">>, void>;
340
436
  export declare const guardPlugin: (pluginOptions: void) => import("@genkit-ai/ai").GenkitPluginV2;
@@ -1,4 +1,5 @@
1
- import { randomUUID } from 'node:crypto';
1
+ import { publishDecision } from '../core/audit.js';
2
+ import { runGuardModel } from '../util/fallback.js';
2
3
  import { GuardToolError } from '../core/decision.js';
3
4
  import { resolveGuardModels } from '../guard.config.js';
4
5
  import { generateMiddleware, z } from 'genkit';
@@ -27,6 +28,10 @@ const guardConfigSchema = z.object({
27
28
  reversible: z.boolean().optional(),
28
29
  model: z.string().optional(),
29
30
  mode: z.enum(['ner', 'classifier']).optional(),
31
+ fallback: z.object({
32
+ model: z.string(), mode: z.enum(['ner', 'classifier']).optional(),
33
+ labelMappings: z.record(z.string(), z.string().regex(/^[A-Z_]+$/).nullable()).optional(),
34
+ }).optional(),
30
35
  labelMappings: z.record(z.string(), z.string().regex(/^[A-Z_]+$/).nullable()).optional(),
31
36
  vault: z.object({
32
37
  storage: z.any().optional(),
@@ -38,9 +43,11 @@ const guardConfigSchema = z.object({
38
43
  level: z.enum(['debug', 'info', 'warn', 'error']).optional(),
39
44
  serviceName: z.string().optional(),
40
45
  onDecision: z.any().optional(),
46
+ store: z.any().optional(),
41
47
  }).optional(),
42
48
  models: z.object({
43
49
  extractor: z.string().optional(),
50
+ extractorFallback: z.string().optional(),
44
51
  }).optional(),
45
52
  }).passthrough();
46
53
  export const guardMiddleware = generateMiddleware({
@@ -79,16 +86,7 @@ export const guardAction = guard;
79
86
  function createGuardHooks(config) {
80
87
  const logger = createLogger(config);
81
88
  const models = resolveGuardModels(config);
82
- const decide = async (start, fields) => {
83
- const decision = Object.freeze({
84
- schemaVersion: '1', decisionId: randomUUID(), timestamp: new Date().toISOString(),
85
- policyVersion: config?.policyVersion ?? 'unversioned',
86
- latencyMs: Math.max(0, performance.now() - start), ...fields,
87
- });
88
- logger(decision.action === 'block' || decision.action === 'approval-required' ? 'warn' : 'info', 'guard.decision', 'Guard policy decision', { decision });
89
- await config?.logging?.onDecision?.(decision);
90
- return decision;
91
- };
89
+ const decide = (start, fields) => publishDecision(config, start, fields);
92
90
  return {
93
91
  model: async (req, ctx, next) => {
94
92
  const started = performance.now();
@@ -107,7 +105,8 @@ function createGuardHooks(config) {
107
105
  await decide(started, { guard: 'injection', action: 'allow', reasonCode: 'INJECTION_CLEAR' });
108
106
  const intentStarted = performance.now();
109
107
  logger('info', 'guard.intent.analysis.start', 'Analyzing request intent');
110
- const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7, models.extractor);
108
+ const analyze = (model) => analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7, model);
109
+ const intentResult = await runGuardModel(config, 'intent', () => analyze(models.extractor), config?.models?.extractorFallback ? () => analyze(config.models.extractorFallback) : undefined);
111
110
  logger('info', 'guard.intent.analysis.complete', 'Intent analysis completed', {
112
111
  intent: intentResult.intent,
113
112
  score: roundScore(intentResult.score),
@@ -143,7 +142,7 @@ function createGuardHooks(config) {
143
142
  piiDetected: piiMatches.length > 0,
144
143
  piiMatchCount: piiMatches.length,
145
144
  piiTypes,
146
- piiMode: config?.pii?.mode ?? 'ner',
145
+ piiMode: piiResponse.effectiveMode ?? models.mode,
147
146
  classifierOutputPresent: Boolean(piiResponse.classifier),
148
147
  });
149
148
  req.metadata = {
@@ -156,6 +155,9 @@ function createGuardHooks(config) {
156
155
  piiModel: config?.pii?.model,
157
156
  piiMode: config?.pii?.mode,
158
157
  piiClassifierOutput: piiResponse.classifier,
158
+ piiEffectiveModel: piiResponse.effectiveModel,
159
+ piiEffectiveMode: piiResponse.effectiveMode,
160
+ piiUsedFallback: piiResponse.usedFallback,
159
161
  };
160
162
  const res = await next(req, ctx);
161
163
  const unmaskedResponse = await unmaskObject(res, getGuardState(ctx).tokenizers);
@@ -212,6 +214,9 @@ function createGuardHooks(config) {
212
214
  piiDetected: piiMatches.length > 0,
213
215
  piiTypes,
214
216
  piiMatchCount: piiMatches.length,
217
+ piiEffectiveModel: piiResponse.effectiveModel,
218
+ piiEffectiveMode: piiResponse.effectiveMode,
219
+ piiUsedFallback: piiResponse.usedFallback,
215
220
  };
216
221
  logger(piiMatches.length > 0 ? 'warn' : 'info', 'guard.tool.pii.checked', 'Tool request PII scan completed', {
217
222
  toolName,
@@ -326,6 +331,9 @@ async function scanPII(text, config) {
326
331
  return {
327
332
  matches: [],
328
333
  classifier: undefined,
334
+ effectiveModel: undefined,
335
+ effectiveMode: undefined,
336
+ usedFallback: false,
329
337
  };
330
338
  }
331
339
  const models = resolveGuardModels(config);
@@ -333,7 +341,8 @@ async function scanPII(text, config) {
333
341
  model: models.pii,
334
342
  mode: models.mode,
335
343
  labelMappings: config?.pii?.labelMappings,
336
- });
344
+ fallback: config?.pii?.fallback,
345
+ }, config);
337
346
  }
338
347
  function getGuardState(ctx = {}) {
339
348
  ctx.context = ctx.context || {};
@@ -1,3 +1,4 @@
1
+ import type { GuardConfig } from '../middleware/middleware.js';
1
2
  import type { PiiLabelMappings } from '../guard.config.js';
2
3
  export type PiiMatch = {
3
4
  type: string;
@@ -11,12 +12,11 @@ export type PrivacyFilterSpan = {
11
12
  end?: number;
12
13
  score?: number;
13
14
  };
14
- export declare function detectPII(text: string, opts?: {
15
- model?: string;
16
- mode?: 'ner' | 'classifier';
17
- labelMappings?: PiiLabelMappings;
18
- }): Promise<{
15
+ export declare function detectPII(text: string, opts?: GuardConfig['pii'], config?: GuardConfig): Promise<{
19
16
  matches: PiiMatch[];
20
17
  classifier: any;
18
+ effectiveModel: string;
19
+ effectiveMode: "ner" | "classifier";
20
+ usedFallback: boolean;
21
21
  }>;
22
22
  export declare function privacyFilterOutputToMatches(text: string, output: unknown, labelMappings?: PiiLabelMappings): PiiMatch[];
@@ -1,3 +1,4 @@
1
+ import { runGuardModel } from '../util/fallback.js';
1
2
  import { ModelSingleton } from '../util/singleton.js';
2
3
  const PRIVACY_FILTER_TYPE_MAP = {
3
4
  account_number: 'ACCOUNT_NUMBER',
@@ -25,7 +26,7 @@ const REGEX_RULES = [
25
26
  // CREDIT CARD (keep your existing one if needed)
26
27
  { type: 'CREDIT_CARD', pattern: /\b(?:\d[ -]*?){13,16}\b/g }
27
28
  ];
28
- export async function detectPII(text, opts) {
29
+ export async function detectPII(text, opts, config) {
29
30
  const mode = opts?.mode ?? 'ner';
30
31
  const model = opts?.model;
31
32
  const results = [];
@@ -34,16 +35,27 @@ export async function detectPII(text, opts) {
34
35
  const matches = text.match(rule.pattern) || [];
35
36
  matches.forEach(m => results.push({ type: rule.type, value: m }));
36
37
  }
37
- // ---- NER ----
38
+ // Only model loading/inference is retried. Label mapping errors are not model failures.
39
+ const infer = async (selected, usedFallback = false) => {
40
+ const selectedMode = selected.mode ?? mode;
41
+ const pipeline = selectedMode === 'ner'
42
+ ? await ModelSingleton.getNER(selected.model)
43
+ : await ModelSingleton.getPIIClassifier(selected.model);
44
+ const output = selectedMode === 'ner' ? await pipeline(text)
45
+ : await pipeline(text, { aggregation_strategy: 'simple' });
46
+ if (!Array.isArray(output))
47
+ throw new Error('Invalid PII model output');
48
+ return { output, mode: selectedMode, labelMappings: selected.labelMappings, usedFallback,
49
+ model: selected.model ?? (selectedMode === 'ner' ? 'Xenova/bert-base-NER' : 'openai/privacy-filter') };
50
+ };
51
+ const selected = await runGuardModel(config, 'pii', () => infer({ model, mode, labelMappings: opts?.labelMappings }), opts?.fallback ? () => infer(opts.fallback, true) : undefined);
38
52
  let classifierOutput = undefined;
39
- if (mode === 'ner') {
40
- const ner = await ModelSingleton.getNER(model);
41
- const entities = await ner(text);
42
- for (const e of entities) {
43
- const mappedType = mappedLabel(e.entity_group ?? e.entity, opts?.labelMappings);
53
+ if (selected.mode === 'ner') {
54
+ for (const e of selected.output) {
55
+ const mappedType = mappedLabel(e.entity_group ?? e.entity, selected.labelMappings);
44
56
  if (mappedType !== undefined) {
45
57
  if (mappedType)
46
- results.push(...privacyFilterOutputToMatches(text, [e], opts?.labelMappings));
58
+ results.push(...privacyFilterOutputToMatches(text, [e], selected.labelMappings));
47
59
  }
48
60
  else if (e.entity && e.entity.includes('PER')) {
49
61
  results.push({ type: 'NAME', value: (e.word || '').replace(/##/g, '') });
@@ -51,19 +63,18 @@ export async function detectPII(text, opts) {
51
63
  }
52
64
  }
53
65
  else {
54
- // Privacy Filter is a token-classification model. Aggregation produces complete spans
55
- // rather than individual BIOES-labelled tokens.
56
- const cls = await ModelSingleton.getPIIClassifier(model);
57
- classifierOutput = await cls(text, { aggregation_strategy: 'simple' });
58
- for (const match of privacyFilterOutputToMatches(text, classifierOutput, opts?.labelMappings)) {
59
- if (!results.some((existing) => existing.value === match.value)) {
66
+ classifierOutput = selected.output;
67
+ for (const match of privacyFilterOutputToMatches(text, classifierOutput, selected.labelMappings)) {
68
+ if (!results.some((existing) => existing.value === match.value))
60
69
  results.push(match);
61
- }
62
70
  }
63
71
  }
64
72
  return {
65
73
  matches: results,
66
- classifier: classifierOutput
74
+ classifier: classifierOutput,
75
+ effectiveModel: selected.model,
76
+ effectiveMode: selected.mode,
77
+ usedFallback: selected.usedFallback,
67
78
  };
68
79
  }
69
80
  export function privacyFilterOutputToMatches(text, output, labelMappings) {
@@ -0,0 +1,8 @@
1
+ import type { GuardConfig } from '../middleware/middleware.js';
2
+ export declare class GuardModelError extends Error {
3
+ readonly guard: 'intent' | 'pii';
4
+ readonly code = "MODEL_UNAVAILABLE";
5
+ constructor(guard: 'intent' | 'pii');
6
+ }
7
+ /** One fallback attempt per operation, only after a model operation throws. */
8
+ export declare function runGuardModel<T>(config: GuardConfig | undefined, guard: 'intent' | 'pii', primary: () => Promise<T>, fallback?: () => Promise<T>): Promise<T>;
@@ -0,0 +1,32 @@
1
+ import { publishDecision } from '../core/audit.js';
2
+ export class GuardModelError extends Error {
3
+ guard;
4
+ code = 'MODEL_UNAVAILABLE';
5
+ constructor(guard) {
6
+ // Do not attach underlying errors: inference errors can contain request content.
7
+ super(`Primary and fallback ${guard} models failed`);
8
+ this.guard = guard;
9
+ this.name = 'GuardModelError';
10
+ }
11
+ }
12
+ /** One fallback attempt per operation, only after a model operation throws. */
13
+ export async function runGuardModel(config, guard, primary, fallback) {
14
+ const start = performance.now();
15
+ try {
16
+ return await primary();
17
+ }
18
+ catch (error) {
19
+ if (!fallback)
20
+ throw error; // Preserve existing failure behavior without opt-in.
21
+ }
22
+ let result;
23
+ try {
24
+ result = await fallback();
25
+ }
26
+ catch {
27
+ await publishDecision(config, start, { guard, action: 'block', reasonCode: 'MODEL_UNAVAILABLE' });
28
+ throw new GuardModelError(guard);
29
+ }
30
+ await publishDecision(config, start, { guard, action: 'allow', reasonCode: 'MODEL_FALLBACK_USED' });
31
+ return result;
32
+ }
package/package.json CHANGED
@@ -22,7 +22,7 @@
22
22
  "huggingface"
23
23
  ],
24
24
  "license": "Apache-2.0",
25
- "version": "0.0.14",
25
+ "version": "0.1.0",
26
26
  "type": "module",
27
27
  "exports": "./dist/index.js",
28
28
  "types": "./dist/index.d.ts",
@@ -36,7 +36,7 @@
36
36
  "prepare-models": "node scripts/download-model.js",
37
37
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
38
38
  "build": "npm run clean && tsc",
39
- "test": "npm run test:types && npm run test:privacy-filter && npm run test:storage && npm run test:concurrency && npm run test:config && npm run test:release1",
39
+ "test": "npm run test:types && npm run test:privacy-filter && npm run test:storage && npm run test:concurrency && npm run test:config && npm run test:release1 && npm run test:release2",
40
40
  "test:types": "tsc --noEmit --ignoreConfig --module NodeNext --moduleResolution NodeNext --target ESNext --strict --skipLibCheck scripts/test-types.ts",
41
41
  "test:privacy-filter": "npm run build && node scripts/test-privacy-filter.js",
42
42
  "test:storage": "npm run build && node scripts/test-storage.js",
@@ -45,7 +45,8 @@
45
45
  "prepublishOnly": "npm run build",
46
46
  "test:config": "npm run build && node scripts/test-config.js",
47
47
  "wiki:publish": "node scripts/publish-wiki.js",
48
- "test:release1": "npm run build && node --test scripts/test-release1.js scripts/test-publish-wiki.js"
48
+ "test:release1": "npm run build && node --test scripts/test-release1.js scripts/test-publish-wiki.js",
49
+ "test:release2": "npm run build && node --test scripts/test-release2.js"
49
50
  },
50
51
  "dependencies": {
51
52
  "@huggingface/transformers": "^4.2.0",
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
7
7
 
8
8
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
9
9
  const args = process.argv.slice(2);
10
- let source = join(root, 'docs/wiki-v0.0.14');
10
+ let source = join(root, 'docs/wiki-v0.1.0');
11
11
  let repository = 'https://github.com/IntFlows/genkit-guard.wiki.git';
12
12
  let publish = false;
13
13
  for (let i = 0; i < args.length; i++) {