@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 +10 -0
- package/README.md +1 -0
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/omp/index.mjs +1 -1
- package/dist/{server-gwJGHF82.d.mts → server-BWc1mocm.d.mts} +4 -1
- package/dist/{server-BR1dDA8S.mjs → server-DTSaU_aK.mjs} +269 -144
- package/package.json +1 -1
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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";
|
package/dist/omp/index.mjs
CHANGED
|
@@ -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-
|
|
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
|
-
/**
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
if (typeof content === "string")
|
|
525
|
-
|
|
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))
|
|
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")
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
+
function rewriteSystemText(text, transform, pending, set) {
|
|
558
567
|
const kept = gatePrefix(text);
|
|
559
|
-
|
|
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
|
-
|
|
563
|
-
if (typeof value === "string")
|
|
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)
|
|
567
|
-
|
|
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))
|
|
572
|
-
|
|
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
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
625
|
+
function rewriteContent$2(content, role, toolNames, ctx, pending, set) {
|
|
603
626
|
const textKind = role === "assistant" ? "assistant" : "user";
|
|
604
|
-
if (typeof content === "string")
|
|
605
|
-
|
|
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))
|
|
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")
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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(
|
|
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
|
-
|
|
651
|
+
const copy = {
|
|
624
652
|
...block,
|
|
625
|
-
input:
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
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
|
-
|
|
862
|
-
if (
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
919
|
-
|
|
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
|
-
|
|
927
|
-
if (typeof content === "string")
|
|
928
|
-
|
|
929
|
-
|
|
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))
|
|
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")
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
991
|
+
set(out);
|
|
945
992
|
}
|
|
946
|
-
|
|
947
|
-
if (typeof value === "string")
|
|
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)
|
|
951
|
-
|
|
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))
|
|
956
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
+
queue$1(pending, transform, args, leaf, set);
|
|
1037
|
+
return;
|
|
977
1038
|
}
|
|
978
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1094
|
+
rewriteContent$1(message.content, { kind: "system" }, transform, pending, setContent);
|
|
1022
1095
|
break;
|
|
1023
1096
|
case "user":
|
|
1024
|
-
|
|
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")
|
|
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
|
-
|
|
1033
|
-
out.tool_calls =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
1369
|
-
if (typeof content === "string")
|
|
1370
|
-
|
|
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))
|
|
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")
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
1471
|
+
set(out);
|
|
1386
1472
|
}
|
|
1387
|
-
|
|
1388
|
-
if (typeof value === "string")
|
|
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)
|
|
1392
|
-
|
|
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))
|
|
1397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1506
|
+
queue(pending, transform, args, leaf, set);
|
|
1507
|
+
return;
|
|
1408
1508
|
}
|
|
1409
|
-
|
|
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
|
-
|
|
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())
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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(
|
|
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.
|
|
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",
|