proxitor 0.1.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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/cli.cjs +36 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +37 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +10 -0
- package/dist/index.d.cts +91 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +91 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/proxy.cjs +536 -0
- package/dist/proxy.cjs.map +1 -0
- package/dist/proxy.mjs +454 -0
- package/dist/proxy.mjs.map +1 -0
- package/package.json +85 -0
package/dist/proxy.mjs
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import * as yaml from "js-yaml";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
import { serve } from "@hono/node-server";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
//#region src/utils.ts
|
|
9
|
+
/** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */
|
|
10
|
+
function toArray(value) {
|
|
11
|
+
if (value === void 0) return void 0;
|
|
12
|
+
const arr = Array.isArray(value) ? [...value] : [value];
|
|
13
|
+
return arr.length > 0 ? arr : void 0;
|
|
14
|
+
}
|
|
15
|
+
/** Try to parse an ArrayBuffer as JSON. Returns undefined on failure or empty body. */
|
|
16
|
+
function tryParseBody(raw) {
|
|
17
|
+
if (raw.byteLength === 0) return void 0;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(new TextDecoder().decode(raw));
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/config.ts
|
|
26
|
+
const DEFAULT_CONFIG = {
|
|
27
|
+
host: "0.0.0.0",
|
|
28
|
+
port: 8080,
|
|
29
|
+
openrouterKey: "",
|
|
30
|
+
openrouterBaseUrl: "https://openrouter.ai/api/v1",
|
|
31
|
+
verbose: false,
|
|
32
|
+
bodyLimit: "50mb",
|
|
33
|
+
attributionReferer: "http://localhost",
|
|
34
|
+
attributionTitle: "proxitor"
|
|
35
|
+
};
|
|
36
|
+
/** Fields that need toArray normalization (string | string[] → string[] | undefined) */
|
|
37
|
+
const ARRAY_FIELDS = [
|
|
38
|
+
{
|
|
39
|
+
key: "only",
|
|
40
|
+
apiName: "only"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "order",
|
|
44
|
+
apiName: "order"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "ignore",
|
|
48
|
+
apiName: "ignore"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "quantizations",
|
|
52
|
+
apiName: "quantizations"
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
/** Direct camelCase → snake_case field mappings */
|
|
56
|
+
const DIRECT_FIELDS = [
|
|
57
|
+
{
|
|
58
|
+
key: "sort",
|
|
59
|
+
apiName: "sort"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "maxPrice",
|
|
63
|
+
apiName: "max_price"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "requireParameters",
|
|
67
|
+
apiName: "require_parameters"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "dataCollection",
|
|
71
|
+
apiName: "data_collection"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: "zdr",
|
|
75
|
+
apiName: "zdr"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "enforceDistillableText",
|
|
79
|
+
apiName: "enforce_distillable_text"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "preferredMinThroughput",
|
|
83
|
+
apiName: "preferred_min_throughput"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "preferredMaxLatency",
|
|
87
|
+
apiName: "preferred_max_latency"
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
/** Build the provider routing object for OpenRouter request body injection */
|
|
91
|
+
function buildProviderRouting(provider) {
|
|
92
|
+
if (!provider) return void 0;
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const { key, apiName } of ARRAY_FIELDS) {
|
|
95
|
+
const value = provider[key];
|
|
96
|
+
if (value !== void 0) {
|
|
97
|
+
const normalized = toArray(value);
|
|
98
|
+
if (normalized) result[apiName] = normalized;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const { key, apiName } of DIRECT_FIELDS) {
|
|
102
|
+
const value = provider[key];
|
|
103
|
+
if (value !== void 0) result[apiName] = value;
|
|
104
|
+
}
|
|
105
|
+
if (result.order) result.allow_fallbacks = provider.allowFallbacks ?? true;
|
|
106
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
107
|
+
}
|
|
108
|
+
/** Score a pattern against a model name. Higher = better match. -1 = no match. */
|
|
109
|
+
function matchScore(pattern, modelName) {
|
|
110
|
+
if (pattern === modelName) return modelName.length + 1e3;
|
|
111
|
+
if (pattern.endsWith("*") && modelName.startsWith(pattern.slice(0, -1))) return pattern.length;
|
|
112
|
+
return -1;
|
|
113
|
+
}
|
|
114
|
+
/** Resolve the effective config for a given model by merging global defaults with the best-matching override */
|
|
115
|
+
function resolveModelConfig(config, modelName) {
|
|
116
|
+
const result = {
|
|
117
|
+
provider: config.provider,
|
|
118
|
+
headers: config.headers ? { ...config.headers } : void 0
|
|
119
|
+
};
|
|
120
|
+
if (!modelName || !config.modelOverrides) return result;
|
|
121
|
+
let bestPattern = null;
|
|
122
|
+
let bestScore = -1;
|
|
123
|
+
for (const pattern of Object.keys(config.modelOverrides)) {
|
|
124
|
+
const score = matchScore(pattern, modelName);
|
|
125
|
+
if (score > bestScore) {
|
|
126
|
+
bestScore = score;
|
|
127
|
+
bestPattern = pattern;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (bestPattern) {
|
|
131
|
+
const override = config.modelOverrides[bestPattern];
|
|
132
|
+
if (override?.provider !== void 0) result.provider = override.provider;
|
|
133
|
+
if (override?.headers) result.headers = {
|
|
134
|
+
...result.headers ?? {},
|
|
135
|
+
...override.headers
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
async function loadConfig(options) {
|
|
141
|
+
const config = { ...DEFAULT_CONFIG };
|
|
142
|
+
if (!options.noConfig) {
|
|
143
|
+
const configPath = findConfigFile(options.configPath);
|
|
144
|
+
if (configPath) {
|
|
145
|
+
const fileConfig = readConfigFile(configPath);
|
|
146
|
+
Object.assign(config, fileConfig);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (options.host) config.host = options.host;
|
|
150
|
+
if (options.port) config.port = options.port;
|
|
151
|
+
if (options.verbose) config.verbose = options.verbose;
|
|
152
|
+
if (options.openrouterKey) config.openrouterKey = options.openrouterKey;
|
|
153
|
+
else if (!config.openrouterKey) config.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
|
|
154
|
+
if (!config.openrouterKey) throw new Error("OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.");
|
|
155
|
+
return config;
|
|
156
|
+
}
|
|
157
|
+
/** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */
|
|
158
|
+
function getXdgConfigDir() {
|
|
159
|
+
const xdgHome = process.env.XDG_CONFIG_HOME;
|
|
160
|
+
return xdgHome ? resolve(xdgHome, "proxitor") : join(homedir(), ".config", "proxitor");
|
|
161
|
+
}
|
|
162
|
+
function findConfigFile(explicitPath) {
|
|
163
|
+
if (explicitPath) {
|
|
164
|
+
if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${explicitPath}`);
|
|
165
|
+
return resolve(explicitPath);
|
|
166
|
+
}
|
|
167
|
+
for (const candidate of [
|
|
168
|
+
"proxitor.config.yaml",
|
|
169
|
+
"proxitor.config.yml",
|
|
170
|
+
"proxitor.config.json",
|
|
171
|
+
".proxitor.yaml",
|
|
172
|
+
".proxitor.yml",
|
|
173
|
+
".proxitor.json"
|
|
174
|
+
]) {
|
|
175
|
+
const fullPath = resolve(candidate);
|
|
176
|
+
if (existsSync(fullPath)) return fullPath;
|
|
177
|
+
}
|
|
178
|
+
const xdgDir = getXdgConfigDir();
|
|
179
|
+
for (const candidate of [
|
|
180
|
+
"config.yaml",
|
|
181
|
+
"config.yml",
|
|
182
|
+
"config.json"
|
|
183
|
+
]) {
|
|
184
|
+
const fullPath = join(xdgDir, candidate);
|
|
185
|
+
if (existsSync(fullPath)) return fullPath;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function readConfigFile(filePath) {
|
|
190
|
+
const content = readFileSync(filePath, "utf-8");
|
|
191
|
+
if (filePath.endsWith(".json")) return JSON.parse(content);
|
|
192
|
+
return yaml.load(content);
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/logger.ts
|
|
196
|
+
const logger = consola.withTag("proxitor");
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/proxy/headers.ts
|
|
199
|
+
const HOP_BY_HOP = new Set([
|
|
200
|
+
"connection",
|
|
201
|
+
"keep-alive",
|
|
202
|
+
"proxy-authenticate",
|
|
203
|
+
"proxy-authorization",
|
|
204
|
+
"te",
|
|
205
|
+
"trailer",
|
|
206
|
+
"transfer-encoding",
|
|
207
|
+
"upgrade"
|
|
208
|
+
]);
|
|
209
|
+
/** Headers to strip from client request before forwarding */
|
|
210
|
+
const STRIP_REQUEST = new Set([
|
|
211
|
+
"authorization",
|
|
212
|
+
"x-api-key",
|
|
213
|
+
"host",
|
|
214
|
+
"content-length"
|
|
215
|
+
]);
|
|
216
|
+
/** Headers to strip from upstream response before forwarding */
|
|
217
|
+
const STRIP_RESPONSE = new Set(["content-length", "content-encoding"]);
|
|
218
|
+
/** Filter headers by removing hop-by-hop and an additional blocklist */
|
|
219
|
+
function filterHeaders(incoming, blocklist) {
|
|
220
|
+
const headers = {};
|
|
221
|
+
for (const [key, value] of incoming.entries()) {
|
|
222
|
+
const lower = key.toLowerCase();
|
|
223
|
+
if (HOP_BY_HOP.has(lower)) continue;
|
|
224
|
+
if (blocklist.has(lower)) continue;
|
|
225
|
+
headers[key] = value;
|
|
226
|
+
}
|
|
227
|
+
return headers;
|
|
228
|
+
}
|
|
229
|
+
/** Build request headers for upstream fetch */
|
|
230
|
+
function buildRequestHeaders(incoming, config, inject, extraHeaders) {
|
|
231
|
+
const headers = filterHeaders(incoming, STRIP_REQUEST);
|
|
232
|
+
headers.Authorization = `Bearer ${config.openrouterKey}`;
|
|
233
|
+
headers["HTTP-Referer"] = config.attributionReferer;
|
|
234
|
+
headers["X-Title"] = config.attributionTitle;
|
|
235
|
+
headers["Accept-Encoding"] = "identity";
|
|
236
|
+
if (extraHeaders) Object.assign(headers, extraHeaders);
|
|
237
|
+
if (inject) headers["Content-Type"] = "application/json";
|
|
238
|
+
return headers;
|
|
239
|
+
}
|
|
240
|
+
/** Filter response headers and add SSE-friendly defaults */
|
|
241
|
+
function buildResponseHeaders(from) {
|
|
242
|
+
const headers = filterHeaders(from, STRIP_RESPONSE);
|
|
243
|
+
headers["Cache-Control"] = "no-cache";
|
|
244
|
+
headers["X-Accel-Buffering"] = "no";
|
|
245
|
+
return headers;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/proxy/inject.ts
|
|
249
|
+
/** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */
|
|
250
|
+
function extractModel(rawBody) {
|
|
251
|
+
const json = tryParseBody(rawBody);
|
|
252
|
+
return typeof json?.model === "string" ? json.model : void 0;
|
|
253
|
+
}
|
|
254
|
+
/** Inject provider routing into request body, always overwriting existing value */
|
|
255
|
+
function injectProvider(rawBody, providerRouting) {
|
|
256
|
+
if (rawBody.byteLength === 0) throw new Error("Request body is empty; cannot inject provider");
|
|
257
|
+
let json;
|
|
258
|
+
try {
|
|
259
|
+
json = JSON.parse(new TextDecoder().decode(rawBody));
|
|
260
|
+
} catch (parseError) {
|
|
261
|
+
throw new Error("Request body is not valid JSON; cannot inject provider", { cause: parseError });
|
|
262
|
+
}
|
|
263
|
+
const modified = {
|
|
264
|
+
...json,
|
|
265
|
+
provider: providerRouting
|
|
266
|
+
};
|
|
267
|
+
return new TextEncoder().encode(JSON.stringify(modified)).buffer;
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/proxy/paths.ts
|
|
271
|
+
/**
|
|
272
|
+
* Paths where provider routing is injected into the request body.
|
|
273
|
+
* All three are OpenRouter-supported endpoints:
|
|
274
|
+
* /v1/chat/completions — OpenAI Chat Completions
|
|
275
|
+
* /v1/responses — OpenAI Responses API
|
|
276
|
+
* /v1/messages — Anthropic Messages API
|
|
277
|
+
*/
|
|
278
|
+
const INJECT_PATHS = new Set([
|
|
279
|
+
"/v1/chat/completions",
|
|
280
|
+
"/v1/responses",
|
|
281
|
+
"/v1/messages"
|
|
282
|
+
]);
|
|
283
|
+
/** Check if this request should have provider routing injected */
|
|
284
|
+
function shouldInject(method, path) {
|
|
285
|
+
return method === "POST" && INJECT_PATHS.has(path);
|
|
286
|
+
}
|
|
287
|
+
/** Strip /v1 prefix: /v1/chat/completions → /chat/completions */
|
|
288
|
+
function toUpstreamPath(originalUrl) {
|
|
289
|
+
if (originalUrl.startsWith("/v1")) return originalUrl.slice(3);
|
|
290
|
+
return originalUrl;
|
|
291
|
+
}
|
|
292
|
+
/** Build full upstream URL from request and config */
|
|
293
|
+
function buildUpstreamUrl(originalUrl, config) {
|
|
294
|
+
return `${config.openrouterBaseUrl}${toUpstreamPath(originalUrl)}`;
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/proxy.ts
|
|
298
|
+
function readRequestBody(method, raw, inject, providerRouting) {
|
|
299
|
+
if (["GET", "HEAD"].includes(method)) return void 0;
|
|
300
|
+
if (inject) return injectProvider(raw, providerRouting);
|
|
301
|
+
return raw.byteLength > 0 ? raw : void 0;
|
|
302
|
+
}
|
|
303
|
+
async function fetchUpstream(url, method, headers, body, signal) {
|
|
304
|
+
return fetch(url, {
|
|
305
|
+
method,
|
|
306
|
+
headers,
|
|
307
|
+
body,
|
|
308
|
+
signal,
|
|
309
|
+
duplex: body ? "half" : void 0
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function buildUpstreamResponse(upstream, method) {
|
|
313
|
+
const headers = buildResponseHeaders(upstream.headers);
|
|
314
|
+
if (method === "HEAD" || !upstream.body) return new Response(null, {
|
|
315
|
+
status: upstream.status,
|
|
316
|
+
headers
|
|
317
|
+
});
|
|
318
|
+
return new Response(upstream.body, {
|
|
319
|
+
status: upstream.status,
|
|
320
|
+
headers
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/** Read and process the request body, returning an error response on failure */
|
|
324
|
+
async function readRawBody(request) {
|
|
325
|
+
try {
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
body: await request.arrayBuffer()
|
|
329
|
+
};
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const message = err instanceof Error ? err.message : "Failed to read request body";
|
|
332
|
+
logger.error(message);
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
response: Response.json({ error: {
|
|
336
|
+
message,
|
|
337
|
+
type: "proxy_request_error"
|
|
338
|
+
} }, { status: 400 })
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/** Resolve per-request config: extract model, resolve overrides, build routing and body */
|
|
343
|
+
function resolveRequest(rawBody, config, method, path) {
|
|
344
|
+
const modelName = extractModel(rawBody);
|
|
345
|
+
const resolved = resolveModelConfig(config, modelName);
|
|
346
|
+
const providerRouting = buildProviderRouting(resolved.provider);
|
|
347
|
+
const inject = shouldInject(method, path) && providerRouting !== void 0;
|
|
348
|
+
let body;
|
|
349
|
+
try {
|
|
350
|
+
body = readRequestBody(method, rawBody, inject, providerRouting);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const message = err instanceof Error ? err.message : "Failed to process request body";
|
|
353
|
+
logger.error(message);
|
|
354
|
+
return {
|
|
355
|
+
inject,
|
|
356
|
+
body: void 0,
|
|
357
|
+
modelName,
|
|
358
|
+
headers: resolved.headers,
|
|
359
|
+
error: new Response(JSON.stringify({ error: {
|
|
360
|
+
message,
|
|
361
|
+
type: "proxy_request_error"
|
|
362
|
+
} }), {
|
|
363
|
+
status: 400,
|
|
364
|
+
headers: { "Content-Type": "application/json" }
|
|
365
|
+
})
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
inject,
|
|
370
|
+
body,
|
|
371
|
+
modelName,
|
|
372
|
+
headers: resolved.headers
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/** Execute upstream fetch, returning appropriate error responses on failure */
|
|
376
|
+
async function executeUpstream(upstreamUrl, method, headers, body, signal, path, startedAt) {
|
|
377
|
+
let upstream;
|
|
378
|
+
try {
|
|
379
|
+
upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
382
|
+
logger.warn(`Aborted: ${method} ${path}`);
|
|
383
|
+
return new Response(null, { status: 499 });
|
|
384
|
+
}
|
|
385
|
+
logger.error("Upstream fetch error:", err);
|
|
386
|
+
return Response.json({ error: {
|
|
387
|
+
message: "Proxy failed to reach upstream",
|
|
388
|
+
type: "proxy_upstream_error"
|
|
389
|
+
} }, { status: 502 });
|
|
390
|
+
}
|
|
391
|
+
logger.info(`${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`);
|
|
392
|
+
return buildUpstreamResponse(upstream, method);
|
|
393
|
+
}
|
|
394
|
+
function createProxyServer(config, onReady) {
|
|
395
|
+
const app = new Hono();
|
|
396
|
+
app.get("/health", (c) => {
|
|
397
|
+
const globalRouting = buildProviderRouting(config.provider);
|
|
398
|
+
return c.json({
|
|
399
|
+
ok: true,
|
|
400
|
+
upstream: config.openrouterBaseUrl,
|
|
401
|
+
provider: globalRouting ?? "not configured",
|
|
402
|
+
modelOverrides: config.modelOverrides ? Object.keys(config.modelOverrides) : []
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
app.all("*", async (c) => {
|
|
406
|
+
const method = c.req.method;
|
|
407
|
+
const path = new URL(c.req.url).pathname;
|
|
408
|
+
const upstreamUrl = buildUpstreamUrl(c.req.url, config);
|
|
409
|
+
const startedAt = Date.now();
|
|
410
|
+
const raw = await readRawBody(c.req.raw);
|
|
411
|
+
if (!raw.ok) return raw.response;
|
|
412
|
+
const resolved = resolveRequest(raw.body, config, method, path);
|
|
413
|
+
if (resolved.error) return resolved.error;
|
|
414
|
+
const headers = buildRequestHeaders(c.req.raw.headers, config, resolved.inject, resolved.headers);
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
c.req.raw.signal.addEventListener("abort", () => controller.abort());
|
|
417
|
+
const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : "";
|
|
418
|
+
logger.info(`${method} ${path} → ${upstreamUrl}${resolved.inject ? " [inject]" : ""}${modelLog}`);
|
|
419
|
+
return executeUpstream(upstreamUrl, method, headers, resolved.body, controller.signal, path, startedAt);
|
|
420
|
+
});
|
|
421
|
+
return serve({
|
|
422
|
+
fetch: app.fetch,
|
|
423
|
+
port: config.port,
|
|
424
|
+
hostname: config.host
|
|
425
|
+
}, onReady);
|
|
426
|
+
}
|
|
427
|
+
/** Shutdown deadline: force-close after this many ms */
|
|
428
|
+
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
429
|
+
/** Start the proxy with graceful shutdown on SIGTERM/SIGINT */
|
|
430
|
+
function startProxyServer(config, onReady) {
|
|
431
|
+
const server = createProxyServer(config, onReady);
|
|
432
|
+
let shuttingDown = false;
|
|
433
|
+
function shutdown(signal) {
|
|
434
|
+
if (shuttingDown) return;
|
|
435
|
+
shuttingDown = true;
|
|
436
|
+
logger.info(`${signal} received — draining active connections…`);
|
|
437
|
+
const timer = setTimeout(() => {
|
|
438
|
+
logger.warn("Forcing shutdown — drain timeout exceeded");
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
441
|
+
server.close(() => {
|
|
442
|
+
clearTimeout(timer);
|
|
443
|
+
logger.info("All connections drained — goodbye");
|
|
444
|
+
process.exit(0);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
448
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
449
|
+
return server;
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
export { buildProviderRouting as a, resolveModelConfig as c, logger as i, toArray as l, startProxyServer as n, loadConfig as o, extractModel as r, matchScore as s, createProxyServer as t, tryParseBody as u };
|
|
453
|
+
|
|
454
|
+
//# sourceMappingURL=proxy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.mjs","names":[],"sources":["../src/utils.ts","../src/config.ts","../src/logger.ts","../src/proxy/headers.ts","../src/proxy/inject.ts","../src/proxy/paths.ts","../src/proxy.ts"],"sourcesContent":["/** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */\nexport function toArray(value: string | string[] | undefined): string[] | undefined {\n if (value === undefined) return undefined\n const arr = Array.isArray(value) ? [...value] : [value]\n return arr.length > 0 ? arr : undefined\n}\n\n/** Try to parse an ArrayBuffer as JSON. Returns undefined on failure or empty body. */\nexport function tryParseBody(raw: ArrayBuffer): Record<string, unknown> | undefined {\n if (raw.byteLength === 0) return undefined\n try {\n return JSON.parse(new TextDecoder().decode(raw)) as Record<string, unknown>\n } catch {\n return undefined\n }\n}\n","import { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport * as yaml from 'js-yaml'\nimport { toArray } from './utils.js'\n\n/** Percentile cutoffs for performance thresholds */\nexport type PercentileCutoffs = {\n p50?: number\n p75?: number\n p90?: number\n p99?: number\n}\n\n/** Provider sorting options */\nexport type ProviderSort =\n | 'price'\n | 'throughput'\n | 'latency'\n | { by: 'price' | 'throughput' | 'latency'; partition?: 'model' | 'none' }\n\n/** Maximum pricing for a request */\nexport type MaxPrice = {\n prompt?: number\n completion?: number\n request?: number\n image?: number\n}\n\nexport type ProviderConfig = {\n /** Allow only these providers (e.g. \"deepinfra\" or [\"anthropic\", \"openai\"]) */\n only?: string | string[]\n /** Try providers in this order (e.g. \"anthropic\" or [\"openai\", \"together\"]) */\n order?: string | string[]\n /** Ignore these providers (mirror of only — skip specific providers) */\n ignore?: string | string[]\n /** Allow fallback to other providers (default: true) */\n allowFallbacks?: boolean\n /** Sort providers by price, throughput, or latency */\n sort?: ProviderSort\n /** Filter by quantization levels (e.g. [\"fp8\", \"int4\"]) */\n quantizations?: string[]\n /** Maximum pricing to accept */\n maxPrice?: MaxPrice\n /** Only use providers that support all request parameters (default: false) */\n requireParameters?: boolean\n /** Control data collection policy: \"allow\" or \"deny\" (default: \"allow\") */\n dataCollection?: 'allow' | 'deny'\n /** Restrict routing to Zero Data Retention endpoints */\n zdr?: boolean\n /** Restrict routing to models that allow text distillation */\n enforceDistillableText?: boolean\n /** Preferred minimum throughput (tokens/sec) */\n preferredMinThroughput?: number | PercentileCutoffs\n /** Preferred maximum latency (seconds) */\n preferredMaxLatency?: number | PercentileCutoffs\n}\n\n/** Per-model override: layers on top of global config */\nexport type ModelOverride = {\n /** Override provider routing for matching models */\n provider?: ProviderConfig\n /** Additional headers to merge for matching models */\n headers?: Record<string, string>\n}\n\n/** Result of merging global config with a model-specific override */\nexport type ResolvedModelConfig = {\n provider?: ProviderConfig\n headers?: Record<string, string>\n}\n\nexport type ProxyConfig = {\n host: string\n port: number\n openrouterKey: string\n openrouterBaseUrl: string\n verbose: boolean\n /** Request body size limit (default: \"50mb\") */\n bodyLimit: string\n /** Provider routing configuration (global default) */\n provider?: ProviderConfig\n /** HTTP-Referer for OpenRouter attribution */\n attributionReferer: string\n /** X-Title for OpenRouter attribution */\n attributionTitle: string\n /** Custom headers to add to proxied requests (global default) */\n headers?: Record<string, string>\n /** Per-model config overrides. Keys are exact model names or prefix patterns (e.g. \"claude-*\") */\n modelOverrides?: Record<string, ModelOverride>\n}\n\nconst DEFAULT_CONFIG: ProxyConfig = {\n host: '0.0.0.0',\n port: 8080,\n openrouterKey: '',\n openrouterBaseUrl: 'https://openrouter.ai/api/v1',\n verbose: false,\n bodyLimit: '50mb',\n attributionReferer: 'http://localhost',\n attributionTitle: 'proxitor',\n}\n\ntype LoadConfigOptions = {\n configPath?: string\n noConfig?: boolean\n host?: string\n openrouterKey?: string\n port?: number\n verbose?: boolean\n}\n\n/** Fields that need toArray normalization (string | string[] → string[] | undefined) */\nconst ARRAY_FIELDS: ReadonlyArray<{ key: keyof ProviderConfig; apiName: string }> = [\n { key: 'only', apiName: 'only' },\n { key: 'order', apiName: 'order' },\n { key: 'ignore', apiName: 'ignore' },\n { key: 'quantizations', apiName: 'quantizations' },\n] as const\n\n/** Direct camelCase → snake_case field mappings */\nconst DIRECT_FIELDS: ReadonlyArray<{ key: keyof ProviderConfig; apiName: string }> = [\n { key: 'sort', apiName: 'sort' },\n { key: 'maxPrice', apiName: 'max_price' },\n { key: 'requireParameters', apiName: 'require_parameters' },\n { key: 'dataCollection', apiName: 'data_collection' },\n { key: 'zdr', apiName: 'zdr' },\n { key: 'enforceDistillableText', apiName: 'enforce_distillable_text' },\n { key: 'preferredMinThroughput', apiName: 'preferred_min_throughput' },\n { key: 'preferredMaxLatency', apiName: 'preferred_max_latency' },\n] as const\n\n/** Build the provider routing object for OpenRouter request body injection */\nexport function buildProviderRouting(\n provider?: ProviderConfig,\n): Record<string, unknown> | undefined {\n if (!provider) return undefined\n\n const result: Record<string, unknown> = {}\n\n for (const { key, apiName } of ARRAY_FIELDS) {\n const value = provider[key]\n if (value !== undefined) {\n const normalized = toArray(value as string | string[])\n if (normalized) result[apiName] = normalized\n }\n }\n\n for (const { key, apiName } of DIRECT_FIELDS) {\n const value = provider[key]\n if (value !== undefined) result[apiName] = value\n }\n\n if (result.order) {\n result.allow_fallbacks = provider.allowFallbacks ?? true\n }\n\n return Object.keys(result).length > 0 ? result : undefined\n}\n\n/** Score a pattern against a model name. Higher = better match. -1 = no match. */\nexport function matchScore(pattern: string, modelName: string): number {\n if (pattern === modelName) return modelName.length + 1000\n\n if (pattern.endsWith('*') && modelName.startsWith(pattern.slice(0, -1))) {\n return pattern.length\n }\n\n return -1\n}\n\n/** Resolve the effective config for a given model by merging global defaults with the best-matching override */\nexport function resolveModelConfig(\n config: ProxyConfig,\n modelName?: string,\n): ResolvedModelConfig {\n const result: ResolvedModelConfig = {\n provider: config.provider,\n headers: config.headers ? { ...config.headers } : undefined,\n }\n\n if (!modelName || !config.modelOverrides) return result\n\n let bestPattern: string | null = null\n let bestScore = -1\n\n for (const pattern of Object.keys(config.modelOverrides)) {\n const score = matchScore(pattern, modelName)\n if (score > bestScore) {\n bestScore = score\n bestPattern = pattern\n }\n }\n\n if (bestPattern) {\n const override = config.modelOverrides[bestPattern]\n if (override?.provider !== undefined) {\n result.provider = override.provider\n }\n if (override?.headers) {\n result.headers = { ...(result.headers ?? {}), ...override.headers }\n }\n }\n\n return result\n}\n\nexport async function loadConfig(options: LoadConfigOptions): Promise<ProxyConfig> {\n const config = { ...DEFAULT_CONFIG }\n\n if (!options.noConfig) {\n const configPath = findConfigFile(options.configPath)\n if (configPath) {\n const fileConfig = readConfigFile(configPath)\n Object.assign(config, fileConfig)\n }\n }\n\n if (options.host) config.host = options.host\n if (options.port) config.port = options.port\n if (options.verbose) config.verbose = options.verbose\n\n if (options.openrouterKey) {\n config.openrouterKey = options.openrouterKey\n } else if (!config.openrouterKey) {\n config.openrouterKey = process.env.OPENROUTER_API_KEY ?? ''\n }\n\n if (!config.openrouterKey) {\n throw new Error(\n 'OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.',\n )\n }\n\n return config\n}\n\n/** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */\nfunction getXdgConfigDir(): string {\n const xdgHome = process.env.XDG_CONFIG_HOME\n return xdgHome ? resolve(xdgHome, 'proxitor') : join(homedir(), '.config', 'proxitor')\n}\n\nfunction findConfigFile(explicitPath?: string): string | null {\n if (explicitPath) {\n if (!existsSync(explicitPath)) {\n throw new Error(`Config file not found: ${explicitPath}`)\n }\n return resolve(explicitPath)\n }\n\n const localCandidates = [\n 'proxitor.config.yaml',\n 'proxitor.config.yml',\n 'proxitor.config.json',\n '.proxitor.yaml',\n '.proxitor.yml',\n '.proxitor.json',\n ]\n\n for (const candidate of localCandidates) {\n const fullPath = resolve(candidate)\n if (existsSync(fullPath)) {\n return fullPath\n }\n }\n\n const xdgDir = getXdgConfigDir()\n const xdgCandidates = ['config.yaml', 'config.yml', 'config.json']\n\n for (const candidate of xdgCandidates) {\n const fullPath = join(xdgDir, candidate)\n if (existsSync(fullPath)) {\n return fullPath\n }\n }\n\n return null\n}\n\nfunction readConfigFile(filePath: string): Partial<ProxyConfig> {\n const content = readFileSync(filePath, 'utf-8')\n\n if (filePath.endsWith('.json')) {\n return JSON.parse(content) as Partial<ProxyConfig>\n }\n\n return yaml.load(content) as Partial<ProxyConfig>\n}\n","import { consola } from 'consola'\n\nexport const logger = consola.withTag('proxitor')\n","import type { ProxyConfig } from '../config.js'\n\nconst HOP_BY_HOP = new Set([\n 'connection',\n 'keep-alive',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'transfer-encoding',\n 'upgrade',\n])\n\n/** Headers to strip from client request before forwarding */\nconst STRIP_REQUEST = new Set(['authorization', 'x-api-key', 'host', 'content-length'])\n\n/** Headers to strip from upstream response before forwarding */\nconst STRIP_RESPONSE = new Set(['content-length', 'content-encoding'])\n\n/** Filter headers by removing hop-by-hop and an additional blocklist */\nfunction filterHeaders(\n incoming: Headers,\n blocklist: ReadonlySet<string>,\n): Record<string, string> {\n const headers: Record<string, string> = {}\n for (const [key, value] of incoming.entries()) {\n const lower = key.toLowerCase()\n if (HOP_BY_HOP.has(lower)) continue\n if (blocklist.has(lower)) continue\n headers[key] = value\n }\n return headers\n}\n\n/** Build request headers for upstream fetch */\nexport function buildRequestHeaders(\n incoming: Headers,\n config: ProxyConfig,\n inject: boolean,\n extraHeaders?: Record<string, string>,\n): Record<string, string> {\n const headers = filterHeaders(incoming, STRIP_REQUEST)\n\n headers.Authorization = `Bearer ${config.openrouterKey}`\n headers['HTTP-Referer'] = config.attributionReferer\n headers['X-Title'] = config.attributionTitle\n headers['Accept-Encoding'] = 'identity'\n\n if (extraHeaders) {\n Object.assign(headers, extraHeaders)\n }\n\n if (inject) {\n headers['Content-Type'] = 'application/json'\n }\n\n return headers\n}\n\n/** Filter response headers and add SSE-friendly defaults */\nexport function buildResponseHeaders(from: Headers): Record<string, string> {\n const headers = filterHeaders(from, STRIP_RESPONSE)\n\n headers['Cache-Control'] = 'no-cache'\n headers['X-Accel-Buffering'] = 'no'\n\n return headers\n}\n","import { tryParseBody } from '../utils.js'\n\n/** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */\nexport function extractModel(rawBody: ArrayBuffer): string | undefined {\n const json = tryParseBody(rawBody)\n return typeof json?.model === 'string' ? json.model : undefined\n}\n\n/** Inject provider routing into request body, always overwriting existing value */\nexport function injectProvider(\n rawBody: ArrayBuffer,\n providerRouting: Record<string, unknown>,\n): ArrayBuffer {\n if (rawBody.byteLength === 0) {\n throw new Error('Request body is empty; cannot inject provider')\n }\n\n let json: Record<string, unknown>\n try {\n json = JSON.parse(new TextDecoder().decode(rawBody)) as Record<string, unknown>\n } catch (parseError) {\n throw new Error('Request body is not valid JSON; cannot inject provider', {\n cause: parseError,\n })\n }\n\n const modified = { ...json, provider: providerRouting }\n return new TextEncoder().encode(JSON.stringify(modified)).buffer as ArrayBuffer\n}\n","import type { ProxyConfig } from '../config.js'\n\n/**\n * Paths where provider routing is injected into the request body.\n * All three are OpenRouter-supported endpoints:\n * /v1/chat/completions — OpenAI Chat Completions\n * /v1/responses — OpenAI Responses API\n * /v1/messages — Anthropic Messages API\n */\nexport const INJECT_PATHS = new Set([\n '/v1/chat/completions',\n '/v1/responses',\n '/v1/messages',\n])\n\n/** Check if this request should have provider routing injected */\nexport function shouldInject(method: string, path: string): boolean {\n return method === 'POST' && INJECT_PATHS.has(path)\n}\n\n/** Strip /v1 prefix: /v1/chat/completions → /chat/completions */\nexport function toUpstreamPath(originalUrl: string): string {\n if (originalUrl.startsWith('/v1')) {\n return originalUrl.slice('/v1'.length)\n }\n return originalUrl\n}\n\n/** Build full upstream URL from request and config */\nexport function buildUpstreamUrl(originalUrl: string, config: ProxyConfig): string {\n return `${config.openrouterBaseUrl}${toUpstreamPath(originalUrl)}`\n}\n","import { type HttpBindings, type ServerType, serve } from '@hono/node-server'\nimport { Hono } from 'hono'\nimport { buildProviderRouting, type ProxyConfig, resolveModelConfig } from './config.js'\nimport { logger } from './logger.js'\nimport { buildRequestHeaders, buildResponseHeaders } from './proxy/headers.js'\nimport { extractModel, injectProvider } from './proxy/inject.js'\nimport { buildUpstreamUrl, shouldInject } from './proxy/paths.js'\n\ntype ProxyContext = {\n Variables: {\n config: ProxyConfig\n }\n Bindings: HttpBindings\n}\n\nfunction readRequestBody(\n method: string,\n raw: ArrayBuffer,\n inject: boolean,\n providerRouting: Record<string, unknown>,\n): ArrayBuffer | undefined {\n if (['GET', 'HEAD'].includes(method)) return undefined\n\n if (inject) {\n return injectProvider(raw, providerRouting)\n }\n\n return raw.byteLength > 0 ? raw : undefined\n}\n\nasync function fetchUpstream(\n url: string,\n method: string,\n headers: Record<string, string>,\n body: ArrayBuffer | undefined,\n signal: AbortSignal,\n): Promise<Response> {\n return fetch(url, {\n method,\n headers,\n body,\n signal,\n duplex: body ? 'half' : undefined,\n })\n}\n\nfunction buildUpstreamResponse(upstream: Response, method: string): Response {\n const headers = buildResponseHeaders(upstream.headers)\n\n if (method === 'HEAD' || !upstream.body) {\n return new Response(null, { status: upstream.status, headers })\n }\n\n return new Response(upstream.body, { status: upstream.status, headers })\n}\n\n/** Read and process the request body, returning an error response on failure */\nasync function readRawBody(\n request: Request,\n): Promise<{ ok: true; body: ArrayBuffer } | { ok: false; response: Response }> {\n try {\n const body = await request.arrayBuffer()\n return { ok: true, body }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to read request body'\n logger.error(message)\n return {\n ok: false,\n response: Response.json(\n { error: { message, type: 'proxy_request_error' } },\n { status: 400 },\n ),\n }\n }\n}\n\ntype ResolvedRequest = {\n inject: boolean\n body: ArrayBuffer | undefined\n modelName: string | undefined\n headers: Record<string, string> | undefined\n error?: Response\n}\n\n/** Resolve per-request config: extract model, resolve overrides, build routing and body */\nfunction resolveRequest(\n rawBody: ArrayBuffer,\n config: ProxyConfig,\n method: string,\n path: string,\n): ResolvedRequest {\n const modelName = extractModel(rawBody)\n const resolved = resolveModelConfig(config, modelName)\n const providerRouting = buildProviderRouting(resolved.provider)\n const inject = shouldInject(method, path) && providerRouting !== undefined\n\n let body: ArrayBuffer | undefined\n try {\n body = readRequestBody(\n method,\n rawBody,\n inject,\n providerRouting as Record<string, unknown>,\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to process request body'\n logger.error(message)\n return {\n inject,\n body: undefined,\n modelName,\n headers: resolved.headers,\n error: new Response(\n JSON.stringify({ error: { message, type: 'proxy_request_error' } }),\n { status: 400, headers: { 'Content-Type': 'application/json' } },\n ),\n }\n }\n\n return { inject, body, modelName, headers: resolved.headers }\n}\n\n/** Execute upstream fetch, returning appropriate error responses on failure */\nasync function executeUpstream(\n upstreamUrl: string,\n method: string,\n headers: Record<string, string>,\n body: ArrayBuffer | undefined,\n signal: AbortSignal,\n path: string,\n startedAt: number,\n): Promise<Response> {\n let upstream: Response\n try {\n upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal)\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n logger.warn(`Aborted: ${method} ${path}`)\n return new Response(null, { status: 499 })\n }\n\n logger.error('Upstream fetch error:', err)\n return Response.json(\n {\n error: {\n message: 'Proxy failed to reach upstream',\n type: 'proxy_upstream_error',\n },\n },\n { status: 502 },\n )\n }\n\n logger.info(`${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`)\n return buildUpstreamResponse(upstream, method)\n}\n\nexport function createProxyServer(config: ProxyConfig, onReady?: () => void): ServerType {\n const app = new Hono<ProxyContext>()\n\n app.get('/health', c => {\n const globalRouting = buildProviderRouting(config.provider)\n return c.json({\n ok: true,\n upstream: config.openrouterBaseUrl,\n provider: globalRouting ?? 'not configured',\n modelOverrides: config.modelOverrides ? Object.keys(config.modelOverrides) : [],\n })\n })\n\n app.all('*', async c => {\n const method = c.req.method\n const path = new URL(c.req.url).pathname\n const upstreamUrl = buildUpstreamUrl(c.req.url, config)\n const startedAt = Date.now()\n\n const raw = await readRawBody(c.req.raw)\n if (!raw.ok) return raw.response\n\n const resolved = resolveRequest(raw.body, config, method, path)\n if (resolved.error) return resolved.error\n\n const headers = buildRequestHeaders(\n c.req.raw.headers,\n config,\n resolved.inject,\n resolved.headers,\n )\n\n const controller = new AbortController()\n c.req.raw.signal.addEventListener('abort', () => controller.abort())\n\n const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : ''\n logger.info(\n `${method} ${path} → ${upstreamUrl}${resolved.inject ? ' [inject]' : ''}${modelLog}`,\n )\n\n return executeUpstream(\n upstreamUrl,\n method,\n headers,\n resolved.body,\n controller.signal,\n path,\n startedAt,\n )\n })\n\n return serve(\n {\n fetch: app.fetch,\n port: config.port,\n hostname: config.host,\n },\n onReady,\n )\n}\n\n/** Shutdown deadline: force-close after this many ms */\nconst SHUTDOWN_TIMEOUT_MS = 10_000\n\n/** Start the proxy with graceful shutdown on SIGTERM/SIGINT */\nexport function startProxyServer(config: ProxyConfig, onReady?: () => void): ServerType {\n const server = createProxyServer(config, onReady)\n\n let shuttingDown = false\n\n function shutdown(signal: string) {\n if (shuttingDown) return\n shuttingDown = true\n\n logger.info(`${signal} received — draining active connections…`)\n\n const timer = setTimeout(() => {\n logger.warn('Forcing shutdown — drain timeout exceeded')\n process.exit(1)\n }, SHUTDOWN_TIMEOUT_MS)\n\n server.close(() => {\n clearTimeout(timer)\n logger.info('All connections drained — goodbye')\n process.exit(0)\n })\n }\n\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n process.on('SIGINT', () => shutdown('SIGINT'))\n\n return server\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "proxitor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Lightweight proxy for routing CLI requests (claude-code, codex) to OpenRouter",
|
|
6
|
+
"bin": "./dist/cli.mjs",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"default": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.mjs",
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"prepare": "lefthook install",
|
|
27
|
+
"build": "tsdown",
|
|
28
|
+
"dev": "tsdown --watch",
|
|
29
|
+
"lint": "biome lint ./src",
|
|
30
|
+
"lint:fix": "biome lint --fix ./src",
|
|
31
|
+
"format": "biome format --write ./src",
|
|
32
|
+
"format:check": "biome format ./src",
|
|
33
|
+
"check:biome": "biome check ./src",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:coverage": "vitest run --coverage",
|
|
38
|
+
"check": "npm run typecheck && npm run check:biome && npm run test",
|
|
39
|
+
"prepublishOnly": "npm run build",
|
|
40
|
+
"version-packages": "changeset version",
|
|
41
|
+
"release": "changeset publish"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"proxy",
|
|
45
|
+
"openrouter",
|
|
46
|
+
"cli",
|
|
47
|
+
"claude-code",
|
|
48
|
+
"codex",
|
|
49
|
+
"ai",
|
|
50
|
+
"llm"
|
|
51
|
+
],
|
|
52
|
+
"author": "neiromaster",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.0.0"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/neiromaster/proxitor.git"
|
|
60
|
+
},
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/neiromaster/proxitor/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/neiromaster/proxitor#readme",
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@hono/node-server": "^2.0.4",
|
|
67
|
+
"cac": "^7.0.0",
|
|
68
|
+
"conf": "^15.1.0",
|
|
69
|
+
"consola": "^3.4.2",
|
|
70
|
+
"dotenv": "^17.4.2",
|
|
71
|
+
"hono": "^4.12.23",
|
|
72
|
+
"js-yaml": "^4.2.0"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@biomejs/biome": "^2.4.16",
|
|
76
|
+
"@changesets/cli": "^2.31.0",
|
|
77
|
+
"@types/js-yaml": "^4.0.9",
|
|
78
|
+
"@types/node": "^25.9.1",
|
|
79
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
80
|
+
"lefthook": "^2.1.9",
|
|
81
|
+
"tsdown": "^0.22.1",
|
|
82
|
+
"typescript": "^6.0.3",
|
|
83
|
+
"vitest": "^4.1.8"
|
|
84
|
+
}
|
|
85
|
+
}
|