gheim 0.1.1 → 0.1.3
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 +40 -19
- package/dist/cjs/index.cjs +289 -111
- package/dist/cjs/openai/index.cjs +313 -106
- package/dist/esm/index.js +296 -113
- package/dist/esm/openai/index.js +318 -106
- package/dist/types/core/session.d.ts +42 -6
- package/dist/types/core/session.d.ts.map +1 -1
- package/dist/types/detectors/composite.d.ts +58 -0
- package/dist/types/detectors/composite.d.ts.map +1 -0
- package/dist/types/detectors/index.d.ts +3 -1
- package/dist/types/detectors/index.d.ts.map +1 -1
- package/dist/types/detectors/local.d.ts +65 -2
- package/dist/types/detectors/local.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/normalizers.d.ts +5 -2
- package/dist/types/normalizers.d.ts.map +1 -1
- package/dist/types/openai/_chat.d.ts.map +1 -1
- package/dist/types/openai/_client.d.ts +15 -2
- package/dist/types/openai/_client.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/esm/openai/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
3
8
|
|
|
4
9
|
// src/core/sentinels.ts
|
|
5
10
|
var LABEL_TO_TAG = {
|
|
@@ -228,8 +233,10 @@ class LocalDetector {
|
|
|
228
233
|
chunkTokens;
|
|
229
234
|
chunkOverlapTokens;
|
|
230
235
|
batchSize;
|
|
236
|
+
actualDevice = null;
|
|
231
237
|
_pipeline = null;
|
|
232
238
|
_loadPromise = null;
|
|
239
|
+
_onProgress;
|
|
233
240
|
constructor(opts = {}) {
|
|
234
241
|
this.model = opts.model ?? DEFAULT_MODEL2;
|
|
235
242
|
this.device = opts.device ?? "auto";
|
|
@@ -238,11 +245,13 @@ class LocalDetector {
|
|
|
238
245
|
this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
|
|
239
246
|
this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
|
|
240
247
|
this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
248
|
+
this._onProgress = opts.onProgress;
|
|
241
249
|
if (this.chunkOverlapTokens >= this.chunkTokens) {
|
|
242
250
|
throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
|
|
243
251
|
}
|
|
244
252
|
if (opts.pipelineInstance) {
|
|
245
253
|
this._pipeline = opts.pipelineInstance;
|
|
254
|
+
this.actualDevice = this.device === "auto" ? null : this.device;
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
257
|
async load() {
|
|
@@ -251,24 +260,70 @@ class LocalDetector {
|
|
|
251
260
|
if (this._loadPromise)
|
|
252
261
|
return this._loadPromise;
|
|
253
262
|
this._loadPromise = (async () => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
const mod = await loadTransformersJs();
|
|
264
|
+
const sequence = await this._planDeviceSequence();
|
|
265
|
+
let lastErr;
|
|
266
|
+
for (let i = 0;i < sequence.length; i++) {
|
|
267
|
+
const dev = sequence[i] ?? "wasm";
|
|
268
|
+
try {
|
|
269
|
+
const pipe = await mod.pipeline("token-classification", this.model, {
|
|
270
|
+
device: dev,
|
|
271
|
+
dtype: this.dtype,
|
|
272
|
+
progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
|
|
273
|
+
});
|
|
274
|
+
this._pipeline = pipe;
|
|
275
|
+
this.actualDevice = dev;
|
|
276
|
+
return pipe;
|
|
277
|
+
} catch (e) {
|
|
278
|
+
lastErr = e;
|
|
279
|
+
if (i < sequence.length - 1)
|
|
280
|
+
continue;
|
|
281
|
+
throw e;
|
|
282
|
+
}
|
|
259
283
|
}
|
|
260
|
-
|
|
261
|
-
device: this.device,
|
|
262
|
-
dtype: this.dtype
|
|
263
|
-
});
|
|
264
|
-
this._pipeline = pipe;
|
|
265
|
-
return pipe;
|
|
284
|
+
throw lastErr ?? new Error("LocalDetector.load(): no candidate backends");
|
|
266
285
|
})();
|
|
267
286
|
return this._loadPromise;
|
|
268
287
|
}
|
|
288
|
+
async _planDeviceSequence() {
|
|
289
|
+
if (this.device !== "auto")
|
|
290
|
+
return [this.device];
|
|
291
|
+
const seq = [];
|
|
292
|
+
const nav = typeof navigator !== "undefined" ? navigator : null;
|
|
293
|
+
if (nav?.gpu?.requestAdapter) {
|
|
294
|
+
try {
|
|
295
|
+
const adapter = await nav.gpu.requestAdapter();
|
|
296
|
+
if (adapter)
|
|
297
|
+
seq.push("webgpu");
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
seq.push("wasm");
|
|
301
|
+
return seq;
|
|
302
|
+
}
|
|
303
|
+
_emitProgress(raw, device) {
|
|
304
|
+
if (!this._onProgress)
|
|
305
|
+
return;
|
|
306
|
+
const event = {
|
|
307
|
+
phase: raw.status ?? "progress",
|
|
308
|
+
device
|
|
309
|
+
};
|
|
310
|
+
if (raw.file)
|
|
311
|
+
event.file = raw.file;
|
|
312
|
+
if (raw.status === "progress") {
|
|
313
|
+
if (raw.loaded != null)
|
|
314
|
+
event.loadedBytes = raw.loaded;
|
|
315
|
+
if (raw.total != null)
|
|
316
|
+
event.totalBytes = raw.total;
|
|
317
|
+
if (raw.progress != null) {
|
|
318
|
+
const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
|
|
319
|
+
event.fraction = Math.max(0, Math.min(1, norm));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
this._onProgress(event);
|
|
323
|
+
}
|
|
269
324
|
async detect(text, opts) {
|
|
270
325
|
if (opts?.model && opts.model !== this.model) {
|
|
271
|
-
throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
|
|
326
|
+
throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
|
|
272
327
|
}
|
|
273
328
|
if (!text)
|
|
274
329
|
return [];
|
|
@@ -277,7 +332,7 @@ class LocalDetector {
|
|
|
277
332
|
}
|
|
278
333
|
async detectBatch(texts, opts) {
|
|
279
334
|
if (opts?.model && opts.model !== this.model) {
|
|
280
|
-
throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
|
|
335
|
+
throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
|
|
281
336
|
}
|
|
282
337
|
if (texts.length === 0)
|
|
283
338
|
return [];
|
|
@@ -438,102 +493,225 @@ function chunkByChars(text, chunkChars, overlapChars) {
|
|
|
438
493
|
function normalizeWord(word) {
|
|
439
494
|
return word.replace(/##/g, "").trim();
|
|
440
495
|
}
|
|
441
|
-
|
|
442
|
-
function defaultDetector() {
|
|
443
|
-
const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
|
|
444
|
-
return hasKey ? new RemoteDetector : new LocalDetector;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// src/normalizers.ts
|
|
448
|
-
import { createRequire as createRequire2 } from "node:module";
|
|
449
|
-
var _require = null;
|
|
450
|
-
var _requireResolved = false;
|
|
451
|
-
function _getRequire() {
|
|
452
|
-
if (_requireResolved)
|
|
453
|
-
return _require;
|
|
454
|
-
_requireResolved = true;
|
|
496
|
+
async function loadTransformersJs() {
|
|
455
497
|
try {
|
|
456
|
-
|
|
498
|
+
return await import("@huggingface/transformers");
|
|
457
499
|
} catch {
|
|
458
|
-
|
|
500
|
+
throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
|
|
459
501
|
}
|
|
460
|
-
return _require;
|
|
461
502
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
|
|
503
|
+
// src/detectors/composite.ts
|
|
504
|
+
function luhnOk(s) {
|
|
505
|
+
const digits = [];
|
|
506
|
+
for (const c of s) {
|
|
507
|
+
if (c >= "0" && c <= "9")
|
|
508
|
+
digits.push(Number(c));
|
|
509
|
+
}
|
|
510
|
+
if (digits.length < 13 || digits.length > 19)
|
|
511
|
+
return false;
|
|
512
|
+
let total = 0;
|
|
513
|
+
for (let i = 0;i < digits.length; i++) {
|
|
514
|
+
let d = digits[digits.length - 1 - i] ?? 0;
|
|
515
|
+
if (i % 2 === 1) {
|
|
516
|
+
d *= 2;
|
|
517
|
+
if (d > 9)
|
|
518
|
+
d -= 9;
|
|
519
|
+
}
|
|
520
|
+
total += d;
|
|
470
521
|
}
|
|
522
|
+
return total % 10 === 0;
|
|
471
523
|
}
|
|
472
|
-
function
|
|
473
|
-
|
|
524
|
+
function ibanChOk(raw) {
|
|
525
|
+
const s = raw.replace(/[\s-]/g, "");
|
|
526
|
+
if (s.length !== 21 || !s.startsWith("CH") || !/^\d+$/.test(s.slice(2))) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
const rearranged = s.slice(4) + s.slice(0, 4);
|
|
530
|
+
let numeric = "";
|
|
531
|
+
for (const c of rearranged) {
|
|
532
|
+
numeric += c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c;
|
|
533
|
+
}
|
|
534
|
+
let rem = 0;
|
|
535
|
+
for (const ch of numeric) {
|
|
536
|
+
rem = (rem * 10 + Number(ch)) % 97;
|
|
537
|
+
}
|
|
538
|
+
return rem === 1;
|
|
474
539
|
}
|
|
475
|
-
function
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
540
|
+
function ahvOk(s) {
|
|
541
|
+
const digits = [];
|
|
542
|
+
for (const c of s) {
|
|
543
|
+
if (c >= "0" && c <= "9")
|
|
544
|
+
digits.push(Number(c));
|
|
545
|
+
}
|
|
546
|
+
if (digits.length !== 13)
|
|
547
|
+
return false;
|
|
548
|
+
if (digits[0] !== 7 || digits[1] !== 5 || digits[2] !== 6)
|
|
549
|
+
return false;
|
|
550
|
+
let total = 0;
|
|
551
|
+
for (let i = 0;i < 12; i++) {
|
|
552
|
+
total += (digits[i] ?? 0) * (i % 2 === 0 ? 1 : 3);
|
|
553
|
+
}
|
|
554
|
+
const check = (10 - total % 10) % 10;
|
|
555
|
+
return digits[12] === check;
|
|
487
556
|
}
|
|
488
|
-
function
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
557
|
+
function vatCheOk(s) {
|
|
558
|
+
const digits = [];
|
|
559
|
+
for (const c of s) {
|
|
560
|
+
if (c >= "0" && c <= "9")
|
|
561
|
+
digits.push(Number(c));
|
|
562
|
+
}
|
|
563
|
+
if (digits.length < 9)
|
|
564
|
+
return false;
|
|
565
|
+
const weights = [5, 4, 3, 2, 7, 6, 5, 4];
|
|
566
|
+
let total = 0;
|
|
567
|
+
for (let i = 0;i < 8; i++) {
|
|
568
|
+
total += (digits[i] ?? 0) * (weights[i] ?? 0);
|
|
569
|
+
}
|
|
570
|
+
let expected = 11 - total % 11;
|
|
571
|
+
if (expected === 11)
|
|
572
|
+
expected = 0;
|
|
573
|
+
else if (expected === 10)
|
|
574
|
+
return false;
|
|
575
|
+
return digits[8] === expected;
|
|
496
576
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
577
|
+
var REGEXES = [
|
|
578
|
+
{
|
|
579
|
+
label: "private_email",
|
|
580
|
+
pattern: /\b[\w._%+\-]+@[\w.\-]+\.[A-Za-z]{2,}\b/g
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
label: "private_url",
|
|
584
|
+
pattern: /https?:\/\/[^\s)>\]]+/g
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
label: "private_phone",
|
|
588
|
+
pattern: /(?:\+41|0041|0)\s?(?:\(0\)\s?)?\d{2}[\s\-/.]?\d{3}[\s\-/.]?\d{2}[\s\-/.]?\d{2}\b/g
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
label: "account_number",
|
|
592
|
+
pattern: /\bCH\d{2}(?:[\s-]?\d){17}\b/g,
|
|
593
|
+
validator: ibanChOk
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
label: "account_number",
|
|
597
|
+
pattern: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g,
|
|
598
|
+
validator: ahvOk
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
label: "account_number",
|
|
602
|
+
pattern: /\b756\d{10}\b/g,
|
|
603
|
+
validator: ahvOk
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
label: "account_number",
|
|
607
|
+
pattern: /\bCHE-\d{3}\.\d{3}\.\d{3}(?:\s+(?:MWST|TVA|IVA))?\b/g,
|
|
608
|
+
validator: vatCheOk
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
label: "account_number",
|
|
612
|
+
pattern: /\b(?:\d[\s-]?){15,16}\d\b/g,
|
|
613
|
+
validator: luhnOk
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
label: "private_date",
|
|
617
|
+
pattern: /\b(?:\d{1,2}[./]\d{1,2}[./]\d{4}|\d{4}[./-]\d{1,2}[./-]\d{1,2})\b/g
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
label: "secret",
|
|
621
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
label: "secret",
|
|
625
|
+
pattern: /\bghp_[A-Za-z0-9]{36}\b/g
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
label: "secret",
|
|
629
|
+
pattern: /\bAKIA[A-Z0-9]{16}\b/g
|
|
630
|
+
}
|
|
631
|
+
];
|
|
632
|
+
var URL_TRAIL_TRIM = '.,;:!?")>]';
|
|
633
|
+
function trimUrl(text, start, end) {
|
|
634
|
+
while (end > start && URL_TRAIL_TRIM.includes(text[end - 1] ?? ""))
|
|
635
|
+
end--;
|
|
636
|
+
return [start, end];
|
|
505
637
|
}
|
|
506
|
-
function
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
638
|
+
function findRegexSpans(text) {
|
|
639
|
+
const candidates = [];
|
|
640
|
+
for (const { label, pattern, validator } of REGEXES) {
|
|
641
|
+
pattern.lastIndex = 0;
|
|
642
|
+
let m;
|
|
643
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
644
|
+
let start = m.index;
|
|
645
|
+
let end = m.index + m[0].length;
|
|
646
|
+
if (label === "private_url")
|
|
647
|
+
[start, end] = trimUrl(text, start, end);
|
|
648
|
+
const value = text.slice(start, end);
|
|
649
|
+
if (!value.trim())
|
|
650
|
+
continue;
|
|
651
|
+
if (validator && !validator(value))
|
|
652
|
+
continue;
|
|
653
|
+
candidates.push({
|
|
654
|
+
start,
|
|
655
|
+
end,
|
|
656
|
+
label,
|
|
657
|
+
text: value,
|
|
658
|
+
score: 1
|
|
659
|
+
});
|
|
520
660
|
}
|
|
521
|
-
|
|
522
|
-
|
|
661
|
+
}
|
|
662
|
+
candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
663
|
+
const out = [];
|
|
664
|
+
let lastEnd = -1;
|
|
665
|
+
for (const sp of candidates) {
|
|
666
|
+
if (sp.start < lastEnd)
|
|
667
|
+
continue;
|
|
668
|
+
out.push(sp);
|
|
669
|
+
lastEnd = sp.end;
|
|
670
|
+
}
|
|
671
|
+
return out;
|
|
523
672
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
673
|
+
function maskSpans(text, spans) {
|
|
674
|
+
if (spans.length === 0)
|
|
675
|
+
return text;
|
|
676
|
+
const chars = [...text];
|
|
677
|
+
for (const sp of spans) {
|
|
678
|
+
for (let i = sp.start;i < sp.end && i < chars.length; i++) {
|
|
679
|
+
chars[i] = " ";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return chars.join("");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
class CompositeDetector {
|
|
686
|
+
model;
|
|
687
|
+
constructor(opts) {
|
|
688
|
+
if (!opts || !opts.model) {
|
|
689
|
+
throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
|
|
690
|
+
}
|
|
691
|
+
this.model = opts.model;
|
|
692
|
+
}
|
|
693
|
+
async detect(text, opts) {
|
|
694
|
+
if (!text)
|
|
695
|
+
return [];
|
|
696
|
+
const regexSpans = findRegexSpans(text);
|
|
697
|
+
const masked = maskSpans(text, regexSpans);
|
|
698
|
+
const modelSpans = await this.model.detect(masked, opts);
|
|
699
|
+
const out = [...regexSpans];
|
|
700
|
+
const regexRanges = regexSpans.map((s) => [s.start, s.end]);
|
|
701
|
+
for (const sp of modelSpans) {
|
|
702
|
+
const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
|
|
703
|
+
if (overlaps)
|
|
704
|
+
continue;
|
|
705
|
+
out.push(sp);
|
|
706
|
+
}
|
|
707
|
+
out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// src/detectors/index.ts
|
|
712
|
+
function defaultDetector() {
|
|
713
|
+
const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
|
|
714
|
+
return hasKey ? new RemoteDetector : new LocalDetector;
|
|
537
715
|
}
|
|
538
716
|
|
|
539
717
|
// src/core/session.ts
|
|
@@ -548,8 +726,8 @@ class Session {
|
|
|
548
726
|
resolvedNormalizers = {};
|
|
549
727
|
constructor(opts = {}) {
|
|
550
728
|
if (opts.normalizers) {
|
|
551
|
-
for (const [label,
|
|
552
|
-
this.resolvedNormalizers[label] =
|
|
729
|
+
for (const [label, fn] of Object.entries(opts.normalizers)) {
|
|
730
|
+
this.resolvedNormalizers[label] = fn;
|
|
553
731
|
}
|
|
554
732
|
}
|
|
555
733
|
}
|
|
@@ -573,8 +751,11 @@ class Session {
|
|
|
573
751
|
return sentinel;
|
|
574
752
|
}
|
|
575
753
|
applySpans(text, spans) {
|
|
754
|
+
return this.applySpansDetailed(text, spans).redacted;
|
|
755
|
+
}
|
|
756
|
+
applySpansDetailed(text, spans) {
|
|
576
757
|
if (spans.length === 0)
|
|
577
|
-
return text;
|
|
758
|
+
return { redacted: text, merged: [] };
|
|
578
759
|
const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
|
|
579
760
|
const merged = mergeAdjacent(text, sorted);
|
|
580
761
|
const out = [];
|
|
@@ -590,7 +771,7 @@ class Session {
|
|
|
590
771
|
cursor = end;
|
|
591
772
|
}
|
|
592
773
|
out.push(text.slice(cursor));
|
|
593
|
-
return out.join("");
|
|
774
|
+
return { redacted: out.join(""), merged };
|
|
594
775
|
}
|
|
595
776
|
restore(text) {
|
|
596
777
|
if (Object.keys(this.mapping).length === 0)
|
|
@@ -823,6 +1004,16 @@ function restoreChatResponse(response, session) {
|
|
|
823
1004
|
if (msg && typeof msg.content === "string") {
|
|
824
1005
|
msg.content = session.restore(msg.content);
|
|
825
1006
|
}
|
|
1007
|
+
if (msg && typeof msg.reasoning === "string") {
|
|
1008
|
+
msg.reasoning = session.restore(msg.reasoning);
|
|
1009
|
+
}
|
|
1010
|
+
if (msg && Array.isArray(msg.reasoning_details)) {
|
|
1011
|
+
for (const r of msg.reasoning_details) {
|
|
1012
|
+
if (r && typeof r.text === "string") {
|
|
1013
|
+
r.text = session.restore(r.text);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
826
1017
|
for (const tc of msg?.tool_calls ?? []) {
|
|
827
1018
|
const fn = tc.function;
|
|
828
1019
|
if (fn && typeof fn.arguments === "string") {
|
|
@@ -837,6 +1028,16 @@ function patchChatChunk(chunk, buf) {
|
|
|
837
1028
|
if (typeof delta.content === "string") {
|
|
838
1029
|
delta.content = buf.feed(delta.content);
|
|
839
1030
|
}
|
|
1031
|
+
if (typeof delta.reasoning === "string") {
|
|
1032
|
+
delta.reasoning = buf.feed(delta.reasoning);
|
|
1033
|
+
}
|
|
1034
|
+
if (Array.isArray(delta.reasoning_details)) {
|
|
1035
|
+
for (const r of delta.reasoning_details) {
|
|
1036
|
+
if (r && typeof r.text === "string") {
|
|
1037
|
+
r.text = buf.feed(r.text);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
840
1041
|
}
|
|
841
1042
|
}
|
|
842
1043
|
function makeChatTailChunk(template, content) {
|
|
@@ -1163,9 +1364,20 @@ class OpenAI {
|
|
|
1163
1364
|
gheimStrict;
|
|
1164
1365
|
_clientPromise;
|
|
1165
1366
|
constructor(opts = {}) {
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
|
|
1367
|
+
const {
|
|
1368
|
+
gheimDetector,
|
|
1369
|
+
gheimStrict,
|
|
1370
|
+
openaiClient,
|
|
1371
|
+
clientOptions,
|
|
1372
|
+
...passthrough
|
|
1373
|
+
} = opts;
|
|
1374
|
+
const detector = gheimDetector ?? defaultDetector();
|
|
1375
|
+
this.gheimStrict = gheimStrict ?? true;
|
|
1376
|
+
const innerOpts = {
|
|
1377
|
+
...clientOptions ?? {},
|
|
1378
|
+
...passthrough
|
|
1379
|
+
};
|
|
1380
|
+
this._clientPromise = openaiClient ? Promise.resolve(openaiClient) : loadOpenAI(innerOpts);
|
|
1169
1381
|
this._clientPromise.then((c) => {
|
|
1170
1382
|
this.raw = c;
|
|
1171
1383
|
});
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/** Session state — mirrors gheim-py's Session. */
|
|
2
2
|
import type { Span } from "../detectors/base.ts";
|
|
3
|
-
|
|
3
|
+
/** A surface canonicaliser. Maps a raw surface to a canonical key, or
|
|
4
|
+
* null to fall back to the default NFKC + lowercase key. Re-declared
|
|
5
|
+
* locally so this module doesn't pull in `gheim/normalizers` (which
|
|
6
|
+
* uses Node's `createRequire` and breaks browser bundlers). */
|
|
7
|
+
export type Normalizer = (s: string) => string | null;
|
|
4
8
|
/** Constructor options for Session. */
|
|
5
9
|
export interface SessionOptions {
|
|
6
10
|
/**
|
|
7
11
|
* Optional per-label canonical-form normalizers. Maps a label name
|
|
8
|
-
* (e.g. "private_phone") to
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* (e.g. "private_phone") to a callable Normalizer. Surfaces that fail
|
|
13
|
+
* to parse fall back to the default NFKC+lowercase key.
|
|
14
|
+
*
|
|
15
|
+
* Built-in factories (e164, isoDate, germanTransliteration) live in
|
|
16
|
+
* the `gheim/normalizers` subpath — they pull in optional peer deps
|
|
17
|
+
* (`libphonenumber-js`, `chrono-node`) so they're not loaded by
|
|
18
|
+
* default to keep the browser bundle small and avoid Node-only
|
|
19
|
+
* imports leaking into the main entry.
|
|
11
20
|
*/
|
|
12
|
-
normalizers?: Record<string,
|
|
21
|
+
normalizers?: Record<string, Normalizer>;
|
|
13
22
|
}
|
|
14
23
|
export declare class Session {
|
|
15
24
|
/** Sentinel string ("<PERSON_1>") → original surface form. */
|
|
@@ -19,7 +28,19 @@ export declare class Session {
|
|
|
19
28
|
private resolvedNormalizers;
|
|
20
29
|
constructor(opts?: SessionOptions);
|
|
21
30
|
allocate(label: string, surface: string): string;
|
|
22
|
-
applySpans(text: string, spans: Span[]): string;
|
|
31
|
+
applySpans(text: string, spans: readonly (Span | MergedSpan)[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Like {@link applySpans} but also returns the post-merge span list. UI
|
|
34
|
+
* consumers (segment rendering, bar overlays) typically need both the
|
|
35
|
+
* redacted text and the merged spans so they can highlight the redacted
|
|
36
|
+
* runs at their original character positions; without this method they'd
|
|
37
|
+
* have to call {@link mergeAdjacent} again on the same input. Pure
|
|
38
|
+
* convenience — no extra cost over `applySpans`.
|
|
39
|
+
*/
|
|
40
|
+
applySpansDetailed(text: string, spans: readonly (Span | MergedSpan)[]): {
|
|
41
|
+
redacted: string;
|
|
42
|
+
merged: MergedSpan[];
|
|
43
|
+
};
|
|
23
44
|
restore(text: string): string;
|
|
24
45
|
toJSON(): {
|
|
25
46
|
mapping: Record<string, string>;
|
|
@@ -28,4 +49,19 @@ export declare class Session {
|
|
|
28
49
|
mapping: Record<string, string>;
|
|
29
50
|
}, opts?: SessionOptions): Session;
|
|
30
51
|
}
|
|
52
|
+
/** A span after merge: same shape as input, but no `text`/`score` carried. */
|
|
53
|
+
export interface MergedSpan {
|
|
54
|
+
start: number;
|
|
55
|
+
end: number;
|
|
56
|
+
label: string;
|
|
57
|
+
}
|
|
58
|
+
/** Merge runs of adjacent same-label spans (BPE-split surface fix).
|
|
59
|
+
*
|
|
60
|
+
* Two same-label spans separated by ≤ MERGE_GAP characters of pure
|
|
61
|
+
* whitespace (or directly touching) collapse into one. Models tokenize
|
|
62
|
+
* a phone like "+41 44 268 12 34" into many single-space-separated
|
|
63
|
+
* pieces with the same label; merging recovers the original entity so
|
|
64
|
+
* the demo and SDK both see one PHONE span instead of five.
|
|
65
|
+
*/
|
|
66
|
+
export declare function mergeAdjacent(text: string, spans: readonly (Span | MergedSpan)[]): MergedSpan[];
|
|
31
67
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAGjD;;;gEAGgE;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAkBtD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC1C;AAED,qBAAa,OAAO;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IACrC,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,mBAAmB,CAAkC;gBAEjD,IAAI,GAAE,cAAmB;IAQrC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAiBhD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GAAG,MAAM;IAIvE;;;;;;;OAOG;IACH,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;IAoB7C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAK7B,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAI7C,MAAM,CAAC,QAAQ,CACb,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EACxC,IAAI,GAAE,cAAmB,GACxB,OAAO;CAqBX;AAID,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC,UAAU,EAAE,CAqBd"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite detector: regex front-end + model on the remainder.
|
|
3
|
+
*
|
|
4
|
+
* Production-ready Swiss PII detector that combines two complementary
|
|
5
|
+
* strategies — a port of the Python sibling
|
|
6
|
+
* (`gheim.detectors.composite.CompositeDetector`) with parity-tested
|
|
7
|
+
* regexes and checksum validators.
|
|
8
|
+
*
|
|
9
|
+
* 1. Deterministic regex front-end for structured PII categories where
|
|
10
|
+
* shape is unambiguous and checksums verify identity:
|
|
11
|
+
* - IBAN-CH (mod-97-10), AHV (EAN-13 mod-10), VAT-CHE (mod-11-10),
|
|
12
|
+
* Visa/Mastercard (Luhn)
|
|
13
|
+
* - Email, URL, Swiss phone (+41 / 0041 / 0...), explicit dates
|
|
14
|
+
* (DD.MM.YYYY, YYYY-MM-DD)
|
|
15
|
+
* - API tokens (sk-, ghp_, AKIA...)
|
|
16
|
+
*
|
|
17
|
+
* Spans found here are "trusted" — checksums catch most surface-form
|
|
18
|
+
* imitations; the model is never asked.
|
|
19
|
+
*
|
|
20
|
+
* 2. The fine-tuned model on whatever's left after regex masking.
|
|
21
|
+
* Persons, addresses, and edge-case dates fall here. The model's
|
|
22
|
+
* "single-category mode" failure (where an IBAN/phone-shaped
|
|
23
|
+
* sequence suppresses person/date detection in the same input) is
|
|
24
|
+
* bypassed because the regex already absorbed those tokens — the
|
|
25
|
+
* model sees them as whitespace.
|
|
26
|
+
*
|
|
27
|
+
* Wraps the same `Detector` interface (`detect(text) → Span[]`) as
|
|
28
|
+
* `LocalDetector` so it drops into any code path that already accepts a
|
|
29
|
+
* detector.
|
|
30
|
+
*/
|
|
31
|
+
import type { Detector, Span } from "./base.ts";
|
|
32
|
+
/** Options for {@link CompositeDetector}. */
|
|
33
|
+
export interface CompositeDetectorOptions {
|
|
34
|
+
/** Inner detector — usually a {@link LocalDetector}. Anything with
|
|
35
|
+
* a `detect(text) → Promise<Span[]>` method works. */
|
|
36
|
+
model: Detector;
|
|
37
|
+
}
|
|
38
|
+
/** Regex front-end (with checksum validation) + model on the remainder.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* import { LocalDetector, CompositeDetector } from "gheim";
|
|
42
|
+
* const composite = new CompositeDetector({
|
|
43
|
+
* model: new LocalDetector({ dtype: "q8" }),
|
|
44
|
+
* });
|
|
45
|
+
* const spans = await composite.detect(swissLetter);
|
|
46
|
+
* // → IBAN/phone/email caught by regex (checksum-validated);
|
|
47
|
+
* // person/address/dates caught by the model on the masked remainder.
|
|
48
|
+
*/
|
|
49
|
+
export declare class CompositeDetector implements Detector {
|
|
50
|
+
readonly model: Detector;
|
|
51
|
+
constructor(opts: CompositeDetectorOptions);
|
|
52
|
+
/** Two-pass detection: regex first (with checksum), then model on
|
|
53
|
+
* the masked remainder. Returns a single overlap-resolved span list. */
|
|
54
|
+
detect(text: string, opts?: {
|
|
55
|
+
model?: string;
|
|
56
|
+
}): Promise<Span[]>;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=composite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/detectors/composite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAiNhD,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC;2DACuD;IACvD,KAAK,EAAE,QAAQ,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;gBAEb,IAAI,EAAE,wBAAwB;IAU1C;6EACyE;IACnE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAmBvE"}
|