@waibiwaibig/all-api 0.1.0 → 0.1.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 +16 -8
- package/package.json +1 -1
- package/src/cli.mjs +118 -5
package/README.md
CHANGED
|
@@ -12,25 +12,25 @@ Implemented surface:
|
|
|
12
12
|
|
|
13
13
|
## Use
|
|
14
14
|
|
|
15
|
-
From
|
|
15
|
+
From npm:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
|
|
19
|
-
cd all-api
|
|
20
|
-
npm install
|
|
21
|
-
npm link
|
|
18
|
+
npm install -g @waibiwaibig/all-api
|
|
22
19
|
all-api setup
|
|
23
20
|
```
|
|
24
21
|
|
|
25
|
-
From
|
|
22
|
+
From source:
|
|
26
23
|
|
|
27
24
|
```sh
|
|
28
|
-
|
|
25
|
+
git clone https://github.com/waibiwaibig/all-api.git
|
|
26
|
+
cd all-api
|
|
27
|
+
npm install
|
|
28
|
+
npm link
|
|
29
29
|
all-api setup
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
`setup` asks for the workspace directory, creates an API key, starts the server,
|
|
33
|
-
and
|
|
33
|
+
in the background, prints the connection details, and exits:
|
|
34
34
|
|
|
35
35
|
```text
|
|
36
36
|
Base URL:
|
|
@@ -62,6 +62,12 @@ Create another key:
|
|
|
62
62
|
all-api key create --models claude-code
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Stop the background server:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
all-api stop
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
## Notes
|
|
66
72
|
|
|
67
73
|
- No runtime dependencies.
|
|
@@ -71,3 +77,5 @@ all-api key create --models claude-code
|
|
|
71
77
|
- Codex runs with `--sandbox read-only`.
|
|
72
78
|
- Claude runs with `--permission-mode plan`.
|
|
73
79
|
- Binds to `127.0.0.1` by default.
|
|
80
|
+
- `/v1` is the OpenAI API version prefix. Use `http://127.0.0.1:4011/v1`
|
|
81
|
+
for OpenAI-compatible clients; root paths like `/chat/completions` also work.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import readline from "node:readline/promises";
|
|
@@ -21,6 +21,7 @@ Usage:
|
|
|
21
21
|
all-api init [--config FILE] [--workspace DIR]
|
|
22
22
|
all-api setup [--config FILE] [--workspace DIR] [--host HOST] [--port PORT] [--yes]
|
|
23
23
|
all-api up [--config FILE] [--host HOST] [--port PORT]
|
|
24
|
+
all-api stop [--config FILE]
|
|
24
25
|
all-api detect
|
|
25
26
|
all-api key create [--config FILE] [--models MODEL,MODEL]
|
|
26
27
|
|
|
@@ -28,6 +29,7 @@ Examples:
|
|
|
28
29
|
all-api init --workspace /path/to/repo
|
|
29
30
|
all-api setup
|
|
30
31
|
all-api up
|
|
32
|
+
all-api stop
|
|
31
33
|
all-api key create --models codex-local,claude-code
|
|
32
34
|
`);
|
|
33
35
|
}
|
|
@@ -255,7 +257,7 @@ async function cmdSetup(args) {
|
|
|
255
257
|
if (!/^n/i.test(keep)) {
|
|
256
258
|
config.host = argValue(args, "--host", config.host ?? "127.0.0.1");
|
|
257
259
|
config.port = Number(argValue(args, "--port", config.port ?? DEFAULT_PORT));
|
|
258
|
-
|
|
260
|
+
if (!(await ensureDaemon(configPath, config))) return;
|
|
259
261
|
printEndpoint(config, null);
|
|
260
262
|
return;
|
|
261
263
|
}
|
|
@@ -284,10 +286,97 @@ async function cmdSetup(args) {
|
|
|
284
286
|
console.log(`Created ${configPath}`);
|
|
285
287
|
}
|
|
286
288
|
|
|
287
|
-
|
|
289
|
+
if (!(await ensureDaemon(configPath, config))) return;
|
|
288
290
|
printEndpoint(config, generatedKey);
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
async function ensureDaemon(configPath, config) {
|
|
294
|
+
if (await isServerHealthy(config)) return true;
|
|
295
|
+
|
|
296
|
+
const pidPath = pidFileForConfig(configPath);
|
|
297
|
+
const oldPid = readPid(pidPath);
|
|
298
|
+
if (oldPid && isProcessRunning(oldPid)) {
|
|
299
|
+
console.error(`A process is already recorded for this config: ${oldPid}`);
|
|
300
|
+
console.error(`If it is stale, remove ${pidPath}`);
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
ensureDir(dirname(pidPath));
|
|
306
|
+
const child = spawn(process.execPath, [
|
|
307
|
+
realpathSync(fileURLToPath(import.meta.url)),
|
|
308
|
+
"up",
|
|
309
|
+
"--config",
|
|
310
|
+
configPath,
|
|
311
|
+
"--host",
|
|
312
|
+
config.host,
|
|
313
|
+
"--port",
|
|
314
|
+
String(config.port),
|
|
315
|
+
], {
|
|
316
|
+
detached: true,
|
|
317
|
+
stdio: "ignore",
|
|
318
|
+
});
|
|
319
|
+
child.unref();
|
|
320
|
+
writeFileSync(pidPath, `${child.pid}\n`, { mode: 0o600 });
|
|
321
|
+
|
|
322
|
+
const ready = await waitForHealth(config, 5000);
|
|
323
|
+
if (!ready) {
|
|
324
|
+
console.error("Server did not become ready within 5 seconds.");
|
|
325
|
+
console.error(`PID file: ${pidPath}`);
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function waitForHealth(config, timeoutMs) {
|
|
333
|
+
const started = Date.now();
|
|
334
|
+
while (Date.now() - started < timeoutMs) {
|
|
335
|
+
if (await isServerHealthy(config)) return true;
|
|
336
|
+
await sleep(100);
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function sleep(ms) {
|
|
342
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isServerHealthy(config) {
|
|
346
|
+
return new Promise((resolveHealth) => {
|
|
347
|
+
const host = config.host === "0.0.0.0" ? "127.0.0.1" : config.host;
|
|
348
|
+
const req = http.request(`http://${host}:${config.port}/health`, { method: "GET", timeout: 500 }, (res) => {
|
|
349
|
+
res.resume();
|
|
350
|
+
resolveHealth(res.statusCode === 200);
|
|
351
|
+
});
|
|
352
|
+
req.on("error", () => resolveHealth(false));
|
|
353
|
+
req.on("timeout", () => {
|
|
354
|
+
req.destroy();
|
|
355
|
+
resolveHealth(false);
|
|
356
|
+
});
|
|
357
|
+
req.end();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function pidFileForConfig(configPath) {
|
|
362
|
+
return join(dirname(configPath), `server-${sha256(resolve(configPath)).slice(0, 12)}.pid`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readPid(pidPath) {
|
|
366
|
+
if (!existsSync(pidPath)) return null;
|
|
367
|
+
const pid = Number(readFileSync(pidPath, "utf8").trim());
|
|
368
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isProcessRunning(pid) {
|
|
372
|
+
try {
|
|
373
|
+
process.kill(pid, 0);
|
|
374
|
+
return true;
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
291
380
|
async function ask(question, defaultValue) {
|
|
292
381
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
293
382
|
try {
|
|
@@ -341,6 +430,28 @@ async function cmdUp(args) {
|
|
|
341
430
|
printEndpoint(config, generatedKey);
|
|
342
431
|
}
|
|
343
432
|
|
|
433
|
+
async function cmdStop(args) {
|
|
434
|
+
const configPath = resolve(argValue(args, "--config", DEFAULT_CONFIG));
|
|
435
|
+
const pidPath = pidFileForConfig(configPath);
|
|
436
|
+
const pid = readPid(pidPath);
|
|
437
|
+
if (!pid) {
|
|
438
|
+
console.log("No all-api server PID found.");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
process.kill(-pid, "SIGTERM");
|
|
444
|
+
} catch {
|
|
445
|
+
try {
|
|
446
|
+
process.kill(pid, "SIGTERM");
|
|
447
|
+
} catch {
|
|
448
|
+
// Already stopped.
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
rmSync(pidPath, { force: true });
|
|
452
|
+
console.log(`Stopped all-api server ${pid}.`);
|
|
453
|
+
}
|
|
454
|
+
|
|
344
455
|
function printEndpoint(config, key) {
|
|
345
456
|
const hostForPrint = config.host === "0.0.0.0" ? "localhost" : config.host;
|
|
346
457
|
console.log("");
|
|
@@ -390,7 +501,7 @@ async function route(req, res, config) {
|
|
|
390
501
|
return;
|
|
391
502
|
}
|
|
392
503
|
|
|
393
|
-
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
504
|
+
if (req.method === "GET" && (url.pathname === "/v1/models" || url.pathname === "/models")) {
|
|
394
505
|
sendJson(res, 200, {
|
|
395
506
|
object: "list",
|
|
396
507
|
data: allowedModels(config, auth.key).map((model) => ({
|
|
@@ -403,7 +514,7 @@ async function route(req, res, config) {
|
|
|
403
514
|
return;
|
|
404
515
|
}
|
|
405
516
|
|
|
406
|
-
if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
|
|
517
|
+
if (req.method === "POST" && (url.pathname === "/v1/chat/completions" || url.pathname === "/chat/completions")) {
|
|
407
518
|
const body = await readJsonBody(req);
|
|
408
519
|
const model = config.models.find((m) => m.enabled && m.id === body.model);
|
|
409
520
|
if (!model) {
|
|
@@ -653,6 +764,8 @@ async function main(args = process.argv.slice(2)) {
|
|
|
653
764
|
await cmdSetup(args.slice(1));
|
|
654
765
|
} else if (command === "up") {
|
|
655
766
|
await cmdUp(args.slice(1));
|
|
767
|
+
} else if (command === "stop") {
|
|
768
|
+
await cmdStop(args.slice(1));
|
|
656
769
|
} else if (command === "detect") {
|
|
657
770
|
await cmdDetect();
|
|
658
771
|
} else if (command === "key" && args[1] === "create") {
|