@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.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 url = p.image_url.url;
1868
- if (url.startsWith("data:")) {
1869
- const match = url.match(/^data:([^;]+);base64,(.+)$/);
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: ${url}]` };
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
- if (req.method !== "POST" || !req.url?.includes("/chat/completions")) {
2301
- if (req.method === "GET" && req.url?.includes("/models")) {
2302
- res.writeHead(200, { "Content-Type": "application/json" });
2303
- res.end(
2304
- JSON.stringify({
2305
- object: "list",
2306
- data: [
2307
- { id: "relayplane:auto", object: "model", owned_by: "relayplane" },
2308
- { id: "relayplane:cost", object: "model", owned_by: "relayplane" },
2309
- { id: "relayplane:quality", object: "model", owned_by: "relayplane" }
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: `${targetProvider}/${targetModel}`,
2683
+ routedTo: modelKey,
2586
2684
  taskType,
2587
2685
  confidence,
2588
2686
  durationMs,