proxitor 0.3.0 → 0.5.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/README.md +27 -8
- package/dist/add.mjs +26 -27
- package/dist/add.mjs.map +1 -1
- package/dist/browse.mjs +20 -21
- package/dist/browse.mjs.map +1 -1
- package/dist/cli.mjs +14774 -33
- package/dist/cli.mjs.map +1 -1
- package/dist/config.mjs +6 -5
- package/dist/config.mjs.map +1 -1
- package/dist/config2.mjs +5 -6
- package/dist/config2.mjs.map +1 -1
- package/dist/dist.mjs +1325 -0
- package/dist/dist.mjs.map +1 -0
- package/dist/dist2.mjs +6617 -0
- package/dist/dist2.mjs.map +1 -0
- package/dist/edit.mjs +17 -18
- package/dist/edit.mjs.map +1 -1
- package/dist/list.mjs +4 -4
- package/dist/list.mjs.map +1 -1
- package/dist/prompt.mjs +849 -0
- package/dist/prompt.mjs.map +1 -0
- package/dist/providers.mjs +16 -16
- package/dist/providers.mjs.map +1 -1
- package/dist/remove.mjs +10 -11
- package/dist/remove.mjs.map +1 -1
- package/dist/validate.mjs +9 -9
- package/dist/validate.mjs.map +1 -1
- package/dist/wizard.mjs +222 -0
- package/dist/wizard.mjs.map +1 -0
- package/package.json +1 -16
- package/dist/add.cjs +0 -139
- package/dist/add.cjs.map +0 -1
- package/dist/browse.cjs +0 -88
- package/dist/browse.cjs.map +0 -1
- package/dist/cli.cjs +0 -159
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.mts +0 -1
- package/dist/config.cjs +0 -68
- package/dist/config.cjs.map +0 -1
- package/dist/config2.cjs +0 -75
- package/dist/config2.cjs.map +0 -1
- package/dist/edit.cjs +0 -82
- package/dist/edit.cjs.map +0 -1
- package/dist/index.cjs +0 -12
- package/dist/index.d.cts +0 -261
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts +0 -261
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/list.cjs +0 -33
- package/dist/list.cjs.map +0 -1
- package/dist/providers.cjs +0 -376
- package/dist/providers.cjs.map +0 -1
- package/dist/proxy.cjs +0 -656
- package/dist/proxy.cjs.map +0 -1
- package/dist/proxy.mjs +0 -544
- package/dist/proxy.mjs.map +0 -1
- package/dist/remove.cjs +0 -38
- package/dist/remove.cjs.map +0 -1
- package/dist/validate.cjs +0 -26
- package/dist/validate.cjs.map +0 -1
package/dist/proxy.cjs
DELETED
|
@@ -1,656 +0,0 @@
|
|
|
1
|
-
//#region \0rolldown/runtime.js
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
-
key = keys[i];
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
-
get: ((k) => from[k]).bind(null, key),
|
|
13
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
-
value: mod,
|
|
20
|
-
enumerable: true
|
|
21
|
-
}) : target, mod));
|
|
22
|
-
//#endregion
|
|
23
|
-
let node_fs = require("node:fs");
|
|
24
|
-
let node_os = require("node:os");
|
|
25
|
-
let node_path = require("node:path");
|
|
26
|
-
let js_yaml = require("js-yaml");
|
|
27
|
-
js_yaml = __toESM(js_yaml, 1);
|
|
28
|
-
let zod = require("zod");
|
|
29
|
-
let consola = require("consola");
|
|
30
|
-
let _hono_node_server = require("@hono/node-server");
|
|
31
|
-
let hono = require("hono");
|
|
32
|
-
//#region src/config-schema.ts
|
|
33
|
-
/** Percentile cutoffs for performance thresholds */
|
|
34
|
-
const percentileCutoffsSchema = zod.z.object({
|
|
35
|
-
p50: zod.z.number().positive().optional(),
|
|
36
|
-
p75: zod.z.number().positive().optional(),
|
|
37
|
-
p90: zod.z.number().positive().optional(),
|
|
38
|
-
p99: zod.z.number().positive().optional()
|
|
39
|
-
}).strict();
|
|
40
|
-
/** Provider sorting options */
|
|
41
|
-
const providerSortSchema = zod.z.union([zod.z.enum([
|
|
42
|
-
"price",
|
|
43
|
-
"throughput",
|
|
44
|
-
"latency"
|
|
45
|
-
]), zod.z.object({
|
|
46
|
-
by: zod.z.enum([
|
|
47
|
-
"price",
|
|
48
|
-
"throughput",
|
|
49
|
-
"latency"
|
|
50
|
-
]),
|
|
51
|
-
partition: zod.z.enum(["model", "none"]).optional()
|
|
52
|
-
}).strict()]);
|
|
53
|
-
/** Maximum pricing for a request */
|
|
54
|
-
const maxPriceSchema = zod.z.object({
|
|
55
|
-
prompt: zod.z.number().nonnegative().optional(),
|
|
56
|
-
completion: zod.z.number().nonnegative().optional(),
|
|
57
|
-
request: zod.z.number().nonnegative().optional(),
|
|
58
|
-
image: zod.z.number().nonnegative().optional()
|
|
59
|
-
}).strict();
|
|
60
|
-
/** Provider routing configuration */
|
|
61
|
-
const providerConfigSchema = zod.z.object({
|
|
62
|
-
only: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
|
|
63
|
-
order: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
|
|
64
|
-
ignore: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]).optional(),
|
|
65
|
-
allowFallbacks: zod.z.boolean().optional(),
|
|
66
|
-
sort: providerSortSchema.optional(),
|
|
67
|
-
quantizations: zod.z.array(zod.z.string()).optional(),
|
|
68
|
-
maxPrice: maxPriceSchema.optional(),
|
|
69
|
-
requireParameters: zod.z.boolean().optional(),
|
|
70
|
-
dataCollection: zod.z.enum(["allow", "deny"]).optional(),
|
|
71
|
-
zdr: zod.z.boolean().optional(),
|
|
72
|
-
enforceDistillableText: zod.z.boolean().optional(),
|
|
73
|
-
preferredMinThroughput: zod.z.union([zod.z.number().positive(), percentileCutoffsSchema]).optional(),
|
|
74
|
-
preferredMaxLatency: zod.z.union([zod.z.number().positive(), percentileCutoffsSchema]).optional()
|
|
75
|
-
}).strict();
|
|
76
|
-
/** Per-model override: layers on top of global config */
|
|
77
|
-
const modelOverrideSchema = zod.z.object({
|
|
78
|
-
provider: providerConfigSchema.optional(),
|
|
79
|
-
headers: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
80
|
-
}).strict();
|
|
81
|
-
/** Schema for validating raw file content — all top-level keys optional */
|
|
82
|
-
const proxyConfigFileSchema = zod.z.object({
|
|
83
|
-
host: zod.z.string().min(1),
|
|
84
|
-
port: zod.z.number().int().min(1).max(65535),
|
|
85
|
-
openrouterKey: zod.z.string(),
|
|
86
|
-
openrouterBaseUrl: zod.z.string().url(),
|
|
87
|
-
verbose: zod.z.boolean(),
|
|
88
|
-
bodyLimit: zod.z.string().min(1),
|
|
89
|
-
provider: providerConfigSchema.optional(),
|
|
90
|
-
attributionReferer: zod.z.string().min(1),
|
|
91
|
-
attributionTitle: zod.z.string().min(1),
|
|
92
|
-
headers: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
93
|
-
modelOverrides: zod.z.record(zod.z.string().min(1), modelOverrideSchema).optional()
|
|
94
|
-
}).strict().partial();
|
|
95
|
-
/** Wraps YAML/JSON parse errors with the config file path */
|
|
96
|
-
var ConfigParseError = class extends Error {
|
|
97
|
-
constructor(filePath, cause) {
|
|
98
|
-
super(`Failed to parse config file ${filePath}: ${cause?.message ?? "unknown error"}`, { cause });
|
|
99
|
-
this.name = "ConfigParseError";
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
/** Formats zod validation issues into a readable multi-line message */
|
|
103
|
-
var ConfigValidationError = class extends Error {
|
|
104
|
-
constructor(filePath, zodError) {
|
|
105
|
-
const lines = zodError.issues.map((issue) => {
|
|
106
|
-
return ` ${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`;
|
|
107
|
-
});
|
|
108
|
-
super(`Invalid config in ${filePath}:\n${lines.join("\n")}`);
|
|
109
|
-
this.name = "ConfigValidationError";
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/utils.ts
|
|
114
|
-
/** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */
|
|
115
|
-
function toArray(value) {
|
|
116
|
-
if (value === void 0) return void 0;
|
|
117
|
-
const arr = Array.isArray(value) ? [...value] : [value];
|
|
118
|
-
return arr.length > 0 ? arr : void 0;
|
|
119
|
-
}
|
|
120
|
-
/** Try to parse an ArrayBuffer as JSON. Returns undefined on failure or empty body. */
|
|
121
|
-
function tryParseBody(raw) {
|
|
122
|
-
if (raw.byteLength === 0) return void 0;
|
|
123
|
-
try {
|
|
124
|
-
return JSON.parse(new TextDecoder().decode(raw));
|
|
125
|
-
} catch {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
//#endregion
|
|
130
|
-
//#region src/config.ts
|
|
131
|
-
const DEFAULT_CONFIG = {
|
|
132
|
-
host: "0.0.0.0",
|
|
133
|
-
port: 8080,
|
|
134
|
-
openrouterKey: "",
|
|
135
|
-
openrouterBaseUrl: "https://openrouter.ai/api/v1",
|
|
136
|
-
verbose: false,
|
|
137
|
-
bodyLimit: "50mb",
|
|
138
|
-
attributionReferer: "http://localhost",
|
|
139
|
-
attributionTitle: "proxitor"
|
|
140
|
-
};
|
|
141
|
-
/** Fields that need toArray normalization (string | string[] → string[] | undefined) */
|
|
142
|
-
const ARRAY_FIELDS = [
|
|
143
|
-
{
|
|
144
|
-
key: "only",
|
|
145
|
-
apiName: "only"
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
key: "order",
|
|
149
|
-
apiName: "order"
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
key: "ignore",
|
|
153
|
-
apiName: "ignore"
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
key: "quantizations",
|
|
157
|
-
apiName: "quantizations"
|
|
158
|
-
}
|
|
159
|
-
];
|
|
160
|
-
/** Direct camelCase → snake_case field mappings */
|
|
161
|
-
const DIRECT_FIELDS = [
|
|
162
|
-
{
|
|
163
|
-
key: "sort",
|
|
164
|
-
apiName: "sort"
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
key: "maxPrice",
|
|
168
|
-
apiName: "max_price"
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
key: "requireParameters",
|
|
172
|
-
apiName: "require_parameters"
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
key: "dataCollection",
|
|
176
|
-
apiName: "data_collection"
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
key: "zdr",
|
|
180
|
-
apiName: "zdr"
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
key: "enforceDistillableText",
|
|
184
|
-
apiName: "enforce_distillable_text"
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
key: "preferredMinThroughput",
|
|
188
|
-
apiName: "preferred_min_throughput"
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
key: "preferredMaxLatency",
|
|
192
|
-
apiName: "preferred_max_latency"
|
|
193
|
-
}
|
|
194
|
-
];
|
|
195
|
-
/** Build the provider routing object for OpenRouter request body injection */
|
|
196
|
-
function buildProviderRouting(provider) {
|
|
197
|
-
if (!provider) return void 0;
|
|
198
|
-
const result = {};
|
|
199
|
-
for (const { key, apiName } of ARRAY_FIELDS) {
|
|
200
|
-
const value = provider[key];
|
|
201
|
-
if (value !== void 0) {
|
|
202
|
-
const normalized = toArray(value);
|
|
203
|
-
if (normalized) result[apiName] = normalized;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
for (const { key, apiName } of DIRECT_FIELDS) {
|
|
207
|
-
const value = provider[key];
|
|
208
|
-
if (value !== void 0) result[apiName] = value;
|
|
209
|
-
}
|
|
210
|
-
if (result.order) result.allow_fallbacks = provider.allowFallbacks ?? true;
|
|
211
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
212
|
-
}
|
|
213
|
-
/** Score a pattern against a model name. Higher = better match. -1 = no match. */
|
|
214
|
-
function matchScore(pattern, modelName) {
|
|
215
|
-
if (pattern === modelName) return modelName.length + 1e3;
|
|
216
|
-
if (pattern.endsWith("*") && modelName.startsWith(pattern.slice(0, -1))) return pattern.length;
|
|
217
|
-
return -1;
|
|
218
|
-
}
|
|
219
|
-
/** Resolve the effective config for a given model by merging global defaults with the best-matching override */
|
|
220
|
-
function resolveModelConfig(config, modelName) {
|
|
221
|
-
const result = {
|
|
222
|
-
provider: config.provider,
|
|
223
|
-
headers: config.headers ? { ...config.headers } : void 0
|
|
224
|
-
};
|
|
225
|
-
if (!modelName || !config.modelOverrides) return result;
|
|
226
|
-
let bestPattern = null;
|
|
227
|
-
let bestScore = -1;
|
|
228
|
-
for (const pattern of Object.keys(config.modelOverrides)) {
|
|
229
|
-
const score = matchScore(pattern, modelName);
|
|
230
|
-
if (score > bestScore) {
|
|
231
|
-
bestScore = score;
|
|
232
|
-
bestPattern = pattern;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (bestPattern) {
|
|
236
|
-
const override = config.modelOverrides[bestPattern];
|
|
237
|
-
if (override?.provider !== void 0) result.provider = override.provider;
|
|
238
|
-
if (override?.headers) result.headers = {
|
|
239
|
-
...result.headers ?? {},
|
|
240
|
-
...override.headers
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
245
|
-
async function loadConfig(options) {
|
|
246
|
-
const config = { ...DEFAULT_CONFIG };
|
|
247
|
-
if (!options.noConfig) {
|
|
248
|
-
const configPath = findConfigFile(options.configPath);
|
|
249
|
-
if (configPath) {
|
|
250
|
-
const fileConfig = readConfigFile(configPath);
|
|
251
|
-
Object.assign(config, fileConfig);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (options.host) config.host = options.host;
|
|
255
|
-
if (options.port) config.port = options.port;
|
|
256
|
-
if (options.verbose) config.verbose = options.verbose;
|
|
257
|
-
if (options.openrouterKey) config.openrouterKey = options.openrouterKey;
|
|
258
|
-
else if (!config.openrouterKey) config.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
|
|
259
|
-
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.");
|
|
260
|
-
return config;
|
|
261
|
-
}
|
|
262
|
-
/** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */
|
|
263
|
-
function getXdgConfigDir() {
|
|
264
|
-
const xdgHome = process.env.XDG_CONFIG_HOME;
|
|
265
|
-
return xdgHome ? (0, node_path.resolve)(xdgHome, "proxitor") : (0, node_path.join)((0, node_os.homedir)(), ".config", "proxitor");
|
|
266
|
-
}
|
|
267
|
-
function findConfigFile(explicitPath) {
|
|
268
|
-
if (explicitPath) {
|
|
269
|
-
if (!(0, node_fs.existsSync)(explicitPath)) throw new Error(`Config file not found: ${explicitPath}`);
|
|
270
|
-
return (0, node_path.resolve)(explicitPath);
|
|
271
|
-
}
|
|
272
|
-
for (const candidate of [
|
|
273
|
-
"proxitor.config.yaml",
|
|
274
|
-
"proxitor.config.yml",
|
|
275
|
-
"proxitor.config.json",
|
|
276
|
-
".proxitor.yaml",
|
|
277
|
-
".proxitor.yml",
|
|
278
|
-
".proxitor.json"
|
|
279
|
-
]) {
|
|
280
|
-
const fullPath = (0, node_path.resolve)(candidate);
|
|
281
|
-
if ((0, node_fs.existsSync)(fullPath)) return fullPath;
|
|
282
|
-
}
|
|
283
|
-
const xdgDir = getXdgConfigDir();
|
|
284
|
-
for (const candidate of [
|
|
285
|
-
"config.yaml",
|
|
286
|
-
"config.yml",
|
|
287
|
-
"config.json"
|
|
288
|
-
]) {
|
|
289
|
-
const fullPath = (0, node_path.join)(xdgDir, candidate);
|
|
290
|
-
if ((0, node_fs.existsSync)(fullPath)) return fullPath;
|
|
291
|
-
}
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
function readConfigFile(filePath) {
|
|
295
|
-
const content = (0, node_fs.readFileSync)(filePath, "utf-8");
|
|
296
|
-
let raw;
|
|
297
|
-
try {
|
|
298
|
-
raw = filePath.endsWith(".json") ? JSON.parse(content) : js_yaml.load(content);
|
|
299
|
-
} catch (err) {
|
|
300
|
-
throw new ConfigParseError(filePath, err instanceof Error ? err : void 0);
|
|
301
|
-
}
|
|
302
|
-
const result = proxyConfigFileSchema.safeParse(raw);
|
|
303
|
-
if (!result.success) throw new ConfigValidationError(filePath, result.error);
|
|
304
|
-
return result.data;
|
|
305
|
-
}
|
|
306
|
-
//#endregion
|
|
307
|
-
//#region src/logger.ts
|
|
308
|
-
const logger = consola.consola.withTag("proxitor");
|
|
309
|
-
//#endregion
|
|
310
|
-
//#region src/proxy/headers.ts
|
|
311
|
-
const HOP_BY_HOP = new Set([
|
|
312
|
-
"connection",
|
|
313
|
-
"keep-alive",
|
|
314
|
-
"proxy-authenticate",
|
|
315
|
-
"proxy-authorization",
|
|
316
|
-
"te",
|
|
317
|
-
"trailer",
|
|
318
|
-
"transfer-encoding",
|
|
319
|
-
"upgrade"
|
|
320
|
-
]);
|
|
321
|
-
/** Headers to strip from client request before forwarding */
|
|
322
|
-
const STRIP_REQUEST = new Set([
|
|
323
|
-
"authorization",
|
|
324
|
-
"x-api-key",
|
|
325
|
-
"host",
|
|
326
|
-
"content-length"
|
|
327
|
-
]);
|
|
328
|
-
/** Headers to strip from upstream response before forwarding */
|
|
329
|
-
const STRIP_RESPONSE = new Set(["content-length", "content-encoding"]);
|
|
330
|
-
/** Filter headers by removing hop-by-hop and an additional blocklist */
|
|
331
|
-
function filterHeaders(incoming, blocklist) {
|
|
332
|
-
const headers = {};
|
|
333
|
-
for (const [key, value] of incoming.entries()) {
|
|
334
|
-
const lower = key.toLowerCase();
|
|
335
|
-
if (HOP_BY_HOP.has(lower)) continue;
|
|
336
|
-
if (blocklist.has(lower)) continue;
|
|
337
|
-
headers[key] = value;
|
|
338
|
-
}
|
|
339
|
-
return headers;
|
|
340
|
-
}
|
|
341
|
-
/** Build request headers for upstream fetch */
|
|
342
|
-
function buildRequestHeaders(incoming, config, inject, extraHeaders) {
|
|
343
|
-
const headers = filterHeaders(incoming, STRIP_REQUEST);
|
|
344
|
-
headers.Authorization = `Bearer ${config.openrouterKey}`;
|
|
345
|
-
headers["HTTP-Referer"] = config.attributionReferer;
|
|
346
|
-
headers["X-Title"] = config.attributionTitle;
|
|
347
|
-
headers["Accept-Encoding"] = "identity";
|
|
348
|
-
if (extraHeaders) Object.assign(headers, extraHeaders);
|
|
349
|
-
if (inject) headers["Content-Type"] = "application/json";
|
|
350
|
-
return headers;
|
|
351
|
-
}
|
|
352
|
-
/** Filter response headers and add SSE-friendly defaults */
|
|
353
|
-
function buildResponseHeaders(from) {
|
|
354
|
-
const headers = filterHeaders(from, STRIP_RESPONSE);
|
|
355
|
-
headers["Cache-Control"] = "no-cache";
|
|
356
|
-
headers["X-Accel-Buffering"] = "no";
|
|
357
|
-
return headers;
|
|
358
|
-
}
|
|
359
|
-
//#endregion
|
|
360
|
-
//#region src/proxy/inject.ts
|
|
361
|
-
/** Extract the model name from a raw request body. Returns undefined if not parseable or absent. */
|
|
362
|
-
function extractModel(rawBody) {
|
|
363
|
-
const json = tryParseBody(rawBody);
|
|
364
|
-
return typeof json?.model === "string" ? json.model : void 0;
|
|
365
|
-
}
|
|
366
|
-
/** Inject provider routing into request body, always overwriting existing value */
|
|
367
|
-
function injectProvider(rawBody, providerRouting) {
|
|
368
|
-
if (rawBody.byteLength === 0) throw new Error("Request body is empty; cannot inject provider");
|
|
369
|
-
let json;
|
|
370
|
-
try {
|
|
371
|
-
json = JSON.parse(new TextDecoder().decode(rawBody));
|
|
372
|
-
} catch (parseError) {
|
|
373
|
-
throw new Error("Request body is not valid JSON; cannot inject provider", { cause: parseError });
|
|
374
|
-
}
|
|
375
|
-
const modified = {
|
|
376
|
-
...json,
|
|
377
|
-
provider: providerRouting
|
|
378
|
-
};
|
|
379
|
-
return new TextEncoder().encode(JSON.stringify(modified)).buffer;
|
|
380
|
-
}
|
|
381
|
-
//#endregion
|
|
382
|
-
//#region src/proxy/paths.ts
|
|
383
|
-
/**
|
|
384
|
-
* Paths where provider routing is injected into the request body.
|
|
385
|
-
* All three are OpenRouter-supported endpoints:
|
|
386
|
-
* /v1/chat/completions — OpenAI Chat Completions
|
|
387
|
-
* /v1/responses — OpenAI Responses API
|
|
388
|
-
* /v1/messages — Anthropic Messages API
|
|
389
|
-
*/
|
|
390
|
-
const INJECT_PATHS = new Set([
|
|
391
|
-
"/v1/chat/completions",
|
|
392
|
-
"/v1/responses",
|
|
393
|
-
"/v1/messages"
|
|
394
|
-
]);
|
|
395
|
-
/** Check if this request should have provider routing injected */
|
|
396
|
-
function shouldInject(method, path) {
|
|
397
|
-
return method === "POST" && INJECT_PATHS.has(path);
|
|
398
|
-
}
|
|
399
|
-
/** Strip /v1 prefix from path: /v1/chat/completions → /chat/completions */
|
|
400
|
-
function toUpstreamPath(pathname) {
|
|
401
|
-
if (pathname.startsWith("/v1")) return pathname.slice(3);
|
|
402
|
-
return pathname;
|
|
403
|
-
}
|
|
404
|
-
/** Build full upstream URL from request and config */
|
|
405
|
-
function buildUpstreamUrl(requestUrl, config) {
|
|
406
|
-
const { pathname } = new URL(requestUrl);
|
|
407
|
-
return `${config.openrouterBaseUrl}${toUpstreamPath(pathname)}`;
|
|
408
|
-
}
|
|
409
|
-
//#endregion
|
|
410
|
-
//#region src/proxy.ts
|
|
411
|
-
function readRequestBody(method, raw, inject, providerRouting) {
|
|
412
|
-
if (["GET", "HEAD"].includes(method)) return void 0;
|
|
413
|
-
if (inject) return injectProvider(raw, providerRouting);
|
|
414
|
-
return raw.byteLength > 0 ? raw : void 0;
|
|
415
|
-
}
|
|
416
|
-
async function fetchUpstream(url, method, headers, body, signal) {
|
|
417
|
-
return fetch(url, {
|
|
418
|
-
method,
|
|
419
|
-
headers,
|
|
420
|
-
body,
|
|
421
|
-
signal,
|
|
422
|
-
duplex: body ? "half" : void 0
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
function buildUpstreamResponse(upstream, method) {
|
|
426
|
-
const headers = buildResponseHeaders(upstream.headers);
|
|
427
|
-
if (method === "HEAD" || !upstream.body) return new Response(null, {
|
|
428
|
-
status: upstream.status,
|
|
429
|
-
headers
|
|
430
|
-
});
|
|
431
|
-
return new Response(upstream.body, {
|
|
432
|
-
status: upstream.status,
|
|
433
|
-
headers
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
/** Read and process the request body, returning an error response on failure */
|
|
437
|
-
async function readRawBody(request) {
|
|
438
|
-
try {
|
|
439
|
-
return {
|
|
440
|
-
ok: true,
|
|
441
|
-
body: await request.arrayBuffer()
|
|
442
|
-
};
|
|
443
|
-
} catch (err) {
|
|
444
|
-
const message = err instanceof Error ? err.message : "Failed to read request body";
|
|
445
|
-
logger.error(message);
|
|
446
|
-
return {
|
|
447
|
-
ok: false,
|
|
448
|
-
response: Response.json({ error: {
|
|
449
|
-
message,
|
|
450
|
-
type: "proxy_request_error"
|
|
451
|
-
} }, { status: 400 })
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
/** Resolve per-request config: extract model, resolve overrides, build routing and body */
|
|
456
|
-
function resolveRequest(rawBody, config, method, path) {
|
|
457
|
-
const modelName = extractModel(rawBody);
|
|
458
|
-
const resolved = resolveModelConfig(config, modelName);
|
|
459
|
-
const providerRouting = buildProviderRouting(resolved.provider);
|
|
460
|
-
const inject = shouldInject(method, path) && providerRouting !== void 0;
|
|
461
|
-
let body;
|
|
462
|
-
try {
|
|
463
|
-
body = readRequestBody(method, rawBody, inject, providerRouting);
|
|
464
|
-
} catch (err) {
|
|
465
|
-
const message = err instanceof Error ? err.message : "Failed to process request body";
|
|
466
|
-
logger.error(message);
|
|
467
|
-
return {
|
|
468
|
-
inject,
|
|
469
|
-
body: void 0,
|
|
470
|
-
modelName,
|
|
471
|
-
headers: resolved.headers,
|
|
472
|
-
error: new Response(JSON.stringify({ error: {
|
|
473
|
-
message,
|
|
474
|
-
type: "proxy_request_error"
|
|
475
|
-
} }), {
|
|
476
|
-
status: 400,
|
|
477
|
-
headers: { "Content-Type": "application/json" }
|
|
478
|
-
})
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
inject,
|
|
483
|
-
body,
|
|
484
|
-
modelName,
|
|
485
|
-
headers: resolved.headers
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
/** Execute upstream fetch, returning appropriate error responses on failure */
|
|
489
|
-
async function executeUpstream(upstreamUrl, method, headers, body, signal, path, startedAt) {
|
|
490
|
-
let upstream;
|
|
491
|
-
try {
|
|
492
|
-
upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal);
|
|
493
|
-
} catch (err) {
|
|
494
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
495
|
-
logger.warn(`Aborted: ${method} ${path}`);
|
|
496
|
-
return new Response(null, { status: 499 });
|
|
497
|
-
}
|
|
498
|
-
logger.error("Upstream fetch error:", err);
|
|
499
|
-
return Response.json({ error: {
|
|
500
|
-
message: "Proxy failed to reach upstream",
|
|
501
|
-
type: "proxy_upstream_error"
|
|
502
|
-
} }, { status: 502 });
|
|
503
|
-
}
|
|
504
|
-
logger.info(`${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`);
|
|
505
|
-
return buildUpstreamResponse(upstream, method);
|
|
506
|
-
}
|
|
507
|
-
function createProxyServer(config, onReady) {
|
|
508
|
-
const app = new hono.Hono();
|
|
509
|
-
app.get("/health", (c) => {
|
|
510
|
-
const globalRouting = buildProviderRouting(config.provider);
|
|
511
|
-
return c.json({
|
|
512
|
-
ok: true,
|
|
513
|
-
upstream: config.openrouterBaseUrl,
|
|
514
|
-
provider: globalRouting ?? "not configured",
|
|
515
|
-
modelOverrides: config.modelOverrides ? Object.keys(config.modelOverrides) : []
|
|
516
|
-
});
|
|
517
|
-
});
|
|
518
|
-
app.all("*", async (c) => {
|
|
519
|
-
const method = c.req.method;
|
|
520
|
-
const path = new URL(c.req.url).pathname;
|
|
521
|
-
const upstreamUrl = buildUpstreamUrl(c.req.url, config);
|
|
522
|
-
const startedAt = Date.now();
|
|
523
|
-
const raw = await readRawBody(c.req.raw);
|
|
524
|
-
if (!raw.ok) return raw.response;
|
|
525
|
-
const resolved = resolveRequest(raw.body, config, method, path);
|
|
526
|
-
if (resolved.error) return resolved.error;
|
|
527
|
-
const headers = buildRequestHeaders(c.req.raw.headers, config, resolved.inject, resolved.headers);
|
|
528
|
-
const controller = new AbortController();
|
|
529
|
-
c.req.raw.signal.addEventListener("abort", () => controller.abort());
|
|
530
|
-
const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : "";
|
|
531
|
-
logger.info(`${method} ${path} → ${upstreamUrl}${resolved.inject ? " [inject]" : ""}${modelLog}`);
|
|
532
|
-
return executeUpstream(upstreamUrl, method, headers, resolved.body, controller.signal, path, startedAt);
|
|
533
|
-
});
|
|
534
|
-
return (0, _hono_node_server.serve)({
|
|
535
|
-
fetch: app.fetch,
|
|
536
|
-
port: config.port,
|
|
537
|
-
hostname: config.host
|
|
538
|
-
}, onReady);
|
|
539
|
-
}
|
|
540
|
-
/** Shutdown deadline: force-close after this many ms */
|
|
541
|
-
const SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
542
|
-
/** Start the proxy with graceful shutdown on SIGTERM/SIGINT */
|
|
543
|
-
function startProxyServer(config, onReady) {
|
|
544
|
-
const server = createProxyServer(config, onReady);
|
|
545
|
-
let shuttingDown = false;
|
|
546
|
-
function shutdown(signal) {
|
|
547
|
-
if (shuttingDown) return;
|
|
548
|
-
shuttingDown = true;
|
|
549
|
-
logger.info(`${signal} received — draining active connections…`);
|
|
550
|
-
const timer = setTimeout(() => {
|
|
551
|
-
logger.warn("Forcing shutdown — drain timeout exceeded");
|
|
552
|
-
process.exit(1);
|
|
553
|
-
}, SHUTDOWN_TIMEOUT_MS);
|
|
554
|
-
server.close(() => {
|
|
555
|
-
clearTimeout(timer);
|
|
556
|
-
logger.info("All connections drained — goodbye");
|
|
557
|
-
process.exit(0);
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
561
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
562
|
-
return server;
|
|
563
|
-
}
|
|
564
|
-
//#endregion
|
|
565
|
-
Object.defineProperty(exports, "ConfigParseError", {
|
|
566
|
-
enumerable: true,
|
|
567
|
-
get: function() {
|
|
568
|
-
return ConfigParseError;
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
Object.defineProperty(exports, "ConfigValidationError", {
|
|
572
|
-
enumerable: true,
|
|
573
|
-
get: function() {
|
|
574
|
-
return ConfigValidationError;
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
Object.defineProperty(exports, "__toESM", {
|
|
578
|
-
enumerable: true,
|
|
579
|
-
get: function() {
|
|
580
|
-
return __toESM;
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
Object.defineProperty(exports, "buildProviderRouting", {
|
|
584
|
-
enumerable: true,
|
|
585
|
-
get: function() {
|
|
586
|
-
return buildProviderRouting;
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
Object.defineProperty(exports, "createProxyServer", {
|
|
590
|
-
enumerable: true,
|
|
591
|
-
get: function() {
|
|
592
|
-
return createProxyServer;
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
Object.defineProperty(exports, "extractModel", {
|
|
596
|
-
enumerable: true,
|
|
597
|
-
get: function() {
|
|
598
|
-
return extractModel;
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
Object.defineProperty(exports, "findConfigFile", {
|
|
602
|
-
enumerable: true,
|
|
603
|
-
get: function() {
|
|
604
|
-
return findConfigFile;
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
Object.defineProperty(exports, "loadConfig", {
|
|
608
|
-
enumerable: true,
|
|
609
|
-
get: function() {
|
|
610
|
-
return loadConfig;
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
Object.defineProperty(exports, "logger", {
|
|
614
|
-
enumerable: true,
|
|
615
|
-
get: function() {
|
|
616
|
-
return logger;
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
Object.defineProperty(exports, "matchScore", {
|
|
620
|
-
enumerable: true,
|
|
621
|
-
get: function() {
|
|
622
|
-
return matchScore;
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
Object.defineProperty(exports, "readConfigFile", {
|
|
626
|
-
enumerable: true,
|
|
627
|
-
get: function() {
|
|
628
|
-
return readConfigFile;
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
Object.defineProperty(exports, "resolveModelConfig", {
|
|
632
|
-
enumerable: true,
|
|
633
|
-
get: function() {
|
|
634
|
-
return resolveModelConfig;
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
Object.defineProperty(exports, "startProxyServer", {
|
|
638
|
-
enumerable: true,
|
|
639
|
-
get: function() {
|
|
640
|
-
return startProxyServer;
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
Object.defineProperty(exports, "toArray", {
|
|
644
|
-
enumerable: true,
|
|
645
|
-
get: function() {
|
|
646
|
-
return toArray;
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
Object.defineProperty(exports, "tryParseBody", {
|
|
650
|
-
enumerable: true,
|
|
651
|
-
get: function() {
|
|
652
|
-
return tryParseBody;
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
//# sourceMappingURL=proxy.cjs.map
|