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.
- package/README.md +66 -159
- package/README.zh-CN.md +127 -0
- package/dist/gateway/anthropic-compat.js +841 -0
- package/dist/gateway/config.js +16 -0
- package/dist/gateway/model-load-metrics.js +125 -0
- package/dist/gateway/proxy.js +331 -19
- package/dist/gateway/server-http.js +83 -6
- package/dist/gateway/server.impl.js +1 -1
- package/package.json +2 -1
- package/src/gateway/anthropic-compat.ts +1085 -0
- package/src/gateway/config.ts +29 -0
- package/src/gateway/model-load-metrics.ts +192 -0
- package/src/gateway/proxy.ts +452 -25
- package/src/gateway/server-http.ts +104 -6
- package/src/gateway/server.impl.ts +1 -1
- package/openclaw-autoproxy-1.0.1.tgz +0 -0
|
@@ -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
|
|
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")
|
|
27
|
+
return new URL(rawUrl, "http://localhost");
|
|
18
28
|
}
|
|
19
29
|
catch {
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
+
"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": {
|