@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 CHANGED
@@ -25,6 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/proxy.ts
27
27
  var http = __toESM(require("http"));
28
+ var url = __toESM(require("url"));
28
29
 
29
30
  // src/storage/store.ts
30
31
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
@@ -1591,6 +1592,11 @@ ${input.prompt}` : input.prompt;
1591
1592
  };
1592
1593
 
1593
1594
  // src/proxy.ts
1595
+ var VERSION = "0.1.7";
1596
+ var recentRuns = [];
1597
+ var MAX_RECENT_RUNS = 100;
1598
+ var modelCounts = {};
1599
+ var serverStartTime = 0;
1594
1600
  var DEFAULT_ENDPOINTS = {
1595
1601
  anthropic: {
1596
1602
  baseUrl: "https://api.anthropic.com/v1",
@@ -1887,9 +1893,9 @@ function convertMessagesToGemini(messages) {
1887
1893
  return { text: p.text };
1888
1894
  }
1889
1895
  if (p.type === "image_url" && p.image_url?.url) {
1890
- const url = p.image_url.url;
1891
- if (url.startsWith("data:")) {
1892
- const match = url.match(/^data:([^;]+);base64,(.+)$/);
1896
+ const url2 = p.image_url.url;
1897
+ if (url2.startsWith("data:")) {
1898
+ const match = url2.match(/^data:([^;]+);base64,(.+)$/);
1893
1899
  if (match) {
1894
1900
  return {
1895
1901
  inline_data: {
@@ -1899,7 +1905,7 @@ function convertMessagesToGemini(messages) {
1899
1905
  };
1900
1906
  }
1901
1907
  }
1902
- return { text: `[Image: ${url}]` };
1908
+ return { text: `[Image: ${url2}]` };
1903
1909
  }
1904
1910
  return { text: "" };
1905
1911
  });
@@ -2313,28 +2319,88 @@ async function startProxy(config = {}) {
2313
2319
  };
2314
2320
  const server = http.createServer(async (req, res) => {
2315
2321
  res.setHeader("Access-Control-Allow-Origin", "*");
2316
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
2322
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
2317
2323
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
2318
2324
  if (req.method === "OPTIONS") {
2319
2325
  res.writeHead(204);
2320
2326
  res.end();
2321
2327
  return;
2322
2328
  }
2323
- if (req.method !== "POST" || !req.url?.includes("/chat/completions")) {
2324
- if (req.method === "GET" && req.url?.includes("/models")) {
2325
- res.writeHead(200, { "Content-Type": "application/json" });
2326
- res.end(
2327
- JSON.stringify({
2328
- object: "list",
2329
- data: [
2330
- { id: "relayplane:auto", object: "model", owned_by: "relayplane" },
2331
- { id: "relayplane:cost", object: "model", owned_by: "relayplane" },
2332
- { id: "relayplane:quality", object: "model", owned_by: "relayplane" }
2333
- ]
2334
- })
2335
- );
2336
- return;
2329
+ const parsedUrl = url.parse(req.url || "", true);
2330
+ const pathname = parsedUrl.pathname || "";
2331
+ if (req.method === "GET" && pathname === "/health") {
2332
+ const uptimeMs = Date.now() - serverStartTime;
2333
+ const uptimeSecs = Math.floor(uptimeMs / 1e3);
2334
+ const hours = Math.floor(uptimeSecs / 3600);
2335
+ const mins = Math.floor(uptimeSecs % 3600 / 60);
2336
+ const secs = uptimeSecs % 60;
2337
+ const providers = {};
2338
+ for (const [name, config2] of Object.entries(DEFAULT_ENDPOINTS)) {
2339
+ providers[name] = !!process.env[config2.apiKeyEnv];
2337
2340
  }
2341
+ res.writeHead(200, { "Content-Type": "application/json" });
2342
+ res.end(JSON.stringify({
2343
+ status: "ok",
2344
+ version: VERSION,
2345
+ uptime: `${hours}h ${mins}m ${secs}s`,
2346
+ uptimeMs,
2347
+ providers,
2348
+ totalRuns: recentRuns.length > 0 ? Object.values(modelCounts).reduce((a, b) => a + b, 0) : 0
2349
+ }));
2350
+ return;
2351
+ }
2352
+ if (req.method === "GET" && pathname === "/stats") {
2353
+ const stats = relay.stats();
2354
+ const savings = relay.savingsReport(30);
2355
+ const totalRuns = Object.values(modelCounts).reduce((a, b) => a + b, 0);
2356
+ const modelDistribution = {};
2357
+ for (const [model, count] of Object.entries(modelCounts)) {
2358
+ modelDistribution[model] = {
2359
+ count,
2360
+ percentage: totalRuns > 0 ? (count / totalRuns * 100).toFixed(1) + "%" : "0%"
2361
+ };
2362
+ }
2363
+ res.writeHead(200, { "Content-Type": "application/json" });
2364
+ res.end(JSON.stringify({
2365
+ totalRuns,
2366
+ savings: {
2367
+ estimatedSavingsPercent: savings.savingsPercent.toFixed(1) + "%",
2368
+ actualCostUsd: savings.actualCost.toFixed(4),
2369
+ baselineCostUsd: savings.baselineCost.toFixed(4),
2370
+ savedUsd: savings.savings.toFixed(4)
2371
+ },
2372
+ modelDistribution,
2373
+ byTaskType: stats.byTaskType,
2374
+ period: stats.period
2375
+ }));
2376
+ return;
2377
+ }
2378
+ if (req.method === "GET" && pathname === "/runs") {
2379
+ const limitParam = parsedUrl.query["limit"];
2380
+ const parsedLimit = limitParam ? parseInt(String(limitParam), 10) : 20;
2381
+ const limit = Math.min(Number.isNaN(parsedLimit) ? 20 : parsedLimit, MAX_RECENT_RUNS);
2382
+ res.writeHead(200, { "Content-Type": "application/json" });
2383
+ res.end(JSON.stringify({
2384
+ runs: recentRuns.slice(0, limit),
2385
+ total: recentRuns.length
2386
+ }));
2387
+ return;
2388
+ }
2389
+ if (req.method === "GET" && pathname.includes("/models")) {
2390
+ res.writeHead(200, { "Content-Type": "application/json" });
2391
+ res.end(
2392
+ JSON.stringify({
2393
+ object: "list",
2394
+ data: [
2395
+ { id: "relayplane:auto", object: "model", owned_by: "relayplane" },
2396
+ { id: "relayplane:cost", object: "model", owned_by: "relayplane" },
2397
+ { id: "relayplane:quality", object: "model", owned_by: "relayplane" }
2398
+ ]
2399
+ })
2400
+ );
2401
+ return;
2402
+ }
2403
+ if (req.method !== "POST" || !pathname.includes("/chat/completions")) {
2338
2404
  res.writeHead(404, { "Content-Type": "application/json" });
2339
2405
  res.end(JSON.stringify({ error: "Not found" }));
2340
2406
  return;
@@ -2457,9 +2523,11 @@ async function startProxy(config = {}) {
2457
2523
  return new Promise((resolve, reject) => {
2458
2524
  server.on("error", reject);
2459
2525
  server.listen(port, host, () => {
2526
+ serverStartTime = Date.now();
2460
2527
  console.log(`RelayPlane proxy listening on http://${host}:${port}`);
2461
2528
  console.log(` Models: relayplane:auto, relayplane:cost, relayplane:quality`);
2462
2529
  console.log(` Endpoint: POST /v1/chat/completions`);
2530
+ console.log(` Stats: GET /stats, /runs, /health`);
2463
2531
  console.log(` Streaming: \u2705 Enabled`);
2464
2532
  resolve(server);
2465
2533
  });
@@ -2522,11 +2590,26 @@ async function handleStreamingRequest(res, request, targetProvider, targetModel,
2522
2590
  log(`Streaming error: ${err}`);
2523
2591
  }
2524
2592
  const durationMs = Date.now() - startTime;
2593
+ const modelKey = `${targetProvider}/${targetModel}`;
2594
+ modelCounts[modelKey] = (modelCounts[modelKey] || 0) + 1;
2525
2595
  relay.run({
2526
2596
  prompt: promptText.slice(0, 500),
2527
2597
  taskType,
2528
2598
  model: `${targetProvider}:${targetModel}`
2529
2599
  }).then((runResult) => {
2600
+ recentRuns.unshift({
2601
+ runId: runResult.runId,
2602
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2603
+ model: modelKey,
2604
+ taskType,
2605
+ confidence,
2606
+ mode: routingMode,
2607
+ durationMs,
2608
+ promptPreview: promptText.slice(0, 100) + (promptText.length > 100 ? "..." : "")
2609
+ });
2610
+ if (recentRuns.length > MAX_RECENT_RUNS) {
2611
+ recentRuns.pop();
2612
+ }
2530
2613
  log(`Completed streaming in ${durationMs}ms, runId: ${runResult.runId}`);
2531
2614
  }).catch((err) => {
2532
2615
  log(`Failed to record run: ${err}`);
@@ -2597,15 +2680,30 @@ async function handleNonStreamingRequest(res, request, targetProvider, targetMod
2597
2680
  return;
2598
2681
  }
2599
2682
  const durationMs = Date.now() - startTime;
2683
+ const modelKey = `${targetProvider}/${targetModel}`;
2684
+ modelCounts[modelKey] = (modelCounts[modelKey] || 0) + 1;
2600
2685
  try {
2601
2686
  const runResult = await relay.run({
2602
2687
  prompt: promptText.slice(0, 500),
2603
2688
  taskType,
2604
2689
  model: `${targetProvider}:${targetModel}`
2605
2690
  });
2691
+ recentRuns.unshift({
2692
+ runId: runResult.runId,
2693
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2694
+ model: modelKey,
2695
+ taskType,
2696
+ confidence,
2697
+ mode: routingMode,
2698
+ durationMs,
2699
+ promptPreview: promptText.slice(0, 100) + (promptText.length > 100 ? "..." : "")
2700
+ });
2701
+ if (recentRuns.length > MAX_RECENT_RUNS) {
2702
+ recentRuns.pop();
2703
+ }
2606
2704
  responseData["_relayplane"] = {
2607
2705
  runId: runResult.runId,
2608
- routedTo: `${targetProvider}/${targetModel}`,
2706
+ routedTo: modelKey,
2609
2707
  taskType,
2610
2708
  confidence,
2611
2709
  durationMs,