openclaw-autoproxy 1.0.3 → 1.0.6

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.
@@ -1,5 +1,6 @@
1
1
  import { createServer } from "node:http";
2
2
  import { config } from "./config.js";
3
+ import { DEFAULT_MODEL_HEALTH_WINDOW_MS, getModelHealthWindow, } from "./model-load-metrics.js";
3
4
  import { proxyRequest } from "./proxy.js";
4
5
  function sendJson(response, statusCode, payload) {
5
6
  if (response.writableEnded) {
@@ -11,33 +12,109 @@ function sendJson(response, statusCode, payload) {
11
12
  response.setHeader("content-length", Buffer.byteLength(body));
12
13
  response.end(body);
13
14
  }
14
- function resolvePathname(request) {
15
+ function sendText(response, statusCode, body) {
16
+ if (response.writableEnded) {
17
+ return;
18
+ }
19
+ response.statusCode = statusCode;
20
+ response.setHeader("content-type", "text/plain; charset=utf-8");
21
+ response.setHeader("content-length", Buffer.byteLength(body));
22
+ response.end(body);
23
+ }
24
+ function resolveRequestUrl(request) {
15
25
  const rawUrl = request.url ?? "/";
16
26
  try {
17
- return new URL(rawUrl, "http://localhost").pathname;
27
+ return new URL(rawUrl, "http://localhost");
18
28
  }
19
29
  catch {
20
- return rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`;
30
+ const normalized = rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`;
31
+ return new URL(normalized, "http://localhost");
32
+ }
33
+ }
34
+ function resolvePathname(request) {
35
+ return resolveRequestUrl(request).pathname;
36
+ }
37
+ function formatTableNumber(value) {
38
+ if (!Number.isFinite(value)) {
39
+ return "-";
21
40
  }
41
+ if (Number.isInteger(value)) {
42
+ return String(value);
43
+ }
44
+ return value.toFixed(2).replace(/\.00$/, "").replace(/(\.\d)0$/, "$1");
45
+ }
46
+ function padTableCell(value, width, align) {
47
+ return align === "right" ? value.padStart(width, " ") : value.padEnd(width, " ");
48
+ }
49
+ function buildModelHealthTable(windowHours, models) {
50
+ const columns = [
51
+ { header: "Model", align: "left", value: (row) => row.model },
52
+ {
53
+ header: "Code",
54
+ align: "right",
55
+ value: (row) => row.lastStatusCode === null ? "-" : String(row.lastStatusCode),
56
+ },
57
+ { header: "Avg(ms)", align: "right", value: (row) => formatTableNumber(row.avgResponseMs) },
58
+ { header: "Last(ms)", align: "right", value: (row) => formatTableNumber(row.lastResponseMs) },
59
+ { header: "Count", align: "right", value: (row) => String(row.accessCount) },
60
+ { header: "OK%", align: "right", value: (row) => `${formatTableNumber(row.successRatePct)}%` },
61
+ ];
62
+ const widths = columns.map((column) => {
63
+ const rowWidths = models.map((row) => column.value(row).length);
64
+ return Math.max(column.header.length, ...rowWidths, 1);
65
+ });
66
+ const header = columns
67
+ .map((column, index) => padTableCell(column.header, widths[index] ?? column.header.length, column.align))
68
+ .join(" | ");
69
+ const divider = widths.map((width) => "-".repeat(width)).join("-+-");
70
+ const rows = models.map((row) => columns
71
+ .map((column, index) => padTableCell(column.value(row), widths[index] ?? 0, column.align))
72
+ .join(" | "));
73
+ return [
74
+ `Gateway Health (last ${formatTableNumber(windowHours)}h)`,
75
+ `Status: ok`,
76
+ "",
77
+ header,
78
+ divider,
79
+ ...(rows.length > 0 ? rows : ["No model traffic recorded in the last 12 hours."]),
80
+ ].join("\n");
81
+ }
82
+ function isGatewayApiPath(pathname) {
83
+ return (pathname === "/v1" ||
84
+ pathname.startsWith("/v1/") ||
85
+ pathname === "/anthropic" ||
86
+ pathname.startsWith("/anthropic/"));
22
87
  }
23
88
  async function handleRequest(request, response) {
24
89
  const method = (request.method ?? "GET").toUpperCase();
25
- const pathname = resolvePathname(request);
90
+ const requestUrl = resolveRequestUrl(request);
91
+ const pathname = requestUrl.pathname;
26
92
  if ((method === "GET" || method === "HEAD") && pathname === "/health") {
93
+ const modelHealth = getModelHealthWindow(DEFAULT_MODEL_HEALTH_WINDOW_MS);
94
+ const tableOutput = buildModelHealthTable(modelHealth.windowHours, modelHealth.models);
95
+ if (requestUrl.searchParams.get("format")?.toLowerCase() !== "json") {
96
+ sendText(response, 200, tableOutput);
97
+ return;
98
+ }
27
99
  sendJson(response, 200, {
28
100
  status: "ok",
29
101
  retryStatusCodes: Array.from(config.retryStatusCodes),
30
102
  enabledRouteCount: Object.keys(config.modelRouteMap).length,
103
+ modelHealthWindowHours: modelHealth.windowHours,
104
+ modelHealth: modelHealth.models,
105
+ modelHealthTable: tableOutput,
106
+ modelLoadWindowHours: modelHealth.windowHours,
107
+ modelLoadRanking: modelHealth.models,
31
108
  });
32
109
  return;
33
110
  }
34
- if (pathname === "/v1" || pathname.startsWith("/v1/")) {
111
+ if (isGatewayApiPath(pathname)) {
35
112
  await proxyRequest(request, response);
36
113
  return;
37
114
  }
38
115
  sendJson(response, 404, {
39
116
  error: {
40
- message: "Route not found. Use /v1/* or /health.",
117
+ message: "Route not found. Use /v1/*, /anthropic/*, or /health.",
41
118
  },
42
119
  });
43
120
  }
@@ -21,7 +21,7 @@ export async function startGatewayServer(port = config.port, opts = {}) {
21
21
  });
22
22
  const address = server.address();
23
23
  const resolvedPort = typeof address === "object" && address ? address.port : port;
24
- console.log(`Gateway listening on http://${host}:${resolvedPort} -> ${config.upstreamBaseUrl}`);
24
+ console.log(`Gateway listening on http://${host}:${resolvedPort}`);
25
25
  return {
26
26
  close: async () => {
27
27
  await new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-autoproxy",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Local model-switching proxy gateway with OpenAI-compatible APIs",
5
5
  "type": "module",
6
6
  "main": "dist/gateway/server.js",
@@ -28,6 +28,7 @@
28
28
  "dependencies": {
29
29
  "dotenv": "^17.4.0",
30
30
  "tsx": "^4.20.6",
31
+ "undici": "^7.24.7",
31
32
  "yaml": "^2.8.3"
32
33
  },
33
34
  "devDependencies": {