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,10 @@
1
1
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
2
2
  import { config } from "./config.js";
3
+ import {
4
+ DEFAULT_MODEL_HEALTH_WINDOW_MS,
5
+ getModelHealthWindow,
6
+ type ModelHealthSummary,
7
+ } from "./model-load-metrics.js";
3
8
  import { proxyRequest } from "./proxy.js";
4
9
 
5
10
  function sendJson(response: ServerResponse, statusCode: number, payload: unknown): void {
@@ -14,37 +19,130 @@ function sendJson(response: ServerResponse, statusCode: number, payload: unknown
14
19
  response.end(body);
15
20
  }
16
21
 
17
- function resolvePathname(request: IncomingMessage): string {
22
+ function sendText(response: ServerResponse, statusCode: number, body: string): void {
23
+ if (response.writableEnded) {
24
+ return;
25
+ }
26
+
27
+ response.statusCode = statusCode;
28
+ response.setHeader("content-type", "text/plain; charset=utf-8");
29
+ response.setHeader("content-length", Buffer.byteLength(body));
30
+ response.end(body);
31
+ }
32
+
33
+ function resolveRequestUrl(request: IncomingMessage): URL {
18
34
  const rawUrl = request.url ?? "/";
19
35
 
20
36
  try {
21
- return new URL(rawUrl, "http://localhost").pathname;
37
+ return new URL(rawUrl, "http://localhost");
22
38
  } catch {
23
- return rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`;
39
+ const normalized = rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`;
40
+ return new URL(normalized, "http://localhost");
41
+ }
42
+ }
43
+
44
+ function resolvePathname(request: IncomingMessage): string {
45
+ return resolveRequestUrl(request).pathname;
46
+ }
47
+
48
+ function formatTableNumber(value: number): string {
49
+ if (!Number.isFinite(value)) {
50
+ return "-";
24
51
  }
52
+
53
+ if (Number.isInteger(value)) {
54
+ return String(value);
55
+ }
56
+
57
+ return value.toFixed(2).replace(/\.00$/, "").replace(/(\.\d)0$/, "$1");
58
+ }
59
+
60
+ function padTableCell(value: string, width: number, align: "left" | "right"): string {
61
+ return align === "right" ? value.padStart(width, " ") : value.padEnd(width, " ");
62
+ }
63
+
64
+ function buildModelHealthTable(windowHours: number, models: Array<ModelHealthSummary & { rank: number }>): string {
65
+ const columns = [
66
+ { header: "Model", align: "left" as const, value: (row: ModelHealthSummary & { rank: number }) => row.model },
67
+ {
68
+ header: "Code",
69
+ align: "right" as const,
70
+ value: (row: ModelHealthSummary & { rank: number }) =>
71
+ row.lastStatusCode === null ? "-" : String(row.lastStatusCode),
72
+ },
73
+ { header: "Avg(ms)", align: "right" as const, value: (row: ModelHealthSummary & { rank: number }) => formatTableNumber(row.avgResponseMs) },
74
+ { header: "Last(ms)", align: "right" as const, value: (row: ModelHealthSummary & { rank: number }) => formatTableNumber(row.lastResponseMs) },
75
+ { header: "Count", align: "right" as const, value: (row: ModelHealthSummary & { rank: number }) => String(row.accessCount) },
76
+ { header: "OK%", align: "right" as const, value: (row: ModelHealthSummary & { rank: number }) => `${formatTableNumber(row.successRatePct)}%` },
77
+ ];
78
+
79
+ const widths = columns.map((column) => {
80
+ const rowWidths = models.map((row) => column.value(row).length);
81
+ return Math.max(column.header.length, ...rowWidths, 1);
82
+ });
83
+
84
+ const header = columns
85
+ .map((column, index) => padTableCell(column.header, widths[index] ?? column.header.length, column.align))
86
+ .join(" | ");
87
+ const divider = widths.map((width) => "-".repeat(width)).join("-+-");
88
+ const rows = models.map((row) => columns
89
+ .map((column, index) => padTableCell(column.value(row), widths[index] ?? 0, column.align))
90
+ .join(" | "));
91
+
92
+ return [
93
+ `Gateway Health (last ${formatTableNumber(windowHours)}h)`,
94
+ `Status: ok`,
95
+ "",
96
+ header,
97
+ divider,
98
+ ...(rows.length > 0 ? rows : ["No model traffic recorded in the last 12 hours."]),
99
+ ].join("\n");
100
+ }
101
+
102
+ function isGatewayApiPath(pathname: string): boolean {
103
+ return (
104
+ pathname === "/v1" ||
105
+ pathname.startsWith("/v1/") ||
106
+ pathname === "/anthropic" ||
107
+ pathname.startsWith("/anthropic/")
108
+ );
25
109
  }
26
110
 
27
111
  async function handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
28
112
  const method = (request.method ?? "GET").toUpperCase();
29
- const pathname = resolvePathname(request);
113
+ const requestUrl = resolveRequestUrl(request);
114
+ const pathname = requestUrl.pathname;
30
115
 
31
116
  if ((method === "GET" || method === "HEAD") && pathname === "/health") {
117
+ const modelHealth = getModelHealthWindow(DEFAULT_MODEL_HEALTH_WINDOW_MS);
118
+ const tableOutput = buildModelHealthTable(modelHealth.windowHours, modelHealth.models);
119
+
120
+ if (requestUrl.searchParams.get("format")?.toLowerCase() !== "json") {
121
+ sendText(response, 200, tableOutput);
122
+ return;
123
+ }
124
+
32
125
  sendJson(response, 200, {
33
126
  status: "ok",
34
127
  retryStatusCodes: Array.from(config.retryStatusCodes),
35
128
  enabledRouteCount: Object.keys(config.modelRouteMap).length,
129
+ modelHealthWindowHours: modelHealth.windowHours,
130
+ modelHealth: modelHealth.models,
131
+ modelHealthTable: tableOutput,
132
+ modelLoadWindowHours: modelHealth.windowHours,
133
+ modelLoadRanking: modelHealth.models,
36
134
  });
37
135
  return;
38
136
  }
39
137
 
40
- if (pathname === "/v1" || pathname.startsWith("/v1/")) {
138
+ if (isGatewayApiPath(pathname)) {
41
139
  await proxyRequest(request, response);
42
140
  return;
43
141
  }
44
142
 
45
143
  sendJson(response, 404, {
46
144
  error: {
47
- message: "Route not found. Use /v1/* or /health.",
145
+ message: "Route not found. Use /v1/*, /anthropic/*, or /health.",
48
146
  },
49
147
  });
50
148
  }
@@ -39,7 +39,7 @@ export async function startGatewayServer(
39
39
  const address = server.address();
40
40
  const resolvedPort = typeof address === "object" && address ? (address as AddressInfo).port : port;
41
41
 
42
- console.log(`Gateway listening on http://${host}:${resolvedPort} -> ${config.upstreamBaseUrl}`);
42
+ console.log(`Gateway listening on http://${host}:${resolvedPort}`);
43
43
 
44
44
  return {
45
45
  close: async () => {
Binary file