@skaile/transform-proxy 0.3.0 → 0.4.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 +8 -0
- package/README.md +5 -1
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{config-PKHxejC8.mjs → config-Ceb5AF_a.mjs} +49 -7
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/omp/index.mjs +1 -1
- package/dist/{server-C_eKyCTV.d.mts → server-CicvlQ7q.d.mts} +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Library: `TransformProxy.inspect(sessionId, pipelineId?)` returns host-only transform state, `{ <transformId>: state }` for each stage of the session's pipeline instance (default `defaultPipeline`) that implements the new optional `Transform.inspect?()`. A host such as anon-ai reads forge-anon's mapping through it to show what was anonymized. It is in-process only (no HTTP route, never sent), returns `{}` for an unknown session, an unbuilt pipeline or a failed build, and never builds an instance or creates session state.
|
|
6
|
+
|
|
7
|
+
## 0.3.1
|
|
8
|
+
|
|
9
|
+
- Anthropic walker: the system text that Anthropic's Claude subscription (OAuth) check reads verbatim passes untransformed: the leading Claude Code identity sentence and the leading `x-anthropic-billing-header:` line. The rest of each block is still transformed. forge-anon read `Claude` as a name and the `cc_version` as IP addresses, so every request of a subscription session with anon on got 429 `rate_limit_error` "Error".
|
|
10
|
+
|
|
3
11
|
## 0.3.0
|
|
4
12
|
|
|
5
13
|
- omp extension, **behaviour change**: `/anon on` and `/anon off` switch only the omp process they run in (its subagents and its `/new`, `/resume`, `/fork` sessions included) and no longer save the default. Before, every `/anon on|off` wrote `~/.omp/agent/skaile-transform-proxy.json`, so turning anon off in one window turned it off for every omp started later. `/anon on --global` and `/anon off --global` also save the default. A saved `on` is applied at session start only until the first successful `/anon on|off` in that process, so a plain `/anon off` stays off across `/new`. An existing saved `enabled: true` still turns anon on in new omp processes; run `/anon off --global` to clear it.
|
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ await proxy.dropSession('s1')
|
|
|
85
85
|
await proxy.close()
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
`await proxy.inspect('s1')` lets the host read what the session's transforms hold, e.g. forge-anon's mapping for highlights: `{ <transformId>: state }`, one entry per stage of the pipeline (`inspect('s1', 'anon')`; default `defaultPipeline`) whose `Transform` implements `inspect()`. It is in-process only: there is no HTTP route, and the result is never sent anywhere. An unknown session, a pipeline that has not been built for the session yet, or a failed build gives `{}`; `inspect` never builds an instance or creates session state.
|
|
89
|
+
|
|
88
90
|
## omp extension
|
|
89
91
|
|
|
90
92
|
The package ships an [omp](https://omp.sh) extension (subpath `@skaile/transform-proxy/omp`, declared in the package manifest under `omp.extensions`):
|
|
@@ -128,10 +130,12 @@ A module default-exports a `TransformModule`:
|
|
|
128
130
|
|
|
129
131
|
- `open(config, { baseDir, log })` runs once per proxy. It validates the config and builds shared state. Throwing rejects startup.
|
|
130
132
|
- It returns a `TransformFactory`, which runs once per `(session, pipeline)`: `(ctx: { sessionId, facts, log }) => Transform`.
|
|
131
|
-
- A `Transform` has `instruction?`, `outbound(text, leaf)`, `inbound(text)`, `inboundStream()` and `
|
|
133
|
+
- A `Transform` has `instruction?`, `outbound(text, leaf)`, `inbound(text)`, `inboundStream()`, `dispose?()` and `inspect?()` (host-only state, read through `proxy.inspect`; the walkers never call it).
|
|
132
134
|
|
|
133
135
|
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.
|
|
134
136
|
|
|
137
|
+
Those leading blocks also pass untransformed: a system text that starts with the Claude Code identity sentence (`You are Claude Code, Anthropic's official CLI for Claude.`) keeps that sentence, and one that starts with `x-anthropic-billing-header:` keeps that first line; the rest of the text is transformed. Anthropic's Claude subscription (OAuth) check reads them verbatim and answers 429 `rate_limit_error` with the message `Error` when they differ, and a German NER reads `Claude` as a name and a `cc_version` as an IP address. The host writes these blocks; they carry no user data.
|
|
138
|
+
|
|
135
139
|
`inboundStream().push(c1) + … + flush()` must equal `inbound(c1 + … + cn)` for every chunking.
|
|
136
140
|
|
|
137
141
|
Connectors publish facts through `proxy.session(id).publisher(source)`. Facts are in-process only.
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as TransformProxy } from "./server-
|
|
1
|
+
import { i as TransformProxy } from "./server-CicvlQ7q.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
|
export interface CliEnvironment {
|
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-Ceb5AF_a.mjs";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -323,8 +323,8 @@ async function outbound$2(transform, text, leaf) {
|
|
|
323
323
|
return text === "" ? text : transform.outbound(text, leaf);
|
|
324
324
|
}
|
|
325
325
|
/** A string, or an array of blocks whose `text` blocks are rewritten; anything else is refused. */
|
|
326
|
-
async function rewriteText(content, slot,
|
|
327
|
-
if (typeof content === "string") return
|
|
326
|
+
async function rewriteText(content, slot, rewrite) {
|
|
327
|
+
if (typeof content === "string") return rewrite(content);
|
|
328
328
|
if (!Array.isArray(content)) return malformed$2(slot);
|
|
329
329
|
const out = [];
|
|
330
330
|
for (const block of content) {
|
|
@@ -337,11 +337,30 @@ async function rewriteText(content, slot, leaf, transform) {
|
|
|
337
337
|
if (typeof block.text !== "string") return malformed$2(`${slot} text block`);
|
|
338
338
|
out.push({
|
|
339
339
|
...block,
|
|
340
|
-
text: await
|
|
340
|
+
text: await rewrite(block.text)
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
343
|
return out;
|
|
344
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Host-written system text that Anthropic's Claude subscription (OAuth) check reads verbatim: the
|
|
347
|
+
* billing header line and the Claude Code identity sentence. Rewritten ("CLI for <NAME_…>",
|
|
348
|
+
* "cc_version=<CONTACT_IP_…>"), the request is answered 429 `rate_limit_error` "Error". Neither
|
|
349
|
+
* carries user data.
|
|
350
|
+
*/
|
|
351
|
+
const BILLING_HEADER = "x-anthropic-billing-header:";
|
|
352
|
+
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
353
|
+
/** The leading part of a system text that passes untransformed; `''` when there is none. */
|
|
354
|
+
function gatePrefix(text) {
|
|
355
|
+
if (text.startsWith(CLAUDE_CODE_IDENTITY)) return CLAUDE_CODE_IDENTITY;
|
|
356
|
+
if (!text.startsWith(BILLING_HEADER)) return "";
|
|
357
|
+
const end = text.indexOf("\n");
|
|
358
|
+
return end === -1 ? text : text.slice(0, end);
|
|
359
|
+
}
|
|
360
|
+
async function rewriteSystemText(text, transform) {
|
|
361
|
+
const kept = gatePrefix(text);
|
|
362
|
+
return kept + await outbound$2(transform, text.slice(kept.length), { kind: "system" });
|
|
363
|
+
}
|
|
345
364
|
/** Every string leaf of a JSON value (tool-call input); keys stay as they are. */
|
|
346
365
|
async function rewriteStrings$2(value, leaf, transform) {
|
|
347
366
|
if (typeof value === "string") return outbound$2(transform, value, leaf);
|
|
@@ -421,10 +440,10 @@ async function rewriteContent$2(content, role, toolNames, ctx) {
|
|
|
421
440
|
const toolName = typeof block.tool_use_id === "string" ? toolNames.get(block.tool_use_id) : void 0;
|
|
422
441
|
out.push({
|
|
423
442
|
...block,
|
|
424
|
-
content: await rewriteText(block.content, "tool_result content", {
|
|
443
|
+
content: await rewriteText(block.content, "tool_result content", (text) => outbound$2(ctx.transform, text, {
|
|
425
444
|
kind: "toolResult",
|
|
426
445
|
toolName
|
|
427
|
-
}
|
|
446
|
+
}))
|
|
428
447
|
});
|
|
429
448
|
break;
|
|
430
449
|
}
|
|
@@ -567,7 +586,7 @@ const anthropicWalker = {
|
|
|
567
586
|
async request(body, ctx) {
|
|
568
587
|
if (!isObj$2(body)) return malformed$2("request body");
|
|
569
588
|
const out = { ...body };
|
|
570
|
-
if (body.system !== void 0) out.system = await rewriteText(body.system, "system",
|
|
589
|
+
if (body.system !== void 0) out.system = await rewriteText(body.system, "system", (text) => rewriteSystemText(text, ctx.transform));
|
|
571
590
|
if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
|
|
572
591
|
if (body.messages !== void 0) {
|
|
573
592
|
if (!Array.isArray(body.messages)) return malformed$2("messages");
|
|
@@ -1405,6 +1424,8 @@ async function createTransformProxy(options) {
|
|
|
1405
1424
|
const factories = await openTransforms(options.transforms, options.baseDir, log);
|
|
1406
1425
|
const pipelines = options.pipelines;
|
|
1407
1426
|
const sessions = /* @__PURE__ */ new Map();
|
|
1427
|
+
/** Composed instance -> its stages with their transform ids, for `inspect`. */
|
|
1428
|
+
const stagesOf = /* @__PURE__ */ new WeakMap();
|
|
1408
1429
|
function stateFor(id) {
|
|
1409
1430
|
const existing = sessions.get(id);
|
|
1410
1431
|
if (existing) return existing;
|
|
@@ -1437,7 +1458,12 @@ async function createTransformProxy(options) {
|
|
|
1437
1458
|
}
|
|
1438
1459
|
throw err;
|
|
1439
1460
|
}
|
|
1440
|
-
|
|
1461
|
+
const composed = composeTransforms(stages, log);
|
|
1462
|
+
stagesOf.set(composed, ids.map((id, i) => ({
|
|
1463
|
+
id,
|
|
1464
|
+
transform: stages[i]
|
|
1465
|
+
})));
|
|
1466
|
+
return composed;
|
|
1441
1467
|
}
|
|
1442
1468
|
async function handle(req, res) {
|
|
1443
1469
|
const url = new URL(req.url ?? "/", "http://proxy.invalid");
|
|
@@ -1554,6 +1580,22 @@ async function createTransformProxy(options) {
|
|
|
1554
1580
|
sessions.delete(id);
|
|
1555
1581
|
await disposeInstances(state, log);
|
|
1556
1582
|
},
|
|
1583
|
+
async inspect(sessionId, pipelineId) {
|
|
1584
|
+
const state = sessions.get(sessionId);
|
|
1585
|
+
const pipeline = pipelineId ?? options.defaultPipeline;
|
|
1586
|
+
if (state === void 0 || pipeline === void 0) return {};
|
|
1587
|
+
const pending = state.instances.get(pipeline);
|
|
1588
|
+
if (pending === void 0) return {};
|
|
1589
|
+
let instance;
|
|
1590
|
+
try {
|
|
1591
|
+
instance = await pending;
|
|
1592
|
+
} catch {
|
|
1593
|
+
return {};
|
|
1594
|
+
}
|
|
1595
|
+
const result = {};
|
|
1596
|
+
for (const stage of stagesOf.get(instance) ?? []) if (typeof stage.transform.inspect === "function") result[stage.id] = stage.transform.inspect();
|
|
1597
|
+
return result;
|
|
1598
|
+
},
|
|
1557
1599
|
async close() {
|
|
1558
1600
|
const closed = Promise.withResolvers();
|
|
1559
1601
|
server.close((err) => err ? closed.reject(err) : closed.resolve());
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as TransformModule, a as TransformProxyOptions, c as TransformSpec, d as SessionFactPublisher, f as SessionFacts, g as TransformFactory, h as Transform, 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 TransformSessionContext } from "./server-
|
|
1
|
+
import { _ as TransformModule, a as TransformProxyOptions, c as TransformSpec, d as SessionFactPublisher, f as SessionFacts, g as TransformFactory, h as Transform, 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 TransformSessionContext } from "./server-CicvlQ7q.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 { 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-Ceb5AF_a.mjs";
|
package/dist/omp/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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-Ceb5AF_a.mjs";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -25,6 +25,8 @@ interface Transform {
|
|
|
25
25
|
};
|
|
26
26
|
/** Called once when the proxy drops the session. */
|
|
27
27
|
dispose?(): void | Promise<void>;
|
|
28
|
+
/** Host-only state of this instance (e.g. the anon mapping), read through TransformProxy.inspect. Never called by the walkers, never sent anywhere. */
|
|
29
|
+
inspect?(): unknown;
|
|
28
30
|
}
|
|
29
31
|
interface SessionFact {
|
|
30
32
|
readonly kind: string;
|
|
@@ -86,6 +88,12 @@ interface TransformProxy {
|
|
|
86
88
|
};
|
|
87
89
|
/** Disposes the session's transform instances and forgets its facts and caches. */
|
|
88
90
|
dropSession(id: string): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Host-only, in-process: each stage's `inspect()` result of the session's pipeline instance, keyed by transform id.
|
|
93
|
+
* `{}` when the session or pipeline is unknown, the instance is not built, or its build failed.
|
|
94
|
+
* Never builds an instance or creates session state; there is no HTTP route for it.
|
|
95
|
+
*/
|
|
96
|
+
inspect(sessionId: string, pipelineId?: string): Promise<Record<string, unknown>>;
|
|
89
97
|
close(): Promise<void>;
|
|
90
98
|
}
|
|
91
99
|
declare const UPSTREAM_HEADER = "x-skaile-transform-upstream";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/transform-proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|