@relayplane/proxy 0.1.6 → 0.1.7
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/dist/cli.js +118 -20
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +118 -20
- package/dist/cli.mjs.map +1 -1
- package/dist/index.js +118 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/proxy.ts
|
|
2
2
|
import * as http from "http";
|
|
3
|
+
import * as url from "url";
|
|
3
4
|
|
|
4
5
|
// src/storage/store.ts
|
|
5
6
|
import Database from "better-sqlite3";
|
|
@@ -1570,6 +1571,11 @@ ${input.prompt}` : input.prompt;
|
|
|
1570
1571
|
};
|
|
1571
1572
|
|
|
1572
1573
|
// src/proxy.ts
|
|
1574
|
+
var VERSION = "0.1.7";
|
|
1575
|
+
var recentRuns = [];
|
|
1576
|
+
var MAX_RECENT_RUNS = 100;
|
|
1577
|
+
var modelCounts = {};
|
|
1578
|
+
var serverStartTime = 0;
|
|
1573
1579
|
var DEFAULT_ENDPOINTS = {
|
|
1574
1580
|
anthropic: {
|
|
1575
1581
|
baseUrl: "https://api.anthropic.com/v1",
|
|
@@ -1866,9 +1872,9 @@ function convertMessagesToGemini(messages) {
|
|
|
1866
1872
|
return { text: p.text };
|
|
1867
1873
|
}
|
|
1868
1874
|
if (p.type === "image_url" && p.image_url?.url) {
|
|
1869
|
-
const
|
|
1870
|
-
if (
|
|
1871
|
-
const match =
|
|
1875
|
+
const url2 = p.image_url.url;
|
|
1876
|
+
if (url2.startsWith("data:")) {
|
|
1877
|
+
const match = url2.match(/^data:([^;]+);base64,(.+)$/);
|
|
1872
1878
|
if (match) {
|
|
1873
1879
|
return {
|
|
1874
1880
|
inline_data: {
|
|
@@ -1878,7 +1884,7 @@ function convertMessagesToGemini(messages) {
|
|
|
1878
1884
|
};
|
|
1879
1885
|
}
|
|
1880
1886
|
}
|
|
1881
|
-
return { text: `[Image: ${
|
|
1887
|
+
return { text: `[Image: ${url2}]` };
|
|
1882
1888
|
}
|
|
1883
1889
|
return { text: "" };
|
|
1884
1890
|
});
|
|
@@ -2292,28 +2298,88 @@ async function startProxy(config = {}) {
|
|
|
2292
2298
|
};
|
|
2293
2299
|
const server = http.createServer(async (req, res) => {
|
|
2294
2300
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2295
|
-
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
2301
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2296
2302
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
2297
2303
|
if (req.method === "OPTIONS") {
|
|
2298
2304
|
res.writeHead(204);
|
|
2299
2305
|
res.end();
|
|
2300
2306
|
return;
|
|
2301
2307
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
})
|
|
2314
|
-
);
|
|
2315
|
-
return;
|
|
2308
|
+
const parsedUrl = url.parse(req.url || "", true);
|
|
2309
|
+
const pathname = parsedUrl.pathname || "";
|
|
2310
|
+
if (req.method === "GET" && pathname === "/health") {
|
|
2311
|
+
const uptimeMs = Date.now() - serverStartTime;
|
|
2312
|
+
const uptimeSecs = Math.floor(uptimeMs / 1e3);
|
|
2313
|
+
const hours = Math.floor(uptimeSecs / 3600);
|
|
2314
|
+
const mins = Math.floor(uptimeSecs % 3600 / 60);
|
|
2315
|
+
const secs = uptimeSecs % 60;
|
|
2316
|
+
const providers = {};
|
|
2317
|
+
for (const [name, config2] of Object.entries(DEFAULT_ENDPOINTS)) {
|
|
2318
|
+
providers[name] = !!process.env[config2.apiKeyEnv];
|
|
2316
2319
|
}
|
|
2320
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2321
|
+
res.end(JSON.stringify({
|
|
2322
|
+
status: "ok",
|
|
2323
|
+
version: VERSION,
|
|
2324
|
+
uptime: `${hours}h ${mins}m ${secs}s`,
|
|
2325
|
+
uptimeMs,
|
|
2326
|
+
providers,
|
|
2327
|
+
totalRuns: recentRuns.length > 0 ? Object.values(modelCounts).reduce((a, b) => a + b, 0) : 0
|
|
2328
|
+
}));
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (req.method === "GET" && pathname === "/stats") {
|
|
2332
|
+
const stats = relay.stats();
|
|
2333
|
+
const savings = relay.savingsReport(30);
|
|
2334
|
+
const totalRuns = Object.values(modelCounts).reduce((a, b) => a + b, 0);
|
|
2335
|
+
const modelDistribution = {};
|
|
2336
|
+
for (const [model, count] of Object.entries(modelCounts)) {
|
|
2337
|
+
modelDistribution[model] = {
|
|
2338
|
+
count,
|
|
2339
|
+
percentage: totalRuns > 0 ? (count / totalRuns * 100).toFixed(1) + "%" : "0%"
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2343
|
+
res.end(JSON.stringify({
|
|
2344
|
+
totalRuns,
|
|
2345
|
+
savings: {
|
|
2346
|
+
estimatedSavingsPercent: savings.savingsPercent.toFixed(1) + "%",
|
|
2347
|
+
actualCostUsd: savings.actualCost.toFixed(4),
|
|
2348
|
+
baselineCostUsd: savings.baselineCost.toFixed(4),
|
|
2349
|
+
savedUsd: savings.savings.toFixed(4)
|
|
2350
|
+
},
|
|
2351
|
+
modelDistribution,
|
|
2352
|
+
byTaskType: stats.byTaskType,
|
|
2353
|
+
period: stats.period
|
|
2354
|
+
}));
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
if (req.method === "GET" && pathname === "/runs") {
|
|
2358
|
+
const limitParam = parsedUrl.query["limit"];
|
|
2359
|
+
const parsedLimit = limitParam ? parseInt(String(limitParam), 10) : 20;
|
|
2360
|
+
const limit = Math.min(Number.isNaN(parsedLimit) ? 20 : parsedLimit, MAX_RECENT_RUNS);
|
|
2361
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2362
|
+
res.end(JSON.stringify({
|
|
2363
|
+
runs: recentRuns.slice(0, limit),
|
|
2364
|
+
total: recentRuns.length
|
|
2365
|
+
}));
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (req.method === "GET" && pathname.includes("/models")) {
|
|
2369
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2370
|
+
res.end(
|
|
2371
|
+
JSON.stringify({
|
|
2372
|
+
object: "list",
|
|
2373
|
+
data: [
|
|
2374
|
+
{ id: "relayplane:auto", object: "model", owned_by: "relayplane" },
|
|
2375
|
+
{ id: "relayplane:cost", object: "model", owned_by: "relayplane" },
|
|
2376
|
+
{ id: "relayplane:quality", object: "model", owned_by: "relayplane" }
|
|
2377
|
+
]
|
|
2378
|
+
})
|
|
2379
|
+
);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (req.method !== "POST" || !pathname.includes("/chat/completions")) {
|
|
2317
2383
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2318
2384
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
2319
2385
|
return;
|
|
@@ -2436,9 +2502,11 @@ async function startProxy(config = {}) {
|
|
|
2436
2502
|
return new Promise((resolve, reject) => {
|
|
2437
2503
|
server.on("error", reject);
|
|
2438
2504
|
server.listen(port, host, () => {
|
|
2505
|
+
serverStartTime = Date.now();
|
|
2439
2506
|
console.log(`RelayPlane proxy listening on http://${host}:${port}`);
|
|
2440
2507
|
console.log(` Models: relayplane:auto, relayplane:cost, relayplane:quality`);
|
|
2441
2508
|
console.log(` Endpoint: POST /v1/chat/completions`);
|
|
2509
|
+
console.log(` Stats: GET /stats, /runs, /health`);
|
|
2442
2510
|
console.log(` Streaming: \u2705 Enabled`);
|
|
2443
2511
|
resolve(server);
|
|
2444
2512
|
});
|
|
@@ -2501,11 +2569,26 @@ async function handleStreamingRequest(res, request, targetProvider, targetModel,
|
|
|
2501
2569
|
log(`Streaming error: ${err}`);
|
|
2502
2570
|
}
|
|
2503
2571
|
const durationMs = Date.now() - startTime;
|
|
2572
|
+
const modelKey = `${targetProvider}/${targetModel}`;
|
|
2573
|
+
modelCounts[modelKey] = (modelCounts[modelKey] || 0) + 1;
|
|
2504
2574
|
relay.run({
|
|
2505
2575
|
prompt: promptText.slice(0, 500),
|
|
2506
2576
|
taskType,
|
|
2507
2577
|
model: `${targetProvider}:${targetModel}`
|
|
2508
2578
|
}).then((runResult) => {
|
|
2579
|
+
recentRuns.unshift({
|
|
2580
|
+
runId: runResult.runId,
|
|
2581
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2582
|
+
model: modelKey,
|
|
2583
|
+
taskType,
|
|
2584
|
+
confidence,
|
|
2585
|
+
mode: routingMode,
|
|
2586
|
+
durationMs,
|
|
2587
|
+
promptPreview: promptText.slice(0, 100) + (promptText.length > 100 ? "..." : "")
|
|
2588
|
+
});
|
|
2589
|
+
if (recentRuns.length > MAX_RECENT_RUNS) {
|
|
2590
|
+
recentRuns.pop();
|
|
2591
|
+
}
|
|
2509
2592
|
log(`Completed streaming in ${durationMs}ms, runId: ${runResult.runId}`);
|
|
2510
2593
|
}).catch((err) => {
|
|
2511
2594
|
log(`Failed to record run: ${err}`);
|
|
@@ -2576,15 +2659,30 @@ async function handleNonStreamingRequest(res, request, targetProvider, targetMod
|
|
|
2576
2659
|
return;
|
|
2577
2660
|
}
|
|
2578
2661
|
const durationMs = Date.now() - startTime;
|
|
2662
|
+
const modelKey = `${targetProvider}/${targetModel}`;
|
|
2663
|
+
modelCounts[modelKey] = (modelCounts[modelKey] || 0) + 1;
|
|
2579
2664
|
try {
|
|
2580
2665
|
const runResult = await relay.run({
|
|
2581
2666
|
prompt: promptText.slice(0, 500),
|
|
2582
2667
|
taskType,
|
|
2583
2668
|
model: `${targetProvider}:${targetModel}`
|
|
2584
2669
|
});
|
|
2670
|
+
recentRuns.unshift({
|
|
2671
|
+
runId: runResult.runId,
|
|
2672
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2673
|
+
model: modelKey,
|
|
2674
|
+
taskType,
|
|
2675
|
+
confidence,
|
|
2676
|
+
mode: routingMode,
|
|
2677
|
+
durationMs,
|
|
2678
|
+
promptPreview: promptText.slice(0, 100) + (promptText.length > 100 ? "..." : "")
|
|
2679
|
+
});
|
|
2680
|
+
if (recentRuns.length > MAX_RECENT_RUNS) {
|
|
2681
|
+
recentRuns.pop();
|
|
2682
|
+
}
|
|
2585
2683
|
responseData["_relayplane"] = {
|
|
2586
2684
|
runId: runResult.runId,
|
|
2587
|
-
routedTo:
|
|
2685
|
+
routedTo: modelKey,
|
|
2588
2686
|
taskType,
|
|
2589
2687
|
confidence,
|
|
2590
2688
|
durationMs,
|