@skaile/transform-proxy 0.5.1 → 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,15 @@
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
+
3
13
  ## 0.5.1
4
14
 
5
15
  ### Patch Changes
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
 
package/dist/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as TransformProxy } from "./server-gwJGHF82.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`. */
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as createTransformProxy, s as createFileTraceSink, x as errorMessage, y as resolveProxyConfig } from "./server-BR1dDA8S.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";
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-gwJGHF82.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;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
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-BR1dDA8S.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,4 +1,4 @@
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-BR1dDA8S.mjs";
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";
2
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { randomBytes, randomUUID } from "node:crypto";
@@ -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;
@@ -516,28 +516,37 @@ function refuse$1(block) {
516
516
  function malformed$2(slot) {
517
517
  throw new UnsupportedContentError(`Anthropic ${slot} has an unexpected shape`);
518
518
  }
519
- async function outbound$2(transform, text, leaf) {
520
- 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)));
521
526
  }
522
527
  /** A string, or an array of blocks whose `text` blocks are rewritten; anything else is refused. */
523
- async function rewriteText(content, slot, rewrite) {
524
- if (typeof content === "string") return rewrite(content);
525
- 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);
526
534
  const out = [];
527
535
  for (const block of content) {
528
- if (!isObj$2(block)) return malformed$2(`${slot} block`);
536
+ if (!isObj$2(block)) malformed$2(`${slot} block`);
529
537
  refuse$1(block);
530
538
  if (block.type !== "text") {
531
539
  out.push(block);
532
540
  continue;
533
541
  }
534
- if (typeof block.text !== "string") return malformed$2(`${slot} text block`);
535
- out.push({
536
- ...block,
537
- 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;
538
547
  });
539
548
  }
540
- return out;
549
+ set(out);
541
550
  }
542
551
  /**
543
552
  * Host-written system text that Anthropic's Claude subscription (OAuth) check reads verbatim: the
@@ -554,24 +563,37 @@ function gatePrefix(text) {
554
563
  const end = text.indexOf("\n");
555
564
  return end === -1 ? text : text.slice(0, end);
556
565
  }
557
- async function rewriteSystemText(text, transform) {
566
+ function rewriteSystemText(text, transform, pending, set) {
558
567
  const kept = gatePrefix(text);
559
- 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));
560
569
  }
561
570
  /** Every string leaf of a JSON value (tool-call input); keys stay as they are. */
562
- async function rewriteStrings$2(value, leaf, transform) {
563
- 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
+ }
564
576
  if (Array.isArray(value)) {
565
577
  const out = [];
566
- for (const item of value) out.push(await rewriteStrings$2(item, leaf, transform));
567
- 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;
568
586
  }
569
587
  if (isObj$2(value)) {
570
588
  const out = {};
571
- for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings$2(item, leaf, transform);
572
- 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);
573
596
  }
574
- return value;
575
597
  }
576
598
  /** Every string leaf of a JSON value, restored inbound. */
577
599
  function restoreStrings$1(value, transform) {
@@ -585,7 +607,7 @@ function restoreStrings$1(value, transform) {
585
607
  return value;
586
608
  }
587
609
  /** A thinking block: swaps in the cached model text when its signature is known, else rewrites it. */
588
- async function rewriteThinking(block, ctx) {
610
+ function rewriteThinking(block, ctx, pending) {
589
611
  if (typeof block.thinking !== "string") return malformed$2("thinking block");
590
612
  if (typeof block.signature === "string") {
591
613
  const cached = ctx.signatures.get(signatureKey$1(block.signature));
@@ -594,38 +616,48 @@ async function rewriteThinking(block, ctx) {
594
616
  thinking: cached
595
617
  };
596
618
  }
597
- return {
598
- ...block,
599
- thinking: await outbound$2(ctx.transform, block.thinking, { kind: "thinking" })
600
- };
619
+ const copy = { ...block };
620
+ queue$2(pending, ctx.transform, block.thinking, { kind: "thinking" }, (text) => {
621
+ copy.thinking = text;
622
+ });
623
+ return copy;
601
624
  }
602
- async function rewriteContent$2(content, role, toolNames, ctx) {
625
+ function rewriteContent$2(content, role, toolNames, ctx, pending, set) {
603
626
  const textKind = role === "assistant" ? "assistant" : "user";
604
- if (typeof content === "string") return outbound$2(ctx.transform, content, { kind: textKind });
605
- 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");
606
632
  const out = [];
607
633
  for (const block of content) {
608
- if (!isObj$2(block)) return malformed$2("message content block");
634
+ if (!isObj$2(block)) malformed$2("message content block");
609
635
  refuse$1(block);
610
636
  switch (block.type) {
611
- case "text":
612
- if (typeof block.text !== "string") return malformed$2("text block");
613
- out.push({
614
- ...block,
615
- 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;
616
643
  });
617
644
  break;
645
+ }
618
646
  case "thinking":
619
- out.push(await rewriteThinking(block, ctx));
647
+ out.push(rewriteThinking(block, ctx, pending));
620
648
  break;
621
649
  case "tool_use": {
622
650
  const toolName = typeof block.name === "string" ? block.name : void 0;
623
- out.push({
651
+ const copy = {
624
652
  ...block,
625
- input: await rewriteStrings$2(block.input, {
626
- kind: "toolCall",
627
- toolName
628
- }, 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;
629
661
  });
630
662
  break;
631
663
  }
@@ -635,19 +667,20 @@ async function rewriteContent$2(content, role, toolNames, ctx) {
635
667
  break;
636
668
  }
637
669
  const toolName = typeof block.tool_use_id === "string" ? toolNames.get(block.tool_use_id) : void 0;
638
- out.push({
639
- ...block,
640
- content: await rewriteText(block.content, "tool_result content", (text) => outbound$2(ctx.transform, text, {
641
- kind: "toolResult",
642
- toolName
643
- }))
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;
644
677
  });
645
678
  break;
646
679
  }
647
680
  default: out.push(block);
648
681
  }
649
682
  }
650
- return out;
683
+ set(out);
651
684
  }
652
685
  /** Maps each `tool_use` id to its tool name, so tool results can name their tool. */
653
686
  function toolNamesById(messages) {
@@ -858,21 +891,26 @@ const anthropicWalker = {
858
891
  async request(body, ctx) {
859
892
  if (!isObj$2(body)) return malformed$2("request body");
860
893
  const out = { ...body };
861
- if (body.system !== void 0) out.system = await rewriteText(body.system, "system", (text) => rewriteSystemText(text, ctx.transform));
862
- 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
+ });
863
898
  if (body.messages !== void 0) {
864
899
  if (!Array.isArray(body.messages)) return malformed$2("messages");
865
900
  const toolNames = toolNamesById(body.messages);
866
901
  const messages = [];
867
902
  for (const message of body.messages) {
868
903
  if (!isObj$2(message)) return malformed$2("message");
869
- messages.push({
870
- ...message,
871
- 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;
872
908
  });
873
909
  }
874
910
  out.messages = messages;
875
911
  }
912
+ await runAll$2(pending);
913
+ if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
876
914
  return out;
877
915
  },
878
916
  response(body, ctx) {
@@ -915,47 +953,69 @@ const REFUSED_PARTS$1 = {
915
953
  };
916
954
  /** Reasoning fields used by OpenAI-compatible providers. */
917
955
  const REASONING_FIELDS = ["reasoning_content", "reasoning"];
918
- async function outbound$1(transform, text, leaf) {
919
- 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)));
920
963
  }
921
964
  /** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
922
965
  function malformed$1(slot) {
923
966
  throw new UnsupportedContentError(`OpenAI chat ${slot} has an unexpected shape`);
924
967
  }
925
968
  /** A string, or an array of parts whose `text` parts are rewritten; `null`/absent carries no text; anything else is refused. */
926
- async function rewriteContent$1(content, leaf, transform) {
927
- if (typeof content === "string") return outbound$1(transform, content, leaf);
928
- if (content === void 0 || content === null) return content;
929
- 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");
930
976
  const out = [];
931
977
  for (const part of content) {
932
- if (!isObj$1(part)) return malformed$1("content part");
978
+ if (!isObj$1(part)) malformed$1("content part");
933
979
  if (typeof part.type === "string" && REFUSED_PARTS$1[part.type] === true) throw new UnsupportedContentError(`OpenAI chat ${part.type} content cannot be transformed`);
934
980
  if (part.type !== "text") {
935
981
  out.push(part);
936
982
  continue;
937
983
  }
938
- if (typeof part.text !== "string") return malformed$1("text part");
939
- out.push({
940
- ...part,
941
- 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;
942
989
  });
943
990
  }
944
- return out;
991
+ set(out);
945
992
  }
946
- async function rewriteStrings$1(value, leaf, transform) {
947
- 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
+ }
948
998
  if (Array.isArray(value)) {
949
999
  const out = [];
950
- for (const item of value) out.push(await rewriteStrings$1(item, leaf, transform));
951
- 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;
952
1008
  }
953
1009
  if (isObj$1(value)) {
954
1010
  const out = {};
955
- for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings$1(item, leaf, transform);
956
- 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);
957
1018
  }
958
- return value;
959
1019
  }
960
1020
  function restoreStrings(value, transform) {
961
1021
  if (typeof value === "string") return transform.inbound(value);
@@ -968,14 +1028,23 @@ function restoreStrings(value, transform) {
968
1028
  return value;
969
1029
  }
970
1030
  /** Tool-call arguments are a JSON string: rewrite its string leaves, or the raw text when it is not JSON. */
971
- async function rewriteArguments$1(args, leaf, transform) {
1031
+ function rewriteArguments$1(args, leaf, transform, pending, set) {
972
1032
  let parsed;
973
1033
  try {
974
1034
  parsed = JSON.parse(args);
975
1035
  } catch {
976
- return outbound$1(transform, args, leaf);
1036
+ queue$1(pending, transform, args, leaf, set);
1037
+ return;
977
1038
  }
978
- 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
+ });
979
1048
  }
980
1049
  function restoreArguments$1(args, transform) {
981
1050
  let parsed;
@@ -986,7 +1055,7 @@ function restoreArguments$1(args, transform) {
986
1055
  }
987
1056
  return JSON.stringify(restoreStrings(parsed, transform));
988
1057
  }
989
- async function rewriteToolCalls(calls, transform, names) {
1058
+ function rewriteToolCalls(calls, transform, names, pending) {
990
1059
  if (calls === void 0 || calls === null) return calls;
991
1060
  if (!Array.isArray(calls)) return malformed$1("tool_calls");
992
1061
  const out = [];
@@ -1000,53 +1069,59 @@ async function rewriteToolCalls(calls, transform, names) {
1000
1069
  if (!isObj$1(fn) || typeof fn.arguments !== "string") return malformed$1("function tool call");
1001
1070
  const toolName = typeof fn.name === "string" ? fn.name : void 0;
1002
1071
  if (typeof call.id === "string" && toolName !== void 0) names.set(call.id, toolName);
1072
+ const fnCopy = { ...fn };
1003
1073
  out.push({
1004
1074
  ...call,
1005
- function: {
1006
- ...fn,
1007
- arguments: await rewriteArguments$1(fn.arguments, {
1008
- kind: "toolCall",
1009
- toolName
1010
- }, transform)
1011
- }
1075
+ function: fnCopy
1076
+ });
1077
+ rewriteArguments$1(fn.arguments, {
1078
+ kind: "toolCall",
1079
+ toolName
1080
+ }, transform, pending, (args) => {
1081
+ fnCopy.arguments = args;
1012
1082
  });
1013
1083
  }
1014
1084
  return out;
1015
1085
  }
1016
- async function rewriteMessage(message, transform, names) {
1086
+ function rewriteMessage(message, transform, names, pending) {
1017
1087
  const out = { ...message };
1088
+ const setContent = (content) => {
1089
+ out.content = content;
1090
+ };
1018
1091
  switch (message.role) {
1019
1092
  case "system":
1020
1093
  case "developer":
1021
- out.content = await rewriteContent$1(message.content, { kind: "system" }, transform);
1094
+ rewriteContent$1(message.content, { kind: "system" }, transform, pending, setContent);
1022
1095
  break;
1023
1096
  case "user":
1024
- out.content = await rewriteContent$1(message.content, { kind: "user" }, transform);
1097
+ rewriteContent$1(message.content, { kind: "user" }, transform, pending, setContent);
1025
1098
  break;
1026
1099
  case "assistant":
1027
1100
  for (const field of REASONING_FIELDS) {
1028
1101
  const value = message[field];
1029
- 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
+ });
1030
1105
  else if (value !== void 0 && value !== null) return malformed$1(`assistant ${field}`);
1031
1106
  }
1032
- out.content = await rewriteContent$1(message.content, { kind: "assistant" }, transform);
1033
- 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);
1034
1109
  if (message.tool_calls === void 0) delete out.tool_calls;
1035
1110
  break;
1036
1111
  case "tool": {
1037
1112
  const toolName = typeof message.tool_call_id === "string" ? names.get(message.tool_call_id) : void 0;
1038
- out.content = await rewriteContent$1(message.content, {
1113
+ rewriteContent$1(message.content, {
1039
1114
  kind: "toolResult",
1040
1115
  toolName
1041
- }, transform);
1116
+ }, transform, pending, setContent);
1042
1117
  break;
1043
1118
  }
1044
1119
  case "function": {
1045
1120
  const toolName = typeof message.name === "string" ? message.name : void 0;
1046
- out.content = await rewriteContent$1(message.content, {
1121
+ rewriteContent$1(message.content, {
1047
1122
  kind: "toolResult",
1048
1123
  toolName
1049
- }, transform);
1124
+ }, transform, pending, setContent);
1050
1125
  break;
1051
1126
  }
1052
1127
  default: return malformed$1("message role");
@@ -1298,11 +1373,13 @@ const openaiChatWalker = {
1298
1373
  if (body.messages !== void 0) {
1299
1374
  if (!Array.isArray(body.messages)) return malformed$1("messages");
1300
1375
  const names = /* @__PURE__ */ new Map();
1376
+ const pending = [];
1301
1377
  const messages = [];
1302
1378
  for (const message of body.messages) {
1303
1379
  if (!isObj$1(message)) return malformed$1("message");
1304
- messages.push(await rewriteMessage(message, ctx.transform, names));
1380
+ messages.push(rewriteMessage(message, ctx.transform, names, pending));
1305
1381
  }
1382
+ await runAll$1(pending);
1306
1383
  if (ctx.transform.instruction !== void 0) messages.unshift({
1307
1384
  role: "system",
1308
1385
  content: ctx.transform.instruction
@@ -1357,56 +1434,87 @@ const signatureKey = (itemId) => `responses:${itemId}`;
1357
1434
  function refuse(part) {
1358
1435
  if (typeof part.type === "string" && REFUSED_PARTS[part.type] === true) throw new UnsupportedContentError(`OpenAI responses ${part.type} content cannot be transformed`);
1359
1436
  }
1360
- async function outbound(transform, text, leaf) {
1361
- 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)));
1362
1444
  }
1363
1445
  /** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
1364
1446
  function malformed(slot) {
1365
1447
  throw new UnsupportedContentError(`OpenAI responses ${slot} has an unexpected shape`);
1366
1448
  }
1367
1449
  /** A string, or an array of parts whose `input_text`/`output_text` parts are rewritten; anything else is refused. */
1368
- async function rewriteContent(content, slot, leaf, transform) {
1369
- if (typeof content === "string") return outbound(transform, content, leaf);
1370
- 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);
1371
1456
  const out = [];
1372
1457
  for (const part of content) {
1373
- if (!isObj(part)) return malformed(`${slot} part`);
1458
+ if (!isObj(part)) malformed(`${slot} part`);
1374
1459
  refuse(part);
1375
1460
  if (part.type !== "input_text" && part.type !== "output_text") {
1376
1461
  out.push(part);
1377
1462
  continue;
1378
1463
  }
1379
- if (typeof part.text !== "string") return malformed(`${slot} ${part.type} part`);
1380
- out.push({
1381
- ...part,
1382
- 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;
1383
1469
  });
1384
1470
  }
1385
- return out;
1471
+ set(out);
1386
1472
  }
1387
- async function rewriteStrings(value, leaf, transform) {
1388
- 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
+ }
1389
1478
  if (Array.isArray(value)) {
1390
1479
  const out = [];
1391
- for (const item of value) out.push(await rewriteStrings(item, leaf, transform));
1392
- 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;
1393
1488
  }
1394
1489
  if (isObj(value)) {
1395
1490
  const out = {};
1396
- for (const [key, item] of Object.entries(value)) out[key] = await rewriteStrings(item, leaf, transform);
1397
- 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);
1398
1498
  }
1399
- return value;
1400
1499
  }
1401
1500
  /** Tool-call arguments: a JSON string whose string leaves are rewritten; raw text when it is not JSON. */
1402
- async function rewriteArguments(args, leaf, transform) {
1501
+ function rewriteArguments(args, leaf, transform, pending, set) {
1403
1502
  let parsed;
1404
1503
  try {
1405
1504
  parsed = JSON.parse(args);
1406
1505
  } catch {
1407
- return outbound(transform, args, leaf);
1506
+ queue(pending, transform, args, leaf, set);
1507
+ return;
1408
1508
  }
1409
- 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
+ });
1410
1518
  }
1411
1519
  /** Restores the string values of a JSON text, keeping its formatting; restored originals are JSON-escaped. */
1412
1520
  function restoreArguments(args, transform) {
@@ -1430,16 +1538,26 @@ function cachedSummary(item, ctx) {
1430
1538
  if (!Array.isArray(texts) || !Array.isArray(item.summary) || texts.length !== item.summary.length) return void 0;
1431
1539
  return texts;
1432
1540
  }
1433
- async function rewriteReasoning(item, ctx) {
1541
+ function rewriteReasoning(item, ctx, pending) {
1434
1542
  if (item.summary === void 0) return item;
1435
1543
  if (!Array.isArray(item.summary)) return malformed("reasoning summary");
1436
1544
  for (const part of item.summary) if (!isObj(part) || typeof part.text !== "string") return malformed("reasoning summary part");
1437
1545
  const cached = cachedSummary(item, ctx);
1438
1546
  const summary = [];
1439
- for (const [i, part] of item.summary.entries()) summary.push({
1440
- ...part,
1441
- text: cached !== void 0 ? cached[i] : await outbound(ctx.transform, part.text, { kind: "thinking" })
1442
- });
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
+ }
1443
1561
  return {
1444
1562
  ...item,
1445
1563
  summary
@@ -1455,38 +1573,39 @@ function toolNamesByCallId(items) {
1455
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);
1456
1574
  return names;
1457
1575
  }
1458
- async function rewriteItem(item, toolNames, ctx) {
1576
+ function rewriteItem(item, toolNames, ctx, pending) {
1577
+ const out = { ...item };
1459
1578
  if (item.type === "message" || item.type === void 0 && item.role !== void 0) {
1460
1579
  const kind = roleKind(item.role);
1461
1580
  if (kind === void 0) return malformed("message role");
1462
- return {
1463
- ...item,
1464
- content: await rewriteContent(item.content, "message content", { kind }, ctx.transform)
1465
- };
1581
+ rewriteContent(item.content, "message content", { kind }, ctx.transform, pending, (content) => {
1582
+ out.content = content;
1583
+ });
1584
+ return out;
1466
1585
  }
1467
1586
  switch (item.type) {
1468
1587
  case "function_call": {
1469
1588
  if (typeof item.arguments !== "string") return malformed("function_call arguments");
1470
1589
  const toolName = typeof item.name === "string" ? item.name : void 0;
1471
- return {
1472
- ...item,
1473
- arguments: await rewriteArguments(item.arguments, {
1474
- kind: "toolCall",
1475
- toolName
1476
- }, ctx.transform)
1477
- };
1590
+ rewriteArguments(item.arguments, {
1591
+ kind: "toolCall",
1592
+ toolName
1593
+ }, ctx.transform, pending, (args) => {
1594
+ out.arguments = args;
1595
+ });
1596
+ return out;
1478
1597
  }
1479
1598
  case "function_call_output": {
1480
1599
  const toolName = typeof item.call_id === "string" ? toolNames.get(item.call_id) : void 0;
1481
- return {
1482
- ...item,
1483
- output: await rewriteContent(item.output, "function_call_output output", {
1484
- kind: "toolResult",
1485
- toolName
1486
- }, ctx.transform)
1487
- };
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;
1488
1607
  }
1489
- case "reasoning": return rewriteReasoning(item, ctx);
1608
+ case "reasoning": return rewriteReasoning(item, ctx, pending);
1490
1609
  default: return item;
1491
1610
  }
1492
1611
  }
@@ -1676,20 +1795,26 @@ const openaiResponsesWalker = {
1676
1795
  async request(body, ctx) {
1677
1796
  if (!isObj(body)) return malformed("request body");
1678
1797
  const out = { ...body };
1679
- 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
+ });
1680
1802
  else if (body.instructions !== void 0 && body.instructions !== null) return malformed("instructions");
1681
- const instruction = ctx.transform.instruction;
1682
- if (instruction !== void 0) out.instructions = typeof out.instructions === "string" && out.instructions !== "" ? `${instruction}\n\n${out.instructions}` : instruction;
1683
- 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
+ });
1684
1806
  else if (Array.isArray(body.input)) {
1685
1807
  const toolNames = toolNamesByCallId(body.input);
1686
1808
  const input = [];
1687
1809
  for (const item of body.input) {
1688
1810
  if (!isObj(item)) return malformed("input item");
1689
- input.push(await rewriteItem(item, toolNames, ctx));
1811
+ input.push(rewriteItem(item, toolNames, ctx, pending));
1690
1812
  }
1691
1813
  out.input = input;
1692
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;
1693
1818
  return out;
1694
1819
  },
1695
1820
  response(body, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaile/transform-proxy",
3
- "version": "0.5.1",
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",