@skaile/transform-proxy 0.0.0-stage → 0.2.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/dist/cli.d.mts +17 -0
- package/dist/cli.mjs +116 -0
- package/dist/config-DQPsbYLi.mjs +1717 -0
- package/dist/index.d.mts +88 -0
- package/dist/index.mjs +1 -0
- package/dist/omp/index.d.mts +89 -0
- package/dist/omp/index.mjs +345 -0
- package/dist/server-D5nBnO7T.d.mts +96 -0
- package/package.json +68 -4
|
@@ -0,0 +1,1717 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
// src/log.ts
|
|
9
|
+
/** Default logger: drops everything. Hosts pass their own `Logger`. */
|
|
10
|
+
const silentLogger = {
|
|
11
|
+
debug: () => {},
|
|
12
|
+
info: () => {},
|
|
13
|
+
warn: () => {},
|
|
14
|
+
error: () => {}
|
|
15
|
+
};
|
|
16
|
+
function errorMessage(err) {
|
|
17
|
+
return err instanceof Error ? err.message : String(err);
|
|
18
|
+
}
|
|
19
|
+
// src/chain.ts
|
|
20
|
+
/**
|
|
21
|
+
* Composes the stages of one pipeline into a single transform.
|
|
22
|
+
* Outbound runs stage 1 to n, inbound and streamed inbound run stage n to 1.
|
|
23
|
+
* The empty list is the identity.
|
|
24
|
+
*/
|
|
25
|
+
function composeTransforms(stages, log = silentLogger) {
|
|
26
|
+
const instructions = stages.flatMap((stage) => stage.instruction === void 0 ? [] : [stage.instruction]);
|
|
27
|
+
const reversed = [...stages].reverse();
|
|
28
|
+
return {
|
|
29
|
+
...instructions.length > 0 ? { instruction: instructions.join("\n\n") } : {},
|
|
30
|
+
async outbound(text, leaf) {
|
|
31
|
+
let out = text;
|
|
32
|
+
for (const stage of stages) out = await stage.outbound(out, leaf);
|
|
33
|
+
return out;
|
|
34
|
+
},
|
|
35
|
+
inbound(text) {
|
|
36
|
+
let out = text;
|
|
37
|
+
for (const stage of reversed) out = stage.inbound(out);
|
|
38
|
+
return out;
|
|
39
|
+
},
|
|
40
|
+
inboundStream() {
|
|
41
|
+
return composeStreams(stages.map((stage) => stage.inboundStream()));
|
|
42
|
+
},
|
|
43
|
+
async dispose() {
|
|
44
|
+
for (const stage of stages) try {
|
|
45
|
+
await stage.dispose?.();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
log.error(`transform dispose failed: ${errorMessage(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Streams stage by stage. `streams` is in pipeline order (stage 1 first).
|
|
54
|
+
* push feeds stage n first and passes each result down to stage 1.
|
|
55
|
+
* flush goes n to 1: each stage's tail is pushed through the stages below it.
|
|
56
|
+
*/
|
|
57
|
+
function composeStreams(streams) {
|
|
58
|
+
const reversed = [...streams].reverse();
|
|
59
|
+
return {
|
|
60
|
+
push(chunk) {
|
|
61
|
+
let out = chunk;
|
|
62
|
+
for (const stream of reversed) out = stream.push(out);
|
|
63
|
+
return out;
|
|
64
|
+
},
|
|
65
|
+
flush() {
|
|
66
|
+
let carry = "";
|
|
67
|
+
for (const stream of reversed) carry = (carry === "" ? "" : stream.push(carry)) + stream.flush();
|
|
68
|
+
return carry;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// src/facts.ts
|
|
73
|
+
function createSessionFacts() {
|
|
74
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
75
|
+
let version = 0;
|
|
76
|
+
return {
|
|
77
|
+
facts: {
|
|
78
|
+
get version() {
|
|
79
|
+
return version;
|
|
80
|
+
},
|
|
81
|
+
list(kind) {
|
|
82
|
+
const all = [];
|
|
83
|
+
for (const facts of bySource.values()) for (const fact of facts) if (kind === void 0 || fact.kind === kind) all.push(fact);
|
|
84
|
+
return all;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
publisher(source) {
|
|
88
|
+
return {
|
|
89
|
+
publish(facts) {
|
|
90
|
+
bySource.set(source, facts.map((fact) => ({
|
|
91
|
+
kind: fact.kind,
|
|
92
|
+
value: fact.value,
|
|
93
|
+
source
|
|
94
|
+
})));
|
|
95
|
+
version++;
|
|
96
|
+
},
|
|
97
|
+
clear() {
|
|
98
|
+
bySource.delete(source);
|
|
99
|
+
version++;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// src/modules.ts
|
|
106
|
+
/** Loads and opens every configured transform once. Any failure rejects with the transform id. */
|
|
107
|
+
async function openTransforms(specs, baseDir, log) {
|
|
108
|
+
const factories = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const [id, spec] of Object.entries(specs)) {
|
|
110
|
+
const module = typeof spec.module === "string" ? await loadModule(id, spec.module, baseDir) : spec.module;
|
|
111
|
+
try {
|
|
112
|
+
factories.set(id, await module.open(spec.config, {
|
|
113
|
+
baseDir,
|
|
114
|
+
log
|
|
115
|
+
}));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new Error(`transform "${id}" failed to open: ${errorMessage(err)}`, { cause: err });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return factories;
|
|
121
|
+
}
|
|
122
|
+
async function loadModule(id, specifier, baseDir) {
|
|
123
|
+
let exported;
|
|
124
|
+
try {
|
|
125
|
+
exported = (await import(resolveSpecifier(specifier, baseDir))).default;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new Error(`transform "${id}": cannot load module ${specifier}: ${errorMessage(err)}`, { cause: err });
|
|
128
|
+
}
|
|
129
|
+
if (!isTransformModule(exported)) throw new Error(`transform "${id}": ${specifier} has no default TransformModule export`);
|
|
130
|
+
return exported;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolves a module specifier to a file URL. Relative and absolute paths resolve against `baseDir`.
|
|
134
|
+
* Package names resolve from `baseDir` by the `require` conditions first, then by the `import` condition.
|
|
135
|
+
*/
|
|
136
|
+
function resolveSpecifier(specifier, baseDir) {
|
|
137
|
+
const root = resolve(baseDir);
|
|
138
|
+
if (specifier.startsWith(".") || isAbsolute(specifier)) return pathToFileURL(resolve(root, specifier)).href;
|
|
139
|
+
try {
|
|
140
|
+
return pathToFileURL(createRequire(join(root, "noop.cjs")).resolve(specifier)).href;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
const fallback = resolveImportCondition(specifier, root);
|
|
143
|
+
if (fallback === void 0) throw err;
|
|
144
|
+
return pathToFileURL(fallback).href;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Walks up node_modules from `root` and resolves a package entry through its `import` condition. */
|
|
148
|
+
function resolveImportCondition(specifier, root) {
|
|
149
|
+
const parts = specifier.split("/");
|
|
150
|
+
const nameParts = specifier.startsWith("@") ? 2 : 1;
|
|
151
|
+
const name = parts.slice(0, nameParts).join("/");
|
|
152
|
+
const subpath = parts.length > nameParts ? `./${parts.slice(nameParts).join("/")}` : ".";
|
|
153
|
+
let dir = root;
|
|
154
|
+
for (;;) {
|
|
155
|
+
const pkgDir = join(dir, "node_modules", name);
|
|
156
|
+
const pkgJson = join(pkgDir, "package.json");
|
|
157
|
+
if (existsSync(pkgJson)) {
|
|
158
|
+
const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
|
|
159
|
+
const target = pkg.exports === void 0 ? subpath === "." ? pkg.main ?? "index.js" : void 0 : exportTarget(pkg.exports, subpath);
|
|
160
|
+
return target === void 0 ? void 0 : join(pkgDir, target);
|
|
161
|
+
}
|
|
162
|
+
const parent = dirname(dir);
|
|
163
|
+
if (parent === dir) return void 0;
|
|
164
|
+
dir = parent;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function exportTarget(exports, subpath) {
|
|
168
|
+
return conditionTarget((isRecord$1(exports) && Object.keys(exports).some((key) => key.startsWith(".")) ? exports : { ".": exports })[subpath]);
|
|
169
|
+
}
|
|
170
|
+
function conditionTarget(entry) {
|
|
171
|
+
if (typeof entry === "string") return entry;
|
|
172
|
+
if (!isRecord$1(entry)) return void 0;
|
|
173
|
+
return conditionTarget(entry.import) ?? conditionTarget(entry.default);
|
|
174
|
+
}
|
|
175
|
+
function isTransformModule(value) {
|
|
176
|
+
return isRecord$1(value) && typeof value.id === "string" && typeof value.open === "function";
|
|
177
|
+
}
|
|
178
|
+
function isRecord$1(value) {
|
|
179
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
180
|
+
}
|
|
181
|
+
// src/wire/json-args.ts
|
|
182
|
+
const SIMPLE_ESCAPES = {
|
|
183
|
+
"\"": "\"",
|
|
184
|
+
"\\": "\\",
|
|
185
|
+
"/": "/",
|
|
186
|
+
b: "\b",
|
|
187
|
+
f: "\f",
|
|
188
|
+
n: "\n",
|
|
189
|
+
r: "\r",
|
|
190
|
+
t: " "
|
|
191
|
+
};
|
|
192
|
+
/** The body of a JSON string for `text`, without the surrounding quotes. */
|
|
193
|
+
function escapeJsonBody(text) {
|
|
194
|
+
return JSON.stringify(text).slice(1, -1);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Streaming restorer over raw JSON text. `openString` yields a fresh restorer for each string
|
|
198
|
+
* value, so a placeholder never spans two values. Structural JSON outside strings passes through.
|
|
199
|
+
*/
|
|
200
|
+
function createJsonArgRestorer(openString) {
|
|
201
|
+
let current;
|
|
202
|
+
let escape = "none";
|
|
203
|
+
let hex = "";
|
|
204
|
+
return {
|
|
205
|
+
push(fragment) {
|
|
206
|
+
let out = "";
|
|
207
|
+
for (const ch of fragment) {
|
|
208
|
+
if (current === void 0) {
|
|
209
|
+
out += ch;
|
|
210
|
+
if (ch === "\"") current = openString();
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const restorer = current;
|
|
214
|
+
if (escape === "unicode") {
|
|
215
|
+
hex += ch;
|
|
216
|
+
if (hex.length === 4) {
|
|
217
|
+
out += escapeJsonBody(restorer.push(String.fromCharCode(Number.parseInt(hex, 16))));
|
|
218
|
+
hex = "";
|
|
219
|
+
escape = "none";
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (escape === "backslash") {
|
|
224
|
+
escape = "none";
|
|
225
|
+
if (ch === "u") {
|
|
226
|
+
escape = "unicode";
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
out += escapeJsonBody(restorer.push(SIMPLE_ESCAPES[ch] ?? ch));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (ch === "\\") {
|
|
233
|
+
escape = "backslash";
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (ch === "\"") {
|
|
237
|
+
out += escapeJsonBody(restorer.flush()) + "\"";
|
|
238
|
+
current = void 0;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
out += escapeJsonBody(restorer.push(ch));
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
},
|
|
245
|
+
flush() {
|
|
246
|
+
if (current === void 0) return "";
|
|
247
|
+
const tail = escapeJsonBody(current.flush());
|
|
248
|
+
current = void 0;
|
|
249
|
+
escape = "none";
|
|
250
|
+
hex = "";
|
|
251
|
+
return tail;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// src/wire/sse.ts
|
|
256
|
+
const DELIMITER = /\r?\n\r?\n/;
|
|
257
|
+
function parseSseBlock(raw) {
|
|
258
|
+
let event;
|
|
259
|
+
const data = [];
|
|
260
|
+
for (const line of raw.split(/\r?\n/)) if (line.startsWith("event:")) event = line.slice(6).replace(/^ /, "");
|
|
261
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
|
|
262
|
+
return {
|
|
263
|
+
event,
|
|
264
|
+
data: data.join("\n"),
|
|
265
|
+
raw
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** Serializes one rewritten event. `data` is JSON-encoded; the event name is kept when the source had one. */
|
|
269
|
+
function formatSseEvent(event, data) {
|
|
270
|
+
return `${event === void 0 ? "" : `event: ${event}\n`}data: ${JSON.stringify(data)}\n\n`;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Builds an SSE transformer. `handle` returns the output for one complete event (usually
|
|
274
|
+
* `block.raw` or `formatSseEvent(...)`); `finish` returns output for state still open at end of stream.
|
|
275
|
+
*/
|
|
276
|
+
function createSseFramer(handle, finish = () => "") {
|
|
277
|
+
let buffer = "";
|
|
278
|
+
return {
|
|
279
|
+
push(chunk) {
|
|
280
|
+
buffer += chunk;
|
|
281
|
+
let out = "";
|
|
282
|
+
for (let match = DELIMITER.exec(buffer); match !== null; match = DELIMITER.exec(buffer)) {
|
|
283
|
+
const end = match.index + match[0].length;
|
|
284
|
+
const raw = buffer.slice(0, end);
|
|
285
|
+
buffer = buffer.slice(end);
|
|
286
|
+
out += handle(parseSseBlock(raw));
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
},
|
|
290
|
+
flush() {
|
|
291
|
+
const rest = buffer;
|
|
292
|
+
buffer = "";
|
|
293
|
+
return (rest === "" ? "" : handle(parseSseBlock(rest + "\n\n"))) + finish();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// src/wire/types.ts
|
|
298
|
+
/** Thrown by `request` for attachments (image, document, file parts); the server answers 400 `unsupported_content`. */
|
|
299
|
+
var UnsupportedContentError = class extends Error {
|
|
300
|
+
code = "unsupported_content";
|
|
301
|
+
constructor(message) {
|
|
302
|
+
super(message);
|
|
303
|
+
this.name = "UnsupportedContentError";
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
// src/wire/anthropic.ts
|
|
307
|
+
const isObj$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
308
|
+
/** Block types that carry attachments the proxy cannot pseudonymize. */
|
|
309
|
+
const REFUSED_BLOCKS = {
|
|
310
|
+
image: true,
|
|
311
|
+
document: true,
|
|
312
|
+
search_result: true
|
|
313
|
+
};
|
|
314
|
+
const signatureKey$1 = (signature) => `anthropic:${signature}`;
|
|
315
|
+
function refuse$1(block) {
|
|
316
|
+
if (typeof block.type === "string" && REFUSED_BLOCKS[block.type] === true) throw new UnsupportedContentError(`Anthropic ${block.type} content cannot be transformed`);
|
|
317
|
+
}
|
|
318
|
+
async function outbound$2(transform, text, leaf) {
|
|
319
|
+
return text === "" ? text : transform.outbound(text, leaf);
|
|
320
|
+
}
|
|
321
|
+
/** A string, or an array of blocks whose `text` blocks are rewritten. */
|
|
322
|
+
async function rewriteText(content, leaf, transform) {
|
|
323
|
+
if (typeof content === "string") return outbound$2(transform, content, leaf);
|
|
324
|
+
if (!Array.isArray(content)) return content;
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const block of content) {
|
|
327
|
+
if (!isObj$2(block)) {
|
|
328
|
+
out.push(block);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
refuse$1(block);
|
|
332
|
+
out.push(block.type === "text" && typeof block.text === "string" ? {
|
|
333
|
+
...block,
|
|
334
|
+
text: await outbound$2(transform, block.text, leaf)
|
|
335
|
+
} : block);
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
/** Every string leaf of a JSON value (tool-call input); keys stay as they are. */
|
|
340
|
+
async function rewriteStrings$2(value, leaf, transform) {
|
|
341
|
+
if (typeof value === "string") return outbound$2(transform, value, leaf);
|
|
342
|
+
if (Array.isArray(value)) {
|
|
343
|
+
const out = [];
|
|
344
|
+
for (const item of value) out.push(await rewriteStrings$2(item, leaf, transform));
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
if (isObj$2(value)) {
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings$2(item, leaf, transform);
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
/** Every string leaf of a JSON value, restored inbound. */
|
|
355
|
+
function restoreStrings$1(value, transform) {
|
|
356
|
+
if (typeof value === "string") return transform.inbound(value);
|
|
357
|
+
if (Array.isArray(value)) return value.map((item) => restoreStrings$1(item, transform));
|
|
358
|
+
if (isObj$2(value)) {
|
|
359
|
+
const out = {};
|
|
360
|
+
for (const [key, item] of Object.entries(value)) out[key] = restoreStrings$1(item, transform);
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
/** A thinking block: swaps in the cached model text when its signature is known, else rewrites it. */
|
|
366
|
+
async function rewriteThinking(block, ctx) {
|
|
367
|
+
if (typeof block.signature === "string") {
|
|
368
|
+
const cached = ctx.signatures.get(signatureKey$1(block.signature));
|
|
369
|
+
if (cached !== void 0) return {
|
|
370
|
+
...block,
|
|
371
|
+
thinking: cached
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (typeof block.thinking !== "string") return block;
|
|
375
|
+
return {
|
|
376
|
+
...block,
|
|
377
|
+
thinking: await outbound$2(ctx.transform, block.thinking, { kind: "thinking" })
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
async function rewriteContent$2(content, role, toolNames, ctx) {
|
|
381
|
+
const textKind = role === "assistant" ? "assistant" : "user";
|
|
382
|
+
if (typeof content === "string") return outbound$2(ctx.transform, content, { kind: textKind });
|
|
383
|
+
if (!Array.isArray(content)) return content;
|
|
384
|
+
const out = [];
|
|
385
|
+
for (const block of content) {
|
|
386
|
+
if (!isObj$2(block)) {
|
|
387
|
+
out.push(block);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
refuse$1(block);
|
|
391
|
+
switch (block.type) {
|
|
392
|
+
case "text":
|
|
393
|
+
out.push(typeof block.text === "string" ? {
|
|
394
|
+
...block,
|
|
395
|
+
text: await outbound$2(ctx.transform, block.text, { kind: textKind })
|
|
396
|
+
} : block);
|
|
397
|
+
break;
|
|
398
|
+
case "thinking":
|
|
399
|
+
out.push(await rewriteThinking(block, ctx));
|
|
400
|
+
break;
|
|
401
|
+
case "tool_use": {
|
|
402
|
+
const toolName = typeof block.name === "string" ? block.name : void 0;
|
|
403
|
+
out.push({
|
|
404
|
+
...block,
|
|
405
|
+
input: await rewriteStrings$2(block.input, {
|
|
406
|
+
kind: "toolCall",
|
|
407
|
+
toolName
|
|
408
|
+
}, ctx.transform)
|
|
409
|
+
});
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "tool_result": {
|
|
413
|
+
const toolName = typeof block.tool_use_id === "string" ? toolNames.get(block.tool_use_id) : void 0;
|
|
414
|
+
out.push({
|
|
415
|
+
...block,
|
|
416
|
+
content: await rewriteText(block.content, {
|
|
417
|
+
kind: "toolResult",
|
|
418
|
+
toolName
|
|
419
|
+
}, ctx.transform)
|
|
420
|
+
});
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
default: out.push(block);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
/** Maps each `tool_use` id to its tool name, so tool results can name their tool. */
|
|
429
|
+
function toolNamesById(messages) {
|
|
430
|
+
const names = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const message of messages) {
|
|
432
|
+
if (!isObj$2(message) || !Array.isArray(message.content)) continue;
|
|
433
|
+
for (const block of message.content) if (isObj$2(block) && block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") names.set(block.id, block.name);
|
|
434
|
+
}
|
|
435
|
+
return names;
|
|
436
|
+
}
|
|
437
|
+
function withInstruction(system, instruction) {
|
|
438
|
+
if (system === void 0) return instruction;
|
|
439
|
+
if (typeof system === "string") return `${instruction}\n\n${system}`;
|
|
440
|
+
if (Array.isArray(system)) return [{
|
|
441
|
+
type: "text",
|
|
442
|
+
text: instruction
|
|
443
|
+
}, ...system];
|
|
444
|
+
return system;
|
|
445
|
+
}
|
|
446
|
+
function newBlockState(kind, transform) {
|
|
447
|
+
return {
|
|
448
|
+
kind,
|
|
449
|
+
restore: kind === "tool" ? createJsonArgRestorer(() => transform.inboundStream()) : transform.inboundStream(),
|
|
450
|
+
raw: ""
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/** The delta that carries a block's held-back tail, shaped for the block's kind. */
|
|
454
|
+
function tailDelta(kind, tail) {
|
|
455
|
+
if (kind === "text") return {
|
|
456
|
+
type: "text_delta",
|
|
457
|
+
text: tail
|
|
458
|
+
};
|
|
459
|
+
if (kind === "thinking") return {
|
|
460
|
+
type: "thinking_delta",
|
|
461
|
+
thinking: tail
|
|
462
|
+
};
|
|
463
|
+
return {
|
|
464
|
+
type: "input_json_delta",
|
|
465
|
+
partial_json: tail
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function createStreamTransformer$2(ctx) {
|
|
469
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
470
|
+
const handle = (block) => {
|
|
471
|
+
let payload;
|
|
472
|
+
try {
|
|
473
|
+
payload = JSON.parse(block.data);
|
|
474
|
+
} catch {
|
|
475
|
+
return block.raw;
|
|
476
|
+
}
|
|
477
|
+
if (!isObj$2(payload)) return block.raw;
|
|
478
|
+
const index = typeof payload.index === "number" ? payload.index : void 0;
|
|
479
|
+
switch (payload.type) {
|
|
480
|
+
case "content_block_start": {
|
|
481
|
+
const content = payload.content_block;
|
|
482
|
+
if (index === void 0 || !isObj$2(content)) return block.raw;
|
|
483
|
+
if (content.type === "text" || content.type === "thinking" || content.type === "tool_use") {
|
|
484
|
+
const kind = content.type === "tool_use" ? "tool" : content.type;
|
|
485
|
+
blocks.set(index, newBlockState(kind, ctx.transform));
|
|
486
|
+
}
|
|
487
|
+
return block.raw;
|
|
488
|
+
}
|
|
489
|
+
case "content_block_delta": {
|
|
490
|
+
const state = index === void 0 ? void 0 : blocks.get(index);
|
|
491
|
+
const delta = payload.delta;
|
|
492
|
+
if (state === void 0 || !isObj$2(delta)) return block.raw;
|
|
493
|
+
if (state.kind === "text" && delta.type === "text_delta" && typeof delta.text === "string") return formatSseEvent(block.event, {
|
|
494
|
+
...payload,
|
|
495
|
+
delta: {
|
|
496
|
+
...delta,
|
|
497
|
+
text: state.restore.push(delta.text)
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
if (state.kind === "thinking" && delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
501
|
+
state.raw += delta.thinking;
|
|
502
|
+
return formatSseEvent(block.event, {
|
|
503
|
+
...payload,
|
|
504
|
+
delta: {
|
|
505
|
+
...delta,
|
|
506
|
+
thinking: state.restore.push(delta.thinking)
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (state.kind === "thinking" && delta.type === "signature_delta" && typeof delta.signature === "string") {
|
|
511
|
+
ctx.signatures.set(signatureKey$1(delta.signature), state.raw);
|
|
512
|
+
return block.raw;
|
|
513
|
+
}
|
|
514
|
+
if (state.kind === "tool" && delta.type === "input_json_delta" && typeof delta.partial_json === "string") return formatSseEvent(block.event, {
|
|
515
|
+
...payload,
|
|
516
|
+
delta: {
|
|
517
|
+
...delta,
|
|
518
|
+
partial_json: state.restore.push(delta.partial_json)
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
return block.raw;
|
|
522
|
+
}
|
|
523
|
+
case "content_block_stop": {
|
|
524
|
+
const state = index === void 0 ? void 0 : blocks.get(index);
|
|
525
|
+
if (index === void 0 || state === void 0) return block.raw;
|
|
526
|
+
blocks.delete(index);
|
|
527
|
+
const tail = state.restore.flush();
|
|
528
|
+
if (tail === "") return block.raw;
|
|
529
|
+
return formatSseEvent("content_block_delta", {
|
|
530
|
+
type: "content_block_delta",
|
|
531
|
+
index,
|
|
532
|
+
delta: tailDelta(state.kind, tail)
|
|
533
|
+
}) + block.raw;
|
|
534
|
+
}
|
|
535
|
+
default: return block.raw;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const finish = () => {
|
|
539
|
+
let out = "";
|
|
540
|
+
for (const [index, state] of blocks) {
|
|
541
|
+
const tail = state.restore.flush();
|
|
542
|
+
if (tail !== "") out += formatSseEvent("content_block_delta", {
|
|
543
|
+
type: "content_block_delta",
|
|
544
|
+
index,
|
|
545
|
+
delta: tailDelta(state.kind, tail)
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
blocks.clear();
|
|
549
|
+
return out;
|
|
550
|
+
};
|
|
551
|
+
return createSseFramer(handle, finish);
|
|
552
|
+
}
|
|
553
|
+
const anthropicWalker = {
|
|
554
|
+
async request(body, ctx) {
|
|
555
|
+
if (!isObj$2(body)) return body;
|
|
556
|
+
const out = { ...body };
|
|
557
|
+
if (body.system !== void 0) out.system = await rewriteText(body.system, { kind: "system" }, ctx.transform);
|
|
558
|
+
if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
|
|
559
|
+
if (Array.isArray(body.messages)) {
|
|
560
|
+
const toolNames = toolNamesById(body.messages);
|
|
561
|
+
const messages = [];
|
|
562
|
+
for (const message of body.messages) messages.push(isObj$2(message) ? {
|
|
563
|
+
...message,
|
|
564
|
+
content: await rewriteContent$2(message.content, message.role, toolNames, ctx)
|
|
565
|
+
} : message);
|
|
566
|
+
out.messages = messages;
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
},
|
|
570
|
+
response(body, ctx) {
|
|
571
|
+
if (!isObj$2(body) || !Array.isArray(body.content)) return body;
|
|
572
|
+
const content = body.content.map((block) => {
|
|
573
|
+
if (!isObj$2(block)) return block;
|
|
574
|
+
if (block.type === "text" && typeof block.text === "string") return {
|
|
575
|
+
...block,
|
|
576
|
+
text: ctx.transform.inbound(block.text)
|
|
577
|
+
};
|
|
578
|
+
if (block.type === "thinking" && typeof block.thinking === "string") {
|
|
579
|
+
if (typeof block.signature === "string") ctx.signatures.set(signatureKey$1(block.signature), block.thinking);
|
|
580
|
+
return {
|
|
581
|
+
...block,
|
|
582
|
+
thinking: ctx.transform.inbound(block.thinking)
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (block.type === "tool_use") return {
|
|
586
|
+
...block,
|
|
587
|
+
input: restoreStrings$1(block.input, ctx.transform)
|
|
588
|
+
};
|
|
589
|
+
return block;
|
|
590
|
+
});
|
|
591
|
+
return {
|
|
592
|
+
...body,
|
|
593
|
+
content
|
|
594
|
+
};
|
|
595
|
+
},
|
|
596
|
+
stream(ctx) {
|
|
597
|
+
return createStreamTransformer$2(ctx);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
// src/wire/openai-chat.ts
|
|
601
|
+
const isObj$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
602
|
+
/** Content part types that carry attachments the proxy cannot pseudonymize. */
|
|
603
|
+
const REFUSED_PARTS$1 = {
|
|
604
|
+
image_url: true,
|
|
605
|
+
input_audio: true,
|
|
606
|
+
file: true
|
|
607
|
+
};
|
|
608
|
+
/** Reasoning fields used by OpenAI-compatible providers. */
|
|
609
|
+
const REASONING_FIELDS = ["reasoning_content", "reasoning"];
|
|
610
|
+
async function outbound$1(transform, text, leaf) {
|
|
611
|
+
return text === "" ? text : transform.outbound(text, leaf);
|
|
612
|
+
}
|
|
613
|
+
/** A string, or an array of parts whose `text` parts are rewritten. */
|
|
614
|
+
async function rewriteContent$1(content, leaf, transform) {
|
|
615
|
+
if (typeof content === "string") return outbound$1(transform, content, leaf);
|
|
616
|
+
if (!Array.isArray(content)) return content;
|
|
617
|
+
const out = [];
|
|
618
|
+
for (const part of content) {
|
|
619
|
+
if (!isObj$1(part)) {
|
|
620
|
+
out.push(part);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (typeof part.type === "string" && REFUSED_PARTS$1[part.type] === true) throw new UnsupportedContentError(`OpenAI chat ${part.type} content cannot be transformed`);
|
|
624
|
+
out.push(part.type === "text" && typeof part.text === "string" ? {
|
|
625
|
+
...part,
|
|
626
|
+
text: await outbound$1(transform, part.text, leaf)
|
|
627
|
+
} : part);
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
async function rewriteStrings$1(value, leaf, transform) {
|
|
632
|
+
if (typeof value === "string") return outbound$1(transform, value, leaf);
|
|
633
|
+
if (Array.isArray(value)) {
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const item of value) out.push(await rewriteStrings$1(item, leaf, transform));
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
if (isObj$1(value)) {
|
|
639
|
+
const out = {};
|
|
640
|
+
for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings$1(item, leaf, transform);
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
function restoreStrings(value, transform) {
|
|
646
|
+
if (typeof value === "string") return transform.inbound(value);
|
|
647
|
+
if (Array.isArray(value)) return value.map((item) => restoreStrings(item, transform));
|
|
648
|
+
if (isObj$1(value)) {
|
|
649
|
+
const out = {};
|
|
650
|
+
for (const [key, item] of Object.entries(value)) out[key] = restoreStrings(item, transform);
|
|
651
|
+
return out;
|
|
652
|
+
}
|
|
653
|
+
return value;
|
|
654
|
+
}
|
|
655
|
+
/** Tool-call arguments are a JSON string: rewrite its string leaves, or the raw text when it is not JSON. */
|
|
656
|
+
async function rewriteArguments$1(args, leaf, transform) {
|
|
657
|
+
let parsed;
|
|
658
|
+
try {
|
|
659
|
+
parsed = JSON.parse(args);
|
|
660
|
+
} catch {
|
|
661
|
+
return outbound$1(transform, args, leaf);
|
|
662
|
+
}
|
|
663
|
+
return JSON.stringify(await rewriteStrings$1(parsed, leaf, transform));
|
|
664
|
+
}
|
|
665
|
+
function restoreArguments$1(args, transform) {
|
|
666
|
+
let parsed;
|
|
667
|
+
try {
|
|
668
|
+
parsed = JSON.parse(args);
|
|
669
|
+
} catch {
|
|
670
|
+
return transform.inbound(args);
|
|
671
|
+
}
|
|
672
|
+
return JSON.stringify(restoreStrings(parsed, transform));
|
|
673
|
+
}
|
|
674
|
+
async function rewriteToolCalls(calls, transform, names) {
|
|
675
|
+
if (!Array.isArray(calls)) return calls;
|
|
676
|
+
const out = [];
|
|
677
|
+
for (const call of calls) {
|
|
678
|
+
if (!isObj$1(call) || !isObj$1(call.function)) {
|
|
679
|
+
out.push(call);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const fn = call.function;
|
|
683
|
+
const toolName = typeof fn.name === "string" ? fn.name : void 0;
|
|
684
|
+
if (typeof call.id === "string" && toolName !== void 0) names.set(call.id, toolName);
|
|
685
|
+
out.push(typeof fn.arguments === "string" ? {
|
|
686
|
+
...call,
|
|
687
|
+
function: {
|
|
688
|
+
...fn,
|
|
689
|
+
arguments: await rewriteArguments$1(fn.arguments, {
|
|
690
|
+
kind: "toolCall",
|
|
691
|
+
toolName
|
|
692
|
+
}, transform)
|
|
693
|
+
}
|
|
694
|
+
} : call);
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
async function rewriteMessage(message, transform, names) {
|
|
699
|
+
const out = { ...message };
|
|
700
|
+
switch (message.role) {
|
|
701
|
+
case "system":
|
|
702
|
+
case "developer":
|
|
703
|
+
out.content = await rewriteContent$1(message.content, { kind: "system" }, transform);
|
|
704
|
+
break;
|
|
705
|
+
case "user":
|
|
706
|
+
out.content = await rewriteContent$1(message.content, { kind: "user" }, transform);
|
|
707
|
+
break;
|
|
708
|
+
case "assistant":
|
|
709
|
+
for (const field of REASONING_FIELDS) {
|
|
710
|
+
const value = message[field];
|
|
711
|
+
if (typeof value === "string") out[field] = await outbound$1(transform, value, { kind: "thinking" });
|
|
712
|
+
}
|
|
713
|
+
out.content = await rewriteContent$1(message.content, { kind: "assistant" }, transform);
|
|
714
|
+
out.tool_calls = await rewriteToolCalls(message.tool_calls, transform, names);
|
|
715
|
+
if (message.tool_calls === void 0) delete out.tool_calls;
|
|
716
|
+
break;
|
|
717
|
+
case "tool": {
|
|
718
|
+
const toolName = typeof message.tool_call_id === "string" ? names.get(message.tool_call_id) : void 0;
|
|
719
|
+
out.content = await rewriteContent$1(message.content, {
|
|
720
|
+
kind: "toolResult",
|
|
721
|
+
toolName
|
|
722
|
+
}, transform);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
default: return message;
|
|
726
|
+
}
|
|
727
|
+
if (message.content === void 0) delete out.content;
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
/** The delta carrying every held tail of one choice, or undefined when nothing is held. */
|
|
731
|
+
function flushChoice(state) {
|
|
732
|
+
const delta = {};
|
|
733
|
+
if (state.content !== void 0) {
|
|
734
|
+
const tail = state.content.flush();
|
|
735
|
+
if (tail !== "") delta.content = tail;
|
|
736
|
+
}
|
|
737
|
+
for (const [field, restore] of state.reasoning) {
|
|
738
|
+
const tail = restore.flush();
|
|
739
|
+
if (tail !== "") delta[field] = tail;
|
|
740
|
+
}
|
|
741
|
+
const tools = [];
|
|
742
|
+
for (const [index, restore] of state.tools) {
|
|
743
|
+
const tail = restore.flush();
|
|
744
|
+
if (tail !== "") tools.push({
|
|
745
|
+
index,
|
|
746
|
+
function: { arguments: tail }
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if (tools.length > 0) delta.tool_calls = tools;
|
|
750
|
+
state.content = void 0;
|
|
751
|
+
state.reasoning.clear();
|
|
752
|
+
state.tools.clear();
|
|
753
|
+
return Object.keys(delta).length === 0 ? void 0 : delta;
|
|
754
|
+
}
|
|
755
|
+
function createStreamTransformer$1(ctx) {
|
|
756
|
+
const choices = /* @__PURE__ */ new Map();
|
|
757
|
+
/** The latest chunk's envelope (id, model, ...), reused for tail chunks. */
|
|
758
|
+
let envelope = { object: "chat.completion.chunk" };
|
|
759
|
+
const choiceState = (index) => {
|
|
760
|
+
let state = choices.get(index);
|
|
761
|
+
if (state === void 0) {
|
|
762
|
+
state = {
|
|
763
|
+
reasoning: /* @__PURE__ */ new Map(),
|
|
764
|
+
tools: /* @__PURE__ */ new Map()
|
|
765
|
+
};
|
|
766
|
+
choices.set(index, state);
|
|
767
|
+
}
|
|
768
|
+
return state;
|
|
769
|
+
};
|
|
770
|
+
const tailEvent = (event, index, delta) => formatSseEvent(event, {
|
|
771
|
+
...envelope,
|
|
772
|
+
choices: [{
|
|
773
|
+
index,
|
|
774
|
+
delta,
|
|
775
|
+
finish_reason: null
|
|
776
|
+
}]
|
|
777
|
+
});
|
|
778
|
+
const flushAll = (event) => {
|
|
779
|
+
let out = "";
|
|
780
|
+
for (const [index, state] of choices) {
|
|
781
|
+
const delta = flushChoice(state);
|
|
782
|
+
if (delta !== void 0) out += tailEvent(event, index, delta);
|
|
783
|
+
}
|
|
784
|
+
choices.clear();
|
|
785
|
+
return out;
|
|
786
|
+
};
|
|
787
|
+
const rewriteDelta = (delta, state) => {
|
|
788
|
+
const out = { ...delta };
|
|
789
|
+
let changed = false;
|
|
790
|
+
if (typeof delta.content === "string") {
|
|
791
|
+
state.content ??= ctx.transform.inboundStream();
|
|
792
|
+
out.content = state.content.push(delta.content);
|
|
793
|
+
changed = true;
|
|
794
|
+
}
|
|
795
|
+
for (const field of REASONING_FIELDS) {
|
|
796
|
+
const value = delta[field];
|
|
797
|
+
if (typeof value !== "string") continue;
|
|
798
|
+
let restore = state.reasoning.get(field);
|
|
799
|
+
if (restore === void 0) {
|
|
800
|
+
restore = ctx.transform.inboundStream();
|
|
801
|
+
state.reasoning.set(field, restore);
|
|
802
|
+
}
|
|
803
|
+
out[field] = restore.push(value);
|
|
804
|
+
changed = true;
|
|
805
|
+
}
|
|
806
|
+
if (Array.isArray(delta.tool_calls)) out.tool_calls = delta.tool_calls.map((call) => {
|
|
807
|
+
if (!isObj$1(call) || !isObj$1(call.function) || typeof call.function.arguments !== "string") return call;
|
|
808
|
+
const index = typeof call.index === "number" ? call.index : 0;
|
|
809
|
+
let restore = state.tools.get(index);
|
|
810
|
+
if (restore === void 0) {
|
|
811
|
+
restore = createJsonArgRestorer(() => ctx.transform.inboundStream());
|
|
812
|
+
state.tools.set(index, restore);
|
|
813
|
+
}
|
|
814
|
+
changed = true;
|
|
815
|
+
return {
|
|
816
|
+
...call,
|
|
817
|
+
function: {
|
|
818
|
+
...call.function,
|
|
819
|
+
arguments: restore.push(call.function.arguments)
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
});
|
|
823
|
+
return {
|
|
824
|
+
delta: out,
|
|
825
|
+
changed
|
|
826
|
+
};
|
|
827
|
+
};
|
|
828
|
+
const handle = (block) => {
|
|
829
|
+
if (block.data.trim() === "[DONE]") return flushAll(block.event) + block.raw;
|
|
830
|
+
let payload;
|
|
831
|
+
try {
|
|
832
|
+
payload = JSON.parse(block.data);
|
|
833
|
+
} catch {
|
|
834
|
+
return block.raw;
|
|
835
|
+
}
|
|
836
|
+
if (!isObj$1(payload) || !Array.isArray(payload.choices)) return block.raw;
|
|
837
|
+
const { choices: _choices, ...rest } = payload;
|
|
838
|
+
envelope = rest;
|
|
839
|
+
let changed = false;
|
|
840
|
+
let before = "";
|
|
841
|
+
const rewritten = payload.choices.map((choice) => {
|
|
842
|
+
if (!isObj$1(choice)) return choice;
|
|
843
|
+
const index = typeof choice.index === "number" ? choice.index : 0;
|
|
844
|
+
const state = choiceState(index);
|
|
845
|
+
let next = choice;
|
|
846
|
+
if (isObj$1(choice.delta)) {
|
|
847
|
+
const result = rewriteDelta(choice.delta, state);
|
|
848
|
+
if (result.changed) {
|
|
849
|
+
next = {
|
|
850
|
+
...choice,
|
|
851
|
+
delta: result.delta
|
|
852
|
+
};
|
|
853
|
+
changed = true;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (choice.finish_reason !== null && choice.finish_reason !== void 0) {
|
|
857
|
+
const tail = flushChoice(state);
|
|
858
|
+
choices.delete(index);
|
|
859
|
+
if (tail !== void 0) before += tailEvent(block.event, index, tail);
|
|
860
|
+
}
|
|
861
|
+
return next;
|
|
862
|
+
});
|
|
863
|
+
return before + (changed ? formatSseEvent(block.event, {
|
|
864
|
+
...payload,
|
|
865
|
+
choices: rewritten
|
|
866
|
+
}) : block.raw);
|
|
867
|
+
};
|
|
868
|
+
return createSseFramer(handle, () => flushAll(void 0));
|
|
869
|
+
}
|
|
870
|
+
const openaiChatWalker = {
|
|
871
|
+
async request(body, ctx) {
|
|
872
|
+
if (!isObj$1(body)) return body;
|
|
873
|
+
const out = { ...body };
|
|
874
|
+
if (Array.isArray(body.messages)) {
|
|
875
|
+
const names = /* @__PURE__ */ new Map();
|
|
876
|
+
const messages = [];
|
|
877
|
+
for (const message of body.messages) messages.push(isObj$1(message) ? await rewriteMessage(message, ctx.transform, names) : message);
|
|
878
|
+
if (ctx.transform.instruction !== void 0) messages.unshift({
|
|
879
|
+
role: "system",
|
|
880
|
+
content: ctx.transform.instruction
|
|
881
|
+
});
|
|
882
|
+
out.messages = messages;
|
|
883
|
+
}
|
|
884
|
+
return out;
|
|
885
|
+
},
|
|
886
|
+
response(body, ctx) {
|
|
887
|
+
if (!isObj$1(body) || !Array.isArray(body.choices)) return body;
|
|
888
|
+
const choices = body.choices.map((choice) => {
|
|
889
|
+
if (!isObj$1(choice) || !isObj$1(choice.message)) return choice;
|
|
890
|
+
const message = { ...choice.message };
|
|
891
|
+
if (typeof message.content === "string") message.content = ctx.transform.inbound(message.content);
|
|
892
|
+
for (const field of REASONING_FIELDS) {
|
|
893
|
+
const value = message[field];
|
|
894
|
+
if (typeof value === "string") message[field] = ctx.transform.inbound(value);
|
|
895
|
+
}
|
|
896
|
+
if (Array.isArray(message.tool_calls)) message.tool_calls = message.tool_calls.map((call) => {
|
|
897
|
+
if (!isObj$1(call) || !isObj$1(call.function) || typeof call.function.arguments !== "string") return call;
|
|
898
|
+
return {
|
|
899
|
+
...call,
|
|
900
|
+
function: {
|
|
901
|
+
...call.function,
|
|
902
|
+
arguments: restoreArguments$1(call.function.arguments, ctx.transform)
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
});
|
|
906
|
+
return {
|
|
907
|
+
...choice,
|
|
908
|
+
message
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
return {
|
|
912
|
+
...body,
|
|
913
|
+
choices
|
|
914
|
+
};
|
|
915
|
+
},
|
|
916
|
+
stream(ctx) {
|
|
917
|
+
return createStreamTransformer$1(ctx);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
// src/wire/openai-responses.ts
|
|
921
|
+
const isObj = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
922
|
+
/** Part types that carry attachments the proxy cannot pseudonymize. */
|
|
923
|
+
const REFUSED_PARTS = {
|
|
924
|
+
input_image: true,
|
|
925
|
+
input_file: true,
|
|
926
|
+
input_audio: true
|
|
927
|
+
};
|
|
928
|
+
const signatureKey = (itemId) => `responses:${itemId}`;
|
|
929
|
+
function refuse(part) {
|
|
930
|
+
if (typeof part.type === "string" && REFUSED_PARTS[part.type] === true) throw new UnsupportedContentError(`OpenAI responses ${part.type} content cannot be transformed`);
|
|
931
|
+
}
|
|
932
|
+
async function outbound(transform, text, leaf) {
|
|
933
|
+
return text === "" ? text : transform.outbound(text, leaf);
|
|
934
|
+
}
|
|
935
|
+
/** A string, or an array of parts whose `input_text`/`output_text` parts are rewritten. */
|
|
936
|
+
async function rewriteContent(content, leaf, transform) {
|
|
937
|
+
if (typeof content === "string") return outbound(transform, content, leaf);
|
|
938
|
+
if (!Array.isArray(content)) return content;
|
|
939
|
+
const out = [];
|
|
940
|
+
for (const part of content) {
|
|
941
|
+
if (!isObj(part)) {
|
|
942
|
+
out.push(part);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
refuse(part);
|
|
946
|
+
out.push((part.type === "input_text" || part.type === "output_text") && typeof part.text === "string" ? {
|
|
947
|
+
...part,
|
|
948
|
+
text: await outbound(transform, part.text, leaf)
|
|
949
|
+
} : part);
|
|
950
|
+
}
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
async function rewriteStrings(value, leaf, transform) {
|
|
954
|
+
if (typeof value === "string") return outbound(transform, value, leaf);
|
|
955
|
+
if (Array.isArray(value)) {
|
|
956
|
+
const out = [];
|
|
957
|
+
for (const item of value) out.push(await rewriteStrings(item, leaf, transform));
|
|
958
|
+
return out;
|
|
959
|
+
}
|
|
960
|
+
if (isObj(value)) {
|
|
961
|
+
const out = {};
|
|
962
|
+
for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings(item, leaf, transform);
|
|
963
|
+
return out;
|
|
964
|
+
}
|
|
965
|
+
return value;
|
|
966
|
+
}
|
|
967
|
+
/** Tool-call arguments: a JSON string whose string leaves are rewritten; raw text when it is not JSON. */
|
|
968
|
+
async function rewriteArguments(args, leaf, transform) {
|
|
969
|
+
let parsed;
|
|
970
|
+
try {
|
|
971
|
+
parsed = JSON.parse(args);
|
|
972
|
+
} catch {
|
|
973
|
+
return outbound(transform, args, leaf);
|
|
974
|
+
}
|
|
975
|
+
return JSON.stringify(await rewriteStrings(parsed, leaf, transform));
|
|
976
|
+
}
|
|
977
|
+
/** Restores the string values of a JSON text, keeping its formatting; restored originals are JSON-escaped. */
|
|
978
|
+
function restoreArguments(args, transform) {
|
|
979
|
+
const restorer = createJsonArgRestorer(() => transform.inboundStream());
|
|
980
|
+
return restorer.push(args) + restorer.flush();
|
|
981
|
+
}
|
|
982
|
+
function summaryTexts(item) {
|
|
983
|
+
if (!Array.isArray(item.summary)) return void 0;
|
|
984
|
+
const texts = [];
|
|
985
|
+
for (const part of item.summary) {
|
|
986
|
+
if (!isObj(part) || typeof part.text !== "string") return void 0;
|
|
987
|
+
texts.push(part.text);
|
|
988
|
+
}
|
|
989
|
+
return texts;
|
|
990
|
+
}
|
|
991
|
+
function cachedSummary(item, ctx) {
|
|
992
|
+
if (typeof item.id !== "string") return void 0;
|
|
993
|
+
const cached = ctx.signatures.get(signatureKey(item.id));
|
|
994
|
+
if (cached === void 0) return void 0;
|
|
995
|
+
const texts = JSON.parse(cached);
|
|
996
|
+
if (!Array.isArray(texts) || !Array.isArray(item.summary) || texts.length !== item.summary.length) return void 0;
|
|
997
|
+
return texts;
|
|
998
|
+
}
|
|
999
|
+
async function rewriteReasoning(item, ctx) {
|
|
1000
|
+
if (!Array.isArray(item.summary)) return item;
|
|
1001
|
+
const cached = cachedSummary(item, ctx);
|
|
1002
|
+
const summary = [];
|
|
1003
|
+
for (const [i, part] of item.summary.entries()) if (!isObj(part) || typeof part.text !== "string") summary.push(part);
|
|
1004
|
+
else if (cached !== void 0) summary.push({
|
|
1005
|
+
...part,
|
|
1006
|
+
text: cached[i]
|
|
1007
|
+
});
|
|
1008
|
+
else summary.push({
|
|
1009
|
+
...part,
|
|
1010
|
+
text: await outbound(ctx.transform, part.text, { kind: "thinking" })
|
|
1011
|
+
});
|
|
1012
|
+
return {
|
|
1013
|
+
...item,
|
|
1014
|
+
summary
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function roleKind(role) {
|
|
1018
|
+
if (role === "developer" || role === "system") return "system";
|
|
1019
|
+
if (role === "user") return "user";
|
|
1020
|
+
if (role === "assistant") return "assistant";
|
|
1021
|
+
}
|
|
1022
|
+
function toolNamesByCallId(items) {
|
|
1023
|
+
const names = /* @__PURE__ */ new Map();
|
|
1024
|
+
for (const item of items) if (isObj(item) && item.type === "function_call" && typeof item.call_id === "string" && typeof item.name === "string") names.set(item.call_id, item.name);
|
|
1025
|
+
return names;
|
|
1026
|
+
}
|
|
1027
|
+
async function rewriteItem(item, toolNames, ctx) {
|
|
1028
|
+
if (item.type === "message" || item.type === void 0 && item.role !== void 0) {
|
|
1029
|
+
const kind = roleKind(item.role);
|
|
1030
|
+
if (kind === void 0) return item;
|
|
1031
|
+
return {
|
|
1032
|
+
...item,
|
|
1033
|
+
content: await rewriteContent(item.content, { kind }, ctx.transform)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
switch (item.type) {
|
|
1037
|
+
case "function_call": {
|
|
1038
|
+
if (typeof item.arguments !== "string") return item;
|
|
1039
|
+
const toolName = typeof item.name === "string" ? item.name : void 0;
|
|
1040
|
+
return {
|
|
1041
|
+
...item,
|
|
1042
|
+
arguments: await rewriteArguments(item.arguments, {
|
|
1043
|
+
kind: "toolCall",
|
|
1044
|
+
toolName
|
|
1045
|
+
}, ctx.transform)
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
case "function_call_output": {
|
|
1049
|
+
const toolName = typeof item.call_id === "string" ? toolNames.get(item.call_id) : void 0;
|
|
1050
|
+
return {
|
|
1051
|
+
...item,
|
|
1052
|
+
output: await rewriteContent(item.output, {
|
|
1053
|
+
kind: "toolResult",
|
|
1054
|
+
toolName
|
|
1055
|
+
}, ctx.transform)
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
case "reasoning": return rewriteReasoning(item, ctx);
|
|
1059
|
+
default: return item;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/** Restores one output item; reasoning items store their raw summary in the signature cache first. */
|
|
1063
|
+
function restoreItem(item, ctx) {
|
|
1064
|
+
if (!isObj(item)) return item;
|
|
1065
|
+
const transform = ctx.transform;
|
|
1066
|
+
switch (item.type) {
|
|
1067
|
+
case "message":
|
|
1068
|
+
if (!Array.isArray(item.content)) return item;
|
|
1069
|
+
return {
|
|
1070
|
+
...item,
|
|
1071
|
+
content: item.content.map((part) => restorePart(part, transform))
|
|
1072
|
+
};
|
|
1073
|
+
case "reasoning": {
|
|
1074
|
+
const texts = summaryTexts(item);
|
|
1075
|
+
if (texts === void 0) return item;
|
|
1076
|
+
if (typeof item.id === "string") ctx.signatures.set(signatureKey(item.id), JSON.stringify(texts));
|
|
1077
|
+
return {
|
|
1078
|
+
...item,
|
|
1079
|
+
summary: item.summary.map((part) => ({
|
|
1080
|
+
...part,
|
|
1081
|
+
text: transform.inbound(part.text)
|
|
1082
|
+
}))
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
case "function_call": return typeof item.arguments === "string" ? {
|
|
1086
|
+
...item,
|
|
1087
|
+
arguments: restoreArguments(item.arguments, transform)
|
|
1088
|
+
} : item;
|
|
1089
|
+
default: return item;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function restorePart(part, transform) {
|
|
1093
|
+
return isObj(part) && part.type === "output_text" && typeof part.text === "string" ? {
|
|
1094
|
+
...part,
|
|
1095
|
+
text: transform.inbound(part.text)
|
|
1096
|
+
} : part;
|
|
1097
|
+
}
|
|
1098
|
+
function restoreResponse(body, ctx) {
|
|
1099
|
+
const out = { ...body };
|
|
1100
|
+
if (Array.isArray(body.output)) out.output = body.output.map((item) => restoreItem(item, ctx));
|
|
1101
|
+
if (typeof body.output_text === "string") out.output_text = ctx.transform.inbound(body.output_text);
|
|
1102
|
+
return out;
|
|
1103
|
+
}
|
|
1104
|
+
function createStreamTransformer(ctx) {
|
|
1105
|
+
const streams = /* @__PURE__ */ new Map();
|
|
1106
|
+
const open = (key, factory) => {
|
|
1107
|
+
let state = streams.get(key);
|
|
1108
|
+
if (state === void 0) {
|
|
1109
|
+
state = { restore: factory() };
|
|
1110
|
+
streams.set(key, state);
|
|
1111
|
+
}
|
|
1112
|
+
return state;
|
|
1113
|
+
};
|
|
1114
|
+
/** Emits the tail delta (if any) followed by the rewritten `.done` event. */
|
|
1115
|
+
const closeWith = (block, payload, key, deltaType, done) => {
|
|
1116
|
+
const state = streams.get(key);
|
|
1117
|
+
streams.delete(key);
|
|
1118
|
+
let out = "";
|
|
1119
|
+
const tail = state?.restore.flush() ?? "";
|
|
1120
|
+
if (tail !== "") {
|
|
1121
|
+
const { text: _text, arguments: _args, ...rest } = payload;
|
|
1122
|
+
out += formatSseEvent(block.event === void 0 ? void 0 : deltaType, {
|
|
1123
|
+
...rest,
|
|
1124
|
+
type: deltaType,
|
|
1125
|
+
delta: tail
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
return out + formatSseEvent(block.event, done);
|
|
1129
|
+
};
|
|
1130
|
+
const handle = (block) => {
|
|
1131
|
+
let payload;
|
|
1132
|
+
try {
|
|
1133
|
+
payload = JSON.parse(block.data);
|
|
1134
|
+
} catch {
|
|
1135
|
+
return block.raw;
|
|
1136
|
+
}
|
|
1137
|
+
if (!isObj(payload)) return block.raw;
|
|
1138
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : void 0;
|
|
1139
|
+
const textKey = `text:${itemId}:${String(payload.content_index)}`;
|
|
1140
|
+
const summaryKey = `summary:${itemId}:${String(payload.summary_index)}`;
|
|
1141
|
+
const argsKey = `args:${itemId}`;
|
|
1142
|
+
const textStream = () => ctx.transform.inboundStream();
|
|
1143
|
+
const argStream = () => createJsonArgRestorer(() => ctx.transform.inboundStream());
|
|
1144
|
+
switch (payload.type) {
|
|
1145
|
+
case "response.output_text.delta":
|
|
1146
|
+
if (itemId === void 0 || typeof payload.delta !== "string") return block.raw;
|
|
1147
|
+
return formatSseEvent(block.event, {
|
|
1148
|
+
...payload,
|
|
1149
|
+
delta: open(textKey, textStream).restore.push(payload.delta)
|
|
1150
|
+
});
|
|
1151
|
+
case "response.reasoning_summary_text.delta":
|
|
1152
|
+
if (itemId === void 0 || typeof payload.delta !== "string") return block.raw;
|
|
1153
|
+
return formatSseEvent(block.event, {
|
|
1154
|
+
...payload,
|
|
1155
|
+
delta: open(summaryKey, textStream).restore.push(payload.delta)
|
|
1156
|
+
});
|
|
1157
|
+
case "response.function_call_arguments.delta":
|
|
1158
|
+
if (itemId === void 0 || typeof payload.delta !== "string") return block.raw;
|
|
1159
|
+
return formatSseEvent(block.event, {
|
|
1160
|
+
...payload,
|
|
1161
|
+
delta: open(argsKey, argStream).restore.push(payload.delta)
|
|
1162
|
+
});
|
|
1163
|
+
case "response.output_text.done":
|
|
1164
|
+
if (itemId === void 0 || typeof payload.text !== "string") return block.raw;
|
|
1165
|
+
return closeWith(block, payload, textKey, "response.output_text.delta", {
|
|
1166
|
+
...payload,
|
|
1167
|
+
text: ctx.transform.inbound(payload.text)
|
|
1168
|
+
});
|
|
1169
|
+
case "response.reasoning_summary_text.done":
|
|
1170
|
+
if (itemId === void 0 || typeof payload.text !== "string") return block.raw;
|
|
1171
|
+
return closeWith(block, payload, summaryKey, "response.reasoning_summary_text.delta", {
|
|
1172
|
+
...payload,
|
|
1173
|
+
text: ctx.transform.inbound(payload.text)
|
|
1174
|
+
});
|
|
1175
|
+
case "response.function_call_arguments.done":
|
|
1176
|
+
if (itemId === void 0 || typeof payload.arguments !== "string") return block.raw;
|
|
1177
|
+
return closeWith(block, payload, argsKey, "response.function_call_arguments.delta", {
|
|
1178
|
+
...payload,
|
|
1179
|
+
arguments: restoreArguments(payload.arguments, ctx.transform)
|
|
1180
|
+
});
|
|
1181
|
+
case "response.content_part.done": return isObj(payload.part) ? formatSseEvent(block.event, {
|
|
1182
|
+
...payload,
|
|
1183
|
+
part: restorePart(payload.part, ctx.transform)
|
|
1184
|
+
}) : block.raw;
|
|
1185
|
+
case "response.reasoning_summary_part.done": return isObj(payload.part) && typeof payload.part.text === "string" ? formatSseEvent(block.event, {
|
|
1186
|
+
...payload,
|
|
1187
|
+
part: {
|
|
1188
|
+
...payload.part,
|
|
1189
|
+
text: ctx.transform.inbound(payload.part.text)
|
|
1190
|
+
}
|
|
1191
|
+
}) : block.raw;
|
|
1192
|
+
case "response.output_item.done": return isObj(payload.item) ? formatSseEvent(block.event, {
|
|
1193
|
+
...payload,
|
|
1194
|
+
item: restoreItem(payload.item, ctx)
|
|
1195
|
+
}) : block.raw;
|
|
1196
|
+
case "response.completed":
|
|
1197
|
+
case "response.incomplete": return isObj(payload.response) ? formatSseEvent(block.event, {
|
|
1198
|
+
...payload,
|
|
1199
|
+
response: restoreResponse(payload.response, ctx)
|
|
1200
|
+
}) : block.raw;
|
|
1201
|
+
default: return block.raw;
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
const finish = () => {
|
|
1205
|
+
let out = "";
|
|
1206
|
+
for (const [key, state] of streams) {
|
|
1207
|
+
const tail = state.restore.flush();
|
|
1208
|
+
if (tail === "") continue;
|
|
1209
|
+
const [kind, itemId] = key.split(":");
|
|
1210
|
+
const type = kind === "text" ? "response.output_text.delta" : kind === "summary" ? "response.reasoning_summary_text.delta" : "response.function_call_arguments.delta";
|
|
1211
|
+
out += formatSseEvent(type, {
|
|
1212
|
+
type,
|
|
1213
|
+
item_id: itemId,
|
|
1214
|
+
delta: tail
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
streams.clear();
|
|
1218
|
+
return out;
|
|
1219
|
+
};
|
|
1220
|
+
return createSseFramer(handle, finish);
|
|
1221
|
+
}
|
|
1222
|
+
const openaiResponsesWalker = {
|
|
1223
|
+
async request(body, ctx) {
|
|
1224
|
+
if (!isObj(body)) return body;
|
|
1225
|
+
const out = { ...body };
|
|
1226
|
+
if (typeof body.instructions === "string") out.instructions = await outbound(ctx.transform, body.instructions, { kind: "system" });
|
|
1227
|
+
const instruction = ctx.transform.instruction;
|
|
1228
|
+
if (instruction !== void 0) out.instructions = typeof out.instructions === "string" && out.instructions !== "" ? `${instruction}\n\n${out.instructions}` : instruction;
|
|
1229
|
+
if (typeof body.input === "string") out.input = await outbound(ctx.transform, body.input, { kind: "user" });
|
|
1230
|
+
else if (Array.isArray(body.input)) {
|
|
1231
|
+
const toolNames = toolNamesByCallId(body.input);
|
|
1232
|
+
const input = [];
|
|
1233
|
+
for (const item of body.input) input.push(isObj(item) ? await rewriteItem(item, toolNames, ctx) : item);
|
|
1234
|
+
out.input = input;
|
|
1235
|
+
}
|
|
1236
|
+
return out;
|
|
1237
|
+
},
|
|
1238
|
+
response(body, ctx) {
|
|
1239
|
+
return isObj(body) ? restoreResponse(body, ctx) : body;
|
|
1240
|
+
},
|
|
1241
|
+
stream(ctx) {
|
|
1242
|
+
return createStreamTransformer(ctx);
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
// src/routes.ts
|
|
1246
|
+
/**
|
|
1247
|
+
* The route table. Routes are matched in order, so longer suffixes come first.
|
|
1248
|
+
* Any other path, embeddings included, answers 404 `unsupported_route`.
|
|
1249
|
+
*/
|
|
1250
|
+
const WIRE_ROUTES = [
|
|
1251
|
+
{
|
|
1252
|
+
suffix: "/messages/count_tokens",
|
|
1253
|
+
wire: anthropicWalker,
|
|
1254
|
+
rewriteResponse: false
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
suffix: "/messages",
|
|
1258
|
+
wire: anthropicWalker,
|
|
1259
|
+
rewriteResponse: true
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
suffix: "/chat/completions",
|
|
1263
|
+
wire: openaiChatWalker,
|
|
1264
|
+
rewriteResponse: true
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
suffix: "/responses",
|
|
1268
|
+
wire: openaiResponsesWalker,
|
|
1269
|
+
rewriteResponse: true
|
|
1270
|
+
}
|
|
1271
|
+
];
|
|
1272
|
+
/** Classifies a request by method and path. `undefined` means the route is unsupported. */
|
|
1273
|
+
function classify(method, pathname) {
|
|
1274
|
+
if (method === "GET" && pathname.endsWith("/models")) return { kind: "passthrough" };
|
|
1275
|
+
if (method !== "POST") return void 0;
|
|
1276
|
+
const route = WIRE_ROUTES.find((candidate) => pathname.endsWith(candidate.suffix));
|
|
1277
|
+
return route === void 0 ? void 0 : {
|
|
1278
|
+
kind: "wire",
|
|
1279
|
+
route
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
// src/sessions.ts
|
|
1283
|
+
function createSessionState() {
|
|
1284
|
+
return {
|
|
1285
|
+
facts: createSessionFacts(),
|
|
1286
|
+
signatures: /* @__PURE__ */ new Map(),
|
|
1287
|
+
instances: /* @__PURE__ */ new Map()
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Returns the session's instance for a pipeline, building it on first use.
|
|
1292
|
+
* Concurrent first calls share one build; a failed build is forgotten so the next call retries.
|
|
1293
|
+
*/
|
|
1294
|
+
function instanceFor(state, pipelineId, build) {
|
|
1295
|
+
const existing = state.instances.get(pipelineId);
|
|
1296
|
+
if (existing) return existing;
|
|
1297
|
+
const pending = build().catch((err) => {
|
|
1298
|
+
if (state.instances.get(pipelineId) === pending) state.instances.delete(pipelineId);
|
|
1299
|
+
throw err;
|
|
1300
|
+
});
|
|
1301
|
+
state.instances.set(pipelineId, pending);
|
|
1302
|
+
return pending;
|
|
1303
|
+
}
|
|
1304
|
+
/** Disposes every instance of a session, waiting for pending builds first. Dispose errors are logged. */
|
|
1305
|
+
async function disposeInstances(state, log) {
|
|
1306
|
+
const settled = await Promise.allSettled([...state.instances.values()]);
|
|
1307
|
+
state.instances.clear();
|
|
1308
|
+
for (const result of settled) {
|
|
1309
|
+
if (result.status !== "fulfilled") continue;
|
|
1310
|
+
try {
|
|
1311
|
+
await result.value.dispose?.();
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
log.error(`transform dispose failed: ${errorMessage(err)}`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
// src/server.ts
|
|
1318
|
+
const UPSTREAM_HEADER = "x-skaile-transform-upstream";
|
|
1319
|
+
const SESSION_HEADER = "x-skaile-transform-session";
|
|
1320
|
+
const PIPELINE_HEADER = "x-skaile-transform-pipeline";
|
|
1321
|
+
const TOKEN_HEADER = "x-skaile-transform-token";
|
|
1322
|
+
const DEFAULT_SESSION = "default";
|
|
1323
|
+
const LOOPBACK_HOSTS = {
|
|
1324
|
+
"127.0.0.1": true,
|
|
1325
|
+
localhost: true,
|
|
1326
|
+
"::1": true,
|
|
1327
|
+
"[::1]": true
|
|
1328
|
+
};
|
|
1329
|
+
/** Connection-level headers: never forwarded in either direction. */
|
|
1330
|
+
const HOP_BY_HOP = {
|
|
1331
|
+
connection: true,
|
|
1332
|
+
"keep-alive": true,
|
|
1333
|
+
"proxy-connection": true,
|
|
1334
|
+
te: true,
|
|
1335
|
+
trailer: true,
|
|
1336
|
+
"transfer-encoding": true,
|
|
1337
|
+
upgrade: true
|
|
1338
|
+
};
|
|
1339
|
+
const REQUEST_SKIP = {
|
|
1340
|
+
...HOP_BY_HOP,
|
|
1341
|
+
host: true,
|
|
1342
|
+
"content-length": true,
|
|
1343
|
+
"accept-encoding": true
|
|
1344
|
+
};
|
|
1345
|
+
/** Response headers the proxy re-derives: the body length and encoding after rewriting, and set-cookie (sent via getSetCookie). */
|
|
1346
|
+
const RESPONSE_SKIP = {
|
|
1347
|
+
...HOP_BY_HOP,
|
|
1348
|
+
"content-length": true,
|
|
1349
|
+
"content-encoding": true,
|
|
1350
|
+
"set-cookie": true
|
|
1351
|
+
};
|
|
1352
|
+
async function createTransformProxy(options) {
|
|
1353
|
+
const log = options.log ?? silentLogger;
|
|
1354
|
+
const host = options.host ?? "127.0.0.1";
|
|
1355
|
+
if (!Object.hasOwn(LOOPBACK_HOSTS, host) && options.token === void 0) throw new Error(`transform proxy host ${host} is not loopback: a token is required`);
|
|
1356
|
+
validatePipelines(options);
|
|
1357
|
+
const allowed = options.allowUpstreams === void 0 ? void 0 : new Set(options.allowUpstreams.map(originOf));
|
|
1358
|
+
const factories = await openTransforms(options.transforms, options.baseDir, log);
|
|
1359
|
+
const pipelines = options.pipelines;
|
|
1360
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1361
|
+
function stateFor(id) {
|
|
1362
|
+
const existing = sessions.get(id);
|
|
1363
|
+
if (existing) return existing;
|
|
1364
|
+
const created = createSessionState();
|
|
1365
|
+
sessions.set(id, created);
|
|
1366
|
+
return created;
|
|
1367
|
+
}
|
|
1368
|
+
async function buildInstance(state, sessionId, pipelineId) {
|
|
1369
|
+
const stages = [];
|
|
1370
|
+
const ids = pipelines[pipelineId] ?? [];
|
|
1371
|
+
try {
|
|
1372
|
+
for (const id of ids) {
|
|
1373
|
+
const factory = factories.get(id);
|
|
1374
|
+
if (factory === void 0) throw new Error(`transform "${id}" is not configured`);
|
|
1375
|
+
try {
|
|
1376
|
+
stages.push(await factory({
|
|
1377
|
+
sessionId,
|
|
1378
|
+
facts: state.facts.facts,
|
|
1379
|
+
log
|
|
1380
|
+
}));
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
throw new Error(`transform "${id}" failed to start: ${errorMessage(err)}`, { cause: err });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
for (const stage of stages) try {
|
|
1387
|
+
await stage.dispose?.();
|
|
1388
|
+
} catch (disposeErr) {
|
|
1389
|
+
log.error(`transform dispose failed: ${errorMessage(disposeErr)}`);
|
|
1390
|
+
}
|
|
1391
|
+
throw err;
|
|
1392
|
+
}
|
|
1393
|
+
return composeTransforms(stages, log);
|
|
1394
|
+
}
|
|
1395
|
+
async function handle(req, res) {
|
|
1396
|
+
const url = new URL(req.url ?? "/", "http://proxy.invalid");
|
|
1397
|
+
if (options.token !== void 0 && !tokenMatches(header(req, "x-skaile-transform-token"), options.token)) return sendError(res, 401, "unauthorized", `${TOKEN_HEADER} is missing or wrong`);
|
|
1398
|
+
if (req.method === "OPTIONS") return sendError(res, 405, "method_not_allowed", "OPTIONS is refused");
|
|
1399
|
+
const upstreamValue = header(req, UPSTREAM_HEADER);
|
|
1400
|
+
if (upstreamValue === void 0) return sendError(res, 400, "missing_upstream", `${UPSTREAM_HEADER} is required`);
|
|
1401
|
+
const upstream = parseHttpUrl(upstreamValue);
|
|
1402
|
+
if (upstream === void 0) return sendError(res, 400, "invalid_upstream", `${UPSTREAM_HEADER} must be an http(s) URL`);
|
|
1403
|
+
if (!upstreamAllowed(upstream, allowed)) return sendError(res, 403, "upstream_not_allowed", `upstream ${upstream.origin} is not allowed`);
|
|
1404
|
+
const sessionId = header(req, "x-skaile-transform-session") ?? DEFAULT_SESSION;
|
|
1405
|
+
const pipelineId = header(req, "x-skaile-transform-pipeline") ?? options.defaultPipeline;
|
|
1406
|
+
if (pipelineId === void 0) return sendError(res, 400, "no_pipeline", `${PIPELINE_HEADER} is required`);
|
|
1407
|
+
if (!Object.hasOwn(pipelines, pipelineId)) return sendError(res, 400, "unknown_pipeline", `pipeline "${pipelineId}" is not configured`);
|
|
1408
|
+
const method = req.method ?? "GET";
|
|
1409
|
+
const classified = classify(method, url.pathname);
|
|
1410
|
+
if (classified === void 0) return sendError(res, 404, "unsupported_route", `${method} ${url.pathname} is not proxied`);
|
|
1411
|
+
const forwardTo = `${upstream.origin}${upstream.pathname.replace(/\/+$/, "")}${url.pathname}${url.search}`;
|
|
1412
|
+
const headers = forwardRequestHeaders(req.headers);
|
|
1413
|
+
if (classified.kind === "passthrough") return relayUpstream(res, await fetchUpstream(res, forwardTo, method, headers, void 0), void 0);
|
|
1414
|
+
const { route } = classified;
|
|
1415
|
+
const encoding = header(req, "content-encoding");
|
|
1416
|
+
if (encoding !== void 0 && encoding !== "identity") return sendError(res, 400, "unsupported_encoding", "request bodies must not be content-encoded");
|
|
1417
|
+
const raw = await readBody(req);
|
|
1418
|
+
let parsed;
|
|
1419
|
+
try {
|
|
1420
|
+
parsed = JSON.parse(raw.toString("utf8"));
|
|
1421
|
+
} catch {
|
|
1422
|
+
return sendError(res, 400, "invalid_json", "request body is not JSON");
|
|
1423
|
+
}
|
|
1424
|
+
const state = stateFor(sessionId);
|
|
1425
|
+
let instance;
|
|
1426
|
+
try {
|
|
1427
|
+
instance = await instanceFor(state, pipelineId, () => buildInstance(state, sessionId, pipelineId));
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
return sendError(res, 502, "transform_failed", errorMessage(err));
|
|
1430
|
+
}
|
|
1431
|
+
const ctx = {
|
|
1432
|
+
transform: instance,
|
|
1433
|
+
signatures: state.signatures
|
|
1434
|
+
};
|
|
1435
|
+
let rewritten;
|
|
1436
|
+
try {
|
|
1437
|
+
rewritten = await route.wire.request(parsed, ctx);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
if (err instanceof UnsupportedContentError) return sendError(res, 400, "unsupported_content", err.message);
|
|
1440
|
+
return sendError(res, 502, "transform_failed", errorMessage(err));
|
|
1441
|
+
}
|
|
1442
|
+
return relayUpstream(res, await fetchUpstream(res, forwardTo, method, headers, JSON.stringify(rewritten)), route.rewriteResponse ? {
|
|
1443
|
+
wire: route.wire,
|
|
1444
|
+
ctx
|
|
1445
|
+
} : void 0);
|
|
1446
|
+
}
|
|
1447
|
+
async function fetchUpstream(res, target, method, headers, body) {
|
|
1448
|
+
try {
|
|
1449
|
+
return await fetch(target, {
|
|
1450
|
+
method,
|
|
1451
|
+
headers,
|
|
1452
|
+
body,
|
|
1453
|
+
redirect: "manual"
|
|
1454
|
+
});
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
sendError(res, 502, "upstream_unreachable", errorMessage(err));
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
/** Relays an upstream response. With `rewrite`, a 2xx JSON or SSE body is restored on the way back. */
|
|
1461
|
+
async function relayUpstream(res, upstreamRes, rewrite) {
|
|
1462
|
+
if (upstreamRes === void 0) return;
|
|
1463
|
+
const contentType = upstreamRes.headers.get("content-type") ?? "";
|
|
1464
|
+
const headers = relayHeaders(upstreamRes.headers);
|
|
1465
|
+
if (rewrite !== void 0 && upstreamRes.ok && contentType.startsWith("text/event-stream")) return pipeStream(res, upstreamRes, headers, rewrite.wire.stream(rewrite.ctx), log);
|
|
1466
|
+
if (rewrite !== void 0 && upstreamRes.ok && contentType.includes("application/json")) {
|
|
1467
|
+
const text = await upstreamRes.text();
|
|
1468
|
+
let restored;
|
|
1469
|
+
try {
|
|
1470
|
+
restored = JSON.stringify(rewrite.wire.response(JSON.parse(text), rewrite.ctx));
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
return sendError(res, 502, "transform_failed", errorMessage(err));
|
|
1473
|
+
}
|
|
1474
|
+
res.writeHead(upstreamRes.status, headers);
|
|
1475
|
+
res.end(restored);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
res.writeHead(upstreamRes.status, headers);
|
|
1479
|
+
res.end(Buffer.from(await upstreamRes.arrayBuffer()));
|
|
1480
|
+
}
|
|
1481
|
+
const server = createServer((req, res) => {
|
|
1482
|
+
handle(req, res).catch((err) => {
|
|
1483
|
+
log.error(`request failed: ${errorMessage(err)}`);
|
|
1484
|
+
sendError(res, 500, "internal_error", "transform proxy failed");
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
const listening = Promise.withResolvers();
|
|
1488
|
+
server.once("error", listening.reject);
|
|
1489
|
+
server.listen(options.port ?? 0, host, () => {
|
|
1490
|
+
server.off("error", listening.reject);
|
|
1491
|
+
listening.resolve();
|
|
1492
|
+
});
|
|
1493
|
+
await listening.promise;
|
|
1494
|
+
const address = server.address();
|
|
1495
|
+
return {
|
|
1496
|
+
url: `http://${host.includes(":") ? `[${host}]` : host}:${address.port}`,
|
|
1497
|
+
session(id) {
|
|
1498
|
+
const state = stateFor(id);
|
|
1499
|
+
return {
|
|
1500
|
+
facts: state.facts.facts,
|
|
1501
|
+
publisher: (source) => state.facts.publisher(source)
|
|
1502
|
+
};
|
|
1503
|
+
},
|
|
1504
|
+
async dropSession(id) {
|
|
1505
|
+
const state = sessions.get(id);
|
|
1506
|
+
if (state === void 0) return;
|
|
1507
|
+
sessions.delete(id);
|
|
1508
|
+
await disposeInstances(state, log);
|
|
1509
|
+
},
|
|
1510
|
+
async close() {
|
|
1511
|
+
const closed = Promise.withResolvers();
|
|
1512
|
+
server.close((err) => err ? closed.reject(err) : closed.resolve());
|
|
1513
|
+
server.closeAllConnections();
|
|
1514
|
+
await closed.promise;
|
|
1515
|
+
for (const id of [...sessions.keys()]) {
|
|
1516
|
+
const state = sessions.get(id);
|
|
1517
|
+
sessions.delete(id);
|
|
1518
|
+
if (state !== void 0) await disposeInstances(state, log);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function validatePipelines(options) {
|
|
1524
|
+
for (const [pipelineId, ids] of Object.entries(options.pipelines)) for (const id of ids) if (!Object.hasOwn(options.transforms, id)) throw new Error(`pipeline "${pipelineId}" names unknown transform "${id}"`);
|
|
1525
|
+
if (options.defaultPipeline !== void 0 && !Object.hasOwn(options.pipelines, options.defaultPipeline)) throw new Error(`defaultPipeline "${options.defaultPipeline}" is not a configured pipeline`);
|
|
1526
|
+
}
|
|
1527
|
+
function originOf(value) {
|
|
1528
|
+
const url = parseHttpUrl(value);
|
|
1529
|
+
if (url === void 0) throw new Error(`allowUpstreams entry "${value}" is not an http(s) URL`);
|
|
1530
|
+
return url.origin;
|
|
1531
|
+
}
|
|
1532
|
+
function parseHttpUrl(value) {
|
|
1533
|
+
try {
|
|
1534
|
+
const url = new URL(value);
|
|
1535
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url : void 0;
|
|
1536
|
+
} catch {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function upstreamAllowed(url, allowed) {
|
|
1541
|
+
if (allowed !== void 0) return allowed.has(url.origin);
|
|
1542
|
+
return url.protocol === "https:" || url.protocol === "http:" && Object.hasOwn(LOOPBACK_HOSTS, url.hostname);
|
|
1543
|
+
}
|
|
1544
|
+
function tokenMatches(given, expected) {
|
|
1545
|
+
if (given === void 0) return false;
|
|
1546
|
+
const a = Buffer.from(given);
|
|
1547
|
+
const b = Buffer.from(expected);
|
|
1548
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
1549
|
+
}
|
|
1550
|
+
function header(req, name) {
|
|
1551
|
+
const value = req.headers[name];
|
|
1552
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
1553
|
+
return first === void 0 || first === "" ? void 0 : first;
|
|
1554
|
+
}
|
|
1555
|
+
function forwardRequestHeaders(incoming) {
|
|
1556
|
+
const out = new Headers();
|
|
1557
|
+
for (const [name, value] of Object.entries(incoming)) {
|
|
1558
|
+
if (value === void 0 || name.startsWith("x-skaile-transform-") || Object.hasOwn(REQUEST_SKIP, name)) continue;
|
|
1559
|
+
for (const item of Array.isArray(value) ? value : [value]) out.append(name, item);
|
|
1560
|
+
}
|
|
1561
|
+
out.set("accept-encoding", "identity");
|
|
1562
|
+
return out;
|
|
1563
|
+
}
|
|
1564
|
+
function relayHeaders(headers) {
|
|
1565
|
+
const out = {};
|
|
1566
|
+
headers.forEach((value, name) => {
|
|
1567
|
+
if (!Object.hasOwn(RESPONSE_SKIP, name)) out[name] = value;
|
|
1568
|
+
});
|
|
1569
|
+
const cookies = headers.getSetCookie();
|
|
1570
|
+
if (cookies.length > 0) out["set-cookie"] = cookies;
|
|
1571
|
+
return out;
|
|
1572
|
+
}
|
|
1573
|
+
async function readBody(req) {
|
|
1574
|
+
const chunks = [];
|
|
1575
|
+
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1576
|
+
return Buffer.concat(chunks);
|
|
1577
|
+
}
|
|
1578
|
+
/** Streams an SSE body through the transformer, chunk by chunk, decoding UTF-8 across chunk boundaries. */
|
|
1579
|
+
async function pipeStream(res, upstreamRes, headers, transformer, log) {
|
|
1580
|
+
res.writeHead(upstreamRes.status, headers);
|
|
1581
|
+
const reader = upstreamRes.body?.getReader();
|
|
1582
|
+
if (reader === void 0) {
|
|
1583
|
+
res.end();
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const decoder = new TextDecoder();
|
|
1587
|
+
try {
|
|
1588
|
+
for (;;) {
|
|
1589
|
+
const { done, value } = await reader.read();
|
|
1590
|
+
if (done) break;
|
|
1591
|
+
await writeText(res, transformer.push(decoder.decode(value, { stream: true })));
|
|
1592
|
+
if (res.destroyed) {
|
|
1593
|
+
await reader.cancel();
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
await writeText(res, transformer.push(decoder.decode()) + transformer.flush());
|
|
1598
|
+
res.end();
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
log.error(`stream relay failed: ${errorMessage(err)}`);
|
|
1601
|
+
res.destroy();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function writeText(res, text) {
|
|
1605
|
+
if (text === "" || res.write(text)) return Promise.resolve();
|
|
1606
|
+
const drained = Promise.withResolvers();
|
|
1607
|
+
const done = () => {
|
|
1608
|
+
res.off("drain", done);
|
|
1609
|
+
res.off("close", done);
|
|
1610
|
+
drained.resolve();
|
|
1611
|
+
};
|
|
1612
|
+
res.once("drain", done);
|
|
1613
|
+
res.once("close", done);
|
|
1614
|
+
return drained.promise;
|
|
1615
|
+
}
|
|
1616
|
+
/** Writes the error envelope `{ error: { type, message } }`. */
|
|
1617
|
+
function sendError(res, status, type, message) {
|
|
1618
|
+
if (res.headersSent) {
|
|
1619
|
+
res.destroy();
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
1623
|
+
res.end(JSON.stringify({ error: {
|
|
1624
|
+
type,
|
|
1625
|
+
message
|
|
1626
|
+
} }));
|
|
1627
|
+
}
|
|
1628
|
+
// src/config.ts
|
|
1629
|
+
const CONFIG_FILE_NAME = "transform-proxy.yaml";
|
|
1630
|
+
/** The implicit lookup locations, in order: `./.skaile/` then `~/.config/skaile/`. */
|
|
1631
|
+
function configCandidates(cwd, home) {
|
|
1632
|
+
return [join(cwd, ".skaile", CONFIG_FILE_NAME), join(home, ".config", "skaile", CONFIG_FILE_NAME)];
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Finds the config file: the explicit path when given (it must exist), otherwise the first
|
|
1636
|
+
* existing implicit candidate. Returns `undefined` when no implicit candidate exists.
|
|
1637
|
+
*/
|
|
1638
|
+
function findProxyConfig(lookup) {
|
|
1639
|
+
if (lookup.explicit !== void 0) {
|
|
1640
|
+
const path = resolve(lookup.cwd, lookup.explicit);
|
|
1641
|
+
if (!existsSync(path)) throw new Error(`transform proxy config not found: ${path}`);
|
|
1642
|
+
return path;
|
|
1643
|
+
}
|
|
1644
|
+
return configCandidates(lookup.cwd, lookup.home).find((candidate) => existsSync(candidate));
|
|
1645
|
+
}
|
|
1646
|
+
/** Finds and loads the config. Throws, naming the searched paths, when none is found. */
|
|
1647
|
+
function resolveProxyConfig(lookup) {
|
|
1648
|
+
const path = findProxyConfig(lookup);
|
|
1649
|
+
if (path === void 0) {
|
|
1650
|
+
const searched = configCandidates(lookup.cwd, lookup.home).join(", ");
|
|
1651
|
+
throw new Error(`no transform proxy config found (pass --config, or create one at ${searched})`);
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
path,
|
|
1655
|
+
options: loadProxyConfig(path)
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
const TOP_LEVEL_KEYS = {
|
|
1659
|
+
transforms: true,
|
|
1660
|
+
pipelines: true,
|
|
1661
|
+
defaultPipeline: true,
|
|
1662
|
+
allowUpstreams: true
|
|
1663
|
+
};
|
|
1664
|
+
/** Reads and validates a YAML config file. Unknown keys are rejected so that typos fail loudly. */
|
|
1665
|
+
function loadProxyConfig(path) {
|
|
1666
|
+
const source = parse(readFileSync(path, "utf8"));
|
|
1667
|
+
try {
|
|
1668
|
+
return parseProxyConfig(source ?? {}, dirname(path));
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
throw new Error(`invalid transform proxy config ${path}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
/** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
|
|
1674
|
+
function parseProxyConfig(source, baseDir) {
|
|
1675
|
+
if (!isRecord(source)) throw new Error("the document must be a mapping");
|
|
1676
|
+
for (const key of Object.keys(source)) if (!Object.hasOwn(TOP_LEVEL_KEYS, key)) throw new Error(`unknown key "${key}"`);
|
|
1677
|
+
return {
|
|
1678
|
+
baseDir,
|
|
1679
|
+
transforms: parseTransforms(source.transforms),
|
|
1680
|
+
pipelines: parsePipelines(source.pipelines),
|
|
1681
|
+
...source.defaultPipeline === void 0 ? {} : { defaultPipeline: requireString(source.defaultPipeline, "defaultPipeline") },
|
|
1682
|
+
...source.allowUpstreams === void 0 ? {} : { allowUpstreams: requireStringList(source.allowUpstreams, "allowUpstreams") }
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function parseTransforms(value) {
|
|
1686
|
+
if (value === void 0) return {};
|
|
1687
|
+
if (!isRecord(value)) throw new Error("transforms must be a mapping of transform id to { module, config }");
|
|
1688
|
+
const out = {};
|
|
1689
|
+
for (const [id, spec] of Object.entries(value)) {
|
|
1690
|
+
if (!isRecord(spec)) throw new Error(`transforms.${id} must be a mapping with a module`);
|
|
1691
|
+
const module = requireString(spec.module, `transforms.${id}.module`);
|
|
1692
|
+
out[id] = spec.config === void 0 ? { module } : {
|
|
1693
|
+
module,
|
|
1694
|
+
config: spec.config
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
return out;
|
|
1698
|
+
}
|
|
1699
|
+
function parsePipelines(value) {
|
|
1700
|
+
if (value === void 0) return {};
|
|
1701
|
+
if (!isRecord(value)) throw new Error("pipelines must be a mapping of pipeline id to a list of transform ids");
|
|
1702
|
+
const out = {};
|
|
1703
|
+
for (const [id, stages] of Object.entries(value)) out[id] = requireStringList(stages, `pipelines.${id}`);
|
|
1704
|
+
return out;
|
|
1705
|
+
}
|
|
1706
|
+
function requireString(value, field) {
|
|
1707
|
+
if (typeof value !== "string" || value === "") throw new Error(`${field} must be a non-empty string`);
|
|
1708
|
+
return value;
|
|
1709
|
+
}
|
|
1710
|
+
function requireStringList(value, field) {
|
|
1711
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "")) throw new Error(`${field} must be a list of non-empty strings`);
|
|
1712
|
+
return value;
|
|
1713
|
+
}
|
|
1714
|
+
function isRecord(value) {
|
|
1715
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1716
|
+
}
|
|
1717
|
+
export { composeTransforms as _, parseProxyConfig as a, SESSION_HEADER as c, createTransformProxy as d, openaiResponsesWalker as f, createSessionFacts as g, UnsupportedContentError as h, loadProxyConfig as i, TOKEN_HEADER as l, anthropicWalker as m, configCandidates as n, resolveProxyConfig as o, openaiChatWalker as p, findProxyConfig as r, PIPELINE_HEADER as s, CONFIG_FILE_NAME as t, UPSTREAM_HEADER as u, errorMessage as v, silentLogger as y };
|