@skaile/transform-proxy 0.1.0 → 0.2.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 +5 -0
- package/README.md +22 -0
- package/dist/cli.mjs +1 -1
- package/dist/{config-BKD-ILIz.mjs → config-DQPsbYLi.mjs} +708 -51
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -1
- package/dist/omp/index.d.mts +89 -0
- package/dist/omp/index.mjs +345 -0
- package/package.json +24 -3
package/dist/index.d.mts
CHANGED
|
@@ -83,4 +83,6 @@ interface WireWalker {
|
|
|
83
83
|
stream(ctx: WireContext): SseTransformer;
|
|
84
84
|
}
|
|
85
85
|
export declare const anthropicWalker: WireWalker;
|
|
86
|
+
export declare const openaiChatWalker: WireWalker;
|
|
87
|
+
export declare const openaiResponsesWalker: WireWalker;
|
|
86
88
|
export { type ConfigLookup, type InboundStream, type Logger, PIPELINE_HEADER, type ProxyConfig, type ResolvedProxyConfig, SESSION_HEADER, type SessionFact, type SessionFactPublisher, type SessionFactStore, type SessionFacts, type SseTransformer, TOKEN_HEADER, type TextLeaf, type TextLeafKind, type Transform, type TransformFactory, type TransformModule, type TransformProxy, type TransformProxyOptions, type TransformSessionContext, type TransformSpec, UPSTREAM_HEADER, type WireContext, type WireWalker, createTransformProxy };
|
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,
|
|
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-DQPsbYLi.mjs";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { a as TransformProxyOptions, i as TransformProxy } from "../server-D5nBnO7T.mjs";
|
|
2
|
+
interface OmpModel {
|
|
3
|
+
readonly provider: string;
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly api: string;
|
|
6
|
+
readonly baseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/** A runtime provider override: omp merges `headers` into the provider's existing headers. */
|
|
9
|
+
interface OmpProviderOverride {
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
interface OmpModelRegistry {
|
|
14
|
+
getAll(): readonly OmpModel[];
|
|
15
|
+
find(provider: string, modelId: string): OmpModel | undefined;
|
|
16
|
+
getProviderBaseUrl(provider: string): string | undefined;
|
|
17
|
+
getProviderHeaders(provider: string): Record<string, string> | undefined;
|
|
18
|
+
registerProvider(name: string, config: OmpProviderOverride, sourceId?: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Clears the runtime overrides of every provider registered under `sourceId` and reloads the
|
|
21
|
+
* static (built-in / models.yml) config once. `unregisterProvider` reloads once per provider:
|
|
22
|
+
* about half a second each, with ~80 built-in providers.
|
|
23
|
+
*/
|
|
24
|
+
clearSourceRegistrations(sourceId: string): void;
|
|
25
|
+
}
|
|
26
|
+
type OmpNotifyType = 'info' | 'warning' | 'error';
|
|
27
|
+
interface OmpUi {
|
|
28
|
+
notify(message: string, type?: OmpNotifyType): void;
|
|
29
|
+
setStatus(key: string, text: string | undefined): void;
|
|
30
|
+
}
|
|
31
|
+
interface OmpContext {
|
|
32
|
+
readonly ui: OmpUi;
|
|
33
|
+
readonly cwd: string;
|
|
34
|
+
readonly modelRegistry: OmpModelRegistry;
|
|
35
|
+
readonly sessionManager: {
|
|
36
|
+
getSessionId(): string;
|
|
37
|
+
};
|
|
38
|
+
/** The session's active model: a snapshot that a provider override does not update. */
|
|
39
|
+
readonly model: OmpModel | undefined;
|
|
40
|
+
}
|
|
41
|
+
interface OmpAutocompleteItem {
|
|
42
|
+
value: string;
|
|
43
|
+
label: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
}
|
|
46
|
+
type OmpEventName = 'session_start' | 'before_agent_start' | 'turn_start';
|
|
47
|
+
/** Per-session actions of the extension API. */
|
|
48
|
+
interface OmpSessionActions {
|
|
49
|
+
/** Rebinds the session's active model (without persisting it as the default). */
|
|
50
|
+
setModel(model: OmpModel): Promise<boolean>;
|
|
51
|
+
}
|
|
52
|
+
interface OmpExtensionApi extends OmpSessionActions {
|
|
53
|
+
registerCommand(name: string, options: {
|
|
54
|
+
description?: string;
|
|
55
|
+
getArgumentCompletions?: (argumentPrefix: string) => OmpAutocompleteItem[] | null;
|
|
56
|
+
handler: (args: string, ctx: OmpContext) => Promise<void>;
|
|
57
|
+
}): void;
|
|
58
|
+
on(event: OmpEventName, handler: (event: unknown, ctx: OmpContext) => void | Promise<void>): void;
|
|
59
|
+
}
|
|
60
|
+
interface AnonControllerDeps {
|
|
61
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
62
|
+
readonly home: string;
|
|
63
|
+
/** Default: `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `<omp agent dir>/skaile-transform-proxy.json`. */
|
|
64
|
+
readonly statePath?: string;
|
|
65
|
+
/** Default: `createTransformProxy`. */
|
|
66
|
+
readonly startProxy?: (options: TransformProxyOptions) => Promise<TransformProxy>;
|
|
67
|
+
}
|
|
68
|
+
interface AnonController {
|
|
69
|
+
readonly required: boolean;
|
|
70
|
+
readonly statePath: string;
|
|
71
|
+
/** Handles `/anon <args>`. `session` rebinds the session's active model after a switch. */
|
|
72
|
+
command(args: string, ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
73
|
+
completions(prefix: string): OmpAutocompleteItem[] | null;
|
|
74
|
+
/** Applies the persisted flag (or required mode) and refreshes the status bar. */
|
|
75
|
+
sessionStart(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
76
|
+
/** While on: routes providers that appeared, or were reset, since the last pass. */
|
|
77
|
+
reapply(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The process-wide `/anon` state machine. omp builds one extension instance per session
|
|
81
|
+
* (subagents included) while they share one model registry, so one controller serves them all.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createAnonController(deps: AnonControllerDeps): AnonController;
|
|
84
|
+
/** `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `skaile-transform-proxy.json` in omp's agent dir (`$PI_CODING_AGENT_DIR` or `~/.omp/agent`). */
|
|
85
|
+
export declare function defaultStatePath(env: Readonly<Record<string, string | undefined>>, home: string): string;
|
|
86
|
+
/** Registers the `/anon` command and the event handlers on one session's extension API. */
|
|
87
|
+
export declare function registerAnonExtension(pi: OmpExtensionApi, controller: AnonController): void;
|
|
88
|
+
export default function anonExtension(pi: OmpExtensionApi): void;
|
|
89
|
+
export type { AnonController, AnonControllerDeps, OmpAutocompleteItem, OmpContext, OmpEventName, OmpExtensionApi, OmpModel, OmpModelRegistry, OmpNotifyType, OmpProviderOverride, OmpSessionActions, OmpUi };
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { c as SESSION_HEADER, d as createTransformProxy, i as loadProxyConfig, l as TOKEN_HEADER, n as configCandidates, r as findProxyConfig, u as UPSTREAM_HEADER, v as errorMessage, y as silentLogger } from "../config-DQPsbYLi.mjs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
// src/omp/anon.ts
|
|
7
|
+
/** A running proxy the extension routes to. */
|
|
8
|
+
const URL_ENV = "SKAILE_TRANSFORM_PROXY_URL";
|
|
9
|
+
/** The client token of that proxy. */
|
|
10
|
+
const TOKEN_ENV = "SKAILE_TRANSFORM_PROXY_TOKEN";
|
|
11
|
+
/** `required` (set by a skaile session in required mode): routing is forced on and `/anon off` is refused. */
|
|
12
|
+
const MODE_ENV = "SKAILE_TRANSFORM_PROXY_MODE";
|
|
13
|
+
/** Overrides the path of the persisted enabled flag. */
|
|
14
|
+
const STATE_ENV = "SKAILE_TRANSFORM_PROXY_OMP_STATE";
|
|
15
|
+
/** The footer status key. */
|
|
16
|
+
const STATUS_KEY = "anon";
|
|
17
|
+
/** The model-registry source the proxy overrides are registered under; `/anon off` clears it in one reload. */
|
|
18
|
+
const SOURCE_ID = "skaile-transform-proxy";
|
|
19
|
+
/** The wire APIs the proxy rewrites. Providers on any other API are routed too, and the proxy refuses their requests. */
|
|
20
|
+
const SERVED_APIS = {
|
|
21
|
+
"anthropic-messages": true,
|
|
22
|
+
"openai-completions": true,
|
|
23
|
+
"openai-responses": true
|
|
24
|
+
};
|
|
25
|
+
const USAGE = "usage: /anon on|off|status";
|
|
26
|
+
const VERBS = [
|
|
27
|
+
{
|
|
28
|
+
value: "on",
|
|
29
|
+
label: "on",
|
|
30
|
+
description: "route every provider through the transform proxy"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
value: "off",
|
|
34
|
+
label: "off",
|
|
35
|
+
description: "restore the original provider base URLs"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
value: "status",
|
|
39
|
+
label: "status",
|
|
40
|
+
description: "show the proxy, pipeline and routed providers"
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* The process-wide `/anon` state machine. omp builds one extension instance per session
|
|
45
|
+
* (subagents included) while they share one model registry, so one controller serves them all.
|
|
46
|
+
*/
|
|
47
|
+
function createAnonController(deps) {
|
|
48
|
+
const env = deps.env;
|
|
49
|
+
const required = env[MODE_ENV] === "required";
|
|
50
|
+
const statePath = deps.statePath ?? defaultStatePath(env, deps.home);
|
|
51
|
+
const startProxy = deps.startProxy ?? createTransformProxy;
|
|
52
|
+
const registries = /* @__PURE__ */ new WeakMap();
|
|
53
|
+
let enabled = false;
|
|
54
|
+
let endpoint;
|
|
55
|
+
let starting;
|
|
56
|
+
let sessionId;
|
|
57
|
+
function stateOf(registry) {
|
|
58
|
+
let state = registries.get(registry);
|
|
59
|
+
if (state === void 0) {
|
|
60
|
+
state = {
|
|
61
|
+
originals: /* @__PURE__ */ new Map(),
|
|
62
|
+
routed: /* @__PURE__ */ new Set()
|
|
63
|
+
};
|
|
64
|
+
registries.set(registry, state);
|
|
65
|
+
}
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
async function resolveEndpoint(ctx) {
|
|
69
|
+
if (endpoint !== void 0) return endpoint;
|
|
70
|
+
const url = env[URL_ENV];
|
|
71
|
+
if (url !== void 0 && url !== "") {
|
|
72
|
+
const token = env[TOKEN_ENV];
|
|
73
|
+
endpoint = {
|
|
74
|
+
url,
|
|
75
|
+
...token ? { token } : {},
|
|
76
|
+
source: URL_ENV
|
|
77
|
+
};
|
|
78
|
+
return endpoint;
|
|
79
|
+
}
|
|
80
|
+
starting ??= startInProcess(ctx.cwd).finally(() => {
|
|
81
|
+
starting = void 0;
|
|
82
|
+
});
|
|
83
|
+
endpoint = await starting;
|
|
84
|
+
return endpoint;
|
|
85
|
+
}
|
|
86
|
+
async function startInProcess(cwd) {
|
|
87
|
+
const path = findProxyConfig({
|
|
88
|
+
cwd,
|
|
89
|
+
home: deps.home
|
|
90
|
+
});
|
|
91
|
+
if (path === void 0) {
|
|
92
|
+
const searched = configCandidates(cwd, deps.home).join(" or ");
|
|
93
|
+
throw new Error(`no transform proxy config found: create ${searched}, or set ${URL_ENV} to a running proxy`);
|
|
94
|
+
}
|
|
95
|
+
const options = loadProxyConfig(path);
|
|
96
|
+
if (options.defaultPipeline === void 0) throw new Error(`${path} sets no defaultPipeline: the omp extension sends no pipeline header`);
|
|
97
|
+
return {
|
|
98
|
+
url: (await startProxy({
|
|
99
|
+
...options,
|
|
100
|
+
log: silentLogger
|
|
101
|
+
})).url,
|
|
102
|
+
pipeline: options.defaultPipeline,
|
|
103
|
+
source: `in-process, config ${path}`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function currentSessionId(ctx) {
|
|
107
|
+
if (sessionId === void 0) {
|
|
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
|
+
}
|
|
116
|
+
return sessionId;
|
|
117
|
+
}
|
|
118
|
+
function route(ctx, target) {
|
|
119
|
+
const registry = ctx.modelRegistry;
|
|
120
|
+
const state = stateOf(registry);
|
|
121
|
+
const session = currentSessionId(ctx);
|
|
122
|
+
for (const provider of providerApis(registry).keys()) {
|
|
123
|
+
let original = state.originals.get(provider);
|
|
124
|
+
if (original === void 0) {
|
|
125
|
+
original = snapshot(registry, provider);
|
|
126
|
+
state.originals.set(provider, original);
|
|
127
|
+
}
|
|
128
|
+
if (original.preRouted) continue;
|
|
129
|
+
if (state.routed.has(provider) && registry.getProviderBaseUrl(provider) === target.url) continue;
|
|
130
|
+
const headers = { [SESSION_HEADER]: session };
|
|
131
|
+
if (original.baseUrl !== void 0) headers[UPSTREAM_HEADER] = original.baseUrl;
|
|
132
|
+
if (target.token !== void 0) headers[TOKEN_HEADER] = target.token;
|
|
133
|
+
registry.registerProvider(provider, {
|
|
134
|
+
baseUrl: target.url,
|
|
135
|
+
headers
|
|
136
|
+
}, SOURCE_ID);
|
|
137
|
+
state.routed.add(provider);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Clears the proxy overrides; re-applies the recorded original where another override was layered underneath. */
|
|
141
|
+
function unroute(ctx) {
|
|
142
|
+
const registry = ctx.modelRegistry;
|
|
143
|
+
const state = stateOf(registry);
|
|
144
|
+
registry.clearSourceRegistrations(SOURCE_ID);
|
|
145
|
+
const lost = [];
|
|
146
|
+
for (const provider of state.routed) {
|
|
147
|
+
const original = state.originals.get(provider);
|
|
148
|
+
const restored = registry.getProviderBaseUrl(provider);
|
|
149
|
+
if (original === void 0 || restored === original.baseUrl) continue;
|
|
150
|
+
if (restored === void 0) {
|
|
151
|
+
lost.push(provider);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (original.baseUrl !== void 0) registry.registerProvider(provider, {
|
|
155
|
+
baseUrl: original.baseUrl,
|
|
156
|
+
...original.headers === void 0 ? {} : { headers: original.headers }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
state.routed.clear();
|
|
160
|
+
return lost;
|
|
161
|
+
}
|
|
162
|
+
async function turnOn(ctx, session) {
|
|
163
|
+
let target;
|
|
164
|
+
try {
|
|
165
|
+
target = await resolveEndpoint(ctx);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
ctx.ui.notify(`anon: ${errorMessage(err)}`, "error");
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
route(ctx, target);
|
|
171
|
+
enabled = true;
|
|
172
|
+
await rebind(ctx, session);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/** The session holds its active model as a snapshot: swap in the registry's re-projected one. */
|
|
176
|
+
async function rebind(ctx, session) {
|
|
177
|
+
const active = ctx.model;
|
|
178
|
+
if (active === void 0) return;
|
|
179
|
+
const live = ctx.modelRegistry.find(active.provider, active.id);
|
|
180
|
+
if (live === void 0 || live.baseUrl === active.baseUrl) return;
|
|
181
|
+
if (!await session.setModel(live)) ctx.ui.notify(`anon: could not rebind the active model ${active.provider}/${active.id}; select it again`, "warning");
|
|
182
|
+
}
|
|
183
|
+
function persist(ctx, value) {
|
|
184
|
+
try {
|
|
185
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
186
|
+
writeFileSync(statePath, `${JSON.stringify({ enabled: value }, null, 2)}\n`);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
ctx.ui.notify(`anon: could not save ${statePath}: ${errorMessage(err)}`, "warning");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function showStatus(ctx) {
|
|
192
|
+
ctx.ui.setStatus(STATUS_KEY, enabled ? "anon on" : "anon off");
|
|
193
|
+
}
|
|
194
|
+
function statusText(ctx) {
|
|
195
|
+
const registry = ctx.modelRegistry;
|
|
196
|
+
const state = stateOf(registry);
|
|
197
|
+
const apis = providerApis(registry);
|
|
198
|
+
const lines = [`anon ${enabled ? "on" : "off"}${required ? " (required)" : ""}`];
|
|
199
|
+
if (endpoint !== void 0) {
|
|
200
|
+
lines.push(`proxy: ${endpoint.url} (${endpoint.source})`);
|
|
201
|
+
lines.push(`pipeline: ${endpoint.pipeline ?? "the proxy default"}`);
|
|
202
|
+
}
|
|
203
|
+
if (sessionId !== void 0) lines.push(`session: ${sessionId}`);
|
|
204
|
+
const routed = [...state.routed].sort();
|
|
205
|
+
const refused = routed.flatMap((provider) => {
|
|
206
|
+
const reason = refusalReason(apis.get(provider), state.originals.get(provider));
|
|
207
|
+
return reason === void 0 ? [] : [`${provider} (${reason})`];
|
|
208
|
+
});
|
|
209
|
+
const preRouted = [...state.originals].filter(([, original]) => original.preRouted).map(([provider]) => provider);
|
|
210
|
+
lines.push(`routed (${routed.length}): ${routed.length === 0 ? "none" : routed.join(", ")}`);
|
|
211
|
+
if (refused.length > 0) lines.push(`refused by the proxy (${refused.length}): ${refused.join(", ")}`);
|
|
212
|
+
if (preRouted.length > 0) lines.push(`routed by their own config (${preRouted.length}): ${preRouted.sort().join(", ")}`);
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
required,
|
|
217
|
+
statePath,
|
|
218
|
+
async command(args, ctx, session) {
|
|
219
|
+
switch (args.trim().toLowerCase()) {
|
|
220
|
+
case "on":
|
|
221
|
+
if (await turnOn(ctx, session)) {
|
|
222
|
+
persist(ctx, true);
|
|
223
|
+
const count = stateOf(ctx.modelRegistry).routed.size;
|
|
224
|
+
ctx.ui.notify(`anon on: ${count} provider(s) routed through ${endpoint?.url}`, "info");
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case "off": {
|
|
228
|
+
if (required) {
|
|
229
|
+
ctx.ui.notify(`anon: off refused: this session requires the transform proxy (${MODE_ENV}=required)`, "warning");
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
const lost = unroute(ctx);
|
|
233
|
+
enabled = false;
|
|
234
|
+
await rebind(ctx, session);
|
|
235
|
+
persist(ctx, false);
|
|
236
|
+
ctx.ui.notify("anon off: original provider base URLs restored", "info");
|
|
237
|
+
if (lost.length > 0) ctx.ui.notify(`anon: ${lost.join(", ")} lost their runtime registration (registered by another extension); restart omp to restore them`, "warning");
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "":
|
|
241
|
+
case "status":
|
|
242
|
+
ctx.ui.notify(statusText(ctx), "info");
|
|
243
|
+
break;
|
|
244
|
+
default: ctx.ui.notify(USAGE, "error");
|
|
245
|
+
}
|
|
246
|
+
showStatus(ctx);
|
|
247
|
+
},
|
|
248
|
+
completions(prefix) {
|
|
249
|
+
const needle = prefix.trim().toLowerCase();
|
|
250
|
+
const items = VERBS.filter((item) => item.value.startsWith(needle));
|
|
251
|
+
return items.length > 0 ? items : null;
|
|
252
|
+
},
|
|
253
|
+
async sessionStart(ctx, session) {
|
|
254
|
+
currentSessionId(ctx);
|
|
255
|
+
if (enabled && endpoint !== void 0) {
|
|
256
|
+
route(ctx, endpoint);
|
|
257
|
+
await rebind(ctx, session);
|
|
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);
|
|
262
|
+
},
|
|
263
|
+
async reapply(ctx, session) {
|
|
264
|
+
if (!enabled || endpoint === void 0) return;
|
|
265
|
+
route(ctx, endpoint);
|
|
266
|
+
await rebind(ctx, session);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `skaile-transform-proxy.json` in omp's agent dir (`$PI_CODING_AGENT_DIR` or `~/.omp/agent`). */
|
|
271
|
+
function defaultStatePath(env, home) {
|
|
272
|
+
const explicit = env[STATE_ENV];
|
|
273
|
+
if (explicit !== void 0 && explicit !== "") return explicit;
|
|
274
|
+
const agentDir = env.PI_CODING_AGENT_DIR || join(home, ".omp", "agent");
|
|
275
|
+
return join(agentDir, "skaile-transform-proxy.json");
|
|
276
|
+
}
|
|
277
|
+
function readEnabled(path) {
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
280
|
+
return typeof parsed === "object" && parsed !== null && "enabled" in parsed && parsed.enabled === true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Every provider of the registry with the wire APIs of its models. */
|
|
286
|
+
function providerApis(registry) {
|
|
287
|
+
const out = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const model of registry.getAll()) {
|
|
289
|
+
let apis = out.get(model.provider);
|
|
290
|
+
if (apis === void 0) {
|
|
291
|
+
apis = /* @__PURE__ */ new Set();
|
|
292
|
+
out.set(model.provider, apis);
|
|
293
|
+
}
|
|
294
|
+
apis.add(model.api);
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
function snapshot(registry, provider) {
|
|
299
|
+
const live = registry.getProviderHeaders(provider);
|
|
300
|
+
const headers = live === void 0 ? void 0 : { ...live };
|
|
301
|
+
const preRouted = headers !== void 0 && Object.keys(headers).some((name) => name.toLowerCase() === "x-skaile-transform-upstream");
|
|
302
|
+
const baseUrl = registry.getProviderBaseUrl(provider);
|
|
303
|
+
return {
|
|
304
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
305
|
+
...headers === void 0 || Object.keys(headers).length === 0 ? {} : { headers },
|
|
306
|
+
preRouted
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function refusalReason(apis, original) {
|
|
310
|
+
if (original?.baseUrl === void 0) return "no base URL";
|
|
311
|
+
const unserved = [...apis ?? []].filter((api) => !Object.hasOwn(SERVED_APIS, api));
|
|
312
|
+
return unserved.length === 0 ? void 0 : unserved.join(", ");
|
|
313
|
+
}
|
|
314
|
+
// src/omp/index.ts
|
|
315
|
+
/** omp re-imports the extension per session; the controller (proxy, recorded originals) is process-wide. */
|
|
316
|
+
const CONTROLLER_KEY = Symbol.for("@skaile/transform-proxy/omp");
|
|
317
|
+
/** Registers the `/anon` command and the event handlers on one session's extension API. */
|
|
318
|
+
function registerAnonExtension(pi, controller) {
|
|
319
|
+
pi.registerCommand("anon", {
|
|
320
|
+
description: "Route model requests through the skaile transform proxy: on | off | status",
|
|
321
|
+
getArgumentCompletions: (prefix) => controller.completions(prefix),
|
|
322
|
+
handler: (args, ctx) => guarded(ctx, () => controller.command(args, ctx, pi))
|
|
323
|
+
});
|
|
324
|
+
on(pi, "session_start", (ctx) => controller.sessionStart(ctx, pi));
|
|
325
|
+
on(pi, "before_agent_start", (ctx) => controller.reapply(ctx, pi));
|
|
326
|
+
on(pi, "turn_start", (ctx) => controller.reapply(ctx, pi));
|
|
327
|
+
}
|
|
328
|
+
function anonExtension(pi) {
|
|
329
|
+
const scope = globalThis;
|
|
330
|
+
registerAnonExtension(pi, scope[CONTROLLER_KEY] ??= createAnonController({
|
|
331
|
+
env: process.env,
|
|
332
|
+
home: homedir()
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
function on(pi, event, run) {
|
|
336
|
+
pi.on(event, (_event, ctx) => guarded(ctx, () => run(ctx)));
|
|
337
|
+
}
|
|
338
|
+
async function guarded(ctx, run) {
|
|
339
|
+
try {
|
|
340
|
+
await run();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
ctx.ui.notify(`anon: ${errorMessage(err)}`, "error");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
export { createAnonController, anonExtension as default, defaultStatePath, registerAnonExtension };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/transform-proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/skaile-ai/skaile-transform-proxy/issues"
|
|
14
14
|
},
|
|
15
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"llm",
|
|
17
|
+
"proxy",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"openai",
|
|
20
|
+
"anonymization",
|
|
21
|
+
"transform"
|
|
22
|
+
],
|
|
16
23
|
"engines": {
|
|
17
24
|
"node": ">=22"
|
|
18
25
|
},
|
|
@@ -21,12 +28,26 @@
|
|
|
21
28
|
"types": "./dist/index.d.mts",
|
|
22
29
|
"import": "./dist/index.mjs"
|
|
23
30
|
},
|
|
31
|
+
"./omp": {
|
|
32
|
+
"types": "./dist/omp/index.d.mts",
|
|
33
|
+
"import": "./dist/omp/index.mjs"
|
|
34
|
+
},
|
|
24
35
|
"./package.json": "./package.json"
|
|
25
36
|
},
|
|
37
|
+
"omp": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./dist/omp/index.mjs"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
26
42
|
"bin": {
|
|
27
43
|
"skaile-transform-proxy": "dist/cli.mjs"
|
|
28
44
|
},
|
|
29
|
-
"files": [
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
30
51
|
"publishConfig": {
|
|
31
52
|
"access": "public"
|
|
32
53
|
},
|