openclaw-autoproxy 1.0.0
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 +210 -0
- package/bin/openclaw-autoproxy.js +209 -0
- package/package.json +35 -0
- package/src/gateway/config.ts +431 -0
- package/src/gateway/proxy.ts +641 -0
- package/src/gateway/server-http.ts +63 -0
- package/src/gateway/server.impl.ts +58 -0
- package/src/gateway/server.ts +35 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# openclaw-autoproxy (OpenClaw Auto Gateway)
|
|
2
|
+
|
|
3
|
+
Local proxy gateway that forwards OpenAI-compatible APIs and automatically switches model IDs when upstream returns retryable status codes (for example 412).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- OpenAI-compatible proxy endpoint: `/v1/*`
|
|
8
|
+
- Automatic model fallback on retryable statuses for `model: auto` only (default: 412, 429, 500, 502, 503, 504)
|
|
9
|
+
- Model-based route selection: different models can use different upstream URLs and auth headers
|
|
10
|
+
- Per-model and global fallback chains
|
|
11
|
+
- TypeScript runtime powered by `tsx`
|
|
12
|
+
- Node.js HTTP gateway server (openclaw-style)
|
|
13
|
+
- Cross-platform startup on macOS and Windows (Node.js 18+)
|
|
14
|
+
- Health endpoint: `/health`
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
1. Install Node.js 18 or newer.
|
|
19
|
+
2. Install dependencies:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. Create local env file:
|
|
26
|
+
|
|
27
|
+
macOS/Linux:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cp .env.example .env
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Windows PowerShell:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
Copy-Item .env.example .env
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. Edit `.env` (runtime options) and `routes.yml` (all upstream route mappings and auth).
|
|
40
|
+
5. Start the gateway:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run dev
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Production mode:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm start
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Global CLI Usage
|
|
53
|
+
|
|
54
|
+
You can install this project globally and run it via `openclaw-autoproxy`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm i -g .
|
|
58
|
+
openclaw-autoproxy gateway start
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Watch mode:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
openclaw-autoproxy gateway dev
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Show CLI help:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
openclaw-autoproxy gateway help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Backward-compatible aliases are still supported:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
openclaw-autoproxy start
|
|
77
|
+
openclaw-autoproxy dev
|
|
78
|
+
openclaw-autoproxy help
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## OpenAI-Compatible Calls For 3 Models
|
|
82
|
+
|
|
83
|
+
After starting gateway locally, always call the local OpenAI-style endpoint:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl -X POST http://127.0.0.1:8787/v1/chat/completions \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{
|
|
89
|
+
"model": "GLM-4.7-Flash",
|
|
90
|
+
"messages": [{"role":"user","content":"你好"}]
|
|
91
|
+
}'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl -X POST http://127.0.0.1:8787/v1/chat/completions \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{
|
|
98
|
+
"model": "doubao-seed-2-0-pro-260215",
|
|
99
|
+
"messages": [{"role":"user","content":"你好"}]
|
|
100
|
+
}'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
curl -X POST http://127.0.0.1:8787/v1/chat/completions \
|
|
105
|
+
-H "Content-Type: application/json" \
|
|
106
|
+
-d '{
|
|
107
|
+
"model": "ernie-4.5-turbo-128k",
|
|
108
|
+
"messages": [{"role":"user","content":"你好"}]
|
|
109
|
+
}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
- `ALL /v1/*`: Forward to upstream; automatic model fallback is used only when request model is `auto`.
|
|
115
|
+
- `GET /health`: Health check and active retry status list.
|
|
116
|
+
|
|
117
|
+
## Project Structure
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
src/
|
|
121
|
+
gateway/
|
|
122
|
+
config.ts
|
|
123
|
+
proxy.ts
|
|
124
|
+
server-http.ts
|
|
125
|
+
server.impl.ts
|
|
126
|
+
server.ts
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Example Chat Request
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl -X POST http://127.0.0.1:8787/v1/chat/completions \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-H "Authorization: Bearer <your-upstream-token>" \
|
|
135
|
+
-d '{
|
|
136
|
+
"model": "gpt-4.1",
|
|
137
|
+
"messages": [{"role": "user", "content": "hello"}],
|
|
138
|
+
"temperature": 0.2
|
|
139
|
+
}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then call local gateway:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
curl -X POST http://127.0.0.1:8787/v1/chat/completions \
|
|
146
|
+
-H "Content-Type: application/json" \
|
|
147
|
+
-d '{
|
|
148
|
+
"model": "GLM-4.7-Flash",
|
|
149
|
+
"messages": [{"role":"user","content":"你好"}]
|
|
150
|
+
}'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Helpful Response Headers
|
|
154
|
+
|
|
155
|
+
- `x-gateway-model-used`: The actual model used by this attempt.
|
|
156
|
+
- `x-gateway-attempt-count`: Number of attempts before returning response.
|
|
157
|
+
- `x-gateway-switched`: `1` when model fallback happened in this response.
|
|
158
|
+
|
|
159
|
+
### Switch Notice In Response Data
|
|
160
|
+
|
|
161
|
+
- JSON response: when fallback happened, gateway appends `gateway_notice` at top-level JSON.
|
|
162
|
+
- SSE response: when fallback happened, gateway prepends one event:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
event: gateway_notice
|
|
166
|
+
data: {"fromModel":"...","toModel":"...","triggerStatus":412,...}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Fallback Strategy
|
|
170
|
+
|
|
171
|
+
The gateway behavior is split by request model:
|
|
172
|
+
|
|
173
|
+
1. `model != auto`: pinned mode, only the requested model is used (no automatic switch).
|
|
174
|
+
2. `model == auto`: automatic mode, candidates are all enabled route models from `routes.yml`, and each request uses a round-robin start model.
|
|
175
|
+
|
|
176
|
+
When upstream returns a status in `retryStatusCodes` (from `routes.yml`), automatic mode retries using the next candidate model in the same rotated list. If this key is absent, it falls back to `RETRY_STATUS_CODES` env.
|
|
177
|
+
|
|
178
|
+
## Model Route Configuration
|
|
179
|
+
|
|
180
|
+
`routes.yml` is loaded automatically from the project root.
|
|
181
|
+
|
|
182
|
+
Recommended YAML shape:
|
|
183
|
+
|
|
184
|
+
- `defaults`: optional global auth defaults used by all routes
|
|
185
|
+
- `retryStatusCodes`: optional array of retryable HTTP status codes (for example `[412, 429, 500, 502, 503, 504]`)
|
|
186
|
+
- `routes`: required array of route objects
|
|
187
|
+
|
|
188
|
+
Top-level array is also supported when you do not need global defaults.
|
|
189
|
+
|
|
190
|
+
Each route object supports:
|
|
191
|
+
|
|
192
|
+
- `name`: optional logical route name
|
|
193
|
+
- `url`: upstream URL
|
|
194
|
+
- `model`: model list (or a single string)
|
|
195
|
+
- `authHeader`: optional auth header name
|
|
196
|
+
- `authPrefix`: optional auth value prefix (default `Bearer `)
|
|
197
|
+
- `apiKey`: inline token value (preferred in this setup)
|
|
198
|
+
- `apiKeyEnv`: optional env-based token fallback
|
|
199
|
+
- `headers`: optional fixed headers map
|
|
200
|
+
- `isBaseUrl`: optional boolean to force base URL behavior
|
|
201
|
+
- `enabled`: optional boolean (default `true`), set `false` to disable the route without deleting it
|
|
202
|
+
|
|
203
|
+
`routes.yml` is required and loaded from the project root.
|
|
204
|
+
|
|
205
|
+
## Notes
|
|
206
|
+
|
|
207
|
+
- If client request already includes `Authorization`, gateway forwards it.
|
|
208
|
+
- If client request does not include `Authorization`, gateway uses `UPSTREAM_API_KEY`.
|
|
209
|
+
- Streaming responses are forwarded as stream when an attempt succeeds.
|
|
210
|
+
- Requests with invalid JSON body return `400`.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
13
|
+
const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
|
|
14
|
+
const serverEntryPath = path.join(packageRoot, "src", "gateway", "server.ts");
|
|
15
|
+
const tsxCliPath = path.join(packageRoot, "node_modules", "tsx", "dist", "cli.mjs");
|
|
16
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
17
|
+
|
|
18
|
+
function printHelp() {
|
|
19
|
+
console.log(`openclaw-autoproxy - OpenClaw Auto Gateway CLI
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
openclaw-autoproxy gateway start
|
|
23
|
+
openclaw-autoproxy gateway dev
|
|
24
|
+
openclaw-autoproxy start
|
|
25
|
+
openclaw-autoproxy dev
|
|
26
|
+
openclaw-autoproxy help
|
|
27
|
+
openclaw-autoproxy --version
|
|
28
|
+
|
|
29
|
+
Commands:
|
|
30
|
+
gateway Gateway command group
|
|
31
|
+
start Legacy alias of: openclaw-autoproxy gateway start
|
|
32
|
+
dev Legacy alias of: openclaw-autoproxy gateway dev
|
|
33
|
+
help Show root help
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function printGatewayHelp() {
|
|
38
|
+
console.log(`openclaw-autoproxy gateway - Gateway command group
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
openclaw-autoproxy gateway start
|
|
42
|
+
openclaw-autoproxy gateway dev
|
|
43
|
+
openclaw-autoproxy gateway help
|
|
44
|
+
|
|
45
|
+
Subcommands:
|
|
46
|
+
start Start gateway server (default)
|
|
47
|
+
dev Start gateway in watch mode
|
|
48
|
+
help Show gateway help
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function printVersion() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
55
|
+
const pkg = JSON.parse(raw);
|
|
56
|
+
console.log(pkg.version ?? "unknown");
|
|
57
|
+
} catch {
|
|
58
|
+
console.log("unknown");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runTsxMode(tsxArgs) {
|
|
63
|
+
if (!existsSync(tsxCliPath)) {
|
|
64
|
+
console.error("Missing tsx runtime. Reinstall dependencies and try again.");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const child = spawn(process.execPath, [tsxCliPath, ...tsxArgs], {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
env: process.env,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
child.on("exit", (code, signal) => {
|
|
75
|
+
if (signal) {
|
|
76
|
+
process.kill(process.pid, signal);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.exit(code ?? 0);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runDevMode(extraArgs) {
|
|
85
|
+
runTsxMode(["watch", serverEntryPath, ...extraArgs]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runStartMode(extraArgs) {
|
|
89
|
+
runTsxMode([serverEntryPath, ...extraArgs]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isHelpFlag(value) {
|
|
93
|
+
return value === "help" || value === "--help" || value === "-h";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isVersionFlag(value) {
|
|
97
|
+
return value === "version" || value === "--version" || value === "-v";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveGatewayAction(rawArgs) {
|
|
101
|
+
const subcommand = rawArgs[1] ?? "start";
|
|
102
|
+
|
|
103
|
+
if (isHelpFlag(subcommand)) {
|
|
104
|
+
return { type: "gateway-help" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (subcommand === "start") {
|
|
108
|
+
return {
|
|
109
|
+
type: "gateway-start",
|
|
110
|
+
passthrough: rawArgs.slice(2),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (subcommand === "dev") {
|
|
115
|
+
return {
|
|
116
|
+
type: "gateway-dev",
|
|
117
|
+
passthrough: rawArgs.slice(2),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
type: "error",
|
|
123
|
+
message: `Unknown gateway subcommand: ${subcommand}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveAction(rawArgs) {
|
|
128
|
+
const command = rawArgs[0];
|
|
129
|
+
|
|
130
|
+
if (!command) {
|
|
131
|
+
return {
|
|
132
|
+
type: "gateway-start",
|
|
133
|
+
passthrough: [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isHelpFlag(command)) {
|
|
138
|
+
return { type: "root-help" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isVersionFlag(command)) {
|
|
142
|
+
return { type: "version" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (command === "gateway") {
|
|
146
|
+
return resolveGatewayAction(rawArgs);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (command === "start") {
|
|
150
|
+
return {
|
|
151
|
+
type: "gateway-start",
|
|
152
|
+
passthrough: rawArgs.slice(1),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (command === "dev") {
|
|
157
|
+
return {
|
|
158
|
+
type: "gateway-dev",
|
|
159
|
+
passthrough: rawArgs.slice(1),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
type: "error",
|
|
165
|
+
message: `Unknown command: ${command}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function main() {
|
|
170
|
+
const action = resolveAction(args);
|
|
171
|
+
|
|
172
|
+
if (action.type === "root-help") {
|
|
173
|
+
printHelp();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (action.type === "gateway-help") {
|
|
178
|
+
printGatewayHelp();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (action.type === "version") {
|
|
183
|
+
await printVersion();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (action.type === "gateway-start") {
|
|
188
|
+
runStartMode(action.passthrough);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (action.type === "gateway-dev") {
|
|
193
|
+
runDevMode(action.passthrough);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.error(action.message);
|
|
198
|
+
printHelp();
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
main().catch((error) => {
|
|
203
|
+
console.error(
|
|
204
|
+
error instanceof Error
|
|
205
|
+
? `Failed to run openclaw-autoproxy: ${error.message}`
|
|
206
|
+
: "Failed to run openclaw-autoproxy due to an unknown error.",
|
|
207
|
+
);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-autoproxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local model-switching proxy gateway with OpenAI-compatible APIs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/gateway/server.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"openclaw-autoproxy": "bin/openclaw-autoproxy.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"cli": "node bin/openclaw-autoproxy.js",
|
|
12
|
+
"start": "node bin/openclaw-autoproxy.js gateway start",
|
|
13
|
+
"dev": "node bin/openclaw-autoproxy.js gateway dev",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "echo \"No tests configured\""
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"proxy",
|
|
19
|
+
"gateway",
|
|
20
|
+
"openclaw-autoproxy",
|
|
21
|
+
"openai-compatible",
|
|
22
|
+
"fallback"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"dotenv": "^17.4.0",
|
|
28
|
+
"tsx": "^4.20.6",
|
|
29
|
+
"yaml": "^2.8.3"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^24.6.1",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
}
|
|
35
|
+
}
|