gheim 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.
- package/LICENSE +190 -0
- package/README.md +208 -0
- package/dist/cjs/index.cjs +707 -0
- package/dist/cjs/openai/index.cjs +1291 -0
- package/dist/esm/index.js +668 -0
- package/dist/esm/openai/index.js +1252 -0
- package/dist/types/core/sentinels.d.ts +24 -0
- package/dist/types/core/sentinels.d.ts.map +1 -0
- package/dist/types/core/session.d.ts +31 -0
- package/dist/types/core/session.d.ts.map +1 -0
- package/dist/types/core/stream.d.ts +9 -0
- package/dist/types/core/stream.d.ts.map +1 -0
- package/dist/types/detectors/base.d.ts +28 -0
- package/dist/types/detectors/base.d.ts.map +1 -0
- package/dist/types/detectors/index.d.ts +9 -0
- package/dist/types/detectors/index.d.ts.map +1 -0
- package/dist/types/detectors/local.d.ts +68 -0
- package/dist/types/detectors/local.d.ts.map +1 -0
- package/dist/types/detectors/remote.d.ts +24 -0
- package/dist/types/detectors/remote.d.ts.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/normalizers.d.ts +89 -0
- package/dist/types/normalizers.d.ts.map +1 -0
- package/dist/types/openai/_anonymize_only.d.ts +25 -0
- package/dist/types/openai/_anonymize_only.d.ts.map +1 -0
- package/dist/types/openai/_audio.d.ts +41 -0
- package/dist/types/openai/_audio.d.ts.map +1 -0
- package/dist/types/openai/_base.d.ts +31 -0
- package/dist/types/openai/_base.d.ts.map +1 -0
- package/dist/types/openai/_chat.d.ts +16 -0
- package/dist/types/openai/_chat.d.ts.map +1 -0
- package/dist/types/openai/_client.d.ts +36 -0
- package/dist/types/openai/_client.d.ts.map +1 -0
- package/dist/types/openai/_completions.d.ts +11 -0
- package/dist/types/openai/_completions.d.ts.map +1 -0
- package/dist/types/openai/_responses.d.ts +13 -0
- package/dist/types/openai/_responses.d.ts.map +1 -0
- package/dist/types/openai/_strict.d.ts +15 -0
- package/dist/types/openai/_strict.d.ts.map +1 -0
- package/dist/types/openai/index.d.ts +25 -0
- package/dist/types/openai/index.d.ts.map +1 -0
- package/dist/types/plain.d.ts +34 -0
- package/dist/types/plain.d.ts.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/openai/index.ts
|
|
40
|
+
var exports_openai = {};
|
|
41
|
+
__export(exports_openai, {
|
|
42
|
+
deanonymizeOpenAIStream: () => deanonymizeOpenAIStream,
|
|
43
|
+
deanonymizeOpenAIResponse: () => deanonymizeOpenAIResponse,
|
|
44
|
+
anonymizeOpenAIMessages: () => anonymizeOpenAIMessages,
|
|
45
|
+
STRICT_REASONS: () => STRICT_REASONS,
|
|
46
|
+
OpenAI: () => OpenAI,
|
|
47
|
+
GheimStrictError: () => GheimStrictError,
|
|
48
|
+
AsyncOpenAI: () => AsyncOpenAI
|
|
49
|
+
});
|
|
50
|
+
module.exports = __toCommonJS(exports_openai);
|
|
51
|
+
|
|
52
|
+
// src/core/sentinels.ts
|
|
53
|
+
var LABEL_TO_TAG = {
|
|
54
|
+
private_person: "PERSON",
|
|
55
|
+
private_email: "EMAIL",
|
|
56
|
+
private_phone: "PHONE",
|
|
57
|
+
private_address: "ADDRESS",
|
|
58
|
+
private_url: "URL",
|
|
59
|
+
private_date: "DATE",
|
|
60
|
+
account_number: "ACCOUNT",
|
|
61
|
+
secret: "SECRET"
|
|
62
|
+
};
|
|
63
|
+
var TAG_RE = /[A-Z][A-Z0-9_]*/;
|
|
64
|
+
var INDEX_RE = /\d+/;
|
|
65
|
+
var SENTINEL_RE = new RegExp(`<(?<tag>${TAG_RE.source})_(?<index>${INDEX_RE.source})>`, "g");
|
|
66
|
+
var SENTINEL_FULL_RE = new RegExp(`^<(?<tag>${TAG_RE.source})_(?<index>${INDEX_RE.source})>$`);
|
|
67
|
+
var PARTIAL_SENTINEL_RE = new RegExp(`^<(${TAG_RE.source})?(_(${INDEX_RE.source})?)?>?$`);
|
|
68
|
+
function labelTag(label) {
|
|
69
|
+
return LABEL_TO_TAG[label] ?? label.toUpperCase();
|
|
70
|
+
}
|
|
71
|
+
function renderSentinel(s) {
|
|
72
|
+
return `<${s.tag}_${s.index}>`;
|
|
73
|
+
}
|
|
74
|
+
function parseSentinel(s) {
|
|
75
|
+
const m = SENTINEL_FULL_RE.exec(s);
|
|
76
|
+
if (!m || !m.groups)
|
|
77
|
+
return null;
|
|
78
|
+
return { tag: m.groups.tag, index: Number.parseInt(m.groups.index, 10) };
|
|
79
|
+
}
|
|
80
|
+
function isPossibleSentinelPrefix(buffer) {
|
|
81
|
+
if (!buffer || buffer[0] !== "<")
|
|
82
|
+
return false;
|
|
83
|
+
if (buffer.endsWith(">"))
|
|
84
|
+
return false;
|
|
85
|
+
return PARTIAL_SENTINEL_RE.test(buffer);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/core/stream.ts
|
|
89
|
+
var SENTINEL_AT = /<([A-Z][A-Z0-9_]*)_(\d+)>/y;
|
|
90
|
+
var MAX_SENTINEL_LEN = 64;
|
|
91
|
+
|
|
92
|
+
class StreamDeanonymizer {
|
|
93
|
+
mapping;
|
|
94
|
+
hold = "";
|
|
95
|
+
constructor(mapping) {
|
|
96
|
+
this.mapping = mapping;
|
|
97
|
+
}
|
|
98
|
+
feed(chunk) {
|
|
99
|
+
if (!chunk)
|
|
100
|
+
return "";
|
|
101
|
+
const buffer = this.hold + chunk;
|
|
102
|
+
const out = [];
|
|
103
|
+
let i = 0;
|
|
104
|
+
const n = buffer.length;
|
|
105
|
+
while (i < n) {
|
|
106
|
+
if (buffer[i] !== "<") {
|
|
107
|
+
const lt = buffer.indexOf("<", i);
|
|
108
|
+
if (lt === -1) {
|
|
109
|
+
out.push(buffer.slice(i));
|
|
110
|
+
i = n;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
out.push(buffer.slice(i, lt));
|
|
114
|
+
i = lt;
|
|
115
|
+
}
|
|
116
|
+
SENTINEL_AT.lastIndex = i;
|
|
117
|
+
const m = SENTINEL_AT.exec(buffer);
|
|
118
|
+
if (m && m.index === i) {
|
|
119
|
+
const sentinel = m[0];
|
|
120
|
+
out.push(this.mapping[sentinel] ?? sentinel);
|
|
121
|
+
i = SENTINEL_AT.lastIndex;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const tail = buffer.slice(i);
|
|
125
|
+
if (tail.length < MAX_SENTINEL_LEN && isPossibleSentinelPrefix(tail)) {
|
|
126
|
+
this.hold = tail;
|
|
127
|
+
return out.join("");
|
|
128
|
+
}
|
|
129
|
+
out.push("<");
|
|
130
|
+
i += 1;
|
|
131
|
+
}
|
|
132
|
+
this.hold = "";
|
|
133
|
+
return out.join("");
|
|
134
|
+
}
|
|
135
|
+
flush() {
|
|
136
|
+
const held = this.hold;
|
|
137
|
+
this.hold = "";
|
|
138
|
+
return held;
|
|
139
|
+
}
|
|
140
|
+
async* transform(chunks) {
|
|
141
|
+
for await (const chunk of chunks) {
|
|
142
|
+
const out = this.feed(chunk);
|
|
143
|
+
if (out)
|
|
144
|
+
yield out;
|
|
145
|
+
}
|
|
146
|
+
const tail = this.flush();
|
|
147
|
+
if (tail)
|
|
148
|
+
yield tail;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/plain.ts
|
|
153
|
+
function resolve(session, detector) {
|
|
154
|
+
const det = detector ?? session.detector;
|
|
155
|
+
if (!det) {
|
|
156
|
+
throw new Error("No detector — pass one to anonymizeText or attach to the session.");
|
|
157
|
+
}
|
|
158
|
+
return det;
|
|
159
|
+
}
|
|
160
|
+
async function anonymizeText(text, session, opts) {
|
|
161
|
+
const det = resolve(session, opts?.detector);
|
|
162
|
+
const spans = await det.detect(text, { model: opts?.model });
|
|
163
|
+
return session.applySpans(text, spans);
|
|
164
|
+
}
|
|
165
|
+
function deanonymizeText(text, session) {
|
|
166
|
+
return session.restore(text);
|
|
167
|
+
}
|
|
168
|
+
async function anonymizeMessages(messages, session, opts) {
|
|
169
|
+
const det = resolve(session, opts?.detector);
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const msg of messages) {
|
|
172
|
+
const content = msg.content;
|
|
173
|
+
if (typeof content === "string") {
|
|
174
|
+
const redacted = await anonymizeText(content, session, { detector: det, model: opts?.model });
|
|
175
|
+
out.push({ ...msg, content: redacted });
|
|
176
|
+
} else if (Array.isArray(content)) {
|
|
177
|
+
const parts = await Promise.all(content.map(async (p) => {
|
|
178
|
+
const part = p;
|
|
179
|
+
if (part && part.type === "text" && typeof part.text === "string") {
|
|
180
|
+
const redacted = await anonymizeText(part.text, session, {
|
|
181
|
+
detector: det,
|
|
182
|
+
model: opts?.model
|
|
183
|
+
});
|
|
184
|
+
return { ...part, text: redacted };
|
|
185
|
+
}
|
|
186
|
+
return p;
|
|
187
|
+
}));
|
|
188
|
+
out.push({ ...msg, content: parts });
|
|
189
|
+
} else {
|
|
190
|
+
out.push({ ...msg });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
async function* deanonymizeStream(chunks, session) {
|
|
196
|
+
const buf = new StreamDeanonymizer(session.mapping);
|
|
197
|
+
yield* buf.transform(chunks);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/detectors/base.ts
|
|
201
|
+
function trimSpan(text, start, end) {
|
|
202
|
+
while (start < end && /\s/.test(text[start] ?? ""))
|
|
203
|
+
start += 1;
|
|
204
|
+
while (end > start && /\s/.test(text[end - 1] ?? ""))
|
|
205
|
+
end -= 1;
|
|
206
|
+
if (start >= end)
|
|
207
|
+
return null;
|
|
208
|
+
return { start, end };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/detectors/remote.ts
|
|
212
|
+
var DEFAULT_BASE_URL = "https://api.gheim.ch";
|
|
213
|
+
var DEFAULT_MODEL = "openai/privacy-filter";
|
|
214
|
+
|
|
215
|
+
class RemoteDetector {
|
|
216
|
+
baseUrl;
|
|
217
|
+
apiKey;
|
|
218
|
+
model;
|
|
219
|
+
trimWhitespace;
|
|
220
|
+
fetchImpl;
|
|
221
|
+
constructor(opts = {}) {
|
|
222
|
+
const envBase = typeof process !== "undefined" ? process.env?.GHEIM_BASE_URL : undefined;
|
|
223
|
+
const envKey = typeof process !== "undefined" ? process.env?.GHEIM_API_KEY : undefined;
|
|
224
|
+
this.baseUrl = (opts.baseUrl ?? envBase ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
225
|
+
this.apiKey = opts.apiKey ?? envKey;
|
|
226
|
+
this.model = opts.model ?? DEFAULT_MODEL;
|
|
227
|
+
this.trimWhitespace = opts.trimWhitespace ?? true;
|
|
228
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
229
|
+
}
|
|
230
|
+
async detect(text, opts) {
|
|
231
|
+
if (!text)
|
|
232
|
+
return [];
|
|
233
|
+
const headers = { "Content-Type": "application/json" };
|
|
234
|
+
if (this.apiKey)
|
|
235
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
236
|
+
const resp = await this.fetchImpl(`${this.baseUrl}/v1/detect`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers,
|
|
239
|
+
body: JSON.stringify({ model: opts?.model ?? this.model, text })
|
|
240
|
+
});
|
|
241
|
+
if (!resp.ok) {
|
|
242
|
+
throw new Error(`gheim-server ${resp.status}: ${await resp.text()}`);
|
|
243
|
+
}
|
|
244
|
+
const data = await resp.json();
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const item of data.spans ?? []) {
|
|
247
|
+
let { start, end } = item;
|
|
248
|
+
if (this.trimWhitespace) {
|
|
249
|
+
const trimmed = trimSpan(text, start, end);
|
|
250
|
+
if (!trimmed)
|
|
251
|
+
continue;
|
|
252
|
+
({ start, end } = trimmed);
|
|
253
|
+
}
|
|
254
|
+
out.push({
|
|
255
|
+
label: item.label,
|
|
256
|
+
start,
|
|
257
|
+
end,
|
|
258
|
+
text: text.slice(start, end),
|
|
259
|
+
score: item.score
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// src/detectors/local.ts
|
|
266
|
+
var DEFAULT_MODEL2 = "joelbarmettler/gheim-ch-560m";
|
|
267
|
+
var DEFAULT_CHUNK_TOKENS = 510;
|
|
268
|
+
var DEFAULT_OVERLAP_TOKENS = 32;
|
|
269
|
+
var DEFAULT_BATCH_SIZE = 8;
|
|
270
|
+
|
|
271
|
+
class LocalDetector {
|
|
272
|
+
model;
|
|
273
|
+
device;
|
|
274
|
+
dtype;
|
|
275
|
+
aggregationStrategy;
|
|
276
|
+
chunkTokens;
|
|
277
|
+
chunkOverlapTokens;
|
|
278
|
+
batchSize;
|
|
279
|
+
_pipeline = null;
|
|
280
|
+
_loadPromise = null;
|
|
281
|
+
constructor(opts = {}) {
|
|
282
|
+
this.model = opts.model ?? DEFAULT_MODEL2;
|
|
283
|
+
this.device = opts.device ?? "auto";
|
|
284
|
+
this.dtype = opts.dtype ?? "fp32";
|
|
285
|
+
this.aggregationStrategy = opts.aggregationStrategy ?? "simple";
|
|
286
|
+
this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
|
|
287
|
+
this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
|
|
288
|
+
this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
289
|
+
if (this.chunkOverlapTokens >= this.chunkTokens) {
|
|
290
|
+
throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
|
|
291
|
+
}
|
|
292
|
+
if (opts.pipelineInstance) {
|
|
293
|
+
this._pipeline = opts.pipelineInstance;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async load() {
|
|
297
|
+
if (this._pipeline)
|
|
298
|
+
return this._pipeline;
|
|
299
|
+
if (this._loadPromise)
|
|
300
|
+
return this._loadPromise;
|
|
301
|
+
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");
|
|
307
|
+
}
|
|
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;
|
|
314
|
+
})();
|
|
315
|
+
return this._loadPromise;
|
|
316
|
+
}
|
|
317
|
+
async detect(text, opts) {
|
|
318
|
+
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.`);
|
|
320
|
+
}
|
|
321
|
+
if (!text)
|
|
322
|
+
return [];
|
|
323
|
+
const results = await this.detectBatch([text]);
|
|
324
|
+
return results[0] ?? [];
|
|
325
|
+
}
|
|
326
|
+
async detectBatch(texts, opts) {
|
|
327
|
+
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.`);
|
|
329
|
+
}
|
|
330
|
+
if (texts.length === 0)
|
|
331
|
+
return [];
|
|
332
|
+
const pipe = await this.load();
|
|
333
|
+
const tokenizer = pipe.tokenizer;
|
|
334
|
+
const plan = [];
|
|
335
|
+
for (let i = 0;i < texts.length; i++) {
|
|
336
|
+
const text = texts[i] ?? "";
|
|
337
|
+
if (!text)
|
|
338
|
+
continue;
|
|
339
|
+
const chunks = await this.chunkText(text, tokenizer);
|
|
340
|
+
for (const c of chunks)
|
|
341
|
+
plan.push({ inputIdx: i, charOffset: c.offset, text: c.text });
|
|
342
|
+
}
|
|
343
|
+
const out = texts.map(() => []);
|
|
344
|
+
if (plan.length === 0)
|
|
345
|
+
return out;
|
|
346
|
+
const batchInput = plan.map((p) => p.text);
|
|
347
|
+
const raw = await pipe(batchInput, {
|
|
348
|
+
aggregation_strategy: this.aggregationStrategy,
|
|
349
|
+
batch_size: this.batchSize
|
|
350
|
+
});
|
|
351
|
+
const seen = texts.map(() => new Set);
|
|
352
|
+
for (let p = 0;p < plan.length; p++) {
|
|
353
|
+
const planEntry = plan[p];
|
|
354
|
+
if (!planEntry)
|
|
355
|
+
continue;
|
|
356
|
+
const { inputIdx, charOffset, text: chunkText } = planEntry;
|
|
357
|
+
const fullText = texts[inputIdx] ?? "";
|
|
358
|
+
const inputSeen = seen[inputIdx];
|
|
359
|
+
const inputOut = out[inputIdx];
|
|
360
|
+
if (!inputSeen || !inputOut)
|
|
361
|
+
continue;
|
|
362
|
+
const items = raw[p] ?? [];
|
|
363
|
+
let cursor = 0;
|
|
364
|
+
for (const item of items) {
|
|
365
|
+
const label = String(item.entity_group ?? item.entity ?? "unknown");
|
|
366
|
+
const score = typeof item.score === "number" ? item.score : 1;
|
|
367
|
+
let localStart;
|
|
368
|
+
let localEnd;
|
|
369
|
+
if (item.start === undefined || item.end === undefined) {
|
|
370
|
+
const surface = normalizeWord(item.word ?? "");
|
|
371
|
+
if (!surface)
|
|
372
|
+
continue;
|
|
373
|
+
const idx = chunkText.indexOf(surface, cursor);
|
|
374
|
+
if (idx === -1)
|
|
375
|
+
continue;
|
|
376
|
+
localStart = idx;
|
|
377
|
+
localEnd = idx + surface.length;
|
|
378
|
+
cursor = localEnd;
|
|
379
|
+
} else {
|
|
380
|
+
localStart = item.start;
|
|
381
|
+
localEnd = item.end;
|
|
382
|
+
}
|
|
383
|
+
const start = localStart + charOffset;
|
|
384
|
+
const end = localEnd + charOffset;
|
|
385
|
+
const key = `${start}|${end}|${label}`;
|
|
386
|
+
if (inputSeen.has(key))
|
|
387
|
+
continue;
|
|
388
|
+
inputSeen.add(key);
|
|
389
|
+
inputOut.push({
|
|
390
|
+
label,
|
|
391
|
+
start,
|
|
392
|
+
end,
|
|
393
|
+
text: fullText.slice(start, end),
|
|
394
|
+
score
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const spans of out) {
|
|
399
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
async chunkText(text, tokenizer) {
|
|
404
|
+
if (tokenizer) {
|
|
405
|
+
const tokenChunks = await chunkByTokens(text, tokenizer, this.chunkTokens, this.chunkOverlapTokens);
|
|
406
|
+
if (tokenChunks)
|
|
407
|
+
return tokenChunks;
|
|
408
|
+
}
|
|
409
|
+
return chunkByChars(text, 1500, 200);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function chunkByTokens(text, tokenizer, chunkTokens, overlapTokens) {
|
|
413
|
+
let enc;
|
|
414
|
+
try {
|
|
415
|
+
enc = await tokenizer(text, {
|
|
416
|
+
return_offsets_mapping: true,
|
|
417
|
+
add_special_tokens: false,
|
|
418
|
+
truncation: false
|
|
419
|
+
});
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const offsets = readOffsetMapping(enc);
|
|
424
|
+
if (!offsets)
|
|
425
|
+
return null;
|
|
426
|
+
const real = offsets.filter(([a, b]) => !(a === 0 && b === 0));
|
|
427
|
+
if (real.length === 0)
|
|
428
|
+
return [{ offset: 0, text }];
|
|
429
|
+
if (real.length <= chunkTokens)
|
|
430
|
+
return [{ offset: 0, text }];
|
|
431
|
+
const stride = Math.max(1, chunkTokens - overlapTokens);
|
|
432
|
+
const out = [];
|
|
433
|
+
for (let s = 0;s < real.length; s += stride) {
|
|
434
|
+
const e = Math.min(s + chunkTokens, real.length);
|
|
435
|
+
const startPair = real[s];
|
|
436
|
+
const endPair = real[e - 1];
|
|
437
|
+
if (!startPair || !endPair)
|
|
438
|
+
break;
|
|
439
|
+
const charStart = startPair[0];
|
|
440
|
+
const charEnd = endPair[1];
|
|
441
|
+
out.push({ offset: charStart, text: text.slice(charStart, charEnd) });
|
|
442
|
+
if (e >= real.length)
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
function readOffsetMapping(enc) {
|
|
448
|
+
const om = enc.offset_mapping;
|
|
449
|
+
if (!om)
|
|
450
|
+
return null;
|
|
451
|
+
if (typeof om.tolist === "function") {
|
|
452
|
+
const list = om.tolist();
|
|
453
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
454
|
+
return [];
|
|
455
|
+
const inner = list[0];
|
|
456
|
+
if (Array.isArray(inner) && Array.isArray(inner[0])) {
|
|
457
|
+
return inner;
|
|
458
|
+
}
|
|
459
|
+
return list;
|
|
460
|
+
}
|
|
461
|
+
if (om.data && om.dims && om.dims.length >= 2) {
|
|
462
|
+
const dims = om.dims;
|
|
463
|
+
const pairs = dims[dims.length - 2] ?? 0;
|
|
464
|
+
const data = om.data;
|
|
465
|
+
const out = [];
|
|
466
|
+
for (let i = 0;i < pairs; i++) {
|
|
467
|
+
out.push([Number(data[i * 2]), Number(data[i * 2 + 1])]);
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
function chunkByChars(text, chunkChars, overlapChars) {
|
|
474
|
+
if (text.length <= chunkChars)
|
|
475
|
+
return [{ offset: 0, text }];
|
|
476
|
+
const stride = Math.max(1, chunkChars - overlapChars);
|
|
477
|
+
const out = [];
|
|
478
|
+
for (let s = 0;s < text.length; s += stride) {
|
|
479
|
+
const e = Math.min(s + chunkChars, text.length);
|
|
480
|
+
out.push({ offset: s, text: text.slice(s, e) });
|
|
481
|
+
if (e >= text.length)
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
function normalizeWord(word) {
|
|
487
|
+
return word.replace(/##/g, "").trim();
|
|
488
|
+
}
|
|
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;
|
|
503
|
+
try {
|
|
504
|
+
_require = import_node_module.createRequire("file:///home/joelbarmettler/projects/gheim/packages/gheim-js/src/normalizers.ts");
|
|
505
|
+
} catch {
|
|
506
|
+
_require = null;
|
|
507
|
+
}
|
|
508
|
+
return _require;
|
|
509
|
+
}
|
|
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;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
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
|
+
};
|
|
553
|
+
}
|
|
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;
|
|
568
|
+
}
|
|
569
|
+
return out.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
|
|
570
|
+
};
|
|
571
|
+
}
|
|
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.`);
|
|
583
|
+
}
|
|
584
|
+
return factory();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/core/session.ts
|
|
588
|
+
function normalizeSurface(s) {
|
|
589
|
+
return s.normalize("NFKC").toLowerCase().split(/\s+/).filter(Boolean).join(" ");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
class Session {
|
|
593
|
+
mapping = {};
|
|
594
|
+
coalesce = new Map;
|
|
595
|
+
counters = new Map;
|
|
596
|
+
resolvedNormalizers = {};
|
|
597
|
+
constructor(opts = {}) {
|
|
598
|
+
if (opts.normalizers) {
|
|
599
|
+
for (const [label, spec] of Object.entries(opts.normalizers)) {
|
|
600
|
+
this.resolvedNormalizers[label] = resolveNormalizer(spec);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
allocate(label, surface) {
|
|
605
|
+
const tag = labelTag(label);
|
|
606
|
+
const norm = this.resolvedNormalizers[label];
|
|
607
|
+
let normalized = null;
|
|
608
|
+
if (norm)
|
|
609
|
+
normalized = norm(surface);
|
|
610
|
+
if (normalized === null)
|
|
611
|
+
normalized = normalizeSurface(surface);
|
|
612
|
+
const key = `${tag}\x00${normalized}`;
|
|
613
|
+
const existing = this.coalesce.get(key);
|
|
614
|
+
if (existing)
|
|
615
|
+
return existing;
|
|
616
|
+
const next = (this.counters.get(tag) ?? 0) + 1;
|
|
617
|
+
this.counters.set(tag, next);
|
|
618
|
+
const sentinel = renderSentinel({ tag, index: next });
|
|
619
|
+
this.coalesce.set(key, sentinel);
|
|
620
|
+
this.mapping[sentinel] = surface;
|
|
621
|
+
return sentinel;
|
|
622
|
+
}
|
|
623
|
+
applySpans(text, spans) {
|
|
624
|
+
if (spans.length === 0)
|
|
625
|
+
return text;
|
|
626
|
+
const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
|
|
627
|
+
const merged = mergeAdjacent(text, sorted);
|
|
628
|
+
const out = [];
|
|
629
|
+
let cursor = 0;
|
|
630
|
+
for (const { start, end, label } of merged) {
|
|
631
|
+
if (start < cursor)
|
|
632
|
+
continue;
|
|
633
|
+
if (start < 0 || end > text.length || start >= end)
|
|
634
|
+
continue;
|
|
635
|
+
const surface = text.slice(start, end);
|
|
636
|
+
out.push(text.slice(cursor, start));
|
|
637
|
+
out.push(this.allocate(label, surface));
|
|
638
|
+
cursor = end;
|
|
639
|
+
}
|
|
640
|
+
out.push(text.slice(cursor));
|
|
641
|
+
return out.join("");
|
|
642
|
+
}
|
|
643
|
+
restore(text) {
|
|
644
|
+
if (Object.keys(this.mapping).length === 0)
|
|
645
|
+
return text;
|
|
646
|
+
return text.replace(SENTINEL_RE, (match) => this.mapping[match] ?? match);
|
|
647
|
+
}
|
|
648
|
+
toJSON() {
|
|
649
|
+
return { mapping: { ...this.mapping } };
|
|
650
|
+
}
|
|
651
|
+
static fromJSON(raw, opts = {}) {
|
|
652
|
+
const s = new Session(opts);
|
|
653
|
+
s.mapping = { ...raw.mapping };
|
|
654
|
+
for (const [sentinel, surface] of Object.entries(s.mapping)) {
|
|
655
|
+
const parsed = parseSentinel(sentinel);
|
|
656
|
+
if (!parsed)
|
|
657
|
+
continue;
|
|
658
|
+
let normalized = null;
|
|
659
|
+
for (const [label, fn] of Object.entries(s.resolvedNormalizers)) {
|
|
660
|
+
if (labelTag(label) === parsed.tag) {
|
|
661
|
+
normalized = fn(surface);
|
|
662
|
+
if (normalized !== null)
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (normalized === null)
|
|
667
|
+
normalized = normalizeSurface(surface);
|
|
668
|
+
s.coalesce.set(`${parsed.tag}\x00${normalized}`, sentinel);
|
|
669
|
+
s.counters.set(parsed.tag, Math.max(s.counters.get(parsed.tag) ?? 0, parsed.index));
|
|
670
|
+
}
|
|
671
|
+
return s;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
var MERGE_GAP = 1;
|
|
675
|
+
function mergeAdjacent(text, spans) {
|
|
676
|
+
const out = [];
|
|
677
|
+
for (const span of spans) {
|
|
678
|
+
if (span.start < 0 || span.end > text.length || span.start >= span.end)
|
|
679
|
+
continue;
|
|
680
|
+
const last = out[out.length - 1];
|
|
681
|
+
if (!last) {
|
|
682
|
+
out.push({ start: span.start, end: span.end, label: span.label });
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (span.start < last.end) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const gap = text.slice(last.end, span.start);
|
|
689
|
+
if (span.label === last.label && gap.length <= MERGE_GAP && gap.trim() === "") {
|
|
690
|
+
last.end = span.end;
|
|
691
|
+
} else {
|
|
692
|
+
out.push({ start: span.start, end: span.end, label: span.label });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/openai/_base.ts
|
|
699
|
+
function resolveCallSession(body, defaultDet) {
|
|
700
|
+
const { gheimSession, gheimDetector, ...rest } = body;
|
|
701
|
+
const detector = gheimDetector ?? defaultDet;
|
|
702
|
+
const session = gheimSession ?? new Session;
|
|
703
|
+
if (!session.detector) {
|
|
704
|
+
session.detector = detector;
|
|
705
|
+
}
|
|
706
|
+
return { session, detector, rest };
|
|
707
|
+
}
|
|
708
|
+
async function anonymizeValue(value, session, detector) {
|
|
709
|
+
if (typeof value === "string") {
|
|
710
|
+
if (!value)
|
|
711
|
+
return value;
|
|
712
|
+
const spans = await detector.detect(value);
|
|
713
|
+
return session.applySpans(value, spans);
|
|
714
|
+
}
|
|
715
|
+
if (Array.isArray(value)) {
|
|
716
|
+
if (value.every((x) => typeof x === "number" && Number.isInteger(x))) {
|
|
717
|
+
return value;
|
|
718
|
+
}
|
|
719
|
+
return Promise.all(value.map((v) => anonymizeValue(v, session, detector)));
|
|
720
|
+
}
|
|
721
|
+
return value;
|
|
722
|
+
}
|
|
723
|
+
class StreamingResponse {
|
|
724
|
+
inner;
|
|
725
|
+
session;
|
|
726
|
+
mutate;
|
|
727
|
+
makeTail;
|
|
728
|
+
buf;
|
|
729
|
+
constructor(inner, session, mutate, makeTail) {
|
|
730
|
+
this.inner = inner;
|
|
731
|
+
this.session = session;
|
|
732
|
+
this.mutate = mutate;
|
|
733
|
+
this.makeTail = makeTail;
|
|
734
|
+
this.buf = new StreamDeanonymizer(session.mapping);
|
|
735
|
+
}
|
|
736
|
+
async* [Symbol.asyncIterator]() {
|
|
737
|
+
let last;
|
|
738
|
+
for await (const chunk of this.inner) {
|
|
739
|
+
last = chunk;
|
|
740
|
+
this.mutate(chunk, this.buf);
|
|
741
|
+
yield chunk;
|
|
742
|
+
}
|
|
743
|
+
const tail = this.buf.flush();
|
|
744
|
+
if (tail && last !== undefined && this.makeTail) {
|
|
745
|
+
yield this.makeTail(last, tail);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/openai/_audio.ts
|
|
751
|
+
function patchTranscriptionChunk(chunk, buf) {
|
|
752
|
+
if (typeof chunk.delta === "string") {
|
|
753
|
+
chunk.delta = buf.feed(chunk.delta);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (typeof chunk.text === "string") {
|
|
757
|
+
chunk.text = buf.feed(chunk.text);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
class GheimSpeech {
|
|
762
|
+
innerP;
|
|
763
|
+
defaultDet;
|
|
764
|
+
constructor(innerP, defaultDet) {
|
|
765
|
+
this.innerP = innerP;
|
|
766
|
+
this.defaultDet = defaultDet;
|
|
767
|
+
}
|
|
768
|
+
async create(body) {
|
|
769
|
+
const { session, detector, rest } = resolveCallSession(body, this.defaultDet);
|
|
770
|
+
for (const k of ["input", "instructions"]) {
|
|
771
|
+
if (k in rest) {
|
|
772
|
+
rest[k] = await anonymizeValue(rest[k], session, detector);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const inner = await this.innerP;
|
|
776
|
+
return inner.speech.create(rest);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
class GheimTranscriptions {
|
|
781
|
+
innerP;
|
|
782
|
+
defaultDet;
|
|
783
|
+
constructor(innerP, defaultDet) {
|
|
784
|
+
this.innerP = innerP;
|
|
785
|
+
this.defaultDet = defaultDet;
|
|
786
|
+
}
|
|
787
|
+
async create(body) {
|
|
788
|
+
const { session, detector, rest } = resolveCallSession(body, this.defaultDet);
|
|
789
|
+
if (typeof rest.prompt === "string") {
|
|
790
|
+
rest.prompt = await anonymizeValue(rest.prompt, session, detector);
|
|
791
|
+
}
|
|
792
|
+
const inner = await this.innerP;
|
|
793
|
+
const response = await inner.transcriptions.create(rest);
|
|
794
|
+
if (rest.stream) {
|
|
795
|
+
return new StreamingResponse(response, session, patchTranscriptionChunk);
|
|
796
|
+
}
|
|
797
|
+
const r = response;
|
|
798
|
+
if (typeof r.text === "string")
|
|
799
|
+
r.text = session.restore(r.text);
|
|
800
|
+
return response;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
class GheimTranslations {
|
|
805
|
+
innerP;
|
|
806
|
+
defaultDet;
|
|
807
|
+
constructor(innerP, defaultDet) {
|
|
808
|
+
this.innerP = innerP;
|
|
809
|
+
this.defaultDet = defaultDet;
|
|
810
|
+
}
|
|
811
|
+
async create(body) {
|
|
812
|
+
const { session, detector, rest } = resolveCallSession(body, this.defaultDet);
|
|
813
|
+
if (typeof rest.prompt === "string") {
|
|
814
|
+
rest.prompt = await anonymizeValue(rest.prompt, session, detector);
|
|
815
|
+
}
|
|
816
|
+
const inner = await this.innerP;
|
|
817
|
+
const response = await inner.translations.create(rest);
|
|
818
|
+
const r = response;
|
|
819
|
+
if (typeof r.text === "string")
|
|
820
|
+
r.text = session.restore(r.text);
|
|
821
|
+
return response;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
class GheimAudio {
|
|
826
|
+
speech;
|
|
827
|
+
transcriptions;
|
|
828
|
+
translations;
|
|
829
|
+
constructor(clientPromise, detector) {
|
|
830
|
+
const innerP = clientPromise.then((c) => c.audio);
|
|
831
|
+
this.speech = new GheimSpeech(innerP, detector);
|
|
832
|
+
this.transcriptions = new GheimTranscriptions(innerP, detector);
|
|
833
|
+
this.translations = new GheimTranslations(innerP, detector);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/openai/_chat.ts
|
|
838
|
+
async function anonymizeChatBody(rest, session, detector) {
|
|
839
|
+
const messages = rest.messages;
|
|
840
|
+
if (messages) {
|
|
841
|
+
const cleaned = await anonymizeMessages(messages, session, { detector });
|
|
842
|
+
for (const msg of cleaned) {
|
|
843
|
+
if (typeof msg.name === "string") {
|
|
844
|
+
msg.name = await anonymizeValue(msg.name, session, detector);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
rest.messages = cleaned;
|
|
848
|
+
}
|
|
849
|
+
const tools = rest.tools;
|
|
850
|
+
if (tools) {
|
|
851
|
+
const newTools = [];
|
|
852
|
+
for (const tool of tools) {
|
|
853
|
+
const fn = tool.function;
|
|
854
|
+
if (fn && typeof fn.description === "string") {
|
|
855
|
+
const newFn = {
|
|
856
|
+
...fn,
|
|
857
|
+
description: await anonymizeValue(fn.description, session, detector)
|
|
858
|
+
};
|
|
859
|
+
newTools.push({ ...tool, function: newFn });
|
|
860
|
+
} else {
|
|
861
|
+
newTools.push(tool);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
rest.tools = newTools;
|
|
865
|
+
}
|
|
866
|
+
return rest;
|
|
867
|
+
}
|
|
868
|
+
function restoreChatResponse(response, session) {
|
|
869
|
+
for (const choice of response.choices ?? []) {
|
|
870
|
+
const msg = choice.message;
|
|
871
|
+
if (msg && typeof msg.content === "string") {
|
|
872
|
+
msg.content = session.restore(msg.content);
|
|
873
|
+
}
|
|
874
|
+
for (const tc of msg?.tool_calls ?? []) {
|
|
875
|
+
const fn = tc.function;
|
|
876
|
+
if (fn && typeof fn.arguments === "string") {
|
|
877
|
+
fn.arguments = session.restore(fn.arguments);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function patchChatChunk(chunk, buf) {
|
|
883
|
+
for (const choice of chunk.choices ?? []) {
|
|
884
|
+
const delta = choice.delta;
|
|
885
|
+
if (typeof delta.content === "string") {
|
|
886
|
+
delta.content = buf.feed(delta.content);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function makeChatTailChunk(template, content) {
|
|
891
|
+
return {
|
|
892
|
+
id: template.id,
|
|
893
|
+
object: template.object,
|
|
894
|
+
created: template.created,
|
|
895
|
+
model: template.model,
|
|
896
|
+
choices: [
|
|
897
|
+
{
|
|
898
|
+
index: 0,
|
|
899
|
+
delta: { content, role: undefined },
|
|
900
|
+
finish_reason: null
|
|
901
|
+
}
|
|
902
|
+
]
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
class GheimChatCompletions {
|
|
907
|
+
clientPromise;
|
|
908
|
+
defaultDet;
|
|
909
|
+
constructor(clientPromise, defaultDet) {
|
|
910
|
+
this.clientPromise = clientPromise;
|
|
911
|
+
this.defaultDet = defaultDet;
|
|
912
|
+
}
|
|
913
|
+
async create(body) {
|
|
914
|
+
const { session, detector, rest } = resolveCallSession(body, this.defaultDet);
|
|
915
|
+
const wireBody = await anonymizeChatBody(rest, session, detector);
|
|
916
|
+
const client = await this.clientPromise;
|
|
917
|
+
const response = await client.chat.completions.create(wireBody);
|
|
918
|
+
if (rest.stream) {
|
|
919
|
+
return new StreamingResponse(response, session, patchChatChunk, makeChatTailChunk);
|
|
920
|
+
}
|
|
921
|
+
restoreChatResponse(response, session);
|
|
922
|
+
return response;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
class GheimChat {
|
|
927
|
+
completions;
|
|
928
|
+
constructor(clientPromise, detector) {
|
|
929
|
+
this.completions = new GheimChatCompletions(clientPromise, detector);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/openai/_completions.ts
|
|
934
|
+
function patchLegacyChunk(chunk, buf) {
|
|
935
|
+
for (const choice of chunk.choices ?? []) {
|
|
936
|
+
if (typeof choice.text === "string") {
|
|
937
|
+
choice.text = buf.feed(choice.text);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function makeLegacyTail(template, tail) {
|
|
942
|
+
return {
|
|
943
|
+
id: template.id,
|
|
944
|
+
object: template.object ?? "text_completion.chunk",
|
|
945
|
+
created: template.created ?? 0,
|
|
946
|
+
model: template.model,
|
|
947
|
+
choices: [{ index: 0, text: tail, finish_reason: null }]
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
class GheimCompletions {
|
|
952
|
+
clientPromise;
|
|
953
|
+
defaultDet;
|
|
954
|
+
constructor(clientPromise, defaultDet) {
|
|
955
|
+
this.clientPromise = clientPromise;
|
|
956
|
+
this.defaultDet = defaultDet;
|
|
957
|
+
}
|
|
958
|
+
async create(body) {
|
|
959
|
+
const { session, detector, rest } = resolveCallSession(body, this.defaultDet);
|
|
960
|
+
if ("prompt" in rest) {
|
|
961
|
+
rest.prompt = await anonymizeValue(rest.prompt, session, detector);
|
|
962
|
+
}
|
|
963
|
+
const client = await this.clientPromise;
|
|
964
|
+
const response = await client.completions.create(rest);
|
|
965
|
+
if (rest.stream) {
|
|
966
|
+
return new StreamingResponse(response, session, patchLegacyChunk, makeLegacyTail);
|
|
967
|
+
}
|
|
968
|
+
const r = response;
|
|
969
|
+
for (const choice of r.choices ?? []) {
|
|
970
|
+
if (typeof choice.text === "string") {
|
|
971
|
+
choice.text = session.restore(choice.text);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return response;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/openai/_responses.ts
|
|
979
|
+
var TEXT_DELTA_EVENTS = {
|
|
980
|
+
"response.output_text.delta": "text",
|
|
981
|
+
"response.reasoning_text.delta": "reasoning",
|
|
982
|
+
"response.audio_transcript.delta": "audio",
|
|
983
|
+
"response.function_call_arguments.delta": "function_args",
|
|
984
|
+
"response.custom_tool_call_input.delta": "custom_tool_input"
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
class ResponsesStream {
|
|
988
|
+
inner;
|
|
989
|
+
session;
|
|
990
|
+
bufs = new Map;
|
|
991
|
+
constructor(inner, session) {
|
|
992
|
+
this.inner = inner;
|
|
993
|
+
this.session = session;
|
|
994
|
+
}
|
|
995
|
+
bufFor(channel) {
|
|
996
|
+
let buf = this.bufs.get(channel);
|
|
997
|
+
if (!buf) {
|
|
998
|
+
buf = new StreamDeanonymizer(this.session.mapping);
|
|
999
|
+
this.bufs.set(channel, buf);
|
|
1000
|
+
}
|
|
1001
|
+
return buf;
|
|
1002
|
+
}
|
|
1003
|
+
async* [Symbol.asyncIterator]() {
|
|
1004
|
+
for await (const event of this.inner) {
|
|
1005
|
+
if (typeof event.type === "string") {
|
|
1006
|
+
const channel = TEXT_DELTA_EVENTS[event.type];
|
|
1007
|
+
if (channel && typeof event.delta === "string") {
|
|
1008
|
+
event.delta = this.bufFor(channel).feed(event.delta);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
yield event;
|
|
1012
|
+
}
|
|
1013
|
+
for (const [channel, buf] of this.bufs) {
|
|
1014
|
+
const tail = buf.flush();
|
|
1015
|
+
if (tail) {
|
|
1016
|
+
yield { type: `response.${channel}.delta.tail`, delta: tail };
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
class GheimResponses {
|
|
1023
|
+
clientPromise;
|
|
1024
|
+
defaultDet;
|
|
1025
|
+
constructor(clientPromise, defaultDet) {
|
|
1026
|
+
this.clientPromise = clientPromise;
|
|
1027
|
+
this.defaultDet = defaultDet;
|
|
1028
|
+
}
|
|
1029
|
+
async create(body) {
|
|
1030
|
+
const { session, detector, rest } = resolveCallSession(body, this.defaultDet);
|
|
1031
|
+
for (const key of ["input", "instructions"]) {
|
|
1032
|
+
if (key in rest) {
|
|
1033
|
+
rest[key] = await anonymizeValue(rest[key], session, detector);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const tools = rest.tools;
|
|
1037
|
+
if (tools) {
|
|
1038
|
+
const newTools = [];
|
|
1039
|
+
for (const tool of tools) {
|
|
1040
|
+
const fn = tool.function;
|
|
1041
|
+
if (fn && typeof fn.description === "string") {
|
|
1042
|
+
const newFn = { ...fn, description: await anonymizeValue(fn.description, session, detector) };
|
|
1043
|
+
newTools.push({ ...tool, function: newFn });
|
|
1044
|
+
} else {
|
|
1045
|
+
newTools.push(tool);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
rest.tools = newTools;
|
|
1049
|
+
}
|
|
1050
|
+
const client = await this.clientPromise;
|
|
1051
|
+
const response = await client.responses.create(rest);
|
|
1052
|
+
if (rest.stream) {
|
|
1053
|
+
return new ResponsesStream(response, session);
|
|
1054
|
+
}
|
|
1055
|
+
const r = response;
|
|
1056
|
+
for (const item of r.output ?? []) {
|
|
1057
|
+
for (const part of item.content ?? []) {
|
|
1058
|
+
if (typeof part.text === "string") {
|
|
1059
|
+
part.text = session.restore(part.text);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return response;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/openai/_anonymize_only.ts
|
|
1068
|
+
async function anonymizeFields(body, defaultDet, fields) {
|
|
1069
|
+
const { session, detector, rest } = resolveCallSession(body, defaultDet);
|
|
1070
|
+
for (const f of fields) {
|
|
1071
|
+
if (f in rest) {
|
|
1072
|
+
rest[f] = await anonymizeValue(rest[f], session, detector);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return { rest, session };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
class GheimEmbeddings {
|
|
1079
|
+
clientPromise;
|
|
1080
|
+
defaultDet;
|
|
1081
|
+
constructor(clientPromise, defaultDet) {
|
|
1082
|
+
this.clientPromise = clientPromise;
|
|
1083
|
+
this.defaultDet = defaultDet;
|
|
1084
|
+
}
|
|
1085
|
+
async create(body) {
|
|
1086
|
+
const { rest } = await anonymizeFields(body, this.defaultDet, ["input"]);
|
|
1087
|
+
const client = await this.clientPromise;
|
|
1088
|
+
return client.embeddings.create(rest);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
class GheimModerations {
|
|
1093
|
+
clientPromise;
|
|
1094
|
+
defaultDet;
|
|
1095
|
+
constructor(clientPromise, defaultDet) {
|
|
1096
|
+
this.clientPromise = clientPromise;
|
|
1097
|
+
this.defaultDet = defaultDet;
|
|
1098
|
+
}
|
|
1099
|
+
async create(body) {
|
|
1100
|
+
const { rest } = await anonymizeFields(body, this.defaultDet, ["input"]);
|
|
1101
|
+
const client = await this.clientPromise;
|
|
1102
|
+
return client.moderations.create(rest);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
class GheimImages {
|
|
1107
|
+
clientPromise;
|
|
1108
|
+
defaultDet;
|
|
1109
|
+
constructor(clientPromise, defaultDet) {
|
|
1110
|
+
this.clientPromise = clientPromise;
|
|
1111
|
+
this.defaultDet = defaultDet;
|
|
1112
|
+
}
|
|
1113
|
+
async generate(body) {
|
|
1114
|
+
const { rest } = await anonymizeFields(body, this.defaultDet, ["prompt"]);
|
|
1115
|
+
const client = await this.clientPromise;
|
|
1116
|
+
return client.images.generate(rest);
|
|
1117
|
+
}
|
|
1118
|
+
async edit(body) {
|
|
1119
|
+
const { rest } = await anonymizeFields(body, this.defaultDet, ["prompt"]);
|
|
1120
|
+
const client = await this.clientPromise;
|
|
1121
|
+
return client.images.edit(rest);
|
|
1122
|
+
}
|
|
1123
|
+
async createVariation(body) {
|
|
1124
|
+
const client = await this.clientPromise;
|
|
1125
|
+
const inner = client.images;
|
|
1126
|
+
if (!inner.createVariation) {
|
|
1127
|
+
throw new Error("openai SDK has no images.createVariation in this version");
|
|
1128
|
+
}
|
|
1129
|
+
return inner.createVariation(body);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/openai/_strict.ts
|
|
1134
|
+
var STRICT_REASONS = {
|
|
1135
|
+
beta: "the Assistants API (beta.assistants.*, beta.threads.*) is stateful and " + "stores instructions + messages on OpenAI's servers across calls — " + "wrapping it correctly requires cross-call session persistence which is " + "not yet implemented. OpenAI is steering everyone to the new Responses " + "API; consider client.responses.create(...) instead.",
|
|
1136
|
+
batches: "the Batch API takes a JSONL file containing nested chat/completions/embeddings " + "request bodies; wrapping requires JSONL rewrite + per-line restoration which " + "is not yet implemented. Anonymize each line yourself with anonymizeText " + "before uploading the batch input file.",
|
|
1137
|
+
files: "files.create uploads binary blobs (PDFs, CSVs, audio); document-level " + "redaction is outside gheim's scope. Use a dedicated PDF/document redactor.",
|
|
1138
|
+
uploads: "uploads.* is the multi-step file upload protocol; same reason as files.*.",
|
|
1139
|
+
fineTuning: "fine-tuning sends a training data file by ID — gheim doesn't redact files. " + "Pre-anonymize your training data file before uploading.",
|
|
1140
|
+
vectorStores: "vector_stores ingest pre-uploaded files; redact those files first.",
|
|
1141
|
+
containers: "containers manage server-side execution environments; not in scope."
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
class GheimStrictError extends Error {
|
|
1145
|
+
constructor(message) {
|
|
1146
|
+
super(message);
|
|
1147
|
+
this.name = "GheimStrictError";
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function strictMessage(attr, sub, reason) {
|
|
1151
|
+
return `gheim/openai blocks client.${attr}.${sub} in strict mode: ${reason}
|
|
1152
|
+
` + `To call it anyway with raw PII flowing to OpenAI, use ` + `client.raw.${attr}.${sub}(...). Or construct the client with ` + `gheimStrict: false to silence this check (one-time warning).`;
|
|
1153
|
+
}
|
|
1154
|
+
function makeStrictProxy(attrName, reason, path = []) {
|
|
1155
|
+
return new Proxy(function() {}, {
|
|
1156
|
+
get(_target, prop) {
|
|
1157
|
+
if (typeof prop === "symbol")
|
|
1158
|
+
return;
|
|
1159
|
+
if (prop === "then" || prop === "catch")
|
|
1160
|
+
return;
|
|
1161
|
+
const newPath = [...path, prop];
|
|
1162
|
+
return makeStrictProxy(attrName, reason, newPath);
|
|
1163
|
+
},
|
|
1164
|
+
apply() {
|
|
1165
|
+
const sub = path.join(".");
|
|
1166
|
+
throw new GheimStrictError(strictMessage(attrName, sub || "*", reason));
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
var warnedAttrs = new Set;
|
|
1171
|
+
function makeWarningProxy(attrName, inner, reason) {
|
|
1172
|
+
return new Proxy(inner, {
|
|
1173
|
+
get(target, prop, receiver) {
|
|
1174
|
+
if (!warnedAttrs.has(attrName)) {
|
|
1175
|
+
warnedAttrs.add(attrName);
|
|
1176
|
+
console.warn(`gheim/openai: client.${attrName}.* sends data to OpenAI WITHOUT ` + `gheim PII redaction. ${reason}`);
|
|
1177
|
+
}
|
|
1178
|
+
return Reflect.get(target, prop, receiver);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/openai/_client.ts
|
|
1184
|
+
async function loadOpenAI(opts) {
|
|
1185
|
+
const mod = await import("openai");
|
|
1186
|
+
const Ctor = mod.default ?? mod.OpenAI;
|
|
1187
|
+
if (!Ctor)
|
|
1188
|
+
throw new Error("openai SDK has no OpenAI export");
|
|
1189
|
+
return new Ctor(opts);
|
|
1190
|
+
}
|
|
1191
|
+
function strictGuard(attr, inner, strict) {
|
|
1192
|
+
const reason = STRICT_REASONS[attr];
|
|
1193
|
+
if (!reason)
|
|
1194
|
+
return inner;
|
|
1195
|
+
if (strict)
|
|
1196
|
+
return makeStrictProxy(attr, reason);
|
|
1197
|
+
if (inner === undefined || inner === null)
|
|
1198
|
+
return inner;
|
|
1199
|
+
return makeWarningProxy(attr, inner, reason);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
class OpenAI {
|
|
1203
|
+
chat;
|
|
1204
|
+
completions;
|
|
1205
|
+
responses;
|
|
1206
|
+
embeddings;
|
|
1207
|
+
moderations;
|
|
1208
|
+
audio;
|
|
1209
|
+
images;
|
|
1210
|
+
raw;
|
|
1211
|
+
gheimStrict;
|
|
1212
|
+
_clientPromise;
|
|
1213
|
+
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);
|
|
1217
|
+
this._clientPromise.then((c) => {
|
|
1218
|
+
this.raw = c;
|
|
1219
|
+
});
|
|
1220
|
+
this.chat = new GheimChat(this._clientPromise, detector);
|
|
1221
|
+
this.completions = new GheimCompletions(this._clientPromise, detector);
|
|
1222
|
+
this.responses = new GheimResponses(this._clientPromise, detector);
|
|
1223
|
+
this.embeddings = new GheimEmbeddings(this._clientPromise, detector);
|
|
1224
|
+
this.moderations = new GheimModerations(this._clientPromise, detector);
|
|
1225
|
+
this.audio = new GheimAudio(this._clientPromise, detector);
|
|
1226
|
+
this.images = new GheimImages(this._clientPromise, detector);
|
|
1227
|
+
return new Proxy(this, {
|
|
1228
|
+
get: (target, prop, receiver) => {
|
|
1229
|
+
if (typeof prop === "string" && prop in STRICT_REASONS) {
|
|
1230
|
+
const innerAttr = target.raw && target.raw[prop];
|
|
1231
|
+
return strictGuard(prop, innerAttr, target.gheimStrict);
|
|
1232
|
+
}
|
|
1233
|
+
return Reflect.get(target, prop, receiver);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
async ready() {
|
|
1238
|
+
return this._clientPromise;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
var AsyncOpenAI = OpenAI;
|
|
1242
|
+
|
|
1243
|
+
// src/openai/index.ts
|
|
1244
|
+
function anonymizeOpenAIMessages(messages, session, opts) {
|
|
1245
|
+
return anonymizeMessages(messages, session, opts);
|
|
1246
|
+
}
|
|
1247
|
+
function deanonymizeOpenAIResponse(response, session) {
|
|
1248
|
+
for (const choice of response.choices ?? []) {
|
|
1249
|
+
const msg = choice.message;
|
|
1250
|
+
if (msg && typeof msg.content === "string") {
|
|
1251
|
+
msg.content = session.restore(msg.content);
|
|
1252
|
+
}
|
|
1253
|
+
for (const tc of msg?.tool_calls ?? []) {
|
|
1254
|
+
const fn = tc.function;
|
|
1255
|
+
if (fn && typeof fn.arguments === "string") {
|
|
1256
|
+
fn.arguments = session.restore(fn.arguments);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return response;
|
|
1261
|
+
}
|
|
1262
|
+
async function* deanonymizeOpenAIStream(stream, session) {
|
|
1263
|
+
const buf = new StreamDeanonymizer(session.mapping);
|
|
1264
|
+
let last;
|
|
1265
|
+
for await (const chunk of stream) {
|
|
1266
|
+
last = chunk;
|
|
1267
|
+
for (const choice of chunk.choices ?? []) {
|
|
1268
|
+
const delta = choice.delta;
|
|
1269
|
+
if (typeof delta.content === "string") {
|
|
1270
|
+
delta.content = buf.feed(delta.content);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
yield chunk;
|
|
1274
|
+
}
|
|
1275
|
+
const tail = buf.flush();
|
|
1276
|
+
if (tail && last) {
|
|
1277
|
+
yield {
|
|
1278
|
+
id: last.id,
|
|
1279
|
+
object: last.object,
|
|
1280
|
+
created: last.created,
|
|
1281
|
+
model: last.model,
|
|
1282
|
+
choices: [
|
|
1283
|
+
{
|
|
1284
|
+
index: 0,
|
|
1285
|
+
delta: { content: tail, role: undefined },
|
|
1286
|
+
finish_reason: null
|
|
1287
|
+
}
|
|
1288
|
+
]
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|