@skaile/transform-proxy 0.5.0 → 0.6.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 84c02a9: Less detection work behind the transform proxy, with the same protection.
8
+
9
+ forge-anon: an `AnonSession` caches each text's detection and no longer discards it when the mapping grows; earlier texts are finished again against the new mapping without a detector run. Detection of overlapping `anonymize` calls runs in parallel, placeholder allocation stays serialized. New `session.anonymize(text, { detectors })` restricts the detectors per call, and `session.remember(raw)` restores a model reply and lets its resend skip detection. The `onnx` and `llm` detectors cache model results per chunk text in memory. The proxy module restores through `remember` and takes an optional `leafDetectors` map (detectors per text leaf kind).
10
+
11
+ transform-proxy: the walkers rewrite all text leaves of a request concurrently; refusals are raised before any transform call. Transforms must accept overlapping `outbound` calls.
12
+
13
+ ## 0.5.1
14
+
15
+ ### Patch Changes
16
+
17
+ - 21bab3c: The bin installs its SIGINT, SIGTERM and `--exit-with-stdin` handlers before it prints `listening`. A parent that stopped the proxy right after that line could kill it with the default signal action (exit 143) instead of a clean shutdown (exit 0). `runCli` takes an optional `onStart` callback, called after the proxy listens and before the `listening` line.
18
+
3
19
  ## 0.5.0
4
20
 
5
21
  - Library: the anon trace records what the proxy sends to the provider **after** the pipeline and what the model answers **before** restore, so a reviewer can check that no clear-text personal data reached the provider. `TransformProxyOptions.trace?: TraceSink` (synchronous `record(event: TraceEvent)`) gets one `request` line per proxied request on the wire routes and exactly one `response` or `error` line for it, linked by `requestId`. Without `trace`, nothing is recorded or hashed. A sink that throws is logged (`transform proxy: trace sink failed: <msg>`) and never reaches the client.
package/README.md CHANGED
@@ -197,6 +197,7 @@ A module default-exports a `TransformModule`:
197
197
  - It returns a `TransformFactory`, which runs once per `(session, pipeline)`: `(ctx: { sessionId, facts, log }) => Transform`.
198
198
  - A `Transform` has `instruction?`, `outbound(text, leaf)`, `inbound(text)`, `inboundStream()`, `dispose?()`, `inspect?()` (host-only state, read through `proxy.inspect`; the walkers never call it) and `knownOriginals?()`.
199
199
  - `knownOriginals?(): Iterable<{ kind, value }>` returns the originals the instance has already replaced in this session, each with its kind (forge-anon: its mapping). Only the [leak check](#leak-check) reads them, as source `pipeline`; the values are never sent or logged. `composeTransforms` returns the union of all stages that implement it; a stage that throws is logged and skipped.
200
+ - The walkers first check the whole request (refusals such as attachments or malformed slots come before any `outbound` call), then call `outbound` for every text leaf of the request at once and assemble the body in its original order. A transform must therefore accept overlapping `outbound` calls of one session; a stage that needs ordering (forge-anon allocates placeholders one call at a time) serializes internally. One rejected `outbound` fails the request.
200
201
 
201
202
  The Anthropic walker appends the pipeline `instruction` after the host's system prompt: a string `system` gets it after a blank line, a block array gets it as one more text block at the end, and an absent `system` becomes the instruction. The host's leading blocks stay first, because Claude Code gateways (TeamClaude) refuse Sonnet requests with 429 `rate_limit_error` when the system prompt does not start with the Claude Code blocks. The OpenAI walkers still put the instruction first: a leading `system` message in chat completions, a prefix of `instructions` in responses.
202
203
 
@@ -214,3 +215,4 @@ Connectors publish facts through `proxy.session(id).publisher(source)`. Facts ar
214
215
  - Fields the walkers do not rewrite (`tools`, metadata, unknown fields) are forwarded as they are. The [leak check](#leak-check) warns when one of them carries a known original; it does not block the request.
215
216
  - Leak warnings carry kind, source and JSON path, never the value. Trace files and any other `TraceSink` get the clear text that leaked: keep them local, never commit them, delete them once reviewed.
216
217
  - Mappings, facts, the signature cache and the trace hashes are kept in memory and are lost on restart.
218
+ - omp writes the request of every HTTP 400 answer to `~/.omp/logs/http-400-requests/<id>.json` (mode `0644`). That copy is what omp sent to the proxy, before anonymization, so it holds the personal data in clear text. It stays on the machine; delete it when anon is in use.
package/dist/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as TransformProxy } from "./server-DdXMr4yF.mjs";
1
+ import { i as TransformProxy } from "./server-BWc1mocm.mjs";
2
2
  /** Environment variable that supplies the client token when `--token` is absent. */
3
3
  export declare const TOKEN_ENV = "SKAILE_TRANSFORM_PROXY_TOKEN";
4
4
  /** Environment variable with the trace directory (relative to cwd); overrides `--trace-dir` and the config's `trace.dir`. */
@@ -9,6 +9,11 @@ export interface CliEnvironment {
9
9
  readonly env: Readonly<Record<string, string | undefined>>;
10
10
  readonly stdout: (line: string) => void;
11
11
  readonly stderr: (line: string) => void;
12
+ /**
13
+ * Called once the proxy listens, before the `listening` line: a parent reacts to that line (with SIGTERM, or by
14
+ * closing stdin), so the caller's shutdown handlers must be in place by then.
15
+ */
16
+ readonly onStart?: (run: Required<CliRun>) => void;
12
17
  }
13
18
  /** `proxy` is set only when the server started; the caller owns its shutdown. */
14
19
  export interface CliRun {
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as createTransformProxy, o as resolveProxyConfig, p as createFileTraceSink, x as errorMessage } from "./config-DHxYF3Tv.mjs";
2
+ import { a as createTransformProxy, s as createFileTraceSink, x as errorMessage, y as resolveProxyConfig } from "./server-DTSaU_aK.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -77,12 +77,14 @@ async function runCli(argv, io) {
77
77
  }) }
78
78
  });
79
79
  if (traceDir !== void 0) io.stderr(format("info", `trace: ${traceDir}`, []));
80
- io.stdout(`listening ${proxy.url}`);
81
- return {
80
+ const run = {
82
81
  code: 0,
83
82
  proxy,
84
83
  exitWithStdin: values["exit-with-stdin"] === true
85
84
  };
85
+ io.onStart?.(run);
86
+ io.stdout(`listening ${proxy.url}`);
87
+ return run;
86
88
  } catch (err) {
87
89
  io.stderr(errorMessage(err));
88
90
  return { code: 1 };
@@ -112,27 +114,26 @@ if (isEntryPoint()) {
112
114
  home: homedir(),
113
115
  env: process.env,
114
116
  stdout: (line) => console.log(line),
115
- stderr: (line) => console.error(line)
117
+ stderr: (line) => console.error(line),
118
+ onStart: ({ proxy, exitWithStdin }) => {
119
+ let stopping = false;
120
+ const stop = () => {
121
+ if (stopping) return;
122
+ stopping = true;
123
+ proxy.close().then(() => process.exit(0), (err) => {
124
+ console.error(errorMessage(err));
125
+ process.exit(1);
126
+ });
127
+ };
128
+ process.once("SIGINT", stop);
129
+ process.once("SIGTERM", stop);
130
+ if (exitWithStdin) {
131
+ process.stdin.once("end", stop);
132
+ process.stdin.once("close", stop);
133
+ process.stdin.resume();
134
+ }
135
+ }
116
136
  });
117
137
  if (run.proxy === void 0) process.exitCode = run.code;
118
- else {
119
- const proxy = run.proxy;
120
- let stopping = false;
121
- const stop = () => {
122
- if (stopping) return;
123
- stopping = true;
124
- proxy.close().then(() => process.exit(0), (err) => {
125
- console.error(errorMessage(err));
126
- process.exit(1);
127
- });
128
- };
129
- process.once("SIGINT", stop);
130
- process.once("SIGTERM", stop);
131
- if (run.exitWithStdin === true) {
132
- process.stdin.once("end", stop);
133
- process.stdin.once("close", stop);
134
- process.stdin.resume();
135
- }
136
- }
137
138
  }
138
139
  export { TOKEN_ENV, TRACE_DIR_ENV, runCli };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as TraceSink, a as TransformProxyOptions, b as TransformModule, c as TransformSpec, d as SessionFactPublisher, f as SessionFacts, g as TraceLeak, h as TraceEvent, i as TransformProxy, l as Logger, m as TextLeafKind, n as SESSION_HEADER, o as UPSTREAM_HEADER, p as TextLeaf, r as TOKEN_HEADER, s as createTransformProxy, t as PIPELINE_HEADER, u as SessionFact, v as Transform, x as TransformSessionContext, y as TransformFactory } from "./server-DdXMr4yF.mjs";
1
+ import { _ as TraceSink, a as TransformProxyOptions, b as TransformModule, c as TransformSpec, d as SessionFactPublisher, f as SessionFacts, g as TraceLeak, h as TraceEvent, i as TransformProxy, l as Logger, m as TextLeafKind, n as SESSION_HEADER, o as UPSTREAM_HEADER, p as TextLeaf, r as TOKEN_HEADER, s as createTransformProxy, t as PIPELINE_HEADER, u as SessionFact, v as Transform, x as TransformSessionContext, y as TransformFactory } from "./server-BWc1mocm.mjs";
2
2
  /** One stage's streamed inbound transformer, as returned by `Transform.inboundStream()`. */
3
3
  interface InboundStream {
4
4
  push(chunk: string): string;
@@ -10,14 +10,6 @@ interface InboundStream {
10
10
  * The empty list is the identity.
11
11
  */
12
12
  export declare function composeTransforms(stages: readonly Transform[], log?: Logger): Transform;
13
- /** Default logger: drops everything. Hosts pass their own `Logger`. */
14
- export declare const silentLogger: Logger;
15
- /** One session's facts: the read side for transforms and the per-source publishers for connectors. */
16
- interface SessionFactStore {
17
- readonly facts: SessionFacts;
18
- publisher(source: string): SessionFactPublisher;
19
- }
20
- export declare function createSessionFacts(): SessionFactStore;
21
13
  export declare const CONFIG_FILE_NAME = "transform-proxy.yaml";
22
14
  /** The YAML-backed subset of the proxy options. `baseDir` is the config file's directory. */
23
15
  interface ProxyConfig {
@@ -55,6 +47,14 @@ export declare function resolveProxyConfig(lookup: ConfigLookup): ResolvedProxyC
55
47
  export declare function loadProxyConfig(path: string): ProxyConfig;
56
48
  /** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
57
49
  export declare function parseProxyConfig(source: unknown, baseDir: string): ProxyConfig;
50
+ /** One session's facts: the read side for transforms and the per-source publishers for connectors. */
51
+ interface SessionFactStore {
52
+ readonly facts: SessionFacts;
53
+ publisher(source: string): SessionFactPublisher;
54
+ }
55
+ export declare function createSessionFacts(): SessionFactStore;
56
+ /** Default logger: drops everything. Hosts pass their own `Logger`. */
57
+ export declare const silentLogger: Logger;
58
58
  export declare const DEFAULT_TRACE_MAX_CHARS = 262144;
59
59
  /**
60
60
  * One JSON line (no trailing newline). When the JSON of `content` is longer than `maxChars`,
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- export { t as CONFIG_FILE_NAME, f as DEFAULT_TRACE_MAX_CHARS, s as PIPELINE_HEADER, c as SESSION_HEADER, l as TOKEN_HEADER, u as UPSTREAM_HEADER, v as UnsupportedContentError, _ as anthropicWalker, b as composeTransforms, n as configCandidates, p as createFileTraceSink, y as createSessionFacts, d as createTransformProxy, r as findProxyConfig, i as loadProxyConfig, g as openaiChatWalker, h as openaiResponsesWalker, a as parseProxyConfig, o as resolveProxyConfig, m as serializeTraceEvent, S as silentLogger } from "./config-DHxYF3Tv.mjs";
1
+ export { m as CONFIG_FILE_NAME, o as DEFAULT_TRACE_MAX_CHARS, t as PIPELINE_HEADER, n as SESSION_HEADER, r as TOKEN_HEADER, i as UPSTREAM_HEADER, f as UnsupportedContentError, d as anthropicWalker, b as composeTransforms, h as configCandidates, s as createFileTraceSink, p as createSessionFacts, a as createTransformProxy, g as findProxyConfig, _ as loadProxyConfig, u as openaiChatWalker, l as openaiResponsesWalker, v as parseProxyConfig, y as resolveProxyConfig, c as serializeTraceEvent, S as silentLogger } from "./server-DTSaU_aK.mjs";
@@ -1,7 +1,7 @@
1
- import { c as SESSION_HEADER, i as loadProxyConfig, l as TOKEN_HEADER, n as configCandidates, r as findProxyConfig, u as UPSTREAM_HEADER, x as errorMessage } from "../config-DHxYF3Tv.mjs";
2
- import { randomBytes, randomUUID } from "node:crypto";
1
+ import { _ as loadProxyConfig, g as findProxyConfig, h as configCandidates, i as UPSTREAM_HEADER, n as SESSION_HEADER, r as TOKEN_HEADER, x as errorMessage } from "../server-DTSaU_aK.mjs";
3
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
3
  import { dirname, join, resolve } from "node:path";
4
+ import { randomBytes, randomUUID } from "node:crypto";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { homedir } from "node:os";
7
7
  import { spawn } from "node:child_process";
@@ -14,7 +14,10 @@ interface TextLeaf {
14
14
  interface Transform {
15
15
  /** Added to the system prompt by each wire walker (Anthropic: appended last; OpenAI: placed first); omit when the model needs no instruction. */
16
16
  readonly instruction?: string;
17
- /** Host -> model, one text leaf. A throw refuses the request (502 `transform_failed`); nothing is sent. */
17
+ /**
18
+ * Host -> model, one text leaf. The leaves of one request are rewritten concurrently, so calls of one instance may
19
+ * overlap. A throw refuses the request (502 `transform_failed`); nothing is sent.
20
+ */
18
21
  outbound(text: string, leaf: TextLeaf): Promise<string>;
19
22
  /** Model -> host, one complete text. */
20
23
  inbound(text: string): string;
@@ -146,7 +149,7 @@ interface TransformProxy {
146
149
  /**
147
150
  * Host-only, in-process: each stage's `inspect()` result of the session's pipeline instance, keyed by transform id.
148
151
  * `{}` when the session or pipeline is unknown, the instance is not built, or its build failed.
149
- * Never builds an instance or creates session state; there is no HTTP route for it.
152
+ * Never builds an instance or creates session state; there is no HTTP route for it. Rejects when a stage's `inspect()` throws.
150
153
  */
151
154
  inspect(sessionId: string, pipelineId?: string): Promise<Record<string, unknown>>;
152
155
  close(): Promise<void>;
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
- import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
3
- import { createServer } from "node:http";
4
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
5
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
6
- import { pathToFileURL } from "node:url";
7
4
  import { parse } from "yaml";
5
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
6
+ import { createServer } from "node:http";
7
+ import { pathToFileURL } from "node:url";
8
8
  // src/log.ts
9
9
  /** Default logger: drops everything. Hosts pass their own `Logger`. */
10
10
  const silentLogger = {
@@ -92,6 +92,112 @@ function composeStreams(streams) {
92
92
  }
93
93
  };
94
94
  }
95
+ // src/config.ts
96
+ const CONFIG_FILE_NAME = "transform-proxy.yaml";
97
+ /** The implicit lookup locations, in order: `./.skaile/` then `~/.config/skaile/`. */
98
+ function configCandidates(cwd, home) {
99
+ return [join(cwd, ".skaile", CONFIG_FILE_NAME), join(home, ".config", "skaile", CONFIG_FILE_NAME)];
100
+ }
101
+ /**
102
+ * Finds the config file: the explicit path when given (it must exist), otherwise the first
103
+ * existing implicit candidate. Returns `undefined` when no implicit candidate exists.
104
+ */
105
+ function findProxyConfig(lookup) {
106
+ if (lookup.explicit !== void 0) {
107
+ const path = resolve(lookup.cwd, lookup.explicit);
108
+ if (!existsSync(path)) throw new Error(`transform proxy config not found: ${path}`);
109
+ return path;
110
+ }
111
+ return configCandidates(lookup.cwd, lookup.home).find((candidate) => existsSync(candidate));
112
+ }
113
+ /** Finds and loads the config. Throws, naming the searched paths, when none is found. */
114
+ function resolveProxyConfig(lookup) {
115
+ const path = findProxyConfig(lookup);
116
+ if (path === void 0) {
117
+ const searched = configCandidates(lookup.cwd, lookup.home).join(", ");
118
+ throw new Error(`no transform proxy config found (pass --config, or create one at ${searched})`);
119
+ }
120
+ return {
121
+ path,
122
+ options: loadProxyConfig(path)
123
+ };
124
+ }
125
+ const TOP_LEVEL_KEYS = {
126
+ transforms: true,
127
+ pipelines: true,
128
+ defaultPipeline: true,
129
+ allowUpstreams: true,
130
+ trace: true
131
+ };
132
+ const TRACE_KEYS = {
133
+ dir: true,
134
+ maxChars: true
135
+ };
136
+ /** Reads and validates a YAML config file. Unknown keys are rejected so that typos fail loudly. */
137
+ function loadProxyConfig(path) {
138
+ const source = parse(readFileSync(path, "utf8"));
139
+ try {
140
+ return parseProxyConfig(source ?? {}, dirname(path));
141
+ } catch (err) {
142
+ throw new Error(`invalid transform proxy config ${path}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
143
+ }
144
+ }
145
+ /** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
146
+ function parseProxyConfig(source, baseDir) {
147
+ if (!isRecord$1(source)) throw new Error("the document must be a mapping");
148
+ for (const key of Object.keys(source)) if (!Object.hasOwn(TOP_LEVEL_KEYS, key)) throw new Error(`unknown key "${key}"`);
149
+ return {
150
+ baseDir,
151
+ transforms: parseTransforms(source.transforms),
152
+ pipelines: parsePipelines(source.pipelines),
153
+ ...source.defaultPipeline === void 0 ? {} : { defaultPipeline: requireString(source.defaultPipeline, "defaultPipeline") },
154
+ ...source.allowUpstreams === void 0 ? {} : { allowUpstreams: requireStringList(source.allowUpstreams, "allowUpstreams") },
155
+ ...source.trace === void 0 ? {} : { trace: parseTrace(source.trace, baseDir) }
156
+ };
157
+ }
158
+ function parseTransforms(value) {
159
+ if (value === void 0) return {};
160
+ if (!isRecord$1(value)) throw new Error("transforms must be a mapping of transform id to { module, config }");
161
+ const out = {};
162
+ for (const [id, spec] of Object.entries(value)) {
163
+ if (!isRecord$1(spec)) throw new Error(`transforms.${id} must be a mapping with a module`);
164
+ const module = requireString(spec.module, `transforms.${id}.module`);
165
+ out[id] = spec.config === void 0 ? { module } : {
166
+ module,
167
+ config: spec.config
168
+ };
169
+ }
170
+ return out;
171
+ }
172
+ function parsePipelines(value) {
173
+ if (value === void 0) return {};
174
+ if (!isRecord$1(value)) throw new Error("pipelines must be a mapping of pipeline id to a list of transform ids");
175
+ const out = {};
176
+ for (const [id, stages] of Object.entries(value)) out[id] = requireStringList(stages, `pipelines.${id}`);
177
+ return out;
178
+ }
179
+ function parseTrace(value, baseDir) {
180
+ if (!isRecord$1(value)) throw new Error("trace must be a mapping with a dir");
181
+ for (const key of Object.keys(value)) if (!Object.hasOwn(TRACE_KEYS, key)) throw new Error(`unknown key "trace.${key}"`);
182
+ const dir = resolve(baseDir, requireString(value.dir, "trace.dir"));
183
+ if (value.maxChars === void 0) return { dir };
184
+ if (typeof value.maxChars !== "number" || !Number.isInteger(value.maxChars) || value.maxChars <= 0) throw new Error("trace.maxChars must be a positive integer");
185
+ return {
186
+ dir,
187
+ maxChars: value.maxChars
188
+ };
189
+ }
190
+ function requireString(value, field) {
191
+ if (typeof value !== "string" || value === "") throw new Error(`${field} must be a non-empty string`);
192
+ return value;
193
+ }
194
+ function requireStringList(value, field) {
195
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "")) throw new Error(`${field} must be a list of non-empty strings`);
196
+ return value;
197
+ }
198
+ function isRecord$1(value) {
199
+ return typeof value === "object" && value !== null && !Array.isArray(value);
200
+ }
95
201
  // src/facts.ts
96
202
  function createSessionFacts() {
97
203
  const bySource = /* @__PURE__ */ new Map();
@@ -256,17 +362,17 @@ function resolveImportCondition(specifier, root) {
256
362
  }
257
363
  }
258
364
  function exportTarget(exports, subpath) {
259
- return conditionTarget((isRecord$1(exports) && Object.keys(exports).some((key) => key.startsWith(".")) ? exports : { ".": exports })[subpath]);
365
+ return conditionTarget((isRecord(exports) && Object.keys(exports).some((key) => key.startsWith(".")) ? exports : { ".": exports })[subpath]);
260
366
  }
261
367
  function conditionTarget(entry) {
262
368
  if (typeof entry === "string") return entry;
263
- if (!isRecord$1(entry)) return void 0;
369
+ if (!isRecord(entry)) return void 0;
264
370
  return conditionTarget(entry.import) ?? conditionTarget(entry.default);
265
371
  }
266
372
  function isTransformModule(value) {
267
- return isRecord$1(value) && typeof value.id === "string" && typeof value.open === "function";
373
+ return isRecord(value) && typeof value.id === "string" && typeof value.open === "function";
268
374
  }
269
- function isRecord$1(value) {
375
+ function isRecord(value) {
270
376
  return typeof value === "object" && value !== null && !Array.isArray(value);
271
377
  }
272
378
  // src/wire/json-args.ts
@@ -410,28 +516,37 @@ function refuse$1(block) {
410
516
  function malformed$2(slot) {
411
517
  throw new UnsupportedContentError(`Anthropic ${slot} has an unexpected shape`);
412
518
  }
413
- async function outbound$2(transform, text, leaf) {
414
- return text === "" ? text : transform.outbound(text, leaf);
519
+ /** Starts every queued rewrite at once; resolves when all results are written back, rejects on the first failure. */
520
+ async function runAll$2(pending) {
521
+ await Promise.all(pending.map((run) => run()));
522
+ }
523
+ /** Queues the rewrite of one text leaf; `set` later replaces the original, which stays in place until then. */
524
+ function queue$2(pending, transform, text, leaf, set) {
525
+ if (text !== "") pending.push(async () => set(await transform.outbound(text, leaf)));
415
526
  }
416
527
  /** A string, or an array of blocks whose `text` blocks are rewritten; anything else is refused. */
417
- async function rewriteText(content, slot, rewrite) {
418
- if (typeof content === "string") return rewrite(content);
419
- if (!Array.isArray(content)) return malformed$2(slot);
528
+ function rewriteText(content, slot, rewrite, set) {
529
+ if (typeof content === "string") {
530
+ rewrite(content, set);
531
+ return;
532
+ }
533
+ if (!Array.isArray(content)) malformed$2(slot);
420
534
  const out = [];
421
535
  for (const block of content) {
422
- if (!isObj$2(block)) return malformed$2(`${slot} block`);
536
+ if (!isObj$2(block)) malformed$2(`${slot} block`);
423
537
  refuse$1(block);
424
538
  if (block.type !== "text") {
425
539
  out.push(block);
426
540
  continue;
427
541
  }
428
- if (typeof block.text !== "string") return malformed$2(`${slot} text block`);
429
- out.push({
430
- ...block,
431
- text: await rewrite(block.text)
542
+ if (typeof block.text !== "string") malformed$2(`${slot} text block`);
543
+ const copy = { ...block };
544
+ out.push(copy);
545
+ rewrite(block.text, (text) => {
546
+ copy.text = text;
432
547
  });
433
548
  }
434
- return out;
549
+ set(out);
435
550
  }
436
551
  /**
437
552
  * Host-written system text that Anthropic's Claude subscription (OAuth) check reads verbatim: the
@@ -448,24 +563,37 @@ function gatePrefix(text) {
448
563
  const end = text.indexOf("\n");
449
564
  return end === -1 ? text : text.slice(0, end);
450
565
  }
451
- async function rewriteSystemText(text, transform) {
566
+ function rewriteSystemText(text, transform, pending, set) {
452
567
  const kept = gatePrefix(text);
453
- return kept + await outbound$2(transform, text.slice(kept.length), { kind: "system" });
568
+ queue$2(pending, transform, text.slice(kept.length), { kind: "system" }, (rest) => set(kept + rest));
454
569
  }
455
570
  /** Every string leaf of a JSON value (tool-call input); keys stay as they are. */
456
- async function rewriteStrings$2(value, leaf, transform) {
457
- if (typeof value === "string") return outbound$2(transform, value, leaf);
571
+ function rewriteStrings$2(value, leaf, transform, pending, set) {
572
+ if (typeof value === "string") {
573
+ queue$2(pending, transform, value, leaf, set);
574
+ return;
575
+ }
458
576
  if (Array.isArray(value)) {
459
577
  const out = [];
460
- for (const item of value) out.push(await rewriteStrings$2(item, leaf, transform));
461
- return out;
578
+ for (const item of value) {
579
+ const index = out.push(item) - 1;
580
+ rewriteStrings$2(item, leaf, transform, pending, (rewritten) => {
581
+ out[index] = rewritten;
582
+ });
583
+ }
584
+ set(out);
585
+ return;
462
586
  }
463
587
  if (isObj$2(value)) {
464
588
  const out = {};
465
- for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings$2(item, leaf, transform);
466
- return out;
589
+ for (const [key, item] of Object.entries(value)) {
590
+ out[key] = item;
591
+ rewriteStrings$2(item, leaf, transform, pending, (rewritten) => {
592
+ out[key] = rewritten;
593
+ });
594
+ }
595
+ set(out);
467
596
  }
468
- return value;
469
597
  }
470
598
  /** Every string leaf of a JSON value, restored inbound. */
471
599
  function restoreStrings$1(value, transform) {
@@ -479,7 +607,7 @@ function restoreStrings$1(value, transform) {
479
607
  return value;
480
608
  }
481
609
  /** A thinking block: swaps in the cached model text when its signature is known, else rewrites it. */
482
- async function rewriteThinking(block, ctx) {
610
+ function rewriteThinking(block, ctx, pending) {
483
611
  if (typeof block.thinking !== "string") return malformed$2("thinking block");
484
612
  if (typeof block.signature === "string") {
485
613
  const cached = ctx.signatures.get(signatureKey$1(block.signature));
@@ -488,38 +616,48 @@ async function rewriteThinking(block, ctx) {
488
616
  thinking: cached
489
617
  };
490
618
  }
491
- return {
492
- ...block,
493
- thinking: await outbound$2(ctx.transform, block.thinking, { kind: "thinking" })
494
- };
619
+ const copy = { ...block };
620
+ queue$2(pending, ctx.transform, block.thinking, { kind: "thinking" }, (text) => {
621
+ copy.thinking = text;
622
+ });
623
+ return copy;
495
624
  }
496
- async function rewriteContent$2(content, role, toolNames, ctx) {
625
+ function rewriteContent$2(content, role, toolNames, ctx, pending, set) {
497
626
  const textKind = role === "assistant" ? "assistant" : "user";
498
- if (typeof content === "string") return outbound$2(ctx.transform, content, { kind: textKind });
499
- if (!Array.isArray(content)) return malformed$2("message content");
627
+ if (typeof content === "string") {
628
+ queue$2(pending, ctx.transform, content, { kind: textKind }, set);
629
+ return;
630
+ }
631
+ if (!Array.isArray(content)) malformed$2("message content");
500
632
  const out = [];
501
633
  for (const block of content) {
502
- if (!isObj$2(block)) return malformed$2("message content block");
634
+ if (!isObj$2(block)) malformed$2("message content block");
503
635
  refuse$1(block);
504
636
  switch (block.type) {
505
- case "text":
506
- if (typeof block.text !== "string") return malformed$2("text block");
507
- out.push({
508
- ...block,
509
- text: await outbound$2(ctx.transform, block.text, { kind: textKind })
637
+ case "text": {
638
+ if (typeof block.text !== "string") malformed$2("text block");
639
+ const copy = { ...block };
640
+ out.push(copy);
641
+ queue$2(pending, ctx.transform, block.text, { kind: textKind }, (text) => {
642
+ copy.text = text;
510
643
  });
511
644
  break;
645
+ }
512
646
  case "thinking":
513
- out.push(await rewriteThinking(block, ctx));
647
+ out.push(rewriteThinking(block, ctx, pending));
514
648
  break;
515
649
  case "tool_use": {
516
650
  const toolName = typeof block.name === "string" ? block.name : void 0;
517
- out.push({
651
+ const copy = {
518
652
  ...block,
519
- input: await rewriteStrings$2(block.input, {
520
- kind: "toolCall",
521
- toolName
522
- }, ctx.transform)
653
+ input: block.input
654
+ };
655
+ out.push(copy);
656
+ rewriteStrings$2(block.input, {
657
+ kind: "toolCall",
658
+ toolName
659
+ }, ctx.transform, pending, (input) => {
660
+ copy.input = input;
523
661
  });
524
662
  break;
525
663
  }
@@ -529,19 +667,20 @@ async function rewriteContent$2(content, role, toolNames, ctx) {
529
667
  break;
530
668
  }
531
669
  const toolName = typeof block.tool_use_id === "string" ? toolNames.get(block.tool_use_id) : void 0;
532
- out.push({
533
- ...block,
534
- content: await rewriteText(block.content, "tool_result content", (text) => outbound$2(ctx.transform, text, {
535
- kind: "toolResult",
536
- toolName
537
- }))
670
+ const copy = { ...block };
671
+ out.push(copy);
672
+ rewriteText(block.content, "tool_result content", (text, setText) => queue$2(pending, ctx.transform, text, {
673
+ kind: "toolResult",
674
+ toolName
675
+ }, setText), (rewritten) => {
676
+ copy.content = rewritten;
538
677
  });
539
678
  break;
540
679
  }
541
680
  default: out.push(block);
542
681
  }
543
682
  }
544
- return out;
683
+ set(out);
545
684
  }
546
685
  /** Maps each `tool_use` id to its tool name, so tool results can name their tool. */
547
686
  function toolNamesById(messages) {
@@ -752,21 +891,26 @@ const anthropicWalker = {
752
891
  async request(body, ctx) {
753
892
  if (!isObj$2(body)) return malformed$2("request body");
754
893
  const out = { ...body };
755
- if (body.system !== void 0) out.system = await rewriteText(body.system, "system", (text) => rewriteSystemText(text, ctx.transform));
756
- if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
894
+ const pending = [];
895
+ if (body.system !== void 0) rewriteText(body.system, "system", (text, set) => rewriteSystemText(text, ctx.transform, pending, set), (system) => {
896
+ out.system = system;
897
+ });
757
898
  if (body.messages !== void 0) {
758
899
  if (!Array.isArray(body.messages)) return malformed$2("messages");
759
900
  const toolNames = toolNamesById(body.messages);
760
901
  const messages = [];
761
902
  for (const message of body.messages) {
762
903
  if (!isObj$2(message)) return malformed$2("message");
763
- messages.push({
764
- ...message,
765
- content: await rewriteContent$2(message.content, message.role, toolNames, ctx)
904
+ const copy = { ...message };
905
+ messages.push(copy);
906
+ rewriteContent$2(message.content, message.role, toolNames, ctx, pending, (content) => {
907
+ copy.content = content;
766
908
  });
767
909
  }
768
910
  out.messages = messages;
769
911
  }
912
+ await runAll$2(pending);
913
+ if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
770
914
  return out;
771
915
  },
772
916
  response(body, ctx) {
@@ -809,47 +953,69 @@ const REFUSED_PARTS$1 = {
809
953
  };
810
954
  /** Reasoning fields used by OpenAI-compatible providers. */
811
955
  const REASONING_FIELDS = ["reasoning_content", "reasoning"];
812
- async function outbound$1(transform, text, leaf) {
813
- return text === "" ? text : transform.outbound(text, leaf);
956
+ /** Starts every queued rewrite at once; resolves when all results are written back, rejects on the first failure. */
957
+ async function runAll$1(pending) {
958
+ await Promise.all(pending.map((run) => run()));
959
+ }
960
+ /** Queues the rewrite of one text leaf; `set` later replaces the original, which stays in place until then. */
961
+ function queue$1(pending, transform, text, leaf, set) {
962
+ if (text !== "") pending.push(async () => set(await transform.outbound(text, leaf)));
814
963
  }
815
964
  /** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
816
965
  function malformed$1(slot) {
817
966
  throw new UnsupportedContentError(`OpenAI chat ${slot} has an unexpected shape`);
818
967
  }
819
968
  /** A string, or an array of parts whose `text` parts are rewritten; `null`/absent carries no text; anything else is refused. */
820
- async function rewriteContent$1(content, leaf, transform) {
821
- if (typeof content === "string") return outbound$1(transform, content, leaf);
822
- if (content === void 0 || content === null) return content;
823
- if (!Array.isArray(content)) return malformed$1("message content");
969
+ function rewriteContent$1(content, leaf, transform, pending, set) {
970
+ if (typeof content === "string") {
971
+ queue$1(pending, transform, content, leaf, set);
972
+ return;
973
+ }
974
+ if (content === void 0 || content === null) return;
975
+ if (!Array.isArray(content)) malformed$1("message content");
824
976
  const out = [];
825
977
  for (const part of content) {
826
- if (!isObj$1(part)) return malformed$1("content part");
978
+ if (!isObj$1(part)) malformed$1("content part");
827
979
  if (typeof part.type === "string" && REFUSED_PARTS$1[part.type] === true) throw new UnsupportedContentError(`OpenAI chat ${part.type} content cannot be transformed`);
828
980
  if (part.type !== "text") {
829
981
  out.push(part);
830
982
  continue;
831
983
  }
832
- if (typeof part.text !== "string") return malformed$1("text part");
833
- out.push({
834
- ...part,
835
- text: await outbound$1(transform, part.text, leaf)
984
+ if (typeof part.text !== "string") malformed$1("text part");
985
+ const copy = { ...part };
986
+ out.push(copy);
987
+ queue$1(pending, transform, part.text, leaf, (text) => {
988
+ copy.text = text;
836
989
  });
837
990
  }
838
- return out;
991
+ set(out);
839
992
  }
840
- async function rewriteStrings$1(value, leaf, transform) {
841
- if (typeof value === "string") return outbound$1(transform, value, leaf);
993
+ function rewriteStrings$1(value, leaf, transform, pending, set) {
994
+ if (typeof value === "string") {
995
+ queue$1(pending, transform, value, leaf, set);
996
+ return;
997
+ }
842
998
  if (Array.isArray(value)) {
843
999
  const out = [];
844
- for (const item of value) out.push(await rewriteStrings$1(item, leaf, transform));
845
- return out;
1000
+ for (const item of value) {
1001
+ const index = out.push(item) - 1;
1002
+ rewriteStrings$1(item, leaf, transform, pending, (rewritten) => {
1003
+ out[index] = rewritten;
1004
+ });
1005
+ }
1006
+ set(out);
1007
+ return;
846
1008
  }
847
1009
  if (isObj$1(value)) {
848
1010
  const out = {};
849
- for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings$1(item, leaf, transform);
850
- return out;
1011
+ for (const [key, item] of Object.entries(value)) {
1012
+ out[key] = item;
1013
+ rewriteStrings$1(item, leaf, transform, pending, (rewritten) => {
1014
+ out[key] = rewritten;
1015
+ });
1016
+ }
1017
+ set(out);
851
1018
  }
852
- return value;
853
1019
  }
854
1020
  function restoreStrings(value, transform) {
855
1021
  if (typeof value === "string") return transform.inbound(value);
@@ -862,14 +1028,23 @@ function restoreStrings(value, transform) {
862
1028
  return value;
863
1029
  }
864
1030
  /** Tool-call arguments are a JSON string: rewrite its string leaves, or the raw text when it is not JSON. */
865
- async function rewriteArguments$1(args, leaf, transform) {
1031
+ function rewriteArguments$1(args, leaf, transform, pending, set) {
866
1032
  let parsed;
867
1033
  try {
868
1034
  parsed = JSON.parse(args);
869
1035
  } catch {
870
- return outbound$1(transform, args, leaf);
1036
+ queue$1(pending, transform, args, leaf, set);
1037
+ return;
871
1038
  }
872
- return JSON.stringify(await rewriteStrings$1(parsed, leaf, transform));
1039
+ const leaves = [];
1040
+ let rewritten = parsed;
1041
+ rewriteStrings$1(parsed, leaf, transform, leaves, (value) => {
1042
+ rewritten = value;
1043
+ });
1044
+ pending.push(async () => {
1045
+ await runAll$1(leaves);
1046
+ set(JSON.stringify(rewritten));
1047
+ });
873
1048
  }
874
1049
  function restoreArguments$1(args, transform) {
875
1050
  let parsed;
@@ -880,7 +1055,7 @@ function restoreArguments$1(args, transform) {
880
1055
  }
881
1056
  return JSON.stringify(restoreStrings(parsed, transform));
882
1057
  }
883
- async function rewriteToolCalls(calls, transform, names) {
1058
+ function rewriteToolCalls(calls, transform, names, pending) {
884
1059
  if (calls === void 0 || calls === null) return calls;
885
1060
  if (!Array.isArray(calls)) return malformed$1("tool_calls");
886
1061
  const out = [];
@@ -894,53 +1069,59 @@ async function rewriteToolCalls(calls, transform, names) {
894
1069
  if (!isObj$1(fn) || typeof fn.arguments !== "string") return malformed$1("function tool call");
895
1070
  const toolName = typeof fn.name === "string" ? fn.name : void 0;
896
1071
  if (typeof call.id === "string" && toolName !== void 0) names.set(call.id, toolName);
1072
+ const fnCopy = { ...fn };
897
1073
  out.push({
898
1074
  ...call,
899
- function: {
900
- ...fn,
901
- arguments: await rewriteArguments$1(fn.arguments, {
902
- kind: "toolCall",
903
- toolName
904
- }, transform)
905
- }
1075
+ function: fnCopy
1076
+ });
1077
+ rewriteArguments$1(fn.arguments, {
1078
+ kind: "toolCall",
1079
+ toolName
1080
+ }, transform, pending, (args) => {
1081
+ fnCopy.arguments = args;
906
1082
  });
907
1083
  }
908
1084
  return out;
909
1085
  }
910
- async function rewriteMessage(message, transform, names) {
1086
+ function rewriteMessage(message, transform, names, pending) {
911
1087
  const out = { ...message };
1088
+ const setContent = (content) => {
1089
+ out.content = content;
1090
+ };
912
1091
  switch (message.role) {
913
1092
  case "system":
914
1093
  case "developer":
915
- out.content = await rewriteContent$1(message.content, { kind: "system" }, transform);
1094
+ rewriteContent$1(message.content, { kind: "system" }, transform, pending, setContent);
916
1095
  break;
917
1096
  case "user":
918
- out.content = await rewriteContent$1(message.content, { kind: "user" }, transform);
1097
+ rewriteContent$1(message.content, { kind: "user" }, transform, pending, setContent);
919
1098
  break;
920
1099
  case "assistant":
921
1100
  for (const field of REASONING_FIELDS) {
922
1101
  const value = message[field];
923
- if (typeof value === "string") out[field] = await outbound$1(transform, value, { kind: "thinking" });
1102
+ if (typeof value === "string") queue$1(pending, transform, value, { kind: "thinking" }, (text) => {
1103
+ out[field] = text;
1104
+ });
924
1105
  else if (value !== void 0 && value !== null) return malformed$1(`assistant ${field}`);
925
1106
  }
926
- out.content = await rewriteContent$1(message.content, { kind: "assistant" }, transform);
927
- out.tool_calls = await rewriteToolCalls(message.tool_calls, transform, names);
1107
+ rewriteContent$1(message.content, { kind: "assistant" }, transform, pending, setContent);
1108
+ out.tool_calls = rewriteToolCalls(message.tool_calls, transform, names, pending);
928
1109
  if (message.tool_calls === void 0) delete out.tool_calls;
929
1110
  break;
930
1111
  case "tool": {
931
1112
  const toolName = typeof message.tool_call_id === "string" ? names.get(message.tool_call_id) : void 0;
932
- out.content = await rewriteContent$1(message.content, {
1113
+ rewriteContent$1(message.content, {
933
1114
  kind: "toolResult",
934
1115
  toolName
935
- }, transform);
1116
+ }, transform, pending, setContent);
936
1117
  break;
937
1118
  }
938
1119
  case "function": {
939
1120
  const toolName = typeof message.name === "string" ? message.name : void 0;
940
- out.content = await rewriteContent$1(message.content, {
1121
+ rewriteContent$1(message.content, {
941
1122
  kind: "toolResult",
942
1123
  toolName
943
- }, transform);
1124
+ }, transform, pending, setContent);
944
1125
  break;
945
1126
  }
946
1127
  default: return malformed$1("message role");
@@ -1192,11 +1373,13 @@ const openaiChatWalker = {
1192
1373
  if (body.messages !== void 0) {
1193
1374
  if (!Array.isArray(body.messages)) return malformed$1("messages");
1194
1375
  const names = /* @__PURE__ */ new Map();
1376
+ const pending = [];
1195
1377
  const messages = [];
1196
1378
  for (const message of body.messages) {
1197
1379
  if (!isObj$1(message)) return malformed$1("message");
1198
- messages.push(await rewriteMessage(message, ctx.transform, names));
1380
+ messages.push(rewriteMessage(message, ctx.transform, names, pending));
1199
1381
  }
1382
+ await runAll$1(pending);
1200
1383
  if (ctx.transform.instruction !== void 0) messages.unshift({
1201
1384
  role: "system",
1202
1385
  content: ctx.transform.instruction
@@ -1251,56 +1434,87 @@ const signatureKey = (itemId) => `responses:${itemId}`;
1251
1434
  function refuse(part) {
1252
1435
  if (typeof part.type === "string" && REFUSED_PARTS[part.type] === true) throw new UnsupportedContentError(`OpenAI responses ${part.type} content cannot be transformed`);
1253
1436
  }
1254
- async function outbound(transform, text, leaf) {
1255
- return text === "" ? text : transform.outbound(text, leaf);
1437
+ /** Starts every queued rewrite at once; resolves when all results are written back, rejects on the first failure. */
1438
+ async function runAll(pending) {
1439
+ await Promise.all(pending.map((run) => run()));
1440
+ }
1441
+ /** Queues the rewrite of one text leaf; `set` later replaces the original, which stays in place until then. */
1442
+ function queue(pending, transform, text, leaf, set) {
1443
+ if (text !== "") pending.push(async () => set(await transform.outbound(text, leaf)));
1256
1444
  }
1257
1445
  /** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
1258
1446
  function malformed(slot) {
1259
1447
  throw new UnsupportedContentError(`OpenAI responses ${slot} has an unexpected shape`);
1260
1448
  }
1261
1449
  /** A string, or an array of parts whose `input_text`/`output_text` parts are rewritten; anything else is refused. */
1262
- async function rewriteContent(content, slot, leaf, transform) {
1263
- if (typeof content === "string") return outbound(transform, content, leaf);
1264
- if (!Array.isArray(content)) return malformed(slot);
1450
+ function rewriteContent(content, slot, leaf, transform, pending, set) {
1451
+ if (typeof content === "string") {
1452
+ queue(pending, transform, content, leaf, set);
1453
+ return;
1454
+ }
1455
+ if (!Array.isArray(content)) malformed(slot);
1265
1456
  const out = [];
1266
1457
  for (const part of content) {
1267
- if (!isObj(part)) return malformed(`${slot} part`);
1458
+ if (!isObj(part)) malformed(`${slot} part`);
1268
1459
  refuse(part);
1269
1460
  if (part.type !== "input_text" && part.type !== "output_text") {
1270
1461
  out.push(part);
1271
1462
  continue;
1272
1463
  }
1273
- if (typeof part.text !== "string") return malformed(`${slot} ${part.type} part`);
1274
- out.push({
1275
- ...part,
1276
- text: await outbound(transform, part.text, leaf)
1464
+ if (typeof part.text !== "string") malformed(`${slot} ${part.type} part`);
1465
+ const copy = { ...part };
1466
+ out.push(copy);
1467
+ queue(pending, transform, part.text, leaf, (text) => {
1468
+ copy.text = text;
1277
1469
  });
1278
1470
  }
1279
- return out;
1471
+ set(out);
1280
1472
  }
1281
- async function rewriteStrings(value, leaf, transform) {
1282
- if (typeof value === "string") return outbound(transform, value, leaf);
1473
+ function rewriteStrings(value, leaf, transform, pending, set) {
1474
+ if (typeof value === "string") {
1475
+ queue(pending, transform, value, leaf, set);
1476
+ return;
1477
+ }
1283
1478
  if (Array.isArray(value)) {
1284
1479
  const out = [];
1285
- for (const item of value) out.push(await rewriteStrings(item, leaf, transform));
1286
- return out;
1480
+ for (const item of value) {
1481
+ const index = out.push(item) - 1;
1482
+ rewriteStrings(item, leaf, transform, pending, (rewritten) => {
1483
+ out[index] = rewritten;
1484
+ });
1485
+ }
1486
+ set(out);
1487
+ return;
1287
1488
  }
1288
1489
  if (isObj(value)) {
1289
1490
  const out = {};
1290
- for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings(item, leaf, transform);
1291
- return out;
1491
+ for (const [key, item] of Object.entries(value)) {
1492
+ out[key] = item;
1493
+ rewriteStrings(item, leaf, transform, pending, (rewritten) => {
1494
+ out[key] = rewritten;
1495
+ });
1496
+ }
1497
+ set(out);
1292
1498
  }
1293
- return value;
1294
1499
  }
1295
1500
  /** Tool-call arguments: a JSON string whose string leaves are rewritten; raw text when it is not JSON. */
1296
- async function rewriteArguments(args, leaf, transform) {
1501
+ function rewriteArguments(args, leaf, transform, pending, set) {
1297
1502
  let parsed;
1298
1503
  try {
1299
1504
  parsed = JSON.parse(args);
1300
1505
  } catch {
1301
- return outbound(transform, args, leaf);
1506
+ queue(pending, transform, args, leaf, set);
1507
+ return;
1302
1508
  }
1303
- return JSON.stringify(await rewriteStrings(parsed, leaf, transform));
1509
+ const leaves = [];
1510
+ let rewritten = parsed;
1511
+ rewriteStrings(parsed, leaf, transform, leaves, (value) => {
1512
+ rewritten = value;
1513
+ });
1514
+ pending.push(async () => {
1515
+ await runAll(leaves);
1516
+ set(JSON.stringify(rewritten));
1517
+ });
1304
1518
  }
1305
1519
  /** Restores the string values of a JSON text, keeping its formatting; restored originals are JSON-escaped. */
1306
1520
  function restoreArguments(args, transform) {
@@ -1324,16 +1538,26 @@ function cachedSummary(item, ctx) {
1324
1538
  if (!Array.isArray(texts) || !Array.isArray(item.summary) || texts.length !== item.summary.length) return void 0;
1325
1539
  return texts;
1326
1540
  }
1327
- async function rewriteReasoning(item, ctx) {
1541
+ function rewriteReasoning(item, ctx, pending) {
1328
1542
  if (item.summary === void 0) return item;
1329
1543
  if (!Array.isArray(item.summary)) return malformed("reasoning summary");
1330
1544
  for (const part of item.summary) if (!isObj(part) || typeof part.text !== "string") return malformed("reasoning summary part");
1331
1545
  const cached = cachedSummary(item, ctx);
1332
1546
  const summary = [];
1333
- for (const [i, part] of item.summary.entries()) summary.push({
1334
- ...part,
1335
- text: cached !== void 0 ? cached[i] : await outbound(ctx.transform, part.text, { kind: "thinking" })
1336
- });
1547
+ for (const [i, part] of item.summary.entries()) {
1548
+ if (cached !== void 0) {
1549
+ summary.push({
1550
+ ...part,
1551
+ text: cached[i]
1552
+ });
1553
+ continue;
1554
+ }
1555
+ const copy = { ...part };
1556
+ summary.push(copy);
1557
+ queue(pending, ctx.transform, part.text, { kind: "thinking" }, (text) => {
1558
+ copy.text = text;
1559
+ });
1560
+ }
1337
1561
  return {
1338
1562
  ...item,
1339
1563
  summary
@@ -1349,38 +1573,39 @@ function toolNamesByCallId(items) {
1349
1573
  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);
1350
1574
  return names;
1351
1575
  }
1352
- async function rewriteItem(item, toolNames, ctx) {
1576
+ function rewriteItem(item, toolNames, ctx, pending) {
1577
+ const out = { ...item };
1353
1578
  if (item.type === "message" || item.type === void 0 && item.role !== void 0) {
1354
1579
  const kind = roleKind(item.role);
1355
1580
  if (kind === void 0) return malformed("message role");
1356
- return {
1357
- ...item,
1358
- content: await rewriteContent(item.content, "message content", { kind }, ctx.transform)
1359
- };
1581
+ rewriteContent(item.content, "message content", { kind }, ctx.transform, pending, (content) => {
1582
+ out.content = content;
1583
+ });
1584
+ return out;
1360
1585
  }
1361
1586
  switch (item.type) {
1362
1587
  case "function_call": {
1363
1588
  if (typeof item.arguments !== "string") return malformed("function_call arguments");
1364
1589
  const toolName = typeof item.name === "string" ? item.name : void 0;
1365
- return {
1366
- ...item,
1367
- arguments: await rewriteArguments(item.arguments, {
1368
- kind: "toolCall",
1369
- toolName
1370
- }, ctx.transform)
1371
- };
1590
+ rewriteArguments(item.arguments, {
1591
+ kind: "toolCall",
1592
+ toolName
1593
+ }, ctx.transform, pending, (args) => {
1594
+ out.arguments = args;
1595
+ });
1596
+ return out;
1372
1597
  }
1373
1598
  case "function_call_output": {
1374
1599
  const toolName = typeof item.call_id === "string" ? toolNames.get(item.call_id) : void 0;
1375
- return {
1376
- ...item,
1377
- output: await rewriteContent(item.output, "function_call_output output", {
1378
- kind: "toolResult",
1379
- toolName
1380
- }, ctx.transform)
1381
- };
1600
+ rewriteContent(item.output, "function_call_output output", {
1601
+ kind: "toolResult",
1602
+ toolName
1603
+ }, ctx.transform, pending, (output) => {
1604
+ out.output = output;
1605
+ });
1606
+ return out;
1382
1607
  }
1383
- case "reasoning": return rewriteReasoning(item, ctx);
1608
+ case "reasoning": return rewriteReasoning(item, ctx, pending);
1384
1609
  default: return item;
1385
1610
  }
1386
1611
  }
@@ -1570,20 +1795,26 @@ const openaiResponsesWalker = {
1570
1795
  async request(body, ctx) {
1571
1796
  if (!isObj(body)) return malformed("request body");
1572
1797
  const out = { ...body };
1573
- if (typeof body.instructions === "string") out.instructions = await outbound(ctx.transform, body.instructions, { kind: "system" });
1798
+ const pending = [];
1799
+ if (typeof body.instructions === "string") queue(pending, ctx.transform, body.instructions, { kind: "system" }, (text) => {
1800
+ out.instructions = text;
1801
+ });
1574
1802
  else if (body.instructions !== void 0 && body.instructions !== null) return malformed("instructions");
1575
- const instruction = ctx.transform.instruction;
1576
- if (instruction !== void 0) out.instructions = typeof out.instructions === "string" && out.instructions !== "" ? `${instruction}\n\n${out.instructions}` : instruction;
1577
- if (typeof body.input === "string") out.input = await outbound(ctx.transform, body.input, { kind: "user" });
1803
+ if (typeof body.input === "string") queue(pending, ctx.transform, body.input, { kind: "user" }, (text) => {
1804
+ out.input = text;
1805
+ });
1578
1806
  else if (Array.isArray(body.input)) {
1579
1807
  const toolNames = toolNamesByCallId(body.input);
1580
1808
  const input = [];
1581
1809
  for (const item of body.input) {
1582
1810
  if (!isObj(item)) return malformed("input item");
1583
- input.push(await rewriteItem(item, toolNames, ctx));
1811
+ input.push(rewriteItem(item, toolNames, ctx, pending));
1584
1812
  }
1585
1813
  out.input = input;
1586
1814
  } else if (body.input !== void 0) return malformed("input");
1815
+ await runAll(pending);
1816
+ const instruction = ctx.transform.instruction;
1817
+ if (instruction !== void 0) out.instructions = typeof out.instructions === "string" && out.instructions !== "" ? `${instruction}\n\n${out.instructions}` : instruction;
1587
1818
  return out;
1588
1819
  },
1589
1820
  response(body, ctx) {
@@ -2160,110 +2391,4 @@ function sendError(res, status, type, message) {
2160
2391
  message
2161
2392
  } }));
2162
2393
  }
2163
- // src/config.ts
2164
- const CONFIG_FILE_NAME = "transform-proxy.yaml";
2165
- /** The implicit lookup locations, in order: `./.skaile/` then `~/.config/skaile/`. */
2166
- function configCandidates(cwd, home) {
2167
- return [join(cwd, ".skaile", CONFIG_FILE_NAME), join(home, ".config", "skaile", CONFIG_FILE_NAME)];
2168
- }
2169
- /**
2170
- * Finds the config file: the explicit path when given (it must exist), otherwise the first
2171
- * existing implicit candidate. Returns `undefined` when no implicit candidate exists.
2172
- */
2173
- function findProxyConfig(lookup) {
2174
- if (lookup.explicit !== void 0) {
2175
- const path = resolve(lookup.cwd, lookup.explicit);
2176
- if (!existsSync(path)) throw new Error(`transform proxy config not found: ${path}`);
2177
- return path;
2178
- }
2179
- return configCandidates(lookup.cwd, lookup.home).find((candidate) => existsSync(candidate));
2180
- }
2181
- /** Finds and loads the config. Throws, naming the searched paths, when none is found. */
2182
- function resolveProxyConfig(lookup) {
2183
- const path = findProxyConfig(lookup);
2184
- if (path === void 0) {
2185
- const searched = configCandidates(lookup.cwd, lookup.home).join(", ");
2186
- throw new Error(`no transform proxy config found (pass --config, or create one at ${searched})`);
2187
- }
2188
- return {
2189
- path,
2190
- options: loadProxyConfig(path)
2191
- };
2192
- }
2193
- const TOP_LEVEL_KEYS = {
2194
- transforms: true,
2195
- pipelines: true,
2196
- defaultPipeline: true,
2197
- allowUpstreams: true,
2198
- trace: true
2199
- };
2200
- const TRACE_KEYS = {
2201
- dir: true,
2202
- maxChars: true
2203
- };
2204
- /** Reads and validates a YAML config file. Unknown keys are rejected so that typos fail loudly. */
2205
- function loadProxyConfig(path) {
2206
- const source = parse(readFileSync(path, "utf8"));
2207
- try {
2208
- return parseProxyConfig(source ?? {}, dirname(path));
2209
- } catch (err) {
2210
- throw new Error(`invalid transform proxy config ${path}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2211
- }
2212
- }
2213
- /** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
2214
- function parseProxyConfig(source, baseDir) {
2215
- if (!isRecord(source)) throw new Error("the document must be a mapping");
2216
- for (const key of Object.keys(source)) if (!Object.hasOwn(TOP_LEVEL_KEYS, key)) throw new Error(`unknown key "${key}"`);
2217
- return {
2218
- baseDir,
2219
- transforms: parseTransforms(source.transforms),
2220
- pipelines: parsePipelines(source.pipelines),
2221
- ...source.defaultPipeline === void 0 ? {} : { defaultPipeline: requireString(source.defaultPipeline, "defaultPipeline") },
2222
- ...source.allowUpstreams === void 0 ? {} : { allowUpstreams: requireStringList(source.allowUpstreams, "allowUpstreams") },
2223
- ...source.trace === void 0 ? {} : { trace: parseTrace(source.trace, baseDir) }
2224
- };
2225
- }
2226
- function parseTransforms(value) {
2227
- if (value === void 0) return {};
2228
- if (!isRecord(value)) throw new Error("transforms must be a mapping of transform id to { module, config }");
2229
- const out = {};
2230
- for (const [id, spec] of Object.entries(value)) {
2231
- if (!isRecord(spec)) throw new Error(`transforms.${id} must be a mapping with a module`);
2232
- const module = requireString(spec.module, `transforms.${id}.module`);
2233
- out[id] = spec.config === void 0 ? { module } : {
2234
- module,
2235
- config: spec.config
2236
- };
2237
- }
2238
- return out;
2239
- }
2240
- function parsePipelines(value) {
2241
- if (value === void 0) return {};
2242
- if (!isRecord(value)) throw new Error("pipelines must be a mapping of pipeline id to a list of transform ids");
2243
- const out = {};
2244
- for (const [id, stages] of Object.entries(value)) out[id] = requireStringList(stages, `pipelines.${id}`);
2245
- return out;
2246
- }
2247
- function parseTrace(value, baseDir) {
2248
- if (!isRecord(value)) throw new Error("trace must be a mapping with a dir");
2249
- for (const key of Object.keys(value)) if (!Object.hasOwn(TRACE_KEYS, key)) throw new Error(`unknown key "trace.${key}"`);
2250
- const dir = resolve(baseDir, requireString(value.dir, "trace.dir"));
2251
- if (value.maxChars === void 0) return { dir };
2252
- if (typeof value.maxChars !== "number" || !Number.isInteger(value.maxChars) || value.maxChars <= 0) throw new Error("trace.maxChars must be a positive integer");
2253
- return {
2254
- dir,
2255
- maxChars: value.maxChars
2256
- };
2257
- }
2258
- function requireString(value, field) {
2259
- if (typeof value !== "string" || value === "") throw new Error(`${field} must be a non-empty string`);
2260
- return value;
2261
- }
2262
- function requireStringList(value, field) {
2263
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "")) throw new Error(`${field} must be a list of non-empty strings`);
2264
- return value;
2265
- }
2266
- function isRecord(value) {
2267
- return typeof value === "object" && value !== null && !Array.isArray(value);
2268
- }
2269
- export { silentLogger as S, anthropicWalker as _, parseProxyConfig as a, composeTransforms as b, SESSION_HEADER as c, createTransformProxy as d, DEFAULT_TRACE_MAX_CHARS as f, openaiChatWalker as g, openaiResponsesWalker as h, loadProxyConfig as i, TOKEN_HEADER as l, serializeTraceEvent as m, configCandidates as n, resolveProxyConfig as o, createFileTraceSink as p, findProxyConfig as r, PIPELINE_HEADER as s, CONFIG_FILE_NAME as t, UPSTREAM_HEADER as u, UnsupportedContentError as v, errorMessage as x, createSessionFacts as y };
2394
+ export { silentLogger as S, loadProxyConfig as _, createTransformProxy as a, composeTransforms as b, serializeTraceEvent as c, anthropicWalker as d, UnsupportedContentError as f, findProxyConfig as g, configCandidates as h, UPSTREAM_HEADER as i, openaiResponsesWalker as l, CONFIG_FILE_NAME as m, SESSION_HEADER as n, DEFAULT_TRACE_MAX_CHARS as o, createSessionFacts as p, TOKEN_HEADER as r, createFileTraceSink as s, PIPELINE_HEADER as t, openaiChatWalker as u, parseProxyConfig as v, errorMessage as x, resolveProxyConfig as y };
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@skaile/transform-proxy",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Same-protocol passthrough proxy that runs model requests and replies through a chain of text transforms (Anthropic Messages, OpenAI chat, OpenAI responses)",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/skaile-ai/skaile-transform-proxy.git"
9
+ "url": "git+https://github.com/skaile-ai/forge.git",
10
+ "directory": "packages/transform-proxy"
10
11
  },
11
- "homepage": "https://github.com/skaile-ai/skaile-transform-proxy#readme",
12
+ "homepage": "https://github.com/skaile-ai/forge/tree/main/packages/transform-proxy#readme",
12
13
  "bugs": {
13
- "url": "https://github.com/skaile-ai/skaile-transform-proxy/issues"
14
+ "url": "https://github.com/skaile-ai/forge/issues"
14
15
  },
15
16
  "keywords": [
16
17
  "llm",
@@ -54,9 +55,10 @@
54
55
  "scripts": {
55
56
  "prebuild": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
56
57
  "build": "tsdown",
57
- "prepare": "npm run build",
58
- "test": "bun test",
59
- "typecheck": "tsc --noEmit"
58
+ "prepublishOnly": "npm run build",
59
+ "test": "bun --bun vitest run",
60
+ "typecheck": "tsc --noEmit",
61
+ "check": "bun ../../scripts/check-package.ts"
60
62
  },
61
63
  "dependencies": {
62
64
  "yaml": "^2.9.0"
@@ -64,7 +66,6 @@
64
66
  "devDependencies": {
65
67
  "@types/bun": "^1.3.0",
66
68
  "@types/node": "^22.0.0",
67
- "tsdown": "^0.23.1",
68
69
  "typescript": "^5.9.3"
69
70
  }
70
71
  }