@skaile/transform-proxy 0.2.1 → 0.2.3
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 +12 -0
- package/README.md +9 -4
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +14 -4
- package/dist/{config-Xdn_LYpZ.mjs → config-PKHxejC8.mjs} +102 -60
- package/dist/index.mjs +1 -1
- package/dist/omp/index.d.mts +56 -9
- package/dist/omp/index.mjs +282 -53
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
- omp extension: without `SKAILE_TRANSFORM_PROXY_URL`, `/anon on` starts the proxy as a child process under the system Node (`SKAILE_TRANSFORM_PROXY_NODE`, else `node` from `PATH`; Node ≥ 22) instead of inside omp. omp is a compiled Bun executable whose resolver cannot load transform modules with native or ML dependencies from `node_modules`: under omp 18.14.2, `/anon on` failed with `transform "anon": cannot load module @skaile/forge-anon/proxy: Cannot find package 'ibantools'`. The child runs this package's CLI with the found config, `--port 0 --host 127.0.0.1 --exit-with-stdin` and a random token, which every routed provider sends as `x-skaile-transform-token`. A child that exits before `listening <url>` or stays silent for 60 s fails `/anon on` with its last stderr lines (token redacted). The child exits with omp, does not keep omp's event loop alive, and is replaced on the next turn when it dies. A rotated-out proxy session is no longer dropped: the child keeps its state in memory until it exits, and no request carries that id again.
|
|
6
|
+
- CLI: `--exit-with-stdin` stops the proxy when its stdin closes, so a parent that spawns it with a stdin pipe takes it down on exit, SIGKILL included.
|
|
7
|
+
|
|
8
|
+
## 0.2.2
|
|
9
|
+
|
|
10
|
+
- Fail closed on malformed request bodies: the Anthropic, OpenAI chat and OpenAI responses walkers refuse a JSON body whose shape they cannot classify (a root that is not an object; a `system`, `messages`, `input`, `instructions`, message `content`, text, thinking, tool-call `arguments` or tool-result slot of an unexpected type) with 400 `unsupported_content` instead of forwarding it untransformed. Unknown part, block and item types still pass through.
|
|
11
|
+
- Upstream response `content-type` is matched case-insensitively, so `Text/Event-Stream` and `Application/JSON; charset=utf-8` replies are restored instead of relayed with placeholders.
|
|
12
|
+
- omp extension: a failed rebind of the active model is an activation failure. `/anon on` rolls the routes back, persists nothing and stays `anon off`; in required mode, or when a later turn's rebind fails, the footer shows `anon error`. While anon is on or in error, a `before_provider_request` handler replaces the payload of any request whose model bypasses the proxy with an empty object (omp has no hook that cancels a provider request).
|
|
13
|
+
- omp extension: the proxy session follows the root omp session. On `session_switch` (`/new`, `/resume`, `/fork`) and on a root `session_start` with a new id, every provider is re-routed with the new `x-skaile-transform-session`, and the in-process proxy drops the old session; headless `print` sessions (subagents) keep the parent's id.
|
|
14
|
+
|
|
3
15
|
## 0.2.1
|
|
4
16
|
|
|
5
17
|
- Anthropic walker: the pipeline `instruction` is appended after the host's system prompt (last text block, or after a blank line for a string `system`) instead of being prepended. Claude Code gateways such as TeamClaude answer 429 `rate_limit_error` for Sonnet when the system prompt does not start with the Claude Code blocks; with the instruction first, omp and Claude Code requests for `claude-sonnet-4-5` through the proxy were refused.
|
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ Embeddings are refused on purpose: a vector of pseudonymized input cannot be res
|
|
|
31
31
|
## Refusals
|
|
32
32
|
|
|
33
33
|
- Attachments (image, document, file parts) are refused with 400 `unsupported_content`. Nothing is sent upstream.
|
|
34
|
+
- A body that is not JSON is refused with 400 `invalid_json`. A JSON body whose shape the walker cannot classify is refused with 400 `unsupported_content`: a root that is not an object, or a `system`, `messages`, `input`, `instructions`, message `content`, text, thinking, tool-call `arguments` or tool-result slot of an unexpected type. Unknown part, block and item types still pass through untouched.
|
|
34
35
|
- Unknown routes, pipelines and upstreams are refused before any body is read or transform is built.
|
|
35
36
|
- A transform that throws refuses the request with 502 `transform_failed`. Nothing is sent upstream.
|
|
36
37
|
- Bedrock (SigV4 covers the body) and Vertex are not routed. Refuse them in the caller.
|
|
@@ -59,12 +60,13 @@ Keys are exactly the option fields. An unknown top-level key is an error. Module
|
|
|
59
60
|
## CLI
|
|
60
61
|
|
|
61
62
|
```sh
|
|
62
|
-
skaile-transform-proxy [--config <file>] [--port <n>] [--host <h>] [--token <t>]
|
|
63
|
+
skaile-transform-proxy [--config <file>] [--port <n>] [--host <h>] [--token <t>] [--exit-with-stdin]
|
|
63
64
|
```
|
|
64
65
|
|
|
65
66
|
- The token comes from `--token` or `SKAILE_TRANSFORM_PROXY_TOKEN`.
|
|
66
67
|
- Without `--config`, the proxy reads `./.skaile/transform-proxy.yaml`, then `~/.config/skaile/transform-proxy.yaml`.
|
|
67
68
|
- On start it prints `listening <url>`. It stops on SIGINT or SIGTERM.
|
|
69
|
+
- `--exit-with-stdin` also stops it when its stdin closes. A parent process that spawns the proxy with a stdin pipe and never writes to it takes the proxy down when it exits, even on SIGKILL.
|
|
68
70
|
|
|
69
71
|
## Library
|
|
70
72
|
|
|
@@ -91,13 +93,16 @@ The package ships an [omp](https://omp.sh) extension (subpath `@skaile/transform
|
|
|
91
93
|
omp plugin install @skaile/transform-proxy
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
It adds the command `/anon on|off|status` and the footer status `anon on`
|
|
96
|
+
It adds the command `/anon on|off|status` and the footer status `anon on`, `anon off` or `anon error`. Unless `SKAILE_TRANSFORM_PROXY_URL` names a running proxy, it needs Node ≥ 22 on `PATH` (or its path in `SKAILE_TRANSFORM_PROXY_NODE`).
|
|
95
97
|
|
|
96
98
|
- `/anon on` routes every provider of omp's model registry through the proxy: the provider's `baseUrl` becomes the proxy URL, and the original base URL travels in `x-skaile-transform-upstream`, next to `x-skaile-transform-session` (the omp session id) and the token. The provider's own headers and auth stay in place and are relayed.
|
|
97
99
|
- With `SKAILE_TRANSFORM_PROXY_URL` (and `SKAILE_TRANSFORM_PROXY_TOKEN`) set, it uses that running proxy, and the proxy applies its `defaultPipeline`.
|
|
98
|
-
- Otherwise it starts one proxy
|
|
100
|
+
- Otherwise it starts one proxy per omp process as a child process: the system Node (`SKAILE_TRANSFORM_PROXY_NODE`, else `node` from `PATH`) runs this package's CLI with the config lookup of the CLI (`./.skaile/transform-proxy.yaml`, then `~/.config/skaile/transform-proxy.yaml`), `--port 0 --host 127.0.0.1 --exit-with-stdin` and a random token in `SKAILE_TRANSFORM_PROXY_TOKEN`, which every routed provider sends. That config must set `defaultPipeline`. Without a config, `/anon on` refuses and says where to put one. A child that exits before printing `listening <url>`, or stays silent for 60 s, fails `/anon on` with its last stderr lines.
|
|
101
|
+
- omp is a compiled Bun executable, and its resolver cannot load transform modules with native or ML dependencies (`onnxruntime-node`, `@huggingface/transformers`, `ibantools`) from `node_modules`; Node can. The child exits with omp: its stdin pipe closes when omp exits (SIGKILL included), and omp kills it on a normal exit. It does not keep omp's event loop alive. A child that dies later is replaced on the next turn.
|
|
99
102
|
- Providers on a wire the proxy does not rewrite (Bedrock, Vertex, Gemini, agent bridges, providers without a base URL) are routed too, so the proxy refuses their requests: the setup fails closed. `/anon status` lists them as refused.
|
|
100
|
-
- The session's active model is rebound to the re-routed one, so the next request already uses the new base URL.
|
|
103
|
+
- The session's active model is rebound to the re-routed one, so the next request already uses the new base URL. A rebind that fails is an activation failure: `/anon on` rolls the routes back, saves nothing and stays off. When anon is required, or a later turn's rebind fails while anon is on, the footer shows `anon error`.
|
|
104
|
+
- While anon is on or in error, a `before_provider_request` handler checks every model request: a request whose model does not target the proxy gets its payload replaced with an empty object, so no conversation text leaves, and an error notice names the model. omp has no hook that cancels a provider request, and a throwing handler is only logged, so this replacement is the block.
|
|
105
|
+
- The proxy session follows the root omp session: on `/new`, `/resume`, `/fork` (`session_switch`) and on a root `session_start` with a new session id, every provider is re-routed with the new `x-skaile-transform-session`. The proxy keeps the old session's state in memory until it exits; no request carries that id again. A `session_start` of a headless `print` session (how omp runs subagents) keeps the parent's id, so subagents share its pseudonyms.
|
|
101
106
|
- `/anon off` restores the original base URLs and headers.
|
|
102
107
|
- `/anon status` shows the state, the proxy URL and its source, the pipeline, the session id, and the routed and refused providers.
|
|
103
108
|
- The enabled flag is saved in `skaile-transform-proxy.json` in omp's agent dir (`~/.omp/agent`; `SKAILE_TRANSFORM_PROXY_OMP_STATE` overrides the path) and re-applied when a session starts.
|
package/dist/cli.d.mts
CHANGED
|
@@ -12,6 +12,8 @@ export interface CliEnvironment {
|
|
|
12
12
|
export interface CliRun {
|
|
13
13
|
readonly code: number;
|
|
14
14
|
readonly proxy?: TransformProxy;
|
|
15
|
+
/** `--exit-with-stdin`: the caller stops the proxy when its stdin closes (the parent process is gone). */
|
|
16
|
+
readonly exitWithStdin?: boolean;
|
|
15
17
|
}
|
|
16
18
|
/** Parses the arguments, loads the config and starts the proxy. Never installs signal handlers. */
|
|
17
19
|
export declare function runCli(argv: readonly string[], io: CliEnvironment): Promise<CliRun>;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { d as createTransformProxy, o as resolveProxyConfig, v as errorMessage } from "./config-
|
|
2
|
+
import { d as createTransformProxy, o as resolveProxyConfig, v as errorMessage } from "./config-PKHxejC8.mjs";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -7,7 +7,7 @@ import { parseArgs } from "node:util";
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
/** Environment variable that supplies the client token when `--token` is absent. */
|
|
9
9
|
const TOKEN_ENV = "SKAILE_TRANSFORM_PROXY_TOKEN";
|
|
10
|
-
const USAGE = "usage: skaile-transform-proxy [--config <file>] [--port <n>] [--host <h>] [--token <t>]";
|
|
10
|
+
const USAGE = "usage: skaile-transform-proxy [--config <file>] [--port <n>] [--host <h>] [--token <t>] [--exit-with-stdin]";
|
|
11
11
|
/** Parses the arguments, loads the config and starts the proxy. Never installs signal handlers. */
|
|
12
12
|
async function runCli(argv, io) {
|
|
13
13
|
let values;
|
|
@@ -23,7 +23,8 @@ async function runCli(argv, io) {
|
|
|
23
23
|
help: {
|
|
24
24
|
type: "boolean",
|
|
25
25
|
short: "h"
|
|
26
|
-
}
|
|
26
|
+
},
|
|
27
|
+
"exit-with-stdin": { type: "boolean" }
|
|
27
28
|
}
|
|
28
29
|
}));
|
|
29
30
|
} catch (err) {
|
|
@@ -67,7 +68,8 @@ async function runCli(argv, io) {
|
|
|
67
68
|
io.stdout(`listening ${proxy.url}`);
|
|
68
69
|
return {
|
|
69
70
|
code: 0,
|
|
70
|
-
proxy
|
|
71
|
+
proxy,
|
|
72
|
+
exitWithStdin: values["exit-with-stdin"] === true
|
|
71
73
|
};
|
|
72
74
|
} catch (err) {
|
|
73
75
|
io.stderr(errorMessage(err));
|
|
@@ -103,7 +105,10 @@ if (isEntryPoint()) {
|
|
|
103
105
|
if (run.proxy === void 0) process.exitCode = run.code;
|
|
104
106
|
else {
|
|
105
107
|
const proxy = run.proxy;
|
|
108
|
+
let stopping = false;
|
|
106
109
|
const stop = () => {
|
|
110
|
+
if (stopping) return;
|
|
111
|
+
stopping = true;
|
|
107
112
|
proxy.close().then(() => process.exit(0), (err) => {
|
|
108
113
|
console.error(errorMessage(err));
|
|
109
114
|
process.exit(1);
|
|
@@ -111,6 +116,11 @@ if (isEntryPoint()) {
|
|
|
111
116
|
};
|
|
112
117
|
process.once("SIGINT", stop);
|
|
113
118
|
process.once("SIGTERM", stop);
|
|
119
|
+
if (run.exitWithStdin === true) {
|
|
120
|
+
process.stdin.once("end", stop);
|
|
121
|
+
process.stdin.once("close", stop);
|
|
122
|
+
process.stdin.resume();
|
|
123
|
+
}
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
export { TOKEN_ENV, runCli };
|
|
@@ -315,24 +315,30 @@ const signatureKey$1 = (signature) => `anthropic:${signature}`;
|
|
|
315
315
|
function refuse$1(block) {
|
|
316
316
|
if (typeof block.type === "string" && REFUSED_BLOCKS[block.type] === true) throw new UnsupportedContentError(`Anthropic ${block.type} content cannot be transformed`);
|
|
317
317
|
}
|
|
318
|
+
/** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
|
|
319
|
+
function malformed$2(slot) {
|
|
320
|
+
throw new UnsupportedContentError(`Anthropic ${slot} has an unexpected shape`);
|
|
321
|
+
}
|
|
318
322
|
async function outbound$2(transform, text, leaf) {
|
|
319
323
|
return text === "" ? text : transform.outbound(text, leaf);
|
|
320
324
|
}
|
|
321
|
-
/** A string, or an array of blocks whose `text` blocks are rewritten. */
|
|
322
|
-
async function rewriteText(content, leaf, transform) {
|
|
325
|
+
/** A string, or an array of blocks whose `text` blocks are rewritten; anything else is refused. */
|
|
326
|
+
async function rewriteText(content, slot, leaf, transform) {
|
|
323
327
|
if (typeof content === "string") return outbound$2(transform, content, leaf);
|
|
324
|
-
if (!Array.isArray(content)) return
|
|
328
|
+
if (!Array.isArray(content)) return malformed$2(slot);
|
|
325
329
|
const out = [];
|
|
326
330
|
for (const block of content) {
|
|
327
|
-
if (!isObj$2(block)) {
|
|
331
|
+
if (!isObj$2(block)) return malformed$2(`${slot} block`);
|
|
332
|
+
refuse$1(block);
|
|
333
|
+
if (block.type !== "text") {
|
|
328
334
|
out.push(block);
|
|
329
335
|
continue;
|
|
330
336
|
}
|
|
331
|
-
|
|
332
|
-
out.push(
|
|
337
|
+
if (typeof block.text !== "string") return malformed$2(`${slot} text block`);
|
|
338
|
+
out.push({
|
|
333
339
|
...block,
|
|
334
340
|
text: await outbound$2(transform, block.text, leaf)
|
|
335
|
-
}
|
|
341
|
+
});
|
|
336
342
|
}
|
|
337
343
|
return out;
|
|
338
344
|
}
|
|
@@ -364,6 +370,7 @@ function restoreStrings$1(value, transform) {
|
|
|
364
370
|
}
|
|
365
371
|
/** A thinking block: swaps in the cached model text when its signature is known, else rewrites it. */
|
|
366
372
|
async function rewriteThinking(block, ctx) {
|
|
373
|
+
if (typeof block.thinking !== "string") return malformed$2("thinking block");
|
|
367
374
|
if (typeof block.signature === "string") {
|
|
368
375
|
const cached = ctx.signatures.get(signatureKey$1(block.signature));
|
|
369
376
|
if (cached !== void 0) return {
|
|
@@ -371,7 +378,6 @@ async function rewriteThinking(block, ctx) {
|
|
|
371
378
|
thinking: cached
|
|
372
379
|
};
|
|
373
380
|
}
|
|
374
|
-
if (typeof block.thinking !== "string") return block;
|
|
375
381
|
return {
|
|
376
382
|
...block,
|
|
377
383
|
thinking: await outbound$2(ctx.transform, block.thinking, { kind: "thinking" })
|
|
@@ -380,20 +386,18 @@ async function rewriteThinking(block, ctx) {
|
|
|
380
386
|
async function rewriteContent$2(content, role, toolNames, ctx) {
|
|
381
387
|
const textKind = role === "assistant" ? "assistant" : "user";
|
|
382
388
|
if (typeof content === "string") return outbound$2(ctx.transform, content, { kind: textKind });
|
|
383
|
-
if (!Array.isArray(content)) return content;
|
|
389
|
+
if (!Array.isArray(content)) return malformed$2("message content");
|
|
384
390
|
const out = [];
|
|
385
391
|
for (const block of content) {
|
|
386
|
-
if (!isObj$2(block))
|
|
387
|
-
out.push(block);
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
392
|
+
if (!isObj$2(block)) return malformed$2("message content block");
|
|
390
393
|
refuse$1(block);
|
|
391
394
|
switch (block.type) {
|
|
392
395
|
case "text":
|
|
393
|
-
|
|
396
|
+
if (typeof block.text !== "string") return malformed$2("text block");
|
|
397
|
+
out.push({
|
|
394
398
|
...block,
|
|
395
399
|
text: await outbound$2(ctx.transform, block.text, { kind: textKind })
|
|
396
|
-
}
|
|
400
|
+
});
|
|
397
401
|
break;
|
|
398
402
|
case "thinking":
|
|
399
403
|
out.push(await rewriteThinking(block, ctx));
|
|
@@ -410,10 +414,14 @@ async function rewriteContent$2(content, role, toolNames, ctx) {
|
|
|
410
414
|
break;
|
|
411
415
|
}
|
|
412
416
|
case "tool_result": {
|
|
417
|
+
if (block.content === void 0) {
|
|
418
|
+
out.push(block);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
413
421
|
const toolName = typeof block.tool_use_id === "string" ? toolNames.get(block.tool_use_id) : void 0;
|
|
414
422
|
out.push({
|
|
415
423
|
...block,
|
|
416
|
-
content: await rewriteText(block.content, {
|
|
424
|
+
content: await rewriteText(block.content, "tool_result content", {
|
|
417
425
|
kind: "toolResult",
|
|
418
426
|
toolName
|
|
419
427
|
}, ctx.transform)
|
|
@@ -557,17 +565,21 @@ function createStreamTransformer$2(ctx) {
|
|
|
557
565
|
}
|
|
558
566
|
const anthropicWalker = {
|
|
559
567
|
async request(body, ctx) {
|
|
560
|
-
if (!isObj$2(body)) return body;
|
|
568
|
+
if (!isObj$2(body)) return malformed$2("request body");
|
|
561
569
|
const out = { ...body };
|
|
562
|
-
if (body.system !== void 0) out.system = await rewriteText(body.system, { kind: "system" }, ctx.transform);
|
|
570
|
+
if (body.system !== void 0) out.system = await rewriteText(body.system, "system", { kind: "system" }, ctx.transform);
|
|
563
571
|
if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
|
|
564
|
-
if (
|
|
572
|
+
if (body.messages !== void 0) {
|
|
573
|
+
if (!Array.isArray(body.messages)) return malformed$2("messages");
|
|
565
574
|
const toolNames = toolNamesById(body.messages);
|
|
566
575
|
const messages = [];
|
|
567
|
-
for (const message of body.messages)
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
576
|
+
for (const message of body.messages) {
|
|
577
|
+
if (!isObj$2(message)) return malformed$2("message");
|
|
578
|
+
messages.push({
|
|
579
|
+
...message,
|
|
580
|
+
content: await rewriteContent$2(message.content, message.role, toolNames, ctx)
|
|
581
|
+
});
|
|
582
|
+
}
|
|
571
583
|
out.messages = messages;
|
|
572
584
|
}
|
|
573
585
|
return out;
|
|
@@ -615,21 +627,28 @@ const REASONING_FIELDS = ["reasoning_content", "reasoning"];
|
|
|
615
627
|
async function outbound$1(transform, text, leaf) {
|
|
616
628
|
return text === "" ? text : transform.outbound(text, leaf);
|
|
617
629
|
}
|
|
618
|
-
/** A
|
|
630
|
+
/** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
|
|
631
|
+
function malformed$1(slot) {
|
|
632
|
+
throw new UnsupportedContentError(`OpenAI chat ${slot} has an unexpected shape`);
|
|
633
|
+
}
|
|
634
|
+
/** A string, or an array of parts whose `text` parts are rewritten; `null`/absent carries no text; anything else is refused. */
|
|
619
635
|
async function rewriteContent$1(content, leaf, transform) {
|
|
620
636
|
if (typeof content === "string") return outbound$1(transform, content, leaf);
|
|
621
|
-
if (
|
|
637
|
+
if (content === void 0 || content === null) return content;
|
|
638
|
+
if (!Array.isArray(content)) return malformed$1("message content");
|
|
622
639
|
const out = [];
|
|
623
640
|
for (const part of content) {
|
|
624
|
-
if (!isObj$1(part))
|
|
641
|
+
if (!isObj$1(part)) return malformed$1("content part");
|
|
642
|
+
if (typeof part.type === "string" && REFUSED_PARTS$1[part.type] === true) throw new UnsupportedContentError(`OpenAI chat ${part.type} content cannot be transformed`);
|
|
643
|
+
if (part.type !== "text") {
|
|
625
644
|
out.push(part);
|
|
626
645
|
continue;
|
|
627
646
|
}
|
|
628
|
-
if (typeof part.
|
|
629
|
-
out.push(
|
|
647
|
+
if (typeof part.text !== "string") return malformed$1("text part");
|
|
648
|
+
out.push({
|
|
630
649
|
...part,
|
|
631
650
|
text: await outbound$1(transform, part.text, leaf)
|
|
632
|
-
}
|
|
651
|
+
});
|
|
633
652
|
}
|
|
634
653
|
return out;
|
|
635
654
|
}
|
|
@@ -677,17 +696,20 @@ function restoreArguments$1(args, transform) {
|
|
|
677
696
|
return JSON.stringify(restoreStrings(parsed, transform));
|
|
678
697
|
}
|
|
679
698
|
async function rewriteToolCalls(calls, transform, names) {
|
|
680
|
-
if (
|
|
699
|
+
if (calls === void 0 || calls === null) return calls;
|
|
700
|
+
if (!Array.isArray(calls)) return malformed$1("tool_calls");
|
|
681
701
|
const out = [];
|
|
682
702
|
for (const call of calls) {
|
|
683
|
-
if (!isObj$1(call)
|
|
703
|
+
if (!isObj$1(call)) return malformed$1("tool call");
|
|
704
|
+
if (call.type !== void 0 && call.type !== "function") {
|
|
684
705
|
out.push(call);
|
|
685
706
|
continue;
|
|
686
707
|
}
|
|
687
708
|
const fn = call.function;
|
|
709
|
+
if (!isObj$1(fn) || typeof fn.arguments !== "string") return malformed$1("function tool call");
|
|
688
710
|
const toolName = typeof fn.name === "string" ? fn.name : void 0;
|
|
689
711
|
if (typeof call.id === "string" && toolName !== void 0) names.set(call.id, toolName);
|
|
690
|
-
out.push(
|
|
712
|
+
out.push({
|
|
691
713
|
...call,
|
|
692
714
|
function: {
|
|
693
715
|
...fn,
|
|
@@ -696,7 +718,7 @@ async function rewriteToolCalls(calls, transform, names) {
|
|
|
696
718
|
toolName
|
|
697
719
|
}, transform)
|
|
698
720
|
}
|
|
699
|
-
}
|
|
721
|
+
});
|
|
700
722
|
}
|
|
701
723
|
return out;
|
|
702
724
|
}
|
|
@@ -714,6 +736,7 @@ async function rewriteMessage(message, transform, names) {
|
|
|
714
736
|
for (const field of REASONING_FIELDS) {
|
|
715
737
|
const value = message[field];
|
|
716
738
|
if (typeof value === "string") out[field] = await outbound$1(transform, value, { kind: "thinking" });
|
|
739
|
+
else if (value !== void 0 && value !== null) return malformed$1(`assistant ${field}`);
|
|
717
740
|
}
|
|
718
741
|
out.content = await rewriteContent$1(message.content, { kind: "assistant" }, transform);
|
|
719
742
|
out.tool_calls = await rewriteToolCalls(message.tool_calls, transform, names);
|
|
@@ -727,7 +750,15 @@ async function rewriteMessage(message, transform, names) {
|
|
|
727
750
|
}, transform);
|
|
728
751
|
break;
|
|
729
752
|
}
|
|
730
|
-
|
|
753
|
+
case "function": {
|
|
754
|
+
const toolName = typeof message.name === "string" ? message.name : void 0;
|
|
755
|
+
out.content = await rewriteContent$1(message.content, {
|
|
756
|
+
kind: "toolResult",
|
|
757
|
+
toolName
|
|
758
|
+
}, transform);
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
default: return malformed$1("message role");
|
|
731
762
|
}
|
|
732
763
|
if (message.content === void 0) delete out.content;
|
|
733
764
|
return out;
|
|
@@ -874,12 +905,16 @@ function createStreamTransformer$1(ctx) {
|
|
|
874
905
|
}
|
|
875
906
|
const openaiChatWalker = {
|
|
876
907
|
async request(body, ctx) {
|
|
877
|
-
if (!isObj$1(body)) return body;
|
|
908
|
+
if (!isObj$1(body)) return malformed$1("request body");
|
|
878
909
|
const out = { ...body };
|
|
879
|
-
if (
|
|
910
|
+
if (body.messages !== void 0) {
|
|
911
|
+
if (!Array.isArray(body.messages)) return malformed$1("messages");
|
|
880
912
|
const names = /* @__PURE__ */ new Map();
|
|
881
913
|
const messages = [];
|
|
882
|
-
for (const message of body.messages)
|
|
914
|
+
for (const message of body.messages) {
|
|
915
|
+
if (!isObj$1(message)) return malformed$1("message");
|
|
916
|
+
messages.push(await rewriteMessage(message, ctx.transform, names));
|
|
917
|
+
}
|
|
883
918
|
if (ctx.transform.instruction !== void 0) messages.unshift({
|
|
884
919
|
role: "system",
|
|
885
920
|
content: ctx.transform.instruction
|
|
@@ -937,21 +972,27 @@ function refuse(part) {
|
|
|
937
972
|
async function outbound(transform, text, leaf) {
|
|
938
973
|
return text === "" ? text : transform.outbound(text, leaf);
|
|
939
974
|
}
|
|
940
|
-
/** A
|
|
941
|
-
|
|
975
|
+
/** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
|
|
976
|
+
function malformed(slot) {
|
|
977
|
+
throw new UnsupportedContentError(`OpenAI responses ${slot} has an unexpected shape`);
|
|
978
|
+
}
|
|
979
|
+
/** A string, or an array of parts whose `input_text`/`output_text` parts are rewritten; anything else is refused. */
|
|
980
|
+
async function rewriteContent(content, slot, leaf, transform) {
|
|
942
981
|
if (typeof content === "string") return outbound(transform, content, leaf);
|
|
943
|
-
if (!Array.isArray(content)) return
|
|
982
|
+
if (!Array.isArray(content)) return malformed(slot);
|
|
944
983
|
const out = [];
|
|
945
984
|
for (const part of content) {
|
|
946
|
-
if (!isObj(part)) {
|
|
985
|
+
if (!isObj(part)) return malformed(`${slot} part`);
|
|
986
|
+
refuse(part);
|
|
987
|
+
if (part.type !== "input_text" && part.type !== "output_text") {
|
|
947
988
|
out.push(part);
|
|
948
989
|
continue;
|
|
949
990
|
}
|
|
950
|
-
|
|
951
|
-
out.push(
|
|
991
|
+
if (typeof part.text !== "string") return malformed(`${slot} ${part.type} part`);
|
|
992
|
+
out.push({
|
|
952
993
|
...part,
|
|
953
994
|
text: await outbound(transform, part.text, leaf)
|
|
954
|
-
}
|
|
995
|
+
});
|
|
955
996
|
}
|
|
956
997
|
return out;
|
|
957
998
|
}
|
|
@@ -1002,17 +1043,14 @@ function cachedSummary(item, ctx) {
|
|
|
1002
1043
|
return texts;
|
|
1003
1044
|
}
|
|
1004
1045
|
async function rewriteReasoning(item, ctx) {
|
|
1005
|
-
if (
|
|
1046
|
+
if (item.summary === void 0) return item;
|
|
1047
|
+
if (!Array.isArray(item.summary)) return malformed("reasoning summary");
|
|
1048
|
+
for (const part of item.summary) if (!isObj(part) || typeof part.text !== "string") return malformed("reasoning summary part");
|
|
1006
1049
|
const cached = cachedSummary(item, ctx);
|
|
1007
1050
|
const summary = [];
|
|
1008
|
-
for (const [i, part] of item.summary.entries())
|
|
1009
|
-
else if (cached !== void 0) summary.push({
|
|
1051
|
+
for (const [i, part] of item.summary.entries()) summary.push({
|
|
1010
1052
|
...part,
|
|
1011
|
-
text: cached[i]
|
|
1012
|
-
});
|
|
1013
|
-
else summary.push({
|
|
1014
|
-
...part,
|
|
1015
|
-
text: await outbound(ctx.transform, part.text, { kind: "thinking" })
|
|
1053
|
+
text: cached !== void 0 ? cached[i] : await outbound(ctx.transform, part.text, { kind: "thinking" })
|
|
1016
1054
|
});
|
|
1017
1055
|
return {
|
|
1018
1056
|
...item,
|
|
@@ -1032,15 +1070,15 @@ function toolNamesByCallId(items) {
|
|
|
1032
1070
|
async function rewriteItem(item, toolNames, ctx) {
|
|
1033
1071
|
if (item.type === "message" || item.type === void 0 && item.role !== void 0) {
|
|
1034
1072
|
const kind = roleKind(item.role);
|
|
1035
|
-
if (kind === void 0) return
|
|
1073
|
+
if (kind === void 0) return malformed("message role");
|
|
1036
1074
|
return {
|
|
1037
1075
|
...item,
|
|
1038
|
-
content: await rewriteContent(item.content, { kind }, ctx.transform)
|
|
1076
|
+
content: await rewriteContent(item.content, "message content", { kind }, ctx.transform)
|
|
1039
1077
|
};
|
|
1040
1078
|
}
|
|
1041
1079
|
switch (item.type) {
|
|
1042
1080
|
case "function_call": {
|
|
1043
|
-
if (typeof item.arguments !== "string") return
|
|
1081
|
+
if (typeof item.arguments !== "string") return malformed("function_call arguments");
|
|
1044
1082
|
const toolName = typeof item.name === "string" ? item.name : void 0;
|
|
1045
1083
|
return {
|
|
1046
1084
|
...item,
|
|
@@ -1054,7 +1092,7 @@ async function rewriteItem(item, toolNames, ctx) {
|
|
|
1054
1092
|
const toolName = typeof item.call_id === "string" ? toolNames.get(item.call_id) : void 0;
|
|
1055
1093
|
return {
|
|
1056
1094
|
...item,
|
|
1057
|
-
output: await rewriteContent(item.output, {
|
|
1095
|
+
output: await rewriteContent(item.output, "function_call_output output", {
|
|
1058
1096
|
kind: "toolResult",
|
|
1059
1097
|
toolName
|
|
1060
1098
|
}, ctx.transform)
|
|
@@ -1226,18 +1264,22 @@ function createStreamTransformer(ctx) {
|
|
|
1226
1264
|
}
|
|
1227
1265
|
const openaiResponsesWalker = {
|
|
1228
1266
|
async request(body, ctx) {
|
|
1229
|
-
if (!isObj(body)) return body;
|
|
1267
|
+
if (!isObj(body)) return malformed("request body");
|
|
1230
1268
|
const out = { ...body };
|
|
1231
1269
|
if (typeof body.instructions === "string") out.instructions = await outbound(ctx.transform, body.instructions, { kind: "system" });
|
|
1270
|
+
else if (body.instructions !== void 0 && body.instructions !== null) return malformed("instructions");
|
|
1232
1271
|
const instruction = ctx.transform.instruction;
|
|
1233
1272
|
if (instruction !== void 0) out.instructions = typeof out.instructions === "string" && out.instructions !== "" ? `${instruction}\n\n${out.instructions}` : instruction;
|
|
1234
1273
|
if (typeof body.input === "string") out.input = await outbound(ctx.transform, body.input, { kind: "user" });
|
|
1235
1274
|
else if (Array.isArray(body.input)) {
|
|
1236
1275
|
const toolNames = toolNamesByCallId(body.input);
|
|
1237
1276
|
const input = [];
|
|
1238
|
-
for (const item of body.input)
|
|
1277
|
+
for (const item of body.input) {
|
|
1278
|
+
if (!isObj(item)) return malformed("input item");
|
|
1279
|
+
input.push(await rewriteItem(item, toolNames, ctx));
|
|
1280
|
+
}
|
|
1239
1281
|
out.input = input;
|
|
1240
|
-
}
|
|
1282
|
+
} else if (body.input !== void 0) return malformed("input");
|
|
1241
1283
|
return out;
|
|
1242
1284
|
},
|
|
1243
1285
|
response(body, ctx) {
|
|
@@ -1465,7 +1507,7 @@ async function createTransformProxy(options) {
|
|
|
1465
1507
|
/** Relays an upstream response. With `rewrite`, a 2xx JSON or SSE body is restored on the way back. */
|
|
1466
1508
|
async function relayUpstream(res, upstreamRes, rewrite) {
|
|
1467
1509
|
if (upstreamRes === void 0) return;
|
|
1468
|
-
const contentType = upstreamRes.headers.get("content-type") ?? "";
|
|
1510
|
+
const contentType = (upstreamRes.headers.get("content-type") ?? "").toLowerCase();
|
|
1469
1511
|
const headers = relayHeaders(upstreamRes.headers);
|
|
1470
1512
|
if (rewrite !== void 0 && upstreamRes.ok && contentType.startsWith("text/event-stream")) return pipeStream(res, upstreamRes, headers, rewrite.wire.stream(rewrite.ctx), log);
|
|
1471
1513
|
if (rewrite !== void 0 && upstreamRes.ok && contentType.includes("application/json")) {
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as CONFIG_FILE_NAME, s as PIPELINE_HEADER, c as SESSION_HEADER, l as TOKEN_HEADER, u as UPSTREAM_HEADER, h as UnsupportedContentError, m as anthropicWalker, _ as composeTransforms, n as configCandidates, g as createSessionFacts, d as createTransformProxy, r as findProxyConfig, i as loadProxyConfig, p as openaiChatWalker, f as openaiResponsesWalker, a as parseProxyConfig, o as resolveProxyConfig, y as silentLogger } from "./config-
|
|
1
|
+
export { t as CONFIG_FILE_NAME, s as PIPELINE_HEADER, c as SESSION_HEADER, l as TOKEN_HEADER, u as UPSTREAM_HEADER, h as UnsupportedContentError, m as anthropicWalker, _ as composeTransforms, n as configCandidates, g as createSessionFacts, d as createTransformProxy, r as findProxyConfig, i as loadProxyConfig, p as openaiChatWalker, f as openaiResponsesWalker, a as parseProxyConfig, o as resolveProxyConfig, y as silentLogger } from "./config-PKHxejC8.mjs";
|
package/dist/omp/index.d.mts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
2
|
interface OmpModel {
|
|
3
3
|
readonly provider: string;
|
|
4
4
|
readonly id: string;
|
|
5
5
|
readonly api: string;
|
|
6
6
|
readonly baseUrl?: string;
|
|
7
|
+
/** The provider headers projected onto the model, the transform-proxy headers included. */
|
|
8
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
7
9
|
}
|
|
8
10
|
/** A runtime provider override: omp merges `headers` into the provider's existing headers. */
|
|
9
11
|
interface OmpProviderOverride {
|
|
@@ -35,15 +37,22 @@ interface OmpContext {
|
|
|
35
37
|
readonly sessionManager: {
|
|
36
38
|
getSessionId(): string;
|
|
37
39
|
};
|
|
38
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The session's active model: a snapshot that a provider override does not update. In
|
|
42
|
+
* `before_provider_request` it is the model of that request.
|
|
43
|
+
*/
|
|
39
44
|
readonly model: OmpModel | undefined;
|
|
45
|
+
/** False for headless sessions: subagents, print and RPC mode. */
|
|
46
|
+
readonly hasUI?: boolean;
|
|
47
|
+
/** `tui`, `rpc`, `json` or `print`; subagent sessions run as `print`. */
|
|
48
|
+
readonly mode?: string;
|
|
40
49
|
}
|
|
41
50
|
interface OmpAutocompleteItem {
|
|
42
51
|
value: string;
|
|
43
52
|
label: string;
|
|
44
53
|
description?: string;
|
|
45
54
|
}
|
|
46
|
-
type OmpEventName = 'session_start' | 'before_agent_start' | 'turn_start';
|
|
55
|
+
type OmpEventName = 'session_start' | 'session_switch' | 'before_agent_start' | 'turn_start' | 'before_provider_request';
|
|
47
56
|
/** Per-session actions of the extension API. */
|
|
48
57
|
interface OmpSessionActions {
|
|
49
58
|
/** Rebinds the session's active model (without persisting it as the default). */
|
|
@@ -55,15 +64,37 @@ interface OmpExtensionApi extends OmpSessionActions {
|
|
|
55
64
|
getArgumentCompletions?: (argumentPrefix: string) => OmpAutocompleteItem[] | null;
|
|
56
65
|
handler: (args: string, ctx: OmpContext) => Promise<void>;
|
|
57
66
|
}): void;
|
|
58
|
-
|
|
67
|
+
/** For `before_provider_request`, a non-undefined result replaces the request payload. */
|
|
68
|
+
on(event: OmpEventName, handler: (event: unknown, ctx: OmpContext) => unknown): void;
|
|
69
|
+
}
|
|
70
|
+
/** How the extension spawns its proxy child. */
|
|
71
|
+
interface ProxySpawnOptions {
|
|
72
|
+
readonly cwd: string;
|
|
73
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
74
|
+
readonly stdio: readonly ['pipe', 'pipe', 'pipe'];
|
|
75
|
+
}
|
|
76
|
+
/** The slice of a `node:child_process` `ChildProcess` the extension uses. */
|
|
77
|
+
interface ProxyChildProcess {
|
|
78
|
+
readonly pid?: number | undefined;
|
|
79
|
+
readonly stdin: Writable | null;
|
|
80
|
+
readonly stdout: Readable | null;
|
|
81
|
+
readonly stderr: Readable | null;
|
|
82
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
83
|
+
unref(): void;
|
|
84
|
+
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
85
|
+
on(event: 'error', listener: (err: Error) => void): unknown;
|
|
59
86
|
}
|
|
60
87
|
interface AnonControllerDeps {
|
|
61
88
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
62
89
|
readonly home: string;
|
|
63
90
|
/** Default: `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `<omp agent dir>/skaile-transform-proxy.json`. */
|
|
64
91
|
readonly statePath?: string;
|
|
65
|
-
/** Default: `
|
|
66
|
-
readonly
|
|
92
|
+
/** Spawns the proxy child. Default: `node:child_process` `spawn`. */
|
|
93
|
+
readonly spawnChild?: (command: string, args: readonly string[], options: ProxySpawnOptions) => ProxyChildProcess;
|
|
94
|
+
/** The CLI the child runs. Default: this package's `dist/cli.mjs`, next to the built extension's `dist/omp/`. */
|
|
95
|
+
readonly cliPath?: string;
|
|
96
|
+
/** How long the child may take to print `listening <url>`. Default: 60 s (transform modules may load ML models). */
|
|
97
|
+
readonly listenTimeoutMs?: number;
|
|
67
98
|
}
|
|
68
99
|
interface AnonController {
|
|
69
100
|
readonly required: boolean;
|
|
@@ -71,14 +102,30 @@ interface AnonController {
|
|
|
71
102
|
/** Handles `/anon <args>`. `session` rebinds the session's active model after a switch. */
|
|
72
103
|
command(args: string, ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
73
104
|
completions(prefix: string): OmpAutocompleteItem[] | null;
|
|
74
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Applies the persisted flag (or required mode) and refreshes the status bar. A root session
|
|
107
|
+
* whose omp session id changed rotates the proxy session; subagents keep the parent's id.
|
|
108
|
+
*/
|
|
75
109
|
sessionStart(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
76
|
-
/**
|
|
110
|
+
/** `/new`, `/resume`, `/fork`: the root session changed, so the proxy session rotates. */
|
|
111
|
+
sessionSwitch(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
112
|
+
/** While on (or failed): routes providers that appeared, or were reset, since the last pass. */
|
|
77
113
|
reapply(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* `before_provider_request`: while anon is on, failed or required, a request whose model does
|
|
116
|
+
* not target the transform proxy gets a replacement payload without any conversation text.
|
|
117
|
+
* Returns `undefined` to keep the payload.
|
|
118
|
+
*/
|
|
119
|
+
providerRequest(ctx: OmpContext): Record<string, never> | undefined;
|
|
78
120
|
}
|
|
79
121
|
/**
|
|
80
122
|
* The process-wide `/anon` state machine. omp builds one extension instance per session
|
|
81
123
|
* (subagents included) while they share one model registry, so one controller serves them all.
|
|
124
|
+
*
|
|
125
|
+
* `wanted`: model requests must go through the proxy (required mode, or `/anon on`).
|
|
126
|
+
* `enabled`: every provider is routed and the active model is rebound. `wanted` without
|
|
127
|
+
* `enabled` is the error state: the footer shows `anon error` and `providerRequest` refuses
|
|
128
|
+
* every request that would bypass the proxy.
|
|
82
129
|
*/
|
|
83
130
|
export declare function createAnonController(deps: AnonControllerDeps): AnonController;
|
|
84
131
|
/** `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `skaile-transform-proxy.json` in omp's agent dir (`$PI_CODING_AGENT_DIR` or `~/.omp/agent`). */
|
|
@@ -86,4 +133,4 @@ export declare function defaultStatePath(env: Readonly<Record<string, string | u
|
|
|
86
133
|
/** Registers the `/anon` command and the event handlers on one session's extension API. */
|
|
87
134
|
export declare function registerAnonExtension(pi: OmpExtensionApi, controller: AnonController): void;
|
|
88
135
|
export default function anonExtension(pi: OmpExtensionApi): void;
|
|
89
|
-
export type { AnonController, AnonControllerDeps, OmpAutocompleteItem, OmpContext, OmpEventName, OmpExtensionApi, OmpModel, OmpModelRegistry, OmpNotifyType, OmpProviderOverride, OmpSessionActions, OmpUi };
|
|
136
|
+
export type { AnonController, AnonControllerDeps, OmpAutocompleteItem, OmpContext, OmpEventName, OmpExtensionApi, OmpModel, OmpModelRegistry, OmpNotifyType, OmpProviderOverride, OmpSessionActions, OmpUi, ProxyChildProcess, ProxySpawnOptions };
|
package/dist/omp/index.mjs
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { c as SESSION_HEADER,
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { c as SESSION_HEADER, i as loadProxyConfig, l as TOKEN_HEADER, n as configCandidates, r as findProxyConfig, u as UPSTREAM_HEADER, v as errorMessage } from "../config-PKHxejC8.mjs";
|
|
2
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
6
8
|
// src/omp/anon.ts
|
|
7
9
|
/** A running proxy the extension routes to. */
|
|
8
10
|
const URL_ENV = "SKAILE_TRANSFORM_PROXY_URL";
|
|
9
|
-
/** The client token of that proxy. */
|
|
11
|
+
/** The client token of that proxy; also how the extension hands its own proxy child a random token. */
|
|
10
12
|
const TOKEN_ENV = "SKAILE_TRANSFORM_PROXY_TOKEN";
|
|
13
|
+
/** The Node binary that runs the proxy child; default: `node` from `PATH`. */
|
|
14
|
+
const NODE_ENV = "SKAILE_TRANSFORM_PROXY_NODE";
|
|
11
15
|
/** `required` (set by a skaile session in required mode): routing is forced on and `/anon off` is refused. */
|
|
12
16
|
const MODE_ENV = "SKAILE_TRANSFORM_PROXY_MODE";
|
|
13
17
|
/** Overrides the path of the persisted enabled flag. */
|
|
@@ -43,13 +47,20 @@ const VERBS = [
|
|
|
43
47
|
/**
|
|
44
48
|
* The process-wide `/anon` state machine. omp builds one extension instance per session
|
|
45
49
|
* (subagents included) while they share one model registry, so one controller serves them all.
|
|
50
|
+
*
|
|
51
|
+
* `wanted`: model requests must go through the proxy (required mode, or `/anon on`).
|
|
52
|
+
* `enabled`: every provider is routed and the active model is rebound. `wanted` without
|
|
53
|
+
* `enabled` is the error state: the footer shows `anon error` and `providerRequest` refuses
|
|
54
|
+
* every request that would bypass the proxy.
|
|
46
55
|
*/
|
|
47
56
|
function createAnonController(deps) {
|
|
48
57
|
const env = deps.env;
|
|
49
58
|
const required = env[MODE_ENV] === "required";
|
|
50
59
|
const statePath = deps.statePath ?? defaultStatePath(env, deps.home);
|
|
51
|
-
const
|
|
60
|
+
const spawnChild = deps.spawnChild ?? spawnNode;
|
|
61
|
+
const listenTimeoutMs = deps.listenTimeoutMs ?? 6e4;
|
|
52
62
|
const registries = /* @__PURE__ */ new WeakMap();
|
|
63
|
+
let wanted = required;
|
|
53
64
|
let enabled = false;
|
|
54
65
|
let endpoint;
|
|
55
66
|
let starting;
|
|
@@ -59,14 +70,14 @@ function createAnonController(deps) {
|
|
|
59
70
|
if (state === void 0) {
|
|
60
71
|
state = {
|
|
61
72
|
originals: /* @__PURE__ */ new Map(),
|
|
62
|
-
routed: /* @__PURE__ */ new
|
|
73
|
+
routed: /* @__PURE__ */ new Map()
|
|
63
74
|
};
|
|
64
75
|
registries.set(registry, state);
|
|
65
76
|
}
|
|
66
77
|
return state;
|
|
67
78
|
}
|
|
68
79
|
async function resolveEndpoint(ctx) {
|
|
69
|
-
if (endpoint !== void 0) return endpoint;
|
|
80
|
+
if (endpoint !== void 0 && endpoint.child?.exited !== true) return endpoint;
|
|
70
81
|
const url = env[URL_ENV];
|
|
71
82
|
if (url !== void 0 && url !== "") {
|
|
72
83
|
const token = env[TOKEN_ENV];
|
|
@@ -77,13 +88,17 @@ function createAnonController(deps) {
|
|
|
77
88
|
};
|
|
78
89
|
return endpoint;
|
|
79
90
|
}
|
|
80
|
-
starting ??=
|
|
91
|
+
starting ??= startChild(ctx.cwd).finally(() => {
|
|
81
92
|
starting = void 0;
|
|
82
93
|
});
|
|
83
94
|
endpoint = await starting;
|
|
84
95
|
return endpoint;
|
|
85
96
|
}
|
|
86
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Runs the proxy CLI under the system Node: omp is a compiled Bun executable whose resolver
|
|
99
|
+
* cannot load transform modules with native or ML dependencies from `node_modules`.
|
|
100
|
+
*/
|
|
101
|
+
async function startChild(cwd) {
|
|
87
102
|
const path = findProxyConfig({
|
|
88
103
|
cwd,
|
|
89
104
|
home: deps.home
|
|
@@ -94,27 +109,60 @@ function createAnonController(deps) {
|
|
|
94
109
|
}
|
|
95
110
|
const options = loadProxyConfig(path);
|
|
96
111
|
if (options.defaultPipeline === void 0) throw new Error(`${path} sets no defaultPipeline: the omp extension sends no pipeline header`);
|
|
112
|
+
const token = randomBytes(32).toString("base64url");
|
|
113
|
+
const node = env["SKAILE_TRANSFORM_PROXY_NODE"] || "node";
|
|
114
|
+
const cli = deps.cliPath ?? fileURLToPath(new URL("../cli.mjs", import.meta.url));
|
|
115
|
+
const child = spawnChild(node, [
|
|
116
|
+
cli,
|
|
117
|
+
"--config",
|
|
118
|
+
path,
|
|
119
|
+
"--port",
|
|
120
|
+
"0",
|
|
121
|
+
"--host",
|
|
122
|
+
"127.0.0.1",
|
|
123
|
+
"--exit-with-stdin"
|
|
124
|
+
], {
|
|
125
|
+
cwd,
|
|
126
|
+
env: {
|
|
127
|
+
...env,
|
|
128
|
+
[TOKEN_ENV]: token
|
|
129
|
+
},
|
|
130
|
+
stdio: [
|
|
131
|
+
"pipe",
|
|
132
|
+
"pipe",
|
|
133
|
+
"pipe"
|
|
134
|
+
]
|
|
135
|
+
});
|
|
136
|
+
const state = { exited: false };
|
|
137
|
+
child.on("exit", () => {
|
|
138
|
+
state.exited = true;
|
|
139
|
+
if (endpoint?.child === state) endpoint = void 0;
|
|
140
|
+
});
|
|
97
141
|
return {
|
|
98
|
-
url:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
url: await waitForListening(child, {
|
|
143
|
+
node,
|
|
144
|
+
token,
|
|
145
|
+
timeoutMs: listenTimeoutMs
|
|
146
|
+
}),
|
|
147
|
+
token,
|
|
102
148
|
pipeline: options.defaultPipeline,
|
|
103
|
-
source: `
|
|
149
|
+
source: `child process ${child.pid}, config ${path}`,
|
|
150
|
+
child: state
|
|
104
151
|
};
|
|
105
152
|
}
|
|
106
153
|
function currentSessionId(ctx) {
|
|
107
|
-
|
|
108
|
-
let id;
|
|
109
|
-
try {
|
|
110
|
-
id = ctx.sessionManager.getSessionId();
|
|
111
|
-
} catch {
|
|
112
|
-
id = void 0;
|
|
113
|
-
}
|
|
114
|
-
sessionId = typeof id === "string" && id !== "" ? id : randomUUID();
|
|
115
|
-
}
|
|
154
|
+
sessionId ??= readSessionId(ctx) ?? randomUUID();
|
|
116
155
|
return sessionId;
|
|
117
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Adopts the session id of a root session. The first session of the process is the root;
|
|
159
|
+
* later `session_start`s are subagents (headless, `print` mode) unless the session has a UI
|
|
160
|
+
* or runs in another mode.
|
|
161
|
+
*/
|
|
162
|
+
function enterSession(ctx, root) {
|
|
163
|
+
if (sessionId === void 0) currentSessionId(ctx);
|
|
164
|
+
else if (root) sessionId = readSessionId(ctx) ?? sessionId;
|
|
165
|
+
}
|
|
118
166
|
function route(ctx, target) {
|
|
119
167
|
const registry = ctx.modelRegistry;
|
|
120
168
|
const state = stateOf(registry);
|
|
@@ -126,7 +174,7 @@ function createAnonController(deps) {
|
|
|
126
174
|
state.originals.set(provider, original);
|
|
127
175
|
}
|
|
128
176
|
if (original.preRouted) continue;
|
|
129
|
-
if (state.routed.
|
|
177
|
+
if (state.routed.get(provider) === session && registry.getProviderBaseUrl(provider) === target.url) continue;
|
|
130
178
|
const headers = { [SESSION_HEADER]: session };
|
|
131
179
|
if (original.baseUrl !== void 0) headers[UPSTREAM_HEADER] = original.baseUrl;
|
|
132
180
|
if (target.token !== void 0) headers[TOKEN_HEADER] = target.token;
|
|
@@ -134,7 +182,7 @@ function createAnonController(deps) {
|
|
|
134
182
|
baseUrl: target.url,
|
|
135
183
|
headers
|
|
136
184
|
}, SOURCE_ID);
|
|
137
|
-
state.routed.
|
|
185
|
+
state.routed.set(provider, session);
|
|
138
186
|
}
|
|
139
187
|
}
|
|
140
188
|
/** Clears the proxy overrides; re-applies the recorded original where another override was layered underneath. */
|
|
@@ -143,7 +191,7 @@ function createAnonController(deps) {
|
|
|
143
191
|
const state = stateOf(registry);
|
|
144
192
|
registry.clearSourceRegistrations(SOURCE_ID);
|
|
145
193
|
const lost = [];
|
|
146
|
-
for (const provider of state.routed) {
|
|
194
|
+
for (const provider of state.routed.keys()) {
|
|
147
195
|
const original = state.originals.get(provider);
|
|
148
196
|
const restored = registry.getProviderBaseUrl(provider);
|
|
149
197
|
if (original === void 0 || restored === original.baseUrl) continue;
|
|
@@ -159,26 +207,52 @@ function createAnonController(deps) {
|
|
|
159
207
|
state.routed.clear();
|
|
160
208
|
return lost;
|
|
161
209
|
}
|
|
162
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Routes every provider and rebinds the active model. Returns the failure, or `undefined`.
|
|
212
|
+
* A failed rebind leaves the session on a model that bypasses the proxy: outside required
|
|
213
|
+
* mode the routes are rolled back, so the registry matches the state shown.
|
|
214
|
+
*/
|
|
215
|
+
async function activate(ctx, session) {
|
|
163
216
|
let target;
|
|
164
217
|
try {
|
|
165
218
|
target = await resolveEndpoint(ctx);
|
|
166
219
|
} catch (err) {
|
|
167
|
-
|
|
168
|
-
return
|
|
220
|
+
enabled = false;
|
|
221
|
+
return errorMessage(err);
|
|
169
222
|
}
|
|
170
223
|
route(ctx, target);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
224
|
+
if (await rebind(ctx, session)) {
|
|
225
|
+
enabled = true;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!required) unroute(ctx);
|
|
229
|
+
enabled = false;
|
|
230
|
+
const active = ctx.model;
|
|
231
|
+
return `could not rebind the active model ${active?.provider}/${active?.id} to the transform proxy`;
|
|
174
232
|
}
|
|
175
233
|
/** The session holds its active model as a snapshot: swap in the registry's re-projected one. */
|
|
176
234
|
async function rebind(ctx, session) {
|
|
177
235
|
const active = ctx.model;
|
|
178
|
-
if (active === void 0) return;
|
|
236
|
+
if (active === void 0) return true;
|
|
179
237
|
const live = ctx.modelRegistry.find(active.provider, active.id);
|
|
180
|
-
if (live === void 0
|
|
181
|
-
if (
|
|
238
|
+
if (live === void 0) return false;
|
|
239
|
+
if (live.baseUrl === active.baseUrl && live.headers?.["x-skaile-transform-session"] === active.headers?.["x-skaile-transform-session"]) return true;
|
|
240
|
+
try {
|
|
241
|
+
return await session.setModel(live);
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Why a request of `model` would bypass the proxy, or `undefined` when it targets it. */
|
|
247
|
+
function bypassReason(ctx, model) {
|
|
248
|
+
if (model === void 0) return "the request names no model";
|
|
249
|
+
if (stateOf(ctx.modelRegistry).originals.get(model.provider)?.preRouted === true) return void 0;
|
|
250
|
+
const proxyOrigin = endpoint === void 0 ? void 0 : originOf(endpoint.url);
|
|
251
|
+
if (proxyOrigin === void 0) return "the transform proxy is not available";
|
|
252
|
+
if (model.baseUrl === void 0 || originOf(model.baseUrl) !== proxyOrigin) return `${model.provider}/${model.id} does not target the transform proxy`;
|
|
253
|
+
}
|
|
254
|
+
function refusalHint() {
|
|
255
|
+
return required ? `anon: required mode: model requests are refused until the transform proxy is routed (${MODE_ENV}=required)` : "anon: model requests are refused until /anon on succeeds; /anon off sends them directly";
|
|
182
256
|
}
|
|
183
257
|
function persist(ctx, value) {
|
|
184
258
|
try {
|
|
@@ -188,20 +262,23 @@ function createAnonController(deps) {
|
|
|
188
262
|
ctx.ui.notify(`anon: could not save ${statePath}: ${errorMessage(err)}`, "warning");
|
|
189
263
|
}
|
|
190
264
|
}
|
|
265
|
+
function stateLabel() {
|
|
266
|
+
return enabled ? "on" : wanted ? "error" : "off";
|
|
267
|
+
}
|
|
191
268
|
function showStatus(ctx) {
|
|
192
|
-
ctx.ui.setStatus(STATUS_KEY,
|
|
269
|
+
ctx.ui.setStatus(STATUS_KEY, `anon ${stateLabel()}`);
|
|
193
270
|
}
|
|
194
271
|
function statusText(ctx) {
|
|
195
272
|
const registry = ctx.modelRegistry;
|
|
196
273
|
const state = stateOf(registry);
|
|
197
274
|
const apis = providerApis(registry);
|
|
198
|
-
const lines = [`anon ${
|
|
275
|
+
const lines = [`anon ${stateLabel()}${required ? " (required)" : ""}`];
|
|
199
276
|
if (endpoint !== void 0) {
|
|
200
277
|
lines.push(`proxy: ${endpoint.url} (${endpoint.source})`);
|
|
201
278
|
lines.push(`pipeline: ${endpoint.pipeline ?? "the proxy default"}`);
|
|
202
279
|
}
|
|
203
280
|
if (sessionId !== void 0) lines.push(`session: ${sessionId}`);
|
|
204
|
-
const routed = [...state.routed].sort();
|
|
281
|
+
const routed = [...state.routed.keys()].sort();
|
|
205
282
|
const refused = routed.flatMap((provider) => {
|
|
206
283
|
const reason = refusalReason(apis.get(provider), state.originals.get(provider));
|
|
207
284
|
return reason === void 0 ? [] : [`${provider} (${reason})`];
|
|
@@ -212,26 +289,52 @@ function createAnonController(deps) {
|
|
|
212
289
|
if (preRouted.length > 0) lines.push(`routed by their own config (${preRouted.length}): ${preRouted.sort().join(", ")}`);
|
|
213
290
|
return lines.join("\n");
|
|
214
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Session entry: rotate the proxy session for a new root, then (re-)apply routing when wanted.
|
|
294
|
+
* Rotation only switches the session header: the proxy keeps the old session's state in memory
|
|
295
|
+
* until it exits, and no request carries that id again.
|
|
296
|
+
*/
|
|
297
|
+
async function start(ctx, session, root) {
|
|
298
|
+
enterSession(ctx, root);
|
|
299
|
+
if (!wanted && readEnabled(statePath)) wanted = true;
|
|
300
|
+
if (wanted) {
|
|
301
|
+
const failure = await activate(ctx, session);
|
|
302
|
+
if (failure !== void 0) {
|
|
303
|
+
ctx.ui.notify(`anon: ${failure}`, "error");
|
|
304
|
+
ctx.ui.notify(refusalHint(), "error");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
showStatus(ctx);
|
|
308
|
+
}
|
|
215
309
|
return {
|
|
216
310
|
required,
|
|
217
311
|
statePath,
|
|
218
312
|
async command(args, ctx, session) {
|
|
219
313
|
switch (args.trim().toLowerCase()) {
|
|
220
|
-
case "on":
|
|
221
|
-
|
|
314
|
+
case "on": {
|
|
315
|
+
const before = wanted;
|
|
316
|
+
wanted = true;
|
|
317
|
+
const failure = await activate(ctx, session);
|
|
318
|
+
if (failure === void 0) {
|
|
222
319
|
persist(ctx, true);
|
|
223
320
|
const count = stateOf(ctx.modelRegistry).routed.size;
|
|
224
321
|
ctx.ui.notify(`anon on: ${count} provider(s) routed through ${endpoint?.url}`, "info");
|
|
322
|
+
break;
|
|
225
323
|
}
|
|
324
|
+
wanted = before || required;
|
|
325
|
+
ctx.ui.notify(`anon: ${failure}`, "error");
|
|
326
|
+
if (wanted) ctx.ui.notify(refusalHint(), "error");
|
|
226
327
|
break;
|
|
328
|
+
}
|
|
227
329
|
case "off": {
|
|
228
330
|
if (required) {
|
|
229
331
|
ctx.ui.notify(`anon: off refused: this session requires the transform proxy (${MODE_ENV}=required)`, "warning");
|
|
230
332
|
break;
|
|
231
333
|
}
|
|
232
334
|
const lost = unroute(ctx);
|
|
335
|
+
wanted = false;
|
|
233
336
|
enabled = false;
|
|
234
|
-
await rebind(ctx, session);
|
|
337
|
+
if (!await rebind(ctx, session)) ctx.ui.notify(`anon: could not rebind the active model ${ctx.model?.provider}/${ctx.model?.id}; select it again`, "warning");
|
|
235
338
|
persist(ctx, false);
|
|
236
339
|
ctx.ui.notify("anon off: original provider base URLs restored", "info");
|
|
237
340
|
if (lost.length > 0) ctx.ui.notify(`anon: ${lost.join(", ")} lost their runtime registration (registered by another extension); restart omp to restore them`, "warning");
|
|
@@ -250,20 +353,33 @@ function createAnonController(deps) {
|
|
|
250
353
|
const items = VERBS.filter((item) => item.value.startsWith(needle));
|
|
251
354
|
return items.length > 0 ? items : null;
|
|
252
355
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} else if (required || readEnabled(statePath)) {
|
|
259
|
-
if (!await turnOn(ctx, session) && required) ctx.ui.notify("anon: required mode, but no provider is routed through the transform proxy", "error");
|
|
260
|
-
}
|
|
261
|
-
showStatus(ctx);
|
|
356
|
+
sessionStart(ctx, session) {
|
|
357
|
+
return start(ctx, session, ctx.hasUI === true || ctx.mode !== void 0 && ctx.mode !== "print");
|
|
358
|
+
},
|
|
359
|
+
sessionSwitch(ctx, session) {
|
|
360
|
+
return start(ctx, session, true);
|
|
262
361
|
},
|
|
263
362
|
async reapply(ctx, session) {
|
|
264
|
-
if (!
|
|
265
|
-
|
|
266
|
-
await
|
|
363
|
+
if (!wanted) return;
|
|
364
|
+
const was = enabled;
|
|
365
|
+
const failure = await activate(ctx, session);
|
|
366
|
+
if (failure !== void 0 && was) {
|
|
367
|
+
ctx.ui.notify(`anon: ${failure}`, "error");
|
|
368
|
+
ctx.ui.notify(refusalHint(), "error");
|
|
369
|
+
}
|
|
370
|
+
if (enabled !== was) showStatus(ctx);
|
|
371
|
+
},
|
|
372
|
+
providerRequest(ctx) {
|
|
373
|
+
if (!wanted) return void 0;
|
|
374
|
+
let reason;
|
|
375
|
+
try {
|
|
376
|
+
reason = bypassReason(ctx, ctx.model);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
reason = errorMessage(err);
|
|
379
|
+
}
|
|
380
|
+
if (reason === void 0) return void 0;
|
|
381
|
+
ctx.ui.notify(`anon: model request refused: ${reason}`, "error");
|
|
382
|
+
return {};
|
|
267
383
|
}
|
|
268
384
|
};
|
|
269
385
|
}
|
|
@@ -282,6 +398,101 @@ function readEnabled(path) {
|
|
|
282
398
|
return false;
|
|
283
399
|
}
|
|
284
400
|
}
|
|
401
|
+
function spawnNode(command, args, options) {
|
|
402
|
+
return spawn(command, [...args], {
|
|
403
|
+
cwd: options.cwd,
|
|
404
|
+
env: options.env,
|
|
405
|
+
stdio: [...options.stdio]
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/** The stderr lines a startup failure quotes. */
|
|
409
|
+
const STDERR_TAIL = 10;
|
|
410
|
+
/**
|
|
411
|
+
* Resolves with the URL of the child's `listening <url>` line. A child that exits first, fails
|
|
412
|
+
* to spawn or stays silent past `timeoutMs` (it is then killed) rejects, quoting its last stderr
|
|
413
|
+
* lines with the token redacted. omp exiting kills the child; once listening, the child and its
|
|
414
|
+
* pipes no longer hold omp's event loop, and stdin stays open so the child sees omp go away.
|
|
415
|
+
*/
|
|
416
|
+
function waitForListening(child, how) {
|
|
417
|
+
const killOnExit = () => {
|
|
418
|
+
child.kill("SIGTERM");
|
|
419
|
+
};
|
|
420
|
+
process.on("exit", killOnExit);
|
|
421
|
+
const forgetExitHook = () => {
|
|
422
|
+
process.off("exit", killOnExit);
|
|
423
|
+
};
|
|
424
|
+
child.on("exit", forgetExitHook);
|
|
425
|
+
for (const stream of [
|
|
426
|
+
child.stdin,
|
|
427
|
+
child.stdout,
|
|
428
|
+
child.stderr
|
|
429
|
+
]) stream?.on("error", () => {});
|
|
430
|
+
const listening = Promise.withResolvers();
|
|
431
|
+
const label = `the transform proxy child process (${how.node}, pid ${child.pid})`;
|
|
432
|
+
const tail = [];
|
|
433
|
+
let stderrPartial = "";
|
|
434
|
+
let stdout = "";
|
|
435
|
+
let settled = false;
|
|
436
|
+
function stderrTail() {
|
|
437
|
+
const lines = [...tail, stderrPartial].filter((line) => line.trim() !== "").slice(-10);
|
|
438
|
+
return lines.length === 0 ? "(none)" : lines.map((line) => line.split(how.token).join("[token]")).join("\n");
|
|
439
|
+
}
|
|
440
|
+
function fail(message) {
|
|
441
|
+
if (settled) return;
|
|
442
|
+
settled = true;
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
listening.reject(new Error(message));
|
|
445
|
+
}
|
|
446
|
+
const timer = setTimeout(() => {
|
|
447
|
+
fail(`${label} did not print "listening" within ${duration(how.timeoutMs)} and was killed; last stderr lines:\n${stderrTail()}`);
|
|
448
|
+
child.kill("SIGKILL");
|
|
449
|
+
}, how.timeoutMs);
|
|
450
|
+
child.stderr?.on("data", (chunk) => {
|
|
451
|
+
if (settled) return;
|
|
452
|
+
const lines = (stderrPartial + String(chunk)).split("\n");
|
|
453
|
+
stderrPartial = lines.pop() ?? "";
|
|
454
|
+
tail.push(...lines.map((line) => line.replace(/\r$/, "")));
|
|
455
|
+
tail.splice(0, Math.max(0, tail.length - STDERR_TAIL));
|
|
456
|
+
});
|
|
457
|
+
child.stdout?.on("data", (chunk) => {
|
|
458
|
+
if (settled) return;
|
|
459
|
+
const lines = (stdout + String(chunk)).split("\n");
|
|
460
|
+
stdout = lines.pop() ?? "";
|
|
461
|
+
for (const line of lines) {
|
|
462
|
+
const match = /^listening (\S+)$/.exec(line.trim());
|
|
463
|
+
if (match === null) continue;
|
|
464
|
+
settled = true;
|
|
465
|
+
clearTimeout(timer);
|
|
466
|
+
stdout = "";
|
|
467
|
+
child.unref();
|
|
468
|
+
for (const stream of [
|
|
469
|
+
child.stdin,
|
|
470
|
+
child.stdout,
|
|
471
|
+
child.stderr
|
|
472
|
+
]) stream?.unref?.();
|
|
473
|
+
listening.resolve(match[1]);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
child.on("exit", (code, signal) => {
|
|
478
|
+
if (settled) return;
|
|
479
|
+
const report = () => fail(`${label} exited with ${code === null ? `signal ${signal}` : `code ${code}`} before it was listening; last stderr lines:\n${stderrTail()}`);
|
|
480
|
+
const stderr = child.stderr;
|
|
481
|
+
if (stderr === null || stderr.readableEnded || stderr.destroyed) report();
|
|
482
|
+
else {
|
|
483
|
+
stderr.once("end", report);
|
|
484
|
+
stderr.once("close", report);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
child.on("error", (err) => {
|
|
488
|
+
forgetExitHook();
|
|
489
|
+
fail(`could not start the transform proxy with "${how.node}": ${err.message}; it runs under Node >= 22: put node on PATH or set ${NODE_ENV}`);
|
|
490
|
+
});
|
|
491
|
+
return listening.promise;
|
|
492
|
+
}
|
|
493
|
+
function duration(ms) {
|
|
494
|
+
return ms % 1e3 === 0 ? `${ms / 1e3} s` : `${ms} ms`;
|
|
495
|
+
}
|
|
285
496
|
/** Every provider of the registry with the wire APIs of its models. */
|
|
286
497
|
function providerApis(registry) {
|
|
287
498
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -311,8 +522,24 @@ function refusalReason(apis, original) {
|
|
|
311
522
|
const unserved = [...apis ?? []].filter((api) => !Object.hasOwn(SERVED_APIS, api));
|
|
312
523
|
return unserved.length === 0 ? void 0 : unserved.join(", ");
|
|
313
524
|
}
|
|
525
|
+
function readSessionId(ctx) {
|
|
526
|
+
try {
|
|
527
|
+
const id = ctx.sessionManager.getSessionId();
|
|
528
|
+
return typeof id === "string" && id !== "" ? id : void 0;
|
|
529
|
+
} catch {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** The origin of an http(s) URL; `undefined` for anything else, which never matches the proxy. */
|
|
534
|
+
function originOf(url) {
|
|
535
|
+
try {
|
|
536
|
+
return new URL(url).origin;
|
|
537
|
+
} catch {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
314
541
|
// src/omp/index.ts
|
|
315
|
-
/** omp re-imports the extension per session; the controller (proxy, recorded originals) is process-wide. */
|
|
542
|
+
/** omp re-imports the extension per session; the controller (proxy child, recorded originals) is process-wide. */
|
|
316
543
|
const CONTROLLER_KEY = Symbol.for("@skaile/transform-proxy/omp");
|
|
317
544
|
/** Registers the `/anon` command and the event handlers on one session's extension API. */
|
|
318
545
|
function registerAnonExtension(pi, controller) {
|
|
@@ -322,8 +549,10 @@ function registerAnonExtension(pi, controller) {
|
|
|
322
549
|
handler: (args, ctx) => guarded(ctx, () => controller.command(args, ctx, pi))
|
|
323
550
|
});
|
|
324
551
|
on(pi, "session_start", (ctx) => controller.sessionStart(ctx, pi));
|
|
552
|
+
on(pi, "session_switch", (ctx) => controller.sessionSwitch(ctx, pi));
|
|
325
553
|
on(pi, "before_agent_start", (ctx) => controller.reapply(ctx, pi));
|
|
326
554
|
on(pi, "turn_start", (ctx) => controller.reapply(ctx, pi));
|
|
555
|
+
pi.on("before_provider_request", (_event, ctx) => controller.providerRequest(ctx));
|
|
327
556
|
}
|
|
328
557
|
function anonExtension(pi) {
|
|
329
558
|
const scope = globalThis;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/transform-proxy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|