glmproxy 2.5.1 → 2.6.1
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 +244 -270
- package/anthropic.js +734 -734
- package/bin/cli.js +406 -406
- package/lib/core.js +1486 -1434
- package/lib/fallback-models.json +2 -2
- package/lib/prompts.js +113 -113
- package/openai.js +425 -425
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1,406 +1,406 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import path from "path";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from "url";
|
|
6
|
-
import { promptSelect, promptInput, promptNumber } from "../lib/prompts.js";
|
|
7
|
-
import http from "http";
|
|
8
|
-
import {
|
|
9
|
-
getModelCatalog, loadConfig, createTokenLayer,
|
|
10
|
-
fetchRemoteModelConfig, annotateCreditTiers, resolveTierTargets,
|
|
11
|
-
getLocalGatewayToken, COLORS,
|
|
12
|
-
} from "../lib/core.js";
|
|
13
|
-
import { DEFAULT_PORTS, DEFAULT_HOST, DEFAULT_PROXY_KEY, TEST_PROXY_PORT } from "../lib/constants.js";
|
|
14
|
-
|
|
15
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
|
|
18
|
-
const FLAGS = ["--anthropic", "--openai", "--port", "--host", "--key", "--rate-limit", "--max-messages", "--doctor", "--test-models", "--test", "--limit", "--help", "-h"];
|
|
19
|
-
|
|
20
|
-
// Current effective MAX_MESSAGES env value as a finite number, or Infinity.
|
|
21
|
-
function effectiveMaxMessages() {
|
|
22
|
-
const raw = process.env.MAX_MESSAGES;
|
|
23
|
-
if (!raw) return Infinity;
|
|
24
|
-
const n = parseInt(raw, 10);
|
|
25
|
-
return Number.isFinite(n) && n > 0 ? n : Infinity;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function formatMaxMessages() {
|
|
29
|
-
const n = effectiveMaxMessages();
|
|
30
|
-
return Number.isFinite(n) ? `${n} entries` : "unlimited";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function showHelp() {
|
|
34
|
-
console.log(`
|
|
35
|
-
AutoClaw Gateway CLI
|
|
36
|
-
───────────────────────────────────────────
|
|
37
|
-
Usage:
|
|
38
|
-
npx glmproxy [options]
|
|
39
|
-
glmproxy [options]
|
|
40
|
-
|
|
41
|
-
Options:
|
|
42
|
-
--anthropic Run in Anthropic API format (/v1/messages)
|
|
43
|
-
--openai Run in OpenAI API format (/v1/chat/completions) [default]
|
|
44
|
-
--port <number> Port to listen on (default: ${DEFAULT_PORTS.openai} for OpenAI, ${DEFAULT_PORTS.anthropic} for Anthropic)
|
|
45
|
-
--host <ip> Host to bind (default: ${DEFAULT_HOST})
|
|
46
|
-
--key <string> Authentication key for clients (default: ${DEFAULT_PROXY_KEY})
|
|
47
|
-
--rate-limit <n> Max requests per second per IP (default: 30)
|
|
48
|
-
--max-messages <n> Max message / entity limit (0/unset = unlimited; or 128, 256, 512, 1024)
|
|
49
|
-
If you have a compression system, leaving this unlimited is preferred.
|
|
50
|
-
--doctor Live credit-tier scan of AutoClaw's catalog + routing map
|
|
51
|
-
--test-models Test all configured models against upstream and show live health
|
|
52
|
-
--limit Set or clear the max entity / messages limit (own menu item)
|
|
53
|
-
--help, -h Show this help message
|
|
54
|
-
|
|
55
|
-
Environment:
|
|
56
|
-
MAX_MESSAGES Max messages limit per request (0/unset = unlimited; or 128, 256, 512, 1024)
|
|
57
|
-
PREFER_LOCAL=1 Skip cloud attempts when the local AutoClaw gateway is up
|
|
58
|
-
TRUSTED_PROXIES Comma-separated IPs whose X-Forwarded-For header is trusted
|
|
59
|
-
`);
|
|
60
|
-
process.exit(0);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Cloud-attempt evidence from the isolated test ring: entries are terminal
|
|
64
|
-
// outcomes only. A cloud-served success is its own via!=="local" 200; a
|
|
65
|
-
// locally-served request that the cloud rejected first carries its verdict
|
|
66
|
-
// in cloud_status on that same entry. Scan every entry for this model in
|
|
67
|
-
// this run and derive a compact summary.
|
|
68
|
-
function deriveCloudStatus(entries) {
|
|
69
|
-
if (entries.some((e) => e.via !== "local" && e.status === 200)) {
|
|
70
|
-
return `cloud ${COLORS.GREEN}ok${COLORS.RESET}`;
|
|
71
|
-
}
|
|
72
|
-
const withEvidence = entries.filter((e) => e.cloud_status != null);
|
|
73
|
-
if (!withEvidence.length) return null;
|
|
74
|
-
return `cloud ${withEvidence[withEvidence.length - 1].cloud_status}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function readTestRing(filePath, sinceTs) {
|
|
78
|
-
try {
|
|
79
|
-
const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
80
|
-
if (!Array.isArray(entries)) return [];
|
|
81
|
-
return entries.filter((e) => {
|
|
82
|
-
const ts = Date.parse(e.timestamp || e.ts || "");
|
|
83
|
-
// Tolerate missing timestamps: the ring is capped at 50 entries and the
|
|
84
|
-
// test log is isolated per-run, so anything without one is still ours.
|
|
85
|
-
return Number.isNaN(ts) ? true : ts >= sinceTs;
|
|
86
|
-
});
|
|
87
|
-
} catch (_) { return []; }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function runModelTests() {
|
|
91
|
-
const config = loadConfig({ format: "openai" });
|
|
92
|
-
const catalog = getModelCatalog(config);
|
|
93
|
-
|
|
94
|
-
console.log(`\n 🧪 AutoClaw Model Health Test`);
|
|
95
|
-
console.log(` ───────────────────────────────────────────`);
|
|
96
|
-
|
|
97
|
-
// Spin up a temporary proxy on a test port so requests go through
|
|
98
|
-
// the full pipeline (cloud upstream → local gateway fallback).
|
|
99
|
-
const testPort = TEST_PROXY_PORT;
|
|
100
|
-
const testKey = "model-test-" + Date.now();
|
|
101
|
-
|
|
102
|
-
// Isolated log files: without these, the spawned child's read-modify-write
|
|
103
|
-
// on the shared ring log clobbers entries written by your running proxies
|
|
104
|
-
// (observed: whole batches of results vanishing mid-run).
|
|
105
|
-
const testEnvLog = path.join(process.cwd(), "proxy_requests_test.json");
|
|
106
|
-
const testEnvJsonl = path.join(process.cwd(), "proxy_requests_test.jsonl");
|
|
107
|
-
|
|
108
|
-
const env = {
|
|
109
|
-
...process.env,
|
|
110
|
-
PORT: String(testPort),
|
|
111
|
-
HOST: "127.0.0.1",
|
|
112
|
-
PROXY_KEY: testKey,
|
|
113
|
-
LOG_LEVEL: "silent",
|
|
114
|
-
REQUEST_LOG_FILE: testEnvLog,
|
|
115
|
-
JSONL_FILE: testEnvJsonl,
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const { spawn } = await import("child_process");
|
|
119
|
-
const proxyProc = spawn("node", [path.join(__dirname, "..", "openai.js")], {
|
|
120
|
-
env,
|
|
121
|
-
stdio: "ignore",
|
|
122
|
-
windowsHide: true,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Wait for the test proxy to accept connections
|
|
126
|
-
const ready = await new Promise((resolve) => {
|
|
127
|
-
let tries = 0;
|
|
128
|
-
const interval = setInterval(() => {
|
|
129
|
-
const probe = http.get({ hostname: "127.0.0.1", port: testPort, path: "/healthz" }, (res) => {
|
|
130
|
-
res.resume();
|
|
131
|
-
clearInterval(interval);
|
|
132
|
-
resolve(true);
|
|
133
|
-
});
|
|
134
|
-
probe.on("error", () => {
|
|
135
|
-
if (++tries > 50) { clearInterval(interval); resolve(false); }
|
|
136
|
-
});
|
|
137
|
-
}, 100);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!ready) {
|
|
141
|
-
console.log(` ${COLORS.RED}✗ Could not start test proxy${COLORS.RESET}\n`);
|
|
142
|
-
proxyProc.kill();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const localToken = getLocalGatewayToken();
|
|
147
|
-
console.log(` Local gateway: ${localToken ? `${COLORS.BLUE}available${COLORS.RESET}` : `${COLORS.GRAY}not found${COLORS.RESET}`}`);
|
|
148
|
-
|
|
149
|
-
// Fallback-served requests are invisible in terminal output otherwise —
|
|
150
|
-
// point the operator at the isolated log for per-request attribution.
|
|
151
|
-
console.log(` Request log : ${path.basename(testEnvLog)}\n`);
|
|
152
|
-
|
|
153
|
-
// Cloud-evidence baseline: only entries written from this run onward count.
|
|
154
|
-
const runStart = Date.now() - 1000;
|
|
155
|
-
|
|
156
|
-
for (const model of catalog.models) {
|
|
157
|
-
process.stdout.write(` Testing ${COLORS.CYAN}${model.name}${COLORS.RESET} (${model.id})... `);
|
|
158
|
-
const startTime = Date.now();
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const result = await new Promise((resolve, reject) => {
|
|
162
|
-
const body = JSON.stringify({
|
|
163
|
-
model: model.id,
|
|
164
|
-
messages: [{ role: "user", content: "Reply with only the word PONG" }],
|
|
165
|
-
stream: false,
|
|
166
|
-
});
|
|
167
|
-
const req = http.request({
|
|
168
|
-
hostname: "127.0.0.1",
|
|
169
|
-
port: testPort,
|
|
170
|
-
path: "/v1/chat/completions",
|
|
171
|
-
method: "POST",
|
|
172
|
-
headers: {
|
|
173
|
-
"Content-Type": "application/json",
|
|
174
|
-
Authorization: `Bearer ${testKey}`,
|
|
175
|
-
"Content-Length": Buffer.byteLength(body),
|
|
176
|
-
},
|
|
177
|
-
timeout: 120000,
|
|
178
|
-
}, (res) => {
|
|
179
|
-
let data = "";
|
|
180
|
-
res.on("data", (c) => (data += c));
|
|
181
|
-
res.on("end", () => resolve({ status: res.statusCode, body: data }));
|
|
182
|
-
});
|
|
183
|
-
req.on("error", reject);
|
|
184
|
-
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
185
|
-
req.write(body);
|
|
186
|
-
req.end();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const elapsed = Date.now() - startTime;
|
|
190
|
-
// Give the proxy a beat to flush its ring write before we read it back
|
|
191
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
192
|
-
const cloudStatus = deriveCloudStatus(
|
|
193
|
-
readTestRing(testEnvLog, runStart).filter((e) => e.model === model.id),
|
|
194
|
-
);
|
|
195
|
-
if (result.status === 200) {
|
|
196
|
-
let answer = "";
|
|
197
|
-
let servedBy = "";
|
|
198
|
-
try {
|
|
199
|
-
const parsed = JSON.parse(result.body);
|
|
200
|
-
answer = parsed.choices?.[0]?.message?.content || "";
|
|
201
|
-
// Attribution: responses assembled by the local-agent fallback carry
|
|
202
|
-
// zero usage counters — cloud answers report real token usage.
|
|
203
|
-
servedBy = parsed.usage?.prompt_tokens === 0 && parsed.usage?.completion_tokens === 0
|
|
204
|
-
? ` ${COLORS.MAGENTA}[${cloudStatus ?? "cloud n/a"} → local agent]${COLORS.RESET}`
|
|
205
|
-
: cloudStatus ? ` ${COLORS.GRAY}[${cloudStatus}]${COLORS.RESET}` : "";
|
|
206
|
-
} catch {}
|
|
207
|
-
const preview = answer.length > 40 ? answer.slice(0, 40) + "…" : answer;
|
|
208
|
-
console.log(`${COLORS.BLUE}✔ working${COLORS.RESET}${servedBy} ${COLORS.GRAY}(${elapsed}ms) → ${preview}${COLORS.RESET}`);
|
|
209
|
-
} else {
|
|
210
|
-
let detail = "";
|
|
211
|
-
try { detail = JSON.parse(result.body).error?.message || ""; } catch {}
|
|
212
|
-
console.log(`${COLORS.RED}✗ failed (${result.status})${COLORS.RESET} ${COLORS.GRAY}(${elapsed}ms)${detail ? ` → ${detail}` : ""}`);
|
|
213
|
-
}
|
|
214
|
-
} catch (err) {
|
|
215
|
-
const elapsed = Date.now() - startTime;
|
|
216
|
-
console.log(`${COLORS.RED}✗ error: ${err.message}${COLORS.RESET} ${COLORS.GRAY}(${elapsed}ms)${COLORS.RESET}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
console.log(`\n ${COLORS.GRAY}Legend: [cloud NNN → local agent] = cloud rejected the request (HTTP NNN), the desktop-app fallback served it instead.${COLORS.RESET}`);
|
|
221
|
-
console.log("");
|
|
222
|
-
proxyProc.kill();
|
|
223
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Live credit-tier doctor: remote model-config → runtime catalog → built-in
|
|
227
|
-
// fallback, routed through the SAME annotate/resolve pair the Anthropic
|
|
228
|
-
// entrypoint uses. No duplicated fragment matching here anymore.
|
|
229
|
-
async function runDoctor() {
|
|
230
|
-
const config = loadConfig({ format: "openai" });
|
|
231
|
-
const catalog = getModelCatalog(config);
|
|
232
|
-
|
|
233
|
-
console.log(`\n AutoClaw model doctor`);
|
|
234
|
-
console.log(` ───────────────────────────────────────────`);
|
|
235
|
-
|
|
236
|
-
let source = catalog.source ? path.basename(catalog.source) : "built-in fallback";
|
|
237
|
-
let status = catalog.fallback ? "runtime catalog unavailable" : "runtime catalog loaded";
|
|
238
|
-
|
|
239
|
-
// Read the JWT straight from AutoClaw's token file (silent — the doctor
|
|
240
|
-
// must work even while the desktop app is closed).
|
|
241
|
-
let jwt = null;
|
|
242
|
-
try {
|
|
243
|
-
const tokenLayer = createTokenLayer(config, { info: () => {}, warn: () => {}, error: () => {}, debug: () => {}, success: () => {} });
|
|
244
|
-
jwt = tokenLayer.loadToken();
|
|
245
|
-
} catch (_) {}
|
|
246
|
-
|
|
247
|
-
const remoteModels = await fetchRemoteModelConfig(config, jwt);
|
|
248
|
-
if (remoteModels) {
|
|
249
|
-
source = "remote model-config";
|
|
250
|
-
status = `live credit-tier data (${remoteModels.length} models)`;
|
|
251
|
-
} else if (jwt) {
|
|
252
|
-
status += " · remote fetch failed — heuristic tiers apply";
|
|
253
|
-
} else {
|
|
254
|
-
status += " · no AutoClaw token — heuristic tiers apply";
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const models = annotateCreditTiers(catalog.models, remoteModels);
|
|
258
|
-
const targets = resolveTierTargets(models);
|
|
259
|
-
|
|
260
|
-
console.log(` Source: ${source}`);
|
|
261
|
-
console.log(` Status: ${status}\n`);
|
|
262
|
-
|
|
263
|
-
models.forEach((model, index) => {
|
|
264
|
-
const context = model.contextWindow ? `${Math.round(model.contextWindow / 1024)}K context` : "context unknown";
|
|
265
|
-
const output = model.maxTokens ? `${Math.round(model.maxTokens / 1024)}K max output` : "output unknown";
|
|
266
|
-
const tier = model.creditLevel ? `${model.creditLevel} credit` : "tier unknown";
|
|
267
|
-
console.log(` ${index + 1}. ${model.name} (${model.id}) — ${tier}, ${context}, ${output}`);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
console.log(`\n Claude alias routing (by credit tier):`);
|
|
271
|
-
console.log(` claude-opus-* → ${targets.opus ?? "?"}`);
|
|
272
|
-
console.log(` claude-sonnet-* → ${targets.sonnet ?? "?"}`);
|
|
273
|
-
console.log(` claude-haiku-* → ${targets.haiku ?? "?"}`);
|
|
274
|
-
console.log(` unknown model → ${targets.default ?? "?"}\n`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
278
|
-
showHelp();
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (args.includes("--test-models") || args.includes("--test")) {
|
|
282
|
-
await runModelTests();
|
|
283
|
-
process.exit(0);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (args.includes("--doctor")) {
|
|
287
|
-
await runDoctor();
|
|
288
|
-
process.exit(0);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// --limit: set or clear the max entity / messages limit, then exit.
|
|
292
|
-
if (args.includes("--limit")) {
|
|
293
|
-
const limitIdx = args.indexOf("--limit");
|
|
294
|
-
const value = args[limitIdx + 1];
|
|
295
|
-
if (value && /^\d+$/.test(value)) {
|
|
296
|
-
const n = parseInt(value, 10);
|
|
297
|
-
process.env.MAX_MESSAGES = n > 0 ? String(n) : "";
|
|
298
|
-
}
|
|
299
|
-
console.log(`\n Max messages: ${process.env.MAX_MESSAGES ? `${process.env.MAX_MESSAGES} entries` : `${COLORS.GREEN}unlimited${COLORS.RESET}`}`);
|
|
300
|
-
console.log(` ${COLORS.GRAY}(If you have a compression system, leaving this unlimited is preferred.)${COLORS.RESET}\n`);
|
|
301
|
-
process.exit(0);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Flag parsing
|
|
305
|
-
let isAnthropic = args.includes("--anthropic");
|
|
306
|
-
const portIdx = args.indexOf("--port");
|
|
307
|
-
const keyIdx = args.indexOf("--key");
|
|
308
|
-
const hostIdx = args.indexOf("--host");
|
|
309
|
-
const rateLimitIdx = args.indexOf("--rate-limit");
|
|
310
|
-
const maxMessagesIdx = args.indexOf("--max-messages");
|
|
311
|
-
|
|
312
|
-
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
313
|
-
process.env.PORT = args[portIdx + 1];
|
|
314
|
-
}
|
|
315
|
-
if (keyIdx !== -1 && args[keyIdx + 1]) {
|
|
316
|
-
process.env.PROXY_KEY = args[keyIdx + 1];
|
|
317
|
-
}
|
|
318
|
-
if (hostIdx !== -1 && args[hostIdx + 1]) {
|
|
319
|
-
process.env.HOST = args[hostIdx + 1];
|
|
320
|
-
}
|
|
321
|
-
if (rateLimitIdx !== -1 && args[rateLimitIdx + 1]) {
|
|
322
|
-
process.env.RATE_LIMIT = args[rateLimitIdx + 1];
|
|
323
|
-
}
|
|
324
|
-
if (maxMessagesIdx !== -1 && args[maxMessagesIdx + 1]) {
|
|
325
|
-
const n = parseInt(args[maxMessagesIdx + 1], 10);
|
|
326
|
-
process.env.MAX_MESSAGES = n > 0 ? String(n) : "";
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const hasFlags = FLAGS.some((f) => args.includes(f));
|
|
330
|
-
|
|
331
|
-
// Menu (only on a real TTY with no flags)
|
|
332
|
-
if (!hasFlags && process.stdin.isTTY) {
|
|
333
|
-
for (;;) {
|
|
334
|
-
const action = await promptSelect({
|
|
335
|
-
message: "Choose action:",
|
|
336
|
-
choices: [
|
|
337
|
-
{ name: "Start OpenAI Gateway (/v1/chat/completions)", value: "start_openai" },
|
|
338
|
-
{ name: "Start Anthropic Gateway (/v1/messages)", value: "start_anthropic" },
|
|
339
|
-
{ name: "Set Max Messages Limit (default: unlimited)", value: "limit" },
|
|
340
|
-
{ name: "Run Model Doctor (View catalog & routing)", value: "doctor" },
|
|
341
|
-
{ name: "Test Models (Live proxy health check)", value: "test_models" },
|
|
342
|
-
],
|
|
343
|
-
default: "start_openai",
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
if (action === "doctor") {
|
|
347
|
-
await runDoctor();
|
|
348
|
-
const next = await promptSelect({
|
|
349
|
-
message: "Next action:",
|
|
350
|
-
choices: [
|
|
351
|
-
{ name: "Test all models now", value: "test" },
|
|
352
|
-
{ name: "Back to main menu", value: "back" },
|
|
353
|
-
],
|
|
354
|
-
default: "test",
|
|
355
|
-
});
|
|
356
|
-
if (next === "test") {
|
|
357
|
-
await runModelTests();
|
|
358
|
-
}
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (action === "test_models") {
|
|
363
|
-
await runModelTests();
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (action === "limit") {
|
|
368
|
-
const current = Number.isFinite(config_maxMessages())
|
|
369
|
-
? `${config_maxMessages()} entries`
|
|
370
|
-
: `${COLORS.GREEN}unlimited${COLORS.RESET}`;
|
|
371
|
-
const entityLimit = await promptSelect({
|
|
372
|
-
message: "Max entity / messages limit:",
|
|
373
|
-
hint: ` ${COLORS.GRAY}current: ${current} — a compression system makes unlimited preferred${COLORS.RESET}`,
|
|
374
|
-
choices: [
|
|
375
|
-
{ name: "Unlimited (default)", value: "unlimited" },
|
|
376
|
-
{ name: "128", value: "128" },
|
|
377
|
-
{ name: "256", value: "256" },
|
|
378
|
-
{ name: "512", value: "512" },
|
|
379
|
-
{ name: "1024", value: "1024" },
|
|
380
|
-
],
|
|
381
|
-
default: "unlimited",
|
|
382
|
-
});
|
|
383
|
-
process.env.MAX_MESSAGES = entityLimit === "unlimited" ? "" : entityLimit;
|
|
384
|
-
console.log(` ${COLORS.GRAY}(If you have a compression system, leaving this unlimited is preferred.)${COLORS.RESET}\n`);
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
isAnthropic = action === "start_anthropic";
|
|
389
|
-
const defaultPort = DEFAULT_PORTS[isAnthropic ? "anthropic" : "openai"];
|
|
390
|
-
const port = await promptNumber({ message: "Port:", default: defaultPort });
|
|
391
|
-
const host = await promptInput({ message: "Host:", default: "127.0.0.1" });
|
|
392
|
-
const key = await promptInput({ message: "Auth key:", default: "mewmew" });
|
|
393
|
-
|
|
394
|
-
process.env.PORT = String(port);
|
|
395
|
-
process.env.HOST = host;
|
|
396
|
-
process.env.PROXY_KEY = key;
|
|
397
|
-
break;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const targetFile = isAnthropic
|
|
402
|
-
? path.join(__dirname, "..", "anthropic.js")
|
|
403
|
-
: path.join(__dirname, "..", "openai.js");
|
|
404
|
-
|
|
405
|
-
// Windows dynamic imports need a file:// URL, not a raw drive-letter path
|
|
406
|
-
await import(pathToFileURL(targetFile).href);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
6
|
+
import { promptSelect, promptInput, promptNumber } from "../lib/prompts.js";
|
|
7
|
+
import http from "http";
|
|
8
|
+
import {
|
|
9
|
+
getModelCatalog, loadConfig, createTokenLayer,
|
|
10
|
+
fetchRemoteModelConfig, annotateCreditTiers, resolveTierTargets,
|
|
11
|
+
getLocalGatewayToken, COLORS,
|
|
12
|
+
} from "../lib/core.js";
|
|
13
|
+
import { DEFAULT_PORTS, DEFAULT_HOST, DEFAULT_PROXY_KEY, TEST_PROXY_PORT } from "../lib/constants.js";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
const FLAGS = ["--anthropic", "--openai", "--port", "--host", "--key", "--rate-limit", "--max-messages", "--doctor", "--test-models", "--test", "--limit", "--help", "-h"];
|
|
19
|
+
|
|
20
|
+
// Current effective MAX_MESSAGES env value as a finite number, or Infinity.
|
|
21
|
+
function effectiveMaxMessages() {
|
|
22
|
+
const raw = process.env.MAX_MESSAGES;
|
|
23
|
+
if (!raw) return Infinity;
|
|
24
|
+
const n = parseInt(raw, 10);
|
|
25
|
+
return Number.isFinite(n) && n > 0 ? n : Infinity;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatMaxMessages() {
|
|
29
|
+
const n = effectiveMaxMessages();
|
|
30
|
+
return Number.isFinite(n) ? `${n} entries` : "unlimited";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function showHelp() {
|
|
34
|
+
console.log(`
|
|
35
|
+
AutoClaw Gateway CLI
|
|
36
|
+
───────────────────────────────────────────
|
|
37
|
+
Usage:
|
|
38
|
+
npx glmproxy [options]
|
|
39
|
+
glmproxy [options]
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--anthropic Run in Anthropic API format (/v1/messages)
|
|
43
|
+
--openai Run in OpenAI API format (/v1/chat/completions) [default]
|
|
44
|
+
--port <number> Port to listen on (default: ${DEFAULT_PORTS.openai} for OpenAI, ${DEFAULT_PORTS.anthropic} for Anthropic)
|
|
45
|
+
--host <ip> Host to bind (default: ${DEFAULT_HOST})
|
|
46
|
+
--key <string> Authentication key for clients (default: ${DEFAULT_PROXY_KEY})
|
|
47
|
+
--rate-limit <n> Max requests per second per IP (default: 30)
|
|
48
|
+
--max-messages <n> Max message / entity limit (0/unset = unlimited; or 128, 256, 512, 1024)
|
|
49
|
+
If you have a compression system, leaving this unlimited is preferred.
|
|
50
|
+
--doctor Live credit-tier scan of AutoClaw's catalog + routing map
|
|
51
|
+
--test-models Test all configured models against upstream and show live health
|
|
52
|
+
--limit Set or clear the max entity / messages limit (own menu item)
|
|
53
|
+
--help, -h Show this help message
|
|
54
|
+
|
|
55
|
+
Environment:
|
|
56
|
+
MAX_MESSAGES Max messages limit per request (0/unset = unlimited; or 128, 256, 512, 1024)
|
|
57
|
+
PREFER_LOCAL=1 Skip cloud attempts when the local AutoClaw gateway is up
|
|
58
|
+
TRUSTED_PROXIES Comma-separated IPs whose X-Forwarded-For header is trusted
|
|
59
|
+
`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Cloud-attempt evidence from the isolated test ring: entries are terminal
|
|
64
|
+
// outcomes only. A cloud-served success is its own via!=="local" 200; a
|
|
65
|
+
// locally-served request that the cloud rejected first carries its verdict
|
|
66
|
+
// in cloud_status on that same entry. Scan every entry for this model in
|
|
67
|
+
// this run and derive a compact summary.
|
|
68
|
+
function deriveCloudStatus(entries) {
|
|
69
|
+
if (entries.some((e) => e.via !== "local" && e.status === 200)) {
|
|
70
|
+
return `cloud ${COLORS.GREEN}ok${COLORS.RESET}`;
|
|
71
|
+
}
|
|
72
|
+
const withEvidence = entries.filter((e) => e.cloud_status != null);
|
|
73
|
+
if (!withEvidence.length) return null;
|
|
74
|
+
return `cloud ${withEvidence[withEvidence.length - 1].cloud_status}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readTestRing(filePath, sinceTs) {
|
|
78
|
+
try {
|
|
79
|
+
const entries = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
80
|
+
if (!Array.isArray(entries)) return [];
|
|
81
|
+
return entries.filter((e) => {
|
|
82
|
+
const ts = Date.parse(e.timestamp || e.ts || "");
|
|
83
|
+
// Tolerate missing timestamps: the ring is capped at 50 entries and the
|
|
84
|
+
// test log is isolated per-run, so anything without one is still ours.
|
|
85
|
+
return Number.isNaN(ts) ? true : ts >= sinceTs;
|
|
86
|
+
});
|
|
87
|
+
} catch (_) { return []; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runModelTests() {
|
|
91
|
+
const config = loadConfig({ format: "openai" });
|
|
92
|
+
const catalog = getModelCatalog(config);
|
|
93
|
+
|
|
94
|
+
console.log(`\n 🧪 AutoClaw Model Health Test`);
|
|
95
|
+
console.log(` ───────────────────────────────────────────`);
|
|
96
|
+
|
|
97
|
+
// Spin up a temporary proxy on a test port so requests go through
|
|
98
|
+
// the full pipeline (cloud upstream → local gateway fallback).
|
|
99
|
+
const testPort = TEST_PROXY_PORT;
|
|
100
|
+
const testKey = "model-test-" + Date.now();
|
|
101
|
+
|
|
102
|
+
// Isolated log files: without these, the spawned child's read-modify-write
|
|
103
|
+
// on the shared ring log clobbers entries written by your running proxies
|
|
104
|
+
// (observed: whole batches of results vanishing mid-run).
|
|
105
|
+
const testEnvLog = path.join(process.cwd(), "proxy_requests_test.json");
|
|
106
|
+
const testEnvJsonl = path.join(process.cwd(), "proxy_requests_test.jsonl");
|
|
107
|
+
|
|
108
|
+
const env = {
|
|
109
|
+
...process.env,
|
|
110
|
+
PORT: String(testPort),
|
|
111
|
+
HOST: "127.0.0.1",
|
|
112
|
+
PROXY_KEY: testKey,
|
|
113
|
+
LOG_LEVEL: "silent",
|
|
114
|
+
REQUEST_LOG_FILE: testEnvLog,
|
|
115
|
+
JSONL_FILE: testEnvJsonl,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const { spawn } = await import("child_process");
|
|
119
|
+
const proxyProc = spawn("node", [path.join(__dirname, "..", "openai.js")], {
|
|
120
|
+
env,
|
|
121
|
+
stdio: "ignore",
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Wait for the test proxy to accept connections
|
|
126
|
+
const ready = await new Promise((resolve) => {
|
|
127
|
+
let tries = 0;
|
|
128
|
+
const interval = setInterval(() => {
|
|
129
|
+
const probe = http.get({ hostname: "127.0.0.1", port: testPort, path: "/healthz" }, (res) => {
|
|
130
|
+
res.resume();
|
|
131
|
+
clearInterval(interval);
|
|
132
|
+
resolve(true);
|
|
133
|
+
});
|
|
134
|
+
probe.on("error", () => {
|
|
135
|
+
if (++tries > 50) { clearInterval(interval); resolve(false); }
|
|
136
|
+
});
|
|
137
|
+
}, 100);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!ready) {
|
|
141
|
+
console.log(` ${COLORS.RED}✗ Could not start test proxy${COLORS.RESET}\n`);
|
|
142
|
+
proxyProc.kill();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const localToken = getLocalGatewayToken();
|
|
147
|
+
console.log(` Local gateway: ${localToken ? `${COLORS.BLUE}available${COLORS.RESET}` : `${COLORS.GRAY}not found${COLORS.RESET}`}`);
|
|
148
|
+
|
|
149
|
+
// Fallback-served requests are invisible in terminal output otherwise —
|
|
150
|
+
// point the operator at the isolated log for per-request attribution.
|
|
151
|
+
console.log(` Request log : ${path.basename(testEnvLog)}\n`);
|
|
152
|
+
|
|
153
|
+
// Cloud-evidence baseline: only entries written from this run onward count.
|
|
154
|
+
const runStart = Date.now() - 1000;
|
|
155
|
+
|
|
156
|
+
for (const model of catalog.models) {
|
|
157
|
+
process.stdout.write(` Testing ${COLORS.CYAN}${model.name}${COLORS.RESET} (${model.id})... `);
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await new Promise((resolve, reject) => {
|
|
162
|
+
const body = JSON.stringify({
|
|
163
|
+
model: model.id,
|
|
164
|
+
messages: [{ role: "user", content: "Reply with only the word PONG" }],
|
|
165
|
+
stream: false,
|
|
166
|
+
});
|
|
167
|
+
const req = http.request({
|
|
168
|
+
hostname: "127.0.0.1",
|
|
169
|
+
port: testPort,
|
|
170
|
+
path: "/v1/chat/completions",
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
Authorization: `Bearer ${testKey}`,
|
|
175
|
+
"Content-Length": Buffer.byteLength(body),
|
|
176
|
+
},
|
|
177
|
+
timeout: 120000,
|
|
178
|
+
}, (res) => {
|
|
179
|
+
let data = "";
|
|
180
|
+
res.on("data", (c) => (data += c));
|
|
181
|
+
res.on("end", () => resolve({ status: res.statusCode, body: data }));
|
|
182
|
+
});
|
|
183
|
+
req.on("error", reject);
|
|
184
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
185
|
+
req.write(body);
|
|
186
|
+
req.end();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const elapsed = Date.now() - startTime;
|
|
190
|
+
// Give the proxy a beat to flush its ring write before we read it back
|
|
191
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
192
|
+
const cloudStatus = deriveCloudStatus(
|
|
193
|
+
readTestRing(testEnvLog, runStart).filter((e) => e.model === model.id),
|
|
194
|
+
);
|
|
195
|
+
if (result.status === 200) {
|
|
196
|
+
let answer = "";
|
|
197
|
+
let servedBy = "";
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(result.body);
|
|
200
|
+
answer = parsed.choices?.[0]?.message?.content || "";
|
|
201
|
+
// Attribution: responses assembled by the local-agent fallback carry
|
|
202
|
+
// zero usage counters — cloud answers report real token usage.
|
|
203
|
+
servedBy = parsed.usage?.prompt_tokens === 0 && parsed.usage?.completion_tokens === 0
|
|
204
|
+
? ` ${COLORS.MAGENTA}[${cloudStatus ?? "cloud n/a"} → local agent]${COLORS.RESET}`
|
|
205
|
+
: cloudStatus ? ` ${COLORS.GRAY}[${cloudStatus}]${COLORS.RESET}` : "";
|
|
206
|
+
} catch {}
|
|
207
|
+
const preview = answer.length > 40 ? answer.slice(0, 40) + "…" : answer;
|
|
208
|
+
console.log(`${COLORS.BLUE}✔ working${COLORS.RESET}${servedBy} ${COLORS.GRAY}(${elapsed}ms) → ${preview}${COLORS.RESET}`);
|
|
209
|
+
} else {
|
|
210
|
+
let detail = "";
|
|
211
|
+
try { detail = JSON.parse(result.body).error?.message || ""; } catch {}
|
|
212
|
+
console.log(`${COLORS.RED}✗ failed (${result.status})${COLORS.RESET} ${COLORS.GRAY}(${elapsed}ms)${detail ? ` → ${detail}` : ""}`);
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const elapsed = Date.now() - startTime;
|
|
216
|
+
console.log(`${COLORS.RED}✗ error: ${err.message}${COLORS.RESET} ${COLORS.GRAY}(${elapsed}ms)${COLORS.RESET}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`\n ${COLORS.GRAY}Legend: [cloud NNN → local agent] = cloud rejected the request (HTTP NNN), the desktop-app fallback served it instead.${COLORS.RESET}`);
|
|
221
|
+
console.log("");
|
|
222
|
+
proxyProc.kill();
|
|
223
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Live credit-tier doctor: remote model-config → runtime catalog → built-in
|
|
227
|
+
// fallback, routed through the SAME annotate/resolve pair the Anthropic
|
|
228
|
+
// entrypoint uses. No duplicated fragment matching here anymore.
|
|
229
|
+
async function runDoctor() {
|
|
230
|
+
const config = loadConfig({ format: "openai" });
|
|
231
|
+
const catalog = getModelCatalog(config);
|
|
232
|
+
|
|
233
|
+
console.log(`\n AutoClaw model doctor`);
|
|
234
|
+
console.log(` ───────────────────────────────────────────`);
|
|
235
|
+
|
|
236
|
+
let source = catalog.source ? path.basename(catalog.source) : "built-in fallback";
|
|
237
|
+
let status = catalog.fallback ? "runtime catalog unavailable" : "runtime catalog loaded";
|
|
238
|
+
|
|
239
|
+
// Read the JWT straight from AutoClaw's token file (silent — the doctor
|
|
240
|
+
// must work even while the desktop app is closed).
|
|
241
|
+
let jwt = null;
|
|
242
|
+
try {
|
|
243
|
+
const tokenLayer = createTokenLayer(config, { info: () => {}, warn: () => {}, error: () => {}, debug: () => {}, success: () => {} });
|
|
244
|
+
jwt = tokenLayer.loadToken();
|
|
245
|
+
} catch (_) {}
|
|
246
|
+
|
|
247
|
+
const remoteModels = await fetchRemoteModelConfig(config, jwt);
|
|
248
|
+
if (remoteModels) {
|
|
249
|
+
source = "remote model-config";
|
|
250
|
+
status = `live credit-tier data (${remoteModels.length} models)`;
|
|
251
|
+
} else if (jwt) {
|
|
252
|
+
status += " · remote fetch failed — heuristic tiers apply";
|
|
253
|
+
} else {
|
|
254
|
+
status += " · no AutoClaw token — heuristic tiers apply";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const models = annotateCreditTiers(catalog.models, remoteModels);
|
|
258
|
+
const targets = resolveTierTargets(models);
|
|
259
|
+
|
|
260
|
+
console.log(` Source: ${source}`);
|
|
261
|
+
console.log(` Status: ${status}\n`);
|
|
262
|
+
|
|
263
|
+
models.forEach((model, index) => {
|
|
264
|
+
const context = model.contextWindow ? `${Math.round(model.contextWindow / 1024)}K context` : "context unknown";
|
|
265
|
+
const output = model.maxTokens ? `${Math.round(model.maxTokens / 1024)}K max output` : "output unknown";
|
|
266
|
+
const tier = model.creditLevel ? `${model.creditLevel} credit` : "tier unknown";
|
|
267
|
+
console.log(` ${index + 1}. ${model.name} (${model.id}) — ${tier}, ${context}, ${output}`);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
console.log(`\n Claude alias routing (by credit tier):`);
|
|
271
|
+
console.log(` claude-opus-* → ${targets.opus ?? "?"}`);
|
|
272
|
+
console.log(` claude-sonnet-* → ${targets.sonnet ?? "?"}`);
|
|
273
|
+
console.log(` claude-haiku-* → ${targets.haiku ?? "?"}`);
|
|
274
|
+
console.log(` unknown model → ${targets.default ?? "?"}\n`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
278
|
+
showHelp();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (args.includes("--test-models") || args.includes("--test")) {
|
|
282
|
+
await runModelTests();
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (args.includes("--doctor")) {
|
|
287
|
+
await runDoctor();
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --limit: set or clear the max entity / messages limit, then exit.
|
|
292
|
+
if (args.includes("--limit")) {
|
|
293
|
+
const limitIdx = args.indexOf("--limit");
|
|
294
|
+
const value = args[limitIdx + 1];
|
|
295
|
+
if (value && /^\d+$/.test(value)) {
|
|
296
|
+
const n = parseInt(value, 10);
|
|
297
|
+
process.env.MAX_MESSAGES = n > 0 ? String(n) : "";
|
|
298
|
+
}
|
|
299
|
+
console.log(`\n Max messages: ${process.env.MAX_MESSAGES ? `${process.env.MAX_MESSAGES} entries` : `${COLORS.GREEN}unlimited${COLORS.RESET}`}`);
|
|
300
|
+
console.log(` ${COLORS.GRAY}(If you have a compression system, leaving this unlimited is preferred.)${COLORS.RESET}\n`);
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Flag parsing
|
|
305
|
+
let isAnthropic = args.includes("--anthropic");
|
|
306
|
+
const portIdx = args.indexOf("--port");
|
|
307
|
+
const keyIdx = args.indexOf("--key");
|
|
308
|
+
const hostIdx = args.indexOf("--host");
|
|
309
|
+
const rateLimitIdx = args.indexOf("--rate-limit");
|
|
310
|
+
const maxMessagesIdx = args.indexOf("--max-messages");
|
|
311
|
+
|
|
312
|
+
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
313
|
+
process.env.PORT = args[portIdx + 1];
|
|
314
|
+
}
|
|
315
|
+
if (keyIdx !== -1 && args[keyIdx + 1]) {
|
|
316
|
+
process.env.PROXY_KEY = args[keyIdx + 1];
|
|
317
|
+
}
|
|
318
|
+
if (hostIdx !== -1 && args[hostIdx + 1]) {
|
|
319
|
+
process.env.HOST = args[hostIdx + 1];
|
|
320
|
+
}
|
|
321
|
+
if (rateLimitIdx !== -1 && args[rateLimitIdx + 1]) {
|
|
322
|
+
process.env.RATE_LIMIT = args[rateLimitIdx + 1];
|
|
323
|
+
}
|
|
324
|
+
if (maxMessagesIdx !== -1 && args[maxMessagesIdx + 1]) {
|
|
325
|
+
const n = parseInt(args[maxMessagesIdx + 1], 10);
|
|
326
|
+
process.env.MAX_MESSAGES = n > 0 ? String(n) : "";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const hasFlags = FLAGS.some((f) => args.includes(f));
|
|
330
|
+
|
|
331
|
+
// Menu (only on a real TTY with no flags)
|
|
332
|
+
if (!hasFlags && process.stdin.isTTY) {
|
|
333
|
+
for (;;) {
|
|
334
|
+
const action = await promptSelect({
|
|
335
|
+
message: "Choose action:",
|
|
336
|
+
choices: [
|
|
337
|
+
{ name: "Start OpenAI Gateway (/v1/chat/completions)", value: "start_openai" },
|
|
338
|
+
{ name: "Start Anthropic Gateway (/v1/messages)", value: "start_anthropic" },
|
|
339
|
+
{ name: "Set Max Messages Limit (default: unlimited)", value: "limit" },
|
|
340
|
+
{ name: "Run Model Doctor (View catalog & routing)", value: "doctor" },
|
|
341
|
+
{ name: "Test Models (Live proxy health check)", value: "test_models" },
|
|
342
|
+
],
|
|
343
|
+
default: "start_openai",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (action === "doctor") {
|
|
347
|
+
await runDoctor();
|
|
348
|
+
const next = await promptSelect({
|
|
349
|
+
message: "Next action:",
|
|
350
|
+
choices: [
|
|
351
|
+
{ name: "Test all models now", value: "test" },
|
|
352
|
+
{ name: "Back to main menu", value: "back" },
|
|
353
|
+
],
|
|
354
|
+
default: "test",
|
|
355
|
+
});
|
|
356
|
+
if (next === "test") {
|
|
357
|
+
await runModelTests();
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (action === "test_models") {
|
|
363
|
+
await runModelTests();
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (action === "limit") {
|
|
368
|
+
const current = Number.isFinite(config_maxMessages())
|
|
369
|
+
? `${config_maxMessages()} entries`
|
|
370
|
+
: `${COLORS.GREEN}unlimited${COLORS.RESET}`;
|
|
371
|
+
const entityLimit = await promptSelect({
|
|
372
|
+
message: "Max entity / messages limit:",
|
|
373
|
+
hint: ` ${COLORS.GRAY}current: ${current} — a compression system makes unlimited preferred${COLORS.RESET}`,
|
|
374
|
+
choices: [
|
|
375
|
+
{ name: "Unlimited (default)", value: "unlimited" },
|
|
376
|
+
{ name: "128", value: "128" },
|
|
377
|
+
{ name: "256", value: "256" },
|
|
378
|
+
{ name: "512", value: "512" },
|
|
379
|
+
{ name: "1024", value: "1024" },
|
|
380
|
+
],
|
|
381
|
+
default: "unlimited",
|
|
382
|
+
});
|
|
383
|
+
process.env.MAX_MESSAGES = entityLimit === "unlimited" ? "" : entityLimit;
|
|
384
|
+
console.log(` ${COLORS.GRAY}(If you have a compression system, leaving this unlimited is preferred.)${COLORS.RESET}\n`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
isAnthropic = action === "start_anthropic";
|
|
389
|
+
const defaultPort = DEFAULT_PORTS[isAnthropic ? "anthropic" : "openai"];
|
|
390
|
+
const port = await promptNumber({ message: "Port:", default: defaultPort });
|
|
391
|
+
const host = await promptInput({ message: "Host:", default: "127.0.0.1" });
|
|
392
|
+
const key = await promptInput({ message: "Auth key:", default: "mewmew" });
|
|
393
|
+
|
|
394
|
+
process.env.PORT = String(port);
|
|
395
|
+
process.env.HOST = host;
|
|
396
|
+
process.env.PROXY_KEY = key;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const targetFile = isAnthropic
|
|
402
|
+
? path.join(__dirname, "..", "anthropic.js")
|
|
403
|
+
: path.join(__dirname, "..", "openai.js");
|
|
404
|
+
|
|
405
|
+
// Windows dynamic imports need a file:// URL, not a raw drive-letter path
|
|
406
|
+
await import(pathToFileURL(targetFile).href);
|