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