@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.js CHANGED
@@ -53,6 +53,7 @@ module.exports = __toCommonJS(index_exports);
53
53
 
54
54
  // src/proxy.ts
55
55
  var http = __toESM(require("http"));
56
+ var url = __toESM(require("url"));
56
57
 
57
58
  // src/storage/store.ts
58
59
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
@@ -1623,6 +1624,11 @@ ${input.prompt}` : input.prompt;
1623
1624
  };
1624
1625
 
1625
1626
  // src/proxy.ts
1627
+ var VERSION = "0.1.7";
1628
+ var recentRuns = [];
1629
+ var MAX_RECENT_RUNS = 100;
1630
+ var modelCounts = {};
1631
+ var serverStartTime = 0;
1626
1632
  var DEFAULT_ENDPOINTS = {
1627
1633
  anthropic: {
1628
1634
  baseUrl: "https://api.anthropic.com/v1",
@@ -1919,9 +1925,9 @@ function convertMessagesToGemini(messages) {
1919
1925
  return { text: p.text };
1920
1926
  }
1921
1927
  if (p.type === "image_url" && p.image_url?.url) {
1922
- const url = p.image_url.url;
1923
- if (url.startsWith("data:")) {
1924
- const match = url.match(/^data:([^;]+);base64,(.+)$/);
1928
+ const url2 = p.image_url.url;
1929
+ if (url2.startsWith("data:")) {
1930
+ const match = url2.match(/^data:([^;]+);base64,(.+)$/);
1925
1931
  if (match) {
1926
1932
  return {
1927
1933
  inline_data: {
@@ -1931,7 +1937,7 @@ function convertMessagesToGemini(messages) {
1931
1937
  };
1932
1938
  }
1933
1939
  }
1934
- return { text: `[Image: ${url}]` };
1940
+ return { text: `[Image: ${url2}]` };
1935
1941
  }
1936
1942
  return { text: "" };
1937
1943
  });
@@ -2345,28 +2351,88 @@ async function startProxy(config = {}) {
2345
2351
  };
2346
2352
  const server = http.createServer(async (req, res) => {
2347
2353
  res.setHeader("Access-Control-Allow-Origin", "*");
2348
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
2354
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
2349
2355
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
2350
2356
  if (req.method === "OPTIONS") {
2351
2357
  res.writeHead(204);
2352
2358
  res.end();
2353
2359
  return;
2354
2360
  }
2355
- if (req.method !== "POST" || !req.url?.includes("/chat/completions")) {
2356
- if (req.method === "GET" && req.url?.includes("/models")) {
2357
- res.writeHead(200, { "Content-Type": "application/json" });
2358
- res.end(
2359
- JSON.stringify({
2360
- object: "list",
2361
- data: [
2362
- { id: "relayplane:auto", object: "model", owned_by: "relayplane" },
2363
- { id: "relayplane:cost", object: "model", owned_by: "relayplane" },
2364
- { id: "relayplane:quality", object: "model", owned_by: "relayplane" }
2365
- ]
2366
- })
2367
- );
2368
- return;
2361
+ const parsedUrl = url.parse(req.url || "", true);
2362
+ const pathname = parsedUrl.pathname || "";
2363
+ if (req.method === "GET" && pathname === "/health") {
2364
+ const uptimeMs = Date.now() - serverStartTime;
2365
+ const uptimeSecs = Math.floor(uptimeMs / 1e3);
2366
+ const hours = Math.floor(uptimeSecs / 3600);
2367
+ const mins = Math.floor(uptimeSecs % 3600 / 60);
2368
+ const secs = uptimeSecs % 60;
2369
+ const providers = {};
2370
+ for (const [name, config2] of Object.entries(DEFAULT_ENDPOINTS)) {
2371
+ providers[name] = !!process.env[config2.apiKeyEnv];
2369
2372
  }
2373
+ res.writeHead(200, { "Content-Type": "application/json" });
2374
+ res.end(JSON.stringify({
2375
+ status: "ok",
2376
+ version: VERSION,
2377
+ uptime: `${hours}h ${mins}m ${secs}s`,
2378
+ uptimeMs,
2379
+ providers,
2380
+ totalRuns: recentRuns.length > 0 ? Object.values(modelCounts).reduce((a, b) => a + b, 0) : 0
2381
+ }));
2382
+ return;
2383
+ }
2384
+ if (req.method === "GET" && pathname === "/stats") {
2385
+ const stats = relay.stats();
2386
+ const savings = relay.savingsReport(30);
2387
+ const totalRuns = Object.values(modelCounts).reduce((a, b) => a + b, 0);
2388
+ const modelDistribution = {};
2389
+ for (const [model, count] of Object.entries(modelCounts)) {
2390
+ modelDistribution[model] = {
2391
+ count,
2392
+ percentage: totalRuns > 0 ? (count / totalRuns * 100).toFixed(1) + "%" : "0%"
2393
+ };
2394
+ }
2395
+ res.writeHead(200, { "Content-Type": "application/json" });
2396
+ res.end(JSON.stringify({
2397
+ totalRuns,
2398
+ savings: {
2399
+ estimatedSavingsPercent: savings.savingsPercent.toFixed(1) + "%",
2400
+ actualCostUsd: savings.actualCost.toFixed(4),
2401
+ baselineCostUsd: savings.baselineCost.toFixed(4),
2402
+ savedUsd: savings.savings.toFixed(4)
2403
+ },
2404
+ modelDistribution,
2405
+ byTaskType: stats.byTaskType,
2406
+ period: stats.period
2407
+ }));
2408
+ return;
2409
+ }
2410
+ if (req.method === "GET" && pathname === "/runs") {
2411
+ const limitParam = parsedUrl.query["limit"];
2412
+ const parsedLimit = limitParam ? parseInt(String(limitParam), 10) : 20;
2413
+ const limit = Math.min(Number.isNaN(parsedLimit) ? 20 : parsedLimit, MAX_RECENT_RUNS);
2414
+ res.writeHead(200, { "Content-Type": "application/json" });
2415
+ res.end(JSON.stringify({
2416
+ runs: recentRuns.slice(0, limit),
2417
+ total: recentRuns.length
2418
+ }));
2419
+ return;
2420
+ }
2421
+ if (req.method === "GET" && pathname.includes("/models")) {
2422
+ res.writeHead(200, { "Content-Type": "application/json" });
2423
+ res.end(
2424
+ JSON.stringify({
2425
+ object: "list",
2426
+ data: [
2427
+ { id: "relayplane:auto", object: "model", owned_by: "relayplane" },
2428
+ { id: "relayplane:cost", object: "model", owned_by: "relayplane" },
2429
+ { id: "relayplane:quality", object: "model", owned_by: "relayplane" }
2430
+ ]
2431
+ })
2432
+ );
2433
+ return;
2434
+ }
2435
+ if (req.method !== "POST" || !pathname.includes("/chat/completions")) {
2370
2436
  res.writeHead(404, { "Content-Type": "application/json" });
2371
2437
  res.end(JSON.stringify({ error: "Not found" }));
2372
2438
  return;
@@ -2489,9 +2555,11 @@ async function startProxy(config = {}) {
2489
2555
  return new Promise((resolve, reject) => {
2490
2556
  server.on("error", reject);
2491
2557
  server.listen(port, host, () => {
2558
+ serverStartTime = Date.now();
2492
2559
  console.log(`RelayPlane proxy listening on http://${host}:${port}`);
2493
2560
  console.log(` Models: relayplane:auto, relayplane:cost, relayplane:quality`);
2494
2561
  console.log(` Endpoint: POST /v1/chat/completions`);
2562
+ console.log(` Stats: GET /stats, /runs, /health`);
2495
2563
  console.log(` Streaming: \u2705 Enabled`);
2496
2564
  resolve(server);
2497
2565
  });
@@ -2554,11 +2622,26 @@ async function handleStreamingRequest(res, request, targetProvider, targetModel,
2554
2622
  log(`Streaming error: ${err}`);
2555
2623
  }
2556
2624
  const durationMs = Date.now() - startTime;
2625
+ const modelKey = `${targetProvider}/${targetModel}`;
2626
+ modelCounts[modelKey] = (modelCounts[modelKey] || 0) + 1;
2557
2627
  relay.run({
2558
2628
  prompt: promptText.slice(0, 500),
2559
2629
  taskType,
2560
2630
  model: `${targetProvider}:${targetModel}`
2561
2631
  }).then((runResult) => {
2632
+ recentRuns.unshift({
2633
+ runId: runResult.runId,
2634
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2635
+ model: modelKey,
2636
+ taskType,
2637
+ confidence,
2638
+ mode: routingMode,
2639
+ durationMs,
2640
+ promptPreview: promptText.slice(0, 100) + (promptText.length > 100 ? "..." : "")
2641
+ });
2642
+ if (recentRuns.length > MAX_RECENT_RUNS) {
2643
+ recentRuns.pop();
2644
+ }
2562
2645
  log(`Completed streaming in ${durationMs}ms, runId: ${runResult.runId}`);
2563
2646
  }).catch((err) => {
2564
2647
  log(`Failed to record run: ${err}`);
@@ -2629,15 +2712,30 @@ async function handleNonStreamingRequest(res, request, targetProvider, targetMod
2629
2712
  return;
2630
2713
  }
2631
2714
  const durationMs = Date.now() - startTime;
2715
+ const modelKey = `${targetProvider}/${targetModel}`;
2716
+ modelCounts[modelKey] = (modelCounts[modelKey] || 0) + 1;
2632
2717
  try {
2633
2718
  const runResult = await relay.run({
2634
2719
  prompt: promptText.slice(0, 500),
2635
2720
  taskType,
2636
2721
  model: `${targetProvider}:${targetModel}`
2637
2722
  });
2723
+ recentRuns.unshift({
2724
+ runId: runResult.runId,
2725
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2726
+ model: modelKey,
2727
+ taskType,
2728
+ confidence,
2729
+ mode: routingMode,
2730
+ durationMs,
2731
+ promptPreview: promptText.slice(0, 100) + (promptText.length > 100 ? "..." : "")
2732
+ });
2733
+ if (recentRuns.length > MAX_RECENT_RUNS) {
2734
+ recentRuns.pop();
2735
+ }
2638
2736
  responseData["_relayplane"] = {
2639
2737
  runId: runResult.runId,
2640
- routedTo: `${targetProvider}/${targetModel}`,
2738
+ routedTo: modelKey,
2641
2739
  taskType,
2642
2740
  confidence,
2643
2741
  durationMs,