@skaile/transform-proxy 0.2.6 → 0.3.1
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 +4 -0
- package/README.md +3 -2
- package/dist/cli.mjs +1 -1
- package/dist/{config-PKHxejC8.mjs → config-BZsthgP-.mjs} +25 -6
- package/dist/index.mjs +1 -1
- package/dist/omp/index.d.mts +2 -1
- package/dist/omp/index.mjs +46 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
|
|
3
7
|
## 0.2.6
|
|
4
8
|
|
|
5
9
|
- omp extension: while anon is on, in error or required, images are not sent. The extension sets omp's `images.blockImages` as a runtime override (never persisted), so omp replaces every image in the request, history included, with `Image reading is disabled.`; `/anon off` drops the override. Before, a screenshot from a browser tool made the proxy refuse every later request of the session with 400 `unsupported_content` ("Anthropic image content cannot be transformed"). `/anon status` lists `images: not sent`. An omp without the settings registry gets one warning, and the proxy keeps refusing images.
|
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ The package ships an [omp](https://omp.sh) extension (subpath `@skaile/transform
|
|
|
93
93
|
omp plugin install @skaile/transform-proxy
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
It adds the command `/anon on|off|status` and a compact status: `🟢A` (on), `🔴A` (off) or `🟠A` (error). The colour is in the emoji because omp strips ANSI colour from extension statuses and draws them in its accent colour. Unless `SKAILE_TRANSFORM_PROXY_URL` names a running proxy, it needs Node ≥ 22 on `PATH` (or its path in `SKAILE_TRANSFORM_PROXY_NODE`).
|
|
96
|
+
It adds the command `/anon on|off [--global] | status` and a compact status: `🟢A` (on), `🔴A` (off) or `🟠A` (error). The colour is in the emoji because omp strips ANSI colour from extension statuses and draws them in its accent colour. Unless `SKAILE_TRANSFORM_PROXY_URL` names a running proxy, it needs Node ≥ 22 on `PATH` (or its path in `SKAILE_TRANSFORM_PROXY_NODE`).
|
|
97
97
|
|
|
98
98
|
omp shows extension statuses on an extra line below the status line. To show the status in the status line itself, add the `status` segment and turn the extra line off in `~/.omp/agent/config.yml` (`showHookStatus: false` moves every extension status, not just this one, into the segment):
|
|
99
99
|
|
|
@@ -116,7 +116,8 @@ statusLine:
|
|
|
116
116
|
- 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.
|
|
117
117
|
- `/anon off` restores the original base URLs and headers.
|
|
118
118
|
- `/anon status` shows the state, the proxy URL and its source, the pipeline, the session id, whether images are blocked, and the routed and refused providers.
|
|
119
|
-
-
|
|
119
|
+
- `/anon on` and `/anon off` switch only the omp process they run in: that window, its subagents, and the sessions it opens with `/new`, `/resume` and `/fork`. Other omp windows are not touched, and the saved default does not change.
|
|
120
|
+
- `/anon on --global` and `/anon off --global` also save the default for omp processes started later, in `skaile-transform-proxy.json` in omp's agent dir (`~/.omp/agent`; `SKAILE_TRANSFORM_PROXY_OMP_STATE` overrides the path). A session start applies a saved `on` until the first successful `/anon on|off` in that process; from then on the process follows its own choice. A failed `/anon on --global` saves nothing.
|
|
120
121
|
- `SKAILE_TRANSFORM_PROXY_MODE=required` (set by a skaile session in required mode) turns routing on at session start and refuses `/anon off`.
|
|
121
122
|
|
|
122
123
|
The overrides are registered under the model-registry source `skaile-transform-proxy`, so `/anon off` clears them in one catalog reload. A provider that another extension registered at runtime is taken over while anon is on; `/anon off` restores its base URL and headers, and a provider that existed only at runtime is reported as lost (restart omp to restore it).
|
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-BZsthgP-.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");
|
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-BZsthgP-.mjs";
|
package/dist/omp/index.d.mts
CHANGED
|
@@ -117,7 +117,8 @@ interface AnonController {
|
|
|
117
117
|
command(args: string, ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
118
118
|
completions(prefix: string): OmpAutocompleteItem[] | null;
|
|
119
119
|
/**
|
|
120
|
-
* Applies the
|
|
120
|
+
* Applies the saved default (unless `/anon on|off` already decided in this process) or required
|
|
121
|
+
* mode, and refreshes the status bar. A root session
|
|
121
122
|
* whose omp session id changed rotates the proxy session; subagents keep the parent's id.
|
|
122
123
|
*/
|
|
123
124
|
sessionStart(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
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-BZsthgP-.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";
|
|
@@ -14,7 +14,7 @@ const TOKEN_ENV = "SKAILE_TRANSFORM_PROXY_TOKEN";
|
|
|
14
14
|
const NODE_ENV = "SKAILE_TRANSFORM_PROXY_NODE";
|
|
15
15
|
/** `required` (set by a skaile session in required mode): routing is forced on and `/anon off` is refused. */
|
|
16
16
|
const MODE_ENV = "SKAILE_TRANSFORM_PROXY_MODE";
|
|
17
|
-
/** Overrides the path of the
|
|
17
|
+
/** Overrides the path of the saved default (`/anon on|off --global`). */
|
|
18
18
|
const STATE_ENV = "SKAILE_TRANSFORM_PROXY_OMP_STATE";
|
|
19
19
|
/** The footer status key. */
|
|
20
20
|
const STATUS_KEY = "anon";
|
|
@@ -26,17 +26,29 @@ const SERVED_APIS = {
|
|
|
26
26
|
"openai-completions": true,
|
|
27
27
|
"openai-responses": true
|
|
28
28
|
};
|
|
29
|
-
|
|
29
|
+
/** `/anon on --global` / `/anon off --global` also save the default that new omp sessions start with. */
|
|
30
|
+
const GLOBAL_FLAG = "--global";
|
|
31
|
+
const USAGE = "usage: /anon on|off [--global] | status";
|
|
30
32
|
const VERBS = [
|
|
31
33
|
{
|
|
32
34
|
value: "on",
|
|
33
35
|
label: "on",
|
|
34
|
-
description: "route
|
|
36
|
+
description: "route this session through the transform proxy"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: "on --global",
|
|
40
|
+
label: "on --global",
|
|
41
|
+
description: "on, and the default for new sessions"
|
|
35
42
|
},
|
|
36
43
|
{
|
|
37
44
|
value: "off",
|
|
38
45
|
label: "off",
|
|
39
|
-
description: "restore
|
|
46
|
+
description: "restore this session's original provider base URLs"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
value: "off --global",
|
|
50
|
+
label: "off --global",
|
|
51
|
+
description: "off, and the default for new sessions"
|
|
40
52
|
},
|
|
41
53
|
{
|
|
42
54
|
value: "status",
|
|
@@ -44,6 +56,23 @@ const VERBS = [
|
|
|
44
56
|
description: "show the proxy, pipeline and routed providers"
|
|
45
57
|
}
|
|
46
58
|
];
|
|
59
|
+
/** A parsed `/anon` argument; `undefined` for anything else. */
|
|
60
|
+
function parseCommand(args) {
|
|
61
|
+
const [verb = "", ...flags] = args.trim().toLowerCase().split(/\s+/);
|
|
62
|
+
if (verb === "" || verb === "status") return flags.length === 0 ? {
|
|
63
|
+
verb: "status",
|
|
64
|
+
global: false
|
|
65
|
+
} : void 0;
|
|
66
|
+
if (verb !== "on" && verb !== "off") return void 0;
|
|
67
|
+
if (flags.length === 0) return {
|
|
68
|
+
verb,
|
|
69
|
+
global: false
|
|
70
|
+
};
|
|
71
|
+
return flags.length === 1 && flags[0] === GLOBAL_FLAG ? {
|
|
72
|
+
verb,
|
|
73
|
+
global: true
|
|
74
|
+
} : void 0;
|
|
75
|
+
}
|
|
47
76
|
/**
|
|
48
77
|
* The process-wide `/anon` state machine. omp builds one extension instance per session
|
|
49
78
|
* (subagents included) while they share one model registry, so one controller serves them all.
|
|
@@ -62,6 +91,8 @@ function createAnonController(deps) {
|
|
|
62
91
|
const registries = /* @__PURE__ */ new WeakMap();
|
|
63
92
|
let wanted = required;
|
|
64
93
|
let enabled = false;
|
|
94
|
+
/** The last successful `/anon on|off` in this omp process; once set, the saved default no longer applies here. */
|
|
95
|
+
let local;
|
|
65
96
|
/** Whether this controller holds the image block; only then does it release it. */
|
|
66
97
|
let imagesBlocked = false;
|
|
67
98
|
/** The host answered that it cannot block images; asked once, warned once. */
|
|
@@ -323,7 +354,7 @@ function createAnonController(deps) {
|
|
|
323
354
|
*/
|
|
324
355
|
async function start(ctx, session, root) {
|
|
325
356
|
enterSession(ctx, root);
|
|
326
|
-
if (!wanted && readEnabled(statePath)) wanted = true;
|
|
357
|
+
if (!wanted && local === void 0 && readEnabled(statePath)) wanted = true;
|
|
327
358
|
if (wanted) {
|
|
328
359
|
const failure = await activate(ctx, session);
|
|
329
360
|
if (failure !== void 0) {
|
|
@@ -338,15 +369,18 @@ function createAnonController(deps) {
|
|
|
338
369
|
required,
|
|
339
370
|
statePath,
|
|
340
371
|
async command(args, ctx, session) {
|
|
341
|
-
|
|
372
|
+
const parsed = parseCommand(args);
|
|
373
|
+
const scope = parsed?.global === true ? `this session; default for new sessions: ${parsed.verb}` : "this session";
|
|
374
|
+
switch (parsed?.verb) {
|
|
342
375
|
case "on": {
|
|
343
376
|
const before = wanted;
|
|
344
377
|
wanted = true;
|
|
345
378
|
const failure = await activate(ctx, session);
|
|
346
379
|
if (failure === void 0) {
|
|
347
|
-
|
|
380
|
+
local = true;
|
|
381
|
+
if (parsed.global) persist(ctx, true);
|
|
348
382
|
const count = stateOf(ctx.modelRegistry).routed.size;
|
|
349
|
-
ctx.ui.notify(`anon on: ${count} provider(s) routed through ${endpoint?.url}`, "info");
|
|
383
|
+
ctx.ui.notify(`anon on (${scope}): ${count} provider(s) routed through ${endpoint?.url}`, "info");
|
|
350
384
|
break;
|
|
351
385
|
}
|
|
352
386
|
wanted = before || required;
|
|
@@ -362,13 +396,13 @@ function createAnonController(deps) {
|
|
|
362
396
|
const lost = unroute(ctx);
|
|
363
397
|
wanted = false;
|
|
364
398
|
enabled = false;
|
|
399
|
+
local = false;
|
|
365
400
|
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");
|
|
366
|
-
persist(ctx, false);
|
|
367
|
-
ctx.ui.notify(
|
|
401
|
+
if (parsed.global) persist(ctx, false);
|
|
402
|
+
ctx.ui.notify(`anon off (${scope}): original provider base URLs restored`, "info");
|
|
368
403
|
if (lost.length > 0) ctx.ui.notify(`anon: ${lost.join(", ")} lost their runtime registration (registered by another extension); restart omp to restore them`, "warning");
|
|
369
404
|
break;
|
|
370
405
|
}
|
|
371
|
-
case "":
|
|
372
406
|
case "status":
|
|
373
407
|
ctx.ui.notify(statusText(ctx), "info");
|
|
374
408
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/transform-proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|