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.
@@ -276,8 +276,10 @@ class LocalDetector {
276
276
  chunkTokens;
277
277
  chunkOverlapTokens;
278
278
  batchSize;
279
+ actualDevice = null;
279
280
  _pipeline = null;
280
281
  _loadPromise = null;
282
+ _onProgress;
281
283
  constructor(opts = {}) {
282
284
  this.model = opts.model ?? DEFAULT_MODEL2;
283
285
  this.device = opts.device ?? "auto";
@@ -286,11 +288,13 @@ class LocalDetector {
286
288
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
287
289
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
288
290
  this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
291
+ this._onProgress = opts.onProgress;
289
292
  if (this.chunkOverlapTokens >= this.chunkTokens) {
290
293
  throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
291
294
  }
292
295
  if (opts.pipelineInstance) {
293
296
  this._pipeline = opts.pipelineInstance;
297
+ this.actualDevice = this.device === "auto" ? null : this.device;
294
298
  }
295
299
  }
296
300
  async load() {
@@ -299,24 +303,70 @@ class LocalDetector {
299
303
  if (this._loadPromise)
300
304
  return this._loadPromise;
301
305
  this._loadPromise = (async () => {
302
- let mod;
303
- try {
304
- mod = await import("@huggingface/transformers");
305
- } catch (e) {
306
- throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
306
+ const mod = await loadTransformersJs();
307
+ const sequence = await this._planDeviceSequence();
308
+ let lastErr;
309
+ for (let i = 0;i < sequence.length; i++) {
310
+ const dev = sequence[i] ?? "wasm";
311
+ try {
312
+ const pipe = await mod.pipeline("token-classification", this.model, {
313
+ device: dev,
314
+ dtype: this.dtype,
315
+ progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
316
+ });
317
+ this._pipeline = pipe;
318
+ this.actualDevice = dev;
319
+ return pipe;
320
+ } catch (e) {
321
+ lastErr = e;
322
+ if (i < sequence.length - 1)
323
+ continue;
324
+ throw e;
325
+ }
307
326
  }
308
- const pipe = await mod.pipeline("token-classification", this.model, {
309
- device: this.device,
310
- dtype: this.dtype
311
- });
312
- this._pipeline = pipe;
313
- return pipe;
327
+ throw lastErr ?? new Error("LocalDetector.load(): no candidate backends");
314
328
  })();
315
329
  return this._loadPromise;
316
330
  }
331
+ async _planDeviceSequence() {
332
+ if (this.device !== "auto")
333
+ return [this.device];
334
+ const seq = [];
335
+ const nav = typeof navigator !== "undefined" ? navigator : null;
336
+ if (nav?.gpu?.requestAdapter) {
337
+ try {
338
+ const adapter = await nav.gpu.requestAdapter();
339
+ if (adapter)
340
+ seq.push("webgpu");
341
+ } catch {}
342
+ }
343
+ seq.push("wasm");
344
+ return seq;
345
+ }
346
+ _emitProgress(raw, device) {
347
+ if (!this._onProgress)
348
+ return;
349
+ const event = {
350
+ phase: raw.status ?? "progress",
351
+ device
352
+ };
353
+ if (raw.file)
354
+ event.file = raw.file;
355
+ if (raw.status === "progress") {
356
+ if (raw.loaded != null)
357
+ event.loadedBytes = raw.loaded;
358
+ if (raw.total != null)
359
+ event.totalBytes = raw.total;
360
+ if (raw.progress != null) {
361
+ const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
362
+ event.fraction = Math.max(0, Math.min(1, norm));
363
+ }
364
+ }
365
+ this._onProgress(event);
366
+ }
317
367
  async detect(text, opts) {
318
368
  if (opts?.model && opts.model !== this.model) {
319
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
369
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
320
370
  }
321
371
  if (!text)
322
372
  return [];
@@ -325,7 +375,7 @@ class LocalDetector {
325
375
  }
326
376
  async detectBatch(texts, opts) {
327
377
  if (opts?.model && opts.model !== this.model) {
328
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
378
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
329
379
  }
330
380
  if (texts.length === 0)
331
381
  return [];
@@ -486,102 +536,225 @@ function chunkByChars(text, chunkChars, overlapChars) {
486
536
  function normalizeWord(word) {
487
537
  return word.replace(/##/g, "").trim();
488
538
  }
489
- // src/detectors/index.ts
490
- function defaultDetector() {
491
- const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
492
- return hasKey ? new RemoteDetector : new LocalDetector;
493
- }
494
-
495
- // src/normalizers.ts
496
- var import_node_module = require("node:module");
497
- var _require = null;
498
- var _requireResolved = false;
499
- function _getRequire() {
500
- if (_requireResolved)
501
- return _require;
502
- _requireResolved = true;
539
+ async function loadTransformersJs() {
503
540
  try {
504
- _require = import_node_module.createRequire("file:///home/runner/work/gheim/gheim/packages/gheim-js/src/normalizers.ts");
541
+ return await import("@huggingface/transformers");
505
542
  } catch {
506
- _require = null;
543
+ throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
507
544
  }
508
- return _require;
509
545
  }
510
- function loadSync(name, errMsg) {
511
- const req = _getRequire();
512
- if (!req)
513
- throw new Error(errMsg);
514
- try {
515
- return req(name);
516
- } catch {
517
- throw new Error(errMsg);
518
- }
519
- }
520
- function loadPhoneLib() {
521
- return loadSync("libphonenumber-js", "the e164 normalizer requires libphonenumber-js. " + "Install with: bun add libphonenumber-js");
522
- }
523
- function e164(opts = {}) {
524
- const lib = loadPhoneLib();
525
- return (s) => {
526
- try {
527
- const parsed = lib.parsePhoneNumberFromString(s, opts.region);
528
- if (!parsed || !parsed.isValid())
529
- return null;
530
- return parsed.format("E.164");
531
- } catch {
532
- return null;
546
+ // src/detectors/composite.ts
547
+ function luhnOk(s) {
548
+ const digits = [];
549
+ for (const c of s) {
550
+ if (c >= "0" && c <= "9")
551
+ digits.push(Number(c));
552
+ }
553
+ if (digits.length < 13 || digits.length > 19)
554
+ return false;
555
+ let total = 0;
556
+ for (let i = 0;i < digits.length; i++) {
557
+ let d = digits[digits.length - 1 - i] ?? 0;
558
+ if (i % 2 === 1) {
559
+ d *= 2;
560
+ if (d > 9)
561
+ d -= 9;
533
562
  }
534
- };
563
+ total += d;
564
+ }
565
+ return total % 10 === 0;
535
566
  }
536
- function loadChrono(opts) {
537
- const mod = loadSync("chrono-node", "the isoDate normalizer requires chrono-node. " + "Install with: bun add chrono-node");
538
- if (opts.locale) {
539
- const localeParser = mod[opts.locale];
540
- if (localeParser)
541
- return localeParser;
542
- }
543
- return mod;
544
- }
545
- function isoDate(opts = {}) {
546
- const parser = loadChrono(opts);
547
- return (s) => {
548
- const d = parser.parseDate(s);
549
- if (!d)
550
- return null;
551
- return d.toISOString().slice(0, 10);
552
- };
567
+ function ibanChOk(raw) {
568
+ const s = raw.replace(/[\s-]/g, "");
569
+ if (s.length !== 21 || !s.startsWith("CH") || !/^\d+$/.test(s.slice(2))) {
570
+ return false;
571
+ }
572
+ const rearranged = s.slice(4) + s.slice(0, 4);
573
+ let numeric = "";
574
+ for (const c of rearranged) {
575
+ numeric += c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c;
576
+ }
577
+ let rem = 0;
578
+ for (const ch of numeric) {
579
+ rem = (rem * 10 + Number(ch)) % 97;
580
+ }
581
+ return rem === 1;
553
582
  }
554
- function germanTransliteration() {
555
- const table = {
556
- ä: "ae",
557
- Ä: "ae",
558
- ö: "oe",
559
- Ö: "oe",
560
- ü: "ue",
561
- Ü: "ue",
562
- ß: "ss"
563
- };
564
- return (s) => {
565
- let out = "";
566
- for (const ch of s.normalize("NFKC")) {
567
- out += table[ch] ?? ch;
583
+ function ahvOk(s) {
584
+ const digits = [];
585
+ for (const c of s) {
586
+ if (c >= "0" && c <= "9")
587
+ digits.push(Number(c));
588
+ }
589
+ if (digits.length !== 13)
590
+ return false;
591
+ if (digits[0] !== 7 || digits[1] !== 5 || digits[2] !== 6)
592
+ return false;
593
+ let total = 0;
594
+ for (let i = 0;i < 12; i++) {
595
+ total += (digits[i] ?? 0) * (i % 2 === 0 ? 1 : 3);
596
+ }
597
+ const check = (10 - total % 10) % 10;
598
+ return digits[12] === check;
599
+ }
600
+ function vatCheOk(s) {
601
+ const digits = [];
602
+ for (const c of s) {
603
+ if (c >= "0" && c <= "9")
604
+ digits.push(Number(c));
605
+ }
606
+ if (digits.length < 9)
607
+ return false;
608
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
609
+ let total = 0;
610
+ for (let i = 0;i < 8; i++) {
611
+ total += (digits[i] ?? 0) * (weights[i] ?? 0);
612
+ }
613
+ let expected = 11 - total % 11;
614
+ if (expected === 11)
615
+ expected = 0;
616
+ else if (expected === 10)
617
+ return false;
618
+ return digits[8] === expected;
619
+ }
620
+ var REGEXES = [
621
+ {
622
+ label: "private_email",
623
+ pattern: /\b[\w._%+\-]+@[\w.\-]+\.[A-Za-z]{2,}\b/g
624
+ },
625
+ {
626
+ label: "private_url",
627
+ pattern: /https?:\/\/[^\s)>\]]+/g
628
+ },
629
+ {
630
+ label: "private_phone",
631
+ pattern: /(?:\+41|0041|0)\s?(?:\(0\)\s?)?\d{2}[\s\-/.]?\d{3}[\s\-/.]?\d{2}[\s\-/.]?\d{2}\b/g
632
+ },
633
+ {
634
+ label: "account_number",
635
+ pattern: /\bCH\d{2}(?:[\s-]?\d){17}\b/g,
636
+ validator: ibanChOk
637
+ },
638
+ {
639
+ label: "account_number",
640
+ pattern: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g,
641
+ validator: ahvOk
642
+ },
643
+ {
644
+ label: "account_number",
645
+ pattern: /\b756\d{10}\b/g,
646
+ validator: ahvOk
647
+ },
648
+ {
649
+ label: "account_number",
650
+ pattern: /\bCHE-\d{3}\.\d{3}\.\d{3}(?:\s+(?:MWST|TVA|IVA))?\b/g,
651
+ validator: vatCheOk
652
+ },
653
+ {
654
+ label: "account_number",
655
+ pattern: /\b(?:\d[\s-]?){15,16}\d\b/g,
656
+ validator: luhnOk
657
+ },
658
+ {
659
+ label: "private_date",
660
+ pattern: /\b(?:\d{1,2}[./]\d{1,2}[./]\d{4}|\d{4}[./-]\d{1,2}[./-]\d{1,2})\b/g
661
+ },
662
+ {
663
+ label: "secret",
664
+ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
665
+ },
666
+ {
667
+ label: "secret",
668
+ pattern: /\bghp_[A-Za-z0-9]{36}\b/g
669
+ },
670
+ {
671
+ label: "secret",
672
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g
673
+ }
674
+ ];
675
+ var URL_TRAIL_TRIM = '.,;:!?")>]';
676
+ function trimUrl(text, start, end) {
677
+ while (end > start && URL_TRAIL_TRIM.includes(text[end - 1] ?? ""))
678
+ end--;
679
+ return [start, end];
680
+ }
681
+ function findRegexSpans(text) {
682
+ const candidates = [];
683
+ for (const { label, pattern, validator } of REGEXES) {
684
+ pattern.lastIndex = 0;
685
+ let m;
686
+ while ((m = pattern.exec(text)) !== null) {
687
+ let start = m.index;
688
+ let end = m.index + m[0].length;
689
+ if (label === "private_url")
690
+ [start, end] = trimUrl(text, start, end);
691
+ const value = text.slice(start, end);
692
+ if (!value.trim())
693
+ continue;
694
+ if (validator && !validator(value))
695
+ continue;
696
+ candidates.push({
697
+ start,
698
+ end,
699
+ label,
700
+ text: value,
701
+ score: 1
702
+ });
568
703
  }
569
- return out.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
570
- };
704
+ }
705
+ candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
706
+ const out = [];
707
+ let lastEnd = -1;
708
+ for (const sp of candidates) {
709
+ if (sp.start < lastEnd)
710
+ continue;
711
+ out.push(sp);
712
+ lastEnd = sp.end;
713
+ }
714
+ return out;
571
715
  }
572
- var BUILTINS = {
573
- e164: () => e164(),
574
- iso_date: () => isoDate(),
575
- german: () => germanTransliteration()
576
- };
577
- function resolveNormalizer(spec) {
578
- if (typeof spec === "function")
579
- return spec;
580
- const factory = BUILTINS[spec];
581
- if (!factory) {
582
- throw new Error(`unknown built-in normalizer: ${JSON.stringify(spec)}. ` + `Available: ${Object.keys(BUILTINS).join(", ")}, or pass a callable.`);
716
+ function maskSpans(text, spans) {
717
+ if (spans.length === 0)
718
+ return text;
719
+ const chars = [...text];
720
+ for (const sp of spans) {
721
+ for (let i = sp.start;i < sp.end && i < chars.length; i++) {
722
+ chars[i] = " ";
723
+ }
583
724
  }
584
- return factory();
725
+ return chars.join("");
726
+ }
727
+
728
+ class CompositeDetector {
729
+ model;
730
+ constructor(opts) {
731
+ if (!opts || !opts.model) {
732
+ throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
733
+ }
734
+ this.model = opts.model;
735
+ }
736
+ async detect(text, opts) {
737
+ if (!text)
738
+ return [];
739
+ const regexSpans = findRegexSpans(text);
740
+ const masked = maskSpans(text, regexSpans);
741
+ const modelSpans = await this.model.detect(masked, opts);
742
+ const out = [...regexSpans];
743
+ const regexRanges = regexSpans.map((s) => [s.start, s.end]);
744
+ for (const sp of modelSpans) {
745
+ const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
746
+ if (overlaps)
747
+ continue;
748
+ out.push(sp);
749
+ }
750
+ out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
751
+ return out;
752
+ }
753
+ }
754
+ // src/detectors/index.ts
755
+ function defaultDetector() {
756
+ const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
757
+ return hasKey ? new RemoteDetector : new LocalDetector;
585
758
  }
586
759
 
587
760
  // src/core/session.ts
@@ -596,8 +769,8 @@ class Session {
596
769
  resolvedNormalizers = {};
597
770
  constructor(opts = {}) {
598
771
  if (opts.normalizers) {
599
- for (const [label, spec] of Object.entries(opts.normalizers)) {
600
- this.resolvedNormalizers[label] = resolveNormalizer(spec);
772
+ for (const [label, fn] of Object.entries(opts.normalizers)) {
773
+ this.resolvedNormalizers[label] = fn;
601
774
  }
602
775
  }
603
776
  }
@@ -621,8 +794,11 @@ class Session {
621
794
  return sentinel;
622
795
  }
623
796
  applySpans(text, spans) {
797
+ return this.applySpansDetailed(text, spans).redacted;
798
+ }
799
+ applySpansDetailed(text, spans) {
624
800
  if (spans.length === 0)
625
- return text;
801
+ return { redacted: text, merged: [] };
626
802
  const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
627
803
  const merged = mergeAdjacent(text, sorted);
628
804
  const out = [];
@@ -638,7 +814,7 @@ class Session {
638
814
  cursor = end;
639
815
  }
640
816
  out.push(text.slice(cursor));
641
- return out.join("");
817
+ return { redacted: out.join(""), merged };
642
818
  }
643
819
  restore(text) {
644
820
  if (Object.keys(this.mapping).length === 0)
@@ -871,6 +1047,16 @@ function restoreChatResponse(response, session) {
871
1047
  if (msg && typeof msg.content === "string") {
872
1048
  msg.content = session.restore(msg.content);
873
1049
  }
1050
+ if (msg && typeof msg.reasoning === "string") {
1051
+ msg.reasoning = session.restore(msg.reasoning);
1052
+ }
1053
+ if (msg && Array.isArray(msg.reasoning_details)) {
1054
+ for (const r of msg.reasoning_details) {
1055
+ if (r && typeof r.text === "string") {
1056
+ r.text = session.restore(r.text);
1057
+ }
1058
+ }
1059
+ }
874
1060
  for (const tc of msg?.tool_calls ?? []) {
875
1061
  const fn = tc.function;
876
1062
  if (fn && typeof fn.arguments === "string") {
@@ -885,6 +1071,16 @@ function patchChatChunk(chunk, buf) {
885
1071
  if (typeof delta.content === "string") {
886
1072
  delta.content = buf.feed(delta.content);
887
1073
  }
1074
+ if (typeof delta.reasoning === "string") {
1075
+ delta.reasoning = buf.feed(delta.reasoning);
1076
+ }
1077
+ if (Array.isArray(delta.reasoning_details)) {
1078
+ for (const r of delta.reasoning_details) {
1079
+ if (r && typeof r.text === "string") {
1080
+ r.text = buf.feed(r.text);
1081
+ }
1082
+ }
1083
+ }
888
1084
  }
889
1085
  }
890
1086
  function makeChatTailChunk(template, content) {
@@ -1211,9 +1407,20 @@ class OpenAI {
1211
1407
  gheimStrict;
1212
1408
  _clientPromise;
1213
1409
  constructor(opts = {}) {
1214
- const detector = opts.gheimDetector ?? defaultDetector();
1215
- this.gheimStrict = opts.gheimStrict ?? true;
1216
- this._clientPromise = opts.openaiClient ? Promise.resolve(opts.openaiClient) : loadOpenAI(opts.clientOptions);
1410
+ const {
1411
+ gheimDetector,
1412
+ gheimStrict,
1413
+ openaiClient,
1414
+ clientOptions,
1415
+ ...passthrough
1416
+ } = opts;
1417
+ const detector = gheimDetector ?? defaultDetector();
1418
+ this.gheimStrict = gheimStrict ?? true;
1419
+ const innerOpts = {
1420
+ ...clientOptions ?? {},
1421
+ ...passthrough
1422
+ };
1423
+ this._clientPromise = openaiClient ? Promise.resolve(openaiClient) : loadOpenAI(innerOpts);
1217
1424
  this._clientPromise.then((c) => {
1218
1425
  this.raw = c;
1219
1426
  });