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