mslxdff 0.1.1 → 0.1.2
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 +26 -15
- package/bin/{mslxdfree.js → mslxdff.js} +49 -13
- package/package.json +3 -3
- package/src/daemon.js +4 -4
- package/src/server.js +11 -2
- package/src/state.js +33 -19
package/README.md
CHANGED
|
@@ -14,31 +14,42 @@ Zero runtime dependencies: Node ≥ 20, built-in `node:http`, `node:crypto`, `no
|
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
npm install # no deps actually fetched; just links the bin
|
|
17
|
-
|
|
17
|
+
mslxdff # or: node bin/mslxdff.js
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
First run generates a bearer token, writes it to the state file, and prints it:
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
|
|
23
|
+
mslxdff listening on http://localhost:8989
|
|
24
24
|
auth token: 9b5de021e914...
|
|
25
|
-
endpoint: http://localhost:
|
|
25
|
+
endpoint: http://localhost:8989/v1
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
### Port
|
|
29
|
+
|
|
30
|
+
Default port is **8989**. Persist a different port (and hot-restart the daemon onto it if one is running):
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
mslxdff -port 8000 # set port to 8000; restarts the daemon on 8000
|
|
34
|
+
mslxdff -d # next starts reuse the persisted port (8000)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Priority: `-port` arg > persisted port > `PORT` env > default `8989`.
|
|
38
|
+
|
|
28
39
|
### Daemon (background, stays resident)
|
|
29
40
|
|
|
30
41
|
```
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
mslxdff -d # start detached background daemon
|
|
43
|
+
mslxdff -stop # stop it
|
|
33
44
|
```
|
|
34
45
|
|
|
35
|
-
Logs go to `~/.config/
|
|
46
|
+
Logs go to `~/.config/mslxdff/daemon.log`, the daemon pid to `daemon.pid` (both overridable via `MSLXDFF_DAEMON_DIR`). The daemon keeps running after your shell exits.
|
|
36
47
|
|
|
37
48
|
### Token
|
|
38
49
|
|
|
39
50
|
```
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
mslxdff -showtoken # print the current token (creates one on first use)
|
|
52
|
+
mslxdff -refresh-token # rotate it (prints the new token, does not start the server)
|
|
42
53
|
```
|
|
43
54
|
|
|
44
55
|
## Client configuration
|
|
@@ -46,7 +57,7 @@ mslxdfree -refresh-token # rotate it (prints the new token, does not start the
|
|
|
46
57
|
Point any OpenAI-compatible client at the endpoint with the token:
|
|
47
58
|
|
|
48
59
|
```
|
|
49
|
-
Endpoint: http://localhost:
|
|
60
|
+
Endpoint: http://localhost:8989/v1
|
|
50
61
|
API Key: <the bearer token> (sent as Authorization: Bearer <token>)
|
|
51
62
|
Model: oc/deepseek-v4-flash-free (the oc/ prefix is optional)
|
|
52
63
|
```
|
|
@@ -54,7 +65,7 @@ Model: oc/deepseek-v4-flash-free (the oc/ prefix is optional)
|
|
|
54
65
|
<x-model list>
|
|
55
66
|
|
|
56
67
|
```
|
|
57
|
-
$ curl -H "Authorization: Bearer <token>" http://localhost:
|
|
68
|
+
$ curl -H "Authorization: Bearer <token>" http://localhost:8989/v1/models
|
|
58
69
|
{"object":"list","data":[{"id":"big-pickle",...},{"id":"deepseek-v4-flash-free",...}, ...]}
|
|
59
70
|
</x-model list>
|
|
60
71
|
|
|
@@ -62,9 +73,9 @@ $ curl -H "Authorization: Bearer <token>" http://localhost:8080/v1/models
|
|
|
62
73
|
|
|
63
74
|
| Variable | Default | Purpose |
|
|
64
75
|
|---|---|---|
|
|
65
|
-
| `PORT` | `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
76
|
+
| `PORT` | `8989` | listen port (used when no `-port` arg and no persisted port) |
|
|
77
|
+
| `MSLXDFF_STATE_FILE` | `~/.config/mslxdff/state.json` | token/port state file (mode 0600) |
|
|
78
|
+
| `MSLXDFF_DAEMON_DIR` | `~/.config/mslxdff` | daemon pid + log directory |
|
|
68
79
|
| `UPSTREAM_BASE_URL` | `https://opencode.ai` | upstream base |
|
|
69
80
|
| `UPSTREAM_AUTH_TOKEN` | `public` | upstream `Authorization: Bearer <…>` value |
|
|
70
81
|
| `UPSTREAM_CONNECT_TIMEOUT_MS` | `30000` | upstream connect timeout |
|
|
@@ -72,7 +83,7 @@ $ curl -H "Authorization: Bearer <token>" http://localhost:8080/v1/models
|
|
|
72
83
|
|
|
73
84
|
## Clients
|
|
74
85
|
|
|
75
|
-
Point your OpenAI client at `http://<host>:
|
|
86
|
+
Point your OpenAI client at `http://<host>:8989/v1` (or the equivalent config seen above). Works for streaming and non-streaming chat completions.
|
|
76
87
|
|
|
77
88
|
## Development
|
|
78
89
|
|
|
@@ -80,4 +91,4 @@ Point your OpenAI client at `http://<host>:8080/v1` (or the equivalent config se
|
|
|
80
91
|
npm test # node --test, no network access
|
|
81
92
|
```
|
|
82
93
|
|
|
83
|
-
The reference implementation is 9Router v0.5.45 (`/root/9router`); see `CLAUDE.md`, `CONTEXT.md`, and `docs/adr/` for the contract and the decisions behind it.
|
|
94
|
+
The reference implementation is 9Router v0.5.45 (`/root/9router`); see `CLAUDE.md`, `CONTEXT.md`, and `docs/adr/` for the contract and the decisions behind it.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { startServer } from "../src/server.js";
|
|
2
|
+
import { startServer, resolvePort } from "../src/server.js";
|
|
3
3
|
import { createRouter } from "../src/routes.js";
|
|
4
4
|
import { createUpstreamClient } from "../src/upstream.js";
|
|
5
5
|
import { createModelsService } from "../src/models.js";
|
|
6
|
-
import { loadToken, refreshToken } from "../src/state.js";
|
|
7
|
-
import { startDaemon, stopDaemon, writePid, pidFile, logFile } from "../src/daemon.js";
|
|
6
|
+
import { loadToken, refreshToken, setPort } from "../src/state.js";
|
|
7
|
+
import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid } from "../src/daemon.js";
|
|
8
8
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
|
|
@@ -23,19 +23,43 @@ if (args.includes("-showtoken") || args.includes("--showtoken")) {
|
|
|
23
23
|
if (args.includes("-stop") || args.includes("--stop")) {
|
|
24
24
|
const { stopped, pid, reason } = stopDaemon();
|
|
25
25
|
if (stopped) {
|
|
26
|
-
console.log(`
|
|
26
|
+
console.log(`mslxdff daemon stopped (pid ${pid})`);
|
|
27
27
|
} else {
|
|
28
|
-
console.log(`
|
|
28
|
+
console.log(`mslxdff daemon not running${reason ? ` (${reason})` : ""}`);
|
|
29
|
+
}
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// -port N: persist the port, then restart the daemon on it if one is running.
|
|
34
|
+
// Skip when we ARE the daemon child (it already carries the port via args).
|
|
35
|
+
const portArg = argValue("-port", "--port");
|
|
36
|
+
if (portArg && !process.env.MSLXDFF_DAEMON) {
|
|
37
|
+
const port = Number(portArg);
|
|
38
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
39
|
+
console.error(`invalid port: ${portArg}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
setPort(port);
|
|
43
|
+
const daemon = readPid();
|
|
44
|
+
if (daemon) {
|
|
45
|
+
stopDaemon();
|
|
46
|
+
startDaemon(["-port", String(port)]);
|
|
47
|
+
await waitForHealth(port, 4000);
|
|
48
|
+
console.log(`mslxdff restarted on port ${port} (pid ${readPid()})`);
|
|
49
|
+
console.log(`endpoint: http://localhost:${port}/v1`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`port saved: ${port} (daemon not running; takes effect on next start)`);
|
|
29
52
|
}
|
|
30
53
|
process.exit(0);
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
if (args.includes("-d") || args.includes("--daemon")) {
|
|
34
|
-
if (!process.env.
|
|
57
|
+
if (!process.env.MSLXDFF_DAEMON) {
|
|
35
58
|
// foreground: spawn the detached background instance, then wait for health
|
|
59
|
+
const port = effectivePort();
|
|
36
60
|
const spawnedPid = startDaemon(args.filter((a) => a !== "-d" && a !== "--daemon"));
|
|
37
|
-
await waitForHealth(4000);
|
|
38
|
-
console.log(`
|
|
61
|
+
await waitForHealth(port, 4000);
|
|
62
|
+
console.log(`mslxdff daemon started (pid ${spawnedPid})`);
|
|
39
63
|
console.log(`log: ${logFile()}`);
|
|
40
64
|
console.log(`pid: ${pidFile()}`);
|
|
41
65
|
process.exit(0);
|
|
@@ -52,20 +76,32 @@ const router = createRouter({ token, upstream, models });
|
|
|
52
76
|
const srv = startServer({ router });
|
|
53
77
|
|
|
54
78
|
await srv.ready();
|
|
55
|
-
if (process.env.
|
|
79
|
+
if (process.env.MSLXDFF_DAEMON) {
|
|
56
80
|
writePid(process.pid);
|
|
57
81
|
}
|
|
58
82
|
const addr = srv.server.address();
|
|
59
83
|
const host = addr.address === "0.0.0.0" || addr.address === "::" ? "localhost" : addr.address;
|
|
60
|
-
console.log(`
|
|
84
|
+
console.log(`mslxdff listening on http://${host}:${addr.port}`);
|
|
61
85
|
if (created) {
|
|
62
86
|
console.log(`auth token: ${token}`);
|
|
63
87
|
}
|
|
64
88
|
console.log(`endpoint: http://${host}:${addr.port}/v1`);
|
|
65
89
|
|
|
66
|
-
|
|
90
|
+
function argValue(...names) {
|
|
91
|
+
for (let i = 0; i < args.length; i++) {
|
|
92
|
+
if (names.includes(args[i])) return args[i + 1];
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function effectivePort() {
|
|
98
|
+
const arg = argValue("-port", "--port");
|
|
99
|
+
if (arg) return Number(arg);
|
|
100
|
+
return resolvePort();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function waitForHealth(port, timeoutMs) {
|
|
67
104
|
const start = Date.now();
|
|
68
|
-
const port = Number(process.env.PORT) || 8080;
|
|
69
105
|
while (Date.now() - start < timeoutMs) {
|
|
70
106
|
try {
|
|
71
107
|
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
@@ -75,4 +111,4 @@ async function waitForHealth(timeoutMs) {
|
|
|
75
111
|
}
|
|
76
112
|
await new Promise((r) => setTimeout(r, 100));
|
|
77
113
|
}
|
|
78
|
-
}
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mslxdff",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "测试项目,请勿使用。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"mslxdff": "bin/mslxdff.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node bin/
|
|
10
|
+
"start": "node bin/mslxdff.js",
|
|
11
11
|
"test": "node --test test/"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
package/src/daemon.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
export function daemonDir() {
|
|
8
|
-
return process.env.
|
|
8
|
+
return process.env.MSLXDFF_DAEMON_DIR || join(os.homedir(), ".config", "mslxdff");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function pidFile() {
|
|
@@ -18,16 +18,16 @@ export function logFile() {
|
|
|
18
18
|
|
|
19
19
|
export function startDaemon(args = []) {
|
|
20
20
|
const here = fileURLToPath(import.meta.url);
|
|
21
|
-
const entry = here.endsWith("bin/
|
|
21
|
+
const entry = here.endsWith("bin/mslxdff.js")
|
|
22
22
|
? here
|
|
23
|
-
: join(dirname(here), "..", "bin", "
|
|
23
|
+
: join(dirname(here), "..", "bin", "mslxdff.js");
|
|
24
24
|
const dir = daemonDir();
|
|
25
25
|
mkdirSync(dir, { recursive: true });
|
|
26
26
|
const logFd = openSync(logFile(), "a", 0o600);
|
|
27
27
|
const child = spawn(process.execPath, [entry, ...args, "--daemon"], {
|
|
28
28
|
detached: true,
|
|
29
29
|
stdio: ["ignore", logFd, logFd],
|
|
30
|
-
env: { ...process.env,
|
|
30
|
+
env: { ...process.env, MSLXDFF_DAEMON: "1" },
|
|
31
31
|
});
|
|
32
32
|
child.unref();
|
|
33
33
|
return child.pid;
|
package/src/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer as httpCreateServer } from "node:http";
|
|
2
|
+
import { DEFAULT_PORT, getPort } from "./state.js";
|
|
2
3
|
|
|
3
|
-
export function startServer({ router }, port =
|
|
4
|
+
export function startServer({ router }, port = resolvePort()) {
|
|
4
5
|
const server = httpCreateServer((req, res) => {
|
|
5
6
|
router(req, res).catch((err) => {
|
|
6
7
|
res.statusCode = 500;
|
|
@@ -24,4 +25,12 @@ export function startServer({ router }, port = Number(process.env.PORT) || 8080)
|
|
|
24
25
|
process.on("SIGTERM", close);
|
|
25
26
|
|
|
26
27
|
return { server, ready, close };
|
|
27
|
-
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolvePort() {
|
|
31
|
+
const persisted = getPort();
|
|
32
|
+
if (persisted) return persisted;
|
|
33
|
+
const env = Number(process.env.PORT);
|
|
34
|
+
if (Number.isInteger(env) && env > 0) return env;
|
|
35
|
+
return DEFAULT_PORT;
|
|
36
|
+
}
|
package/src/state.js
CHANGED
|
@@ -3,9 +3,11 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
|
|
6
|
+
export const DEFAULT_PORT = 8989;
|
|
7
|
+
|
|
6
8
|
export function defaultStateFile() {
|
|
7
|
-
return process.env.
|
|
8
|
-
join(os.homedir(), ".config", "
|
|
9
|
+
return process.env.MSLXDFF_STATE_FILE ||
|
|
10
|
+
join(os.homedir(), ".config", "mslxdff", "state.json");
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function generateToken() {
|
|
@@ -13,27 +15,39 @@ export function generateToken() {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export async function loadToken({ file = defaultStateFile() } = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return { token: saved.token, created: false };
|
|
20
|
-
}
|
|
21
|
-
} catch {
|
|
22
|
-
// missing or unreadable → generate fresh below
|
|
18
|
+
const state = readState(file);
|
|
19
|
+
if (typeof state.token === "string" && state.token.length > 0) {
|
|
20
|
+
return { token: state.token, created: false };
|
|
23
21
|
}
|
|
24
|
-
return { token:
|
|
22
|
+
return { token: writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token, created: true };
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
export async function refreshToken({ file = defaultStateFile() } = {}) {
|
|
28
|
-
return
|
|
26
|
+
return writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setPort(port, { file = defaultStateFile() } = {}) {
|
|
30
|
+
const state = readState(file);
|
|
31
|
+
writeState(file, { ...state, port: Number(port) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getPort({ file = defaultStateFile() } = {}) {
|
|
35
|
+
const port = readState(file).port;
|
|
36
|
+
return typeof port === "number" && Number.isInteger(port) && port > 0 ? port : null;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
39
|
+
function readState(file) {
|
|
40
|
+
try {
|
|
41
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
42
|
+
return typeof saved === "object" && saved !== null ? saved : {};
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeState(file, patch) {
|
|
49
|
+
const merged = { ...readState(file), ...patch };
|
|
36
50
|
mkdirSync(dirname(file), { recursive: true });
|
|
37
|
-
writeFileSync(file, JSON.stringify(
|
|
38
|
-
return
|
|
39
|
-
}
|
|
51
|
+
writeFileSync(file, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
52
|
+
return merged;
|
|
53
|
+
}
|