mslxdff 0.1.2 → 0.1.3

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/bin/mslxdff.js CHANGED
@@ -1,12 +1,32 @@
1
1
  #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join } from "node:path";
2
6
  import { startServer, resolvePort } from "../src/server.js";
3
7
  import { createRouter } from "../src/routes.js";
4
8
  import { createUpstreamClient } from "../src/upstream.js";
5
9
  import { createModelsService } from "../src/models.js";
6
- import { loadToken, refreshToken, setPort } from "../src/state.js";
10
+ import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroupsJoined } from "../src/state.js";
7
11
  import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid } from "../src/daemon.js";
12
+ import { createAutoSelector } from "../src/auto.js";
13
+ import { createPeersService } from "../src/peers.js";
14
+ import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
15
+ import { logDir, recentCalls, lastError, appendCall, appendError } from "../src/logs.js";
16
+ const logs = { appendCall, appendError };
8
17
 
9
18
  const args = process.argv.slice(2);
19
+ const VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
20
+
21
+ if (args.includes("-help") || args.includes("--help") || args.includes("-h")) {
22
+ printHelp();
23
+ process.exit(0);
24
+ }
25
+
26
+ if (args.includes("-update") || args.includes("--update")) {
27
+ await updateSelf();
28
+ process.exit(0);
29
+ }
10
30
 
11
31
  if (args.includes("-refresh-token") || args.includes("--refresh-token")) {
12
32
  const token = await refreshToken();
@@ -30,6 +50,169 @@ if (args.includes("-stop") || args.includes("--stop")) {
30
50
  process.exit(0);
31
51
  }
32
52
 
53
+ if (args.includes("-status") || args.includes("--status") || args.includes("-s")) {
54
+ await printStatus();
55
+ process.exit(0);
56
+ }
57
+
58
+ // -creategroup <name> | -group create <name> | -group sync | -group leave <name> | -group list |
59
+ // -addtogroup <leader-host> <name> | -resetban [ip]
60
+ const createGroupArg = argValue("-creategroup", "--creategroup") || groupIs("create", args);
61
+ if (createGroupArg) {
62
+ const groups = createGroupsService({});
63
+ const peers = createPeersService({});
64
+ const name = createGroupArg;
65
+ const { created } = groups.create(name); // the group name is the password
66
+ markJoined({ name, leaderUrl: "", myUrl: "", memberName: "leader" });
67
+ const synced = await syncAllJoinedGroups({ peers, groups });
68
+ const s = synced.find((x) => x.name === name);
69
+ console.log(created ? `group created: ${name}` : `group already exists: ${name}`);
70
+ console.log(`members on this node: ${s ? `${s.total} (failover: ${s.added})` : "?"}`);
71
+ console.log(`others join with: mslxdff -addtogroup <this-node-host> ${name}`);
72
+ process.exit(0);
73
+ }
74
+
75
+ function groupIs(action, argv) {
76
+ const idx = argv.indexOf("-group");
77
+ if (idx < 0) return null;
78
+ return argv[idx + 1] === action ? argv[idx + 2] : null;
79
+ }
80
+
81
+ const groupCmd = argValue("-group", "--group");
82
+ if (groupCmd && groupCmd !== "create") {
83
+ const groups = createGroupsService({});
84
+ const peers = createPeersService({});
85
+ const rest = args.slice(args.indexOf("-group") + 1);
86
+ const [action, a] = rest;
87
+ if (action === "sync") {
88
+ const synced = await syncAllJoinedGroups({ peers, groups });
89
+ if (!synced.length) {
90
+ console.log("not joined to any group (use -addtogroup or -creategroup)");
91
+ } else {
92
+ for (const s of synced) {
93
+ if (s.error) console.log(`${s.name}: sync failed — ${s.error}`);
94
+ else console.log(`${s.name}: ${s.total} member(s), ${s.added} failover target(s) configured`);
95
+ }
96
+ }
97
+ } else if (action === "leave" && a) {
98
+ const before = loadGroupsJoined().filter((g) => g.name === a).length;
99
+ saveGroupsJoined(loadGroupsJoined().filter((g) => g.name !== a));
100
+ const removed = peers.removeByGroup(a);
101
+ console.log(before ? `left group "${a}" (${removed} member(s) removed)` : `not a member of group "${a}"`);
102
+ } else if (action === "list") {
103
+ const all = groups.list();
104
+ const names = Object.keys(all);
105
+ if (names.length) {
106
+ for (const n of names) {
107
+ console.log(`${n} (${Object.keys(all[n].members || {}).length} members)`);
108
+ }
109
+ } else {
110
+ console.log("no groups on this node");
111
+ }
112
+ const joined = loadGroupsJoined();
113
+ if (joined.length) {
114
+ console.log(`\njoined groups (${joined.length}):`);
115
+ for (const g of joined) console.log(` ${g.name} ${g.leaderUrl || "(this node is the leader)"}`);
116
+ }
117
+ } else {
118
+ console.error("usage: mslxdff -creategroup <name> | -group sync | -group leave <name> | -group list");
119
+ }
120
+ process.exit(0);
121
+ }
122
+
123
+ // -addtogroup <leader-host> <name>
124
+ const addToGroupIdx = args.findIndex((x) => x === "-addtogroup" || x === "--addtogroup");
125
+ if (addToGroupIdx >= 0) {
126
+ const [leaderHost, name] = args.slice(addToGroupIdx + 1);
127
+ if (!leaderHost || !name) {
128
+ console.error("usage: mslxdff -addtogroup <leader-host> <name>");
129
+ process.exit(1);
130
+ }
131
+ const groups = createGroupsService({});
132
+ const peers = createPeersService({});
133
+ const myToken = (await loadToken()).token;
134
+ const leaderUrl = leaderHost.includes("://")
135
+ ? leaderHost.replace(/\/+$/, "")
136
+ : `http://${leaderHost}${leaderHost.includes(":") ? "" : ":8989"}`;
137
+ const myPort = effectivePort();
138
+ try {
139
+ const res = await fetch(`${leaderUrl}/v1/groups/join`, {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/json" },
142
+ body: JSON.stringify({ name, key: name, leaderUrl, myPort, token: myToken }),
143
+ });
144
+ if (!res.ok) {
145
+ const text = await res.text().catch(() => "");
146
+ throw new Error(`join failed (HTTP ${res.status}): ${text}`);
147
+ }
148
+ const data = await res.json();
149
+ const myUrl = data.you?.url || "";
150
+ markJoined({ name, leaderUrl, myUrl, memberName: myUrl });
151
+ const synced = await syncAllJoinedGroups({ peers, groups });
152
+ const s = synced.find((x) => x.name === name);
153
+ console.log(`joined group "${name}" at ${leaderUrl}`);
154
+ if (s?.error) console.log(` local failover setup failed: ${s.error}`);
155
+ else console.log(` ${s?.added ?? 0} failover target(s) configured`);
156
+ } catch (err) {
157
+ console.error(`join failed: ${err.message}`);
158
+ process.exit(1);
159
+ }
160
+ process.exit(0);
161
+ }
162
+
163
+ // -resetban [ip]
164
+ const resetBanArg = argValue("-resetban", "--resetban");
165
+ if (resetBanArg !== null || args.includes("-resetban") || args.includes("--resetban")) {
166
+ const bans = createBansService({});
167
+ const ip = resetBanArg || null;
168
+ bans.clear(ip || undefined);
169
+ console.log(ip ? `ban cleared for ${ip}` : "all bans cleared");
170
+ process.exit(0);
171
+ }
172
+
173
+ function markJoined(entry) {
174
+ const { name } = entry;
175
+ const list = loadGroupsJoined().filter((g) => g.name !== name);
176
+ saveGroupsJoined([...list, entry]);
177
+ }
178
+
179
+ const errMsg = (err) => String(err?.message || err);
180
+
181
+ // Sync every joined group into the local peer list. Leaders read their local
182
+ // groups state; members re-register with the leader (idempotent) to get the
183
+ // freshest member list. Returns per-group results.
184
+ async function syncAllJoinedGroups({ peers, groups }) {
185
+ const joined = loadGroupsJoined();
186
+ const myToken = (await loadToken()).token;
187
+ const results = [];
188
+ for (const g of joined) {
189
+ try {
190
+ if (g.leaderUrl) {
191
+ const members = await refreshGroupMembers(g.name, {
192
+ leaderUrl: g.leaderUrl,
193
+ memberName: g.memberName,
194
+ url: g.myUrl,
195
+ token: myToken,
196
+ });
197
+ results.push({
198
+ name: g.name,
199
+ ...syncPeersFromMembers({ peers, members, myUrl: g.myUrl, group: g.name }),
200
+ });
201
+ } else {
202
+ // this node is the leader: […] own entry is always skipped
203
+ const members = groups.list()[g.name]?.members ?? {};
204
+ results.push({
205
+ name: g.name,
206
+ ...syncPeersFromMembers({ peers, members, myUrl: g.myUrl, group: g.name, skipIds: ["leader"] }),
207
+ });
208
+ }
209
+ } catch (err) {
210
+ results.push({ name: g.name, error: errMsg(err) });
211
+ }
212
+ }
213
+ return results;
214
+ }
215
+
33
216
  // -port N: persist the port, then restart the daemon on it if one is running.
34
217
  // Skip when we ARE the daemon child (it already carries the port via args).
35
218
  const portArg = argValue("-port", "--port");
@@ -67,26 +250,74 @@ if (args.includes("-d") || args.includes("--daemon")) {
67
250
  // we ARE the daemon; stdout/stderr already point at the log file via startDaemon stdio
68
251
  }
69
252
 
253
+ // Bare run: if a daemon is already running, show status + help instead of starting another.
254
+ if (!process.env.MSLXDFF_DAEMON && readPid()) {
255
+ await printStatus();
256
+ printHelp();
257
+ process.exit(0);
258
+ }
259
+
70
260
  const { token, created } = await loadToken();
71
261
  const upstream = createUpstreamClient({});
72
262
  const baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
73
- const models = createModelsService({ baseUrl, headers: upstream.headers });
263
+ const models = createModelsService({
264
+ baseUrl,
265
+ headers: upstream.headers,
266
+ refreshMs: refreshIntervalMs(),
267
+ cacheFile: join(logDir(), "models.json"),
268
+ });
269
+ const auto = createAutoSelector({
270
+ cooldownMs: modelCooldownMs(),
271
+ loadCandidates: async () => {
272
+ try {
273
+ return (await models.get()).data.map((m) => m.id);
274
+ } catch {
275
+ return null;
276
+ }
277
+ },
278
+ });
279
+ const peers = createPeersService({ cooldownMs: peerCooldownMs() });
280
+ const groups = createGroupsService({});
281
+ const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
74
282
 
75
- const router = createRouter({ token, upstream, models });
283
+ const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans });
76
284
  const srv = startServer({ router });
77
285
 
78
286
  await srv.ready();
287
+ models.startAutoRefresh();
79
288
  if (process.env.MSLXDFF_DAEMON) {
80
289
  writePid(process.pid);
81
290
  }
82
291
  const addr = srv.server.address();
83
292
  const host = addr.address === "0.0.0.0" || addr.address === "::" ? "localhost" : addr.address;
84
- console.log(`mslxdff listening on http://${host}:${addr.port}`);
293
+ console.log(`mslxdff v${VERSION} listening on http://${host}:${addr.port}`);
85
294
  if (created) {
86
295
  console.log(`auth token: ${token}`);
87
296
  }
88
297
  console.log(`endpoint: http://${host}:${addr.port}/v1`);
89
298
 
299
+ // Periodically pull the freshest member lists for every joined group so a
300
+ // new member becomes a failover peer on all nodes without manual re-joining.
301
+ syncAllJoinedGroups({ peers, groups })
302
+ .then((results) => {
303
+ for (const r of results) {
304
+ if (r.error) console.log(`group sync ${r.name}: failed — ${r.error}`);
305
+ else console.log(`group sync ${r.name}: ${r.total} member(s), ${r.added} peer(s)`);
306
+ }
307
+ })
308
+ .catch((err) => console.log(`group sync: ${errMsg(err)}`));
309
+ const groupSyncTimer = setInterval(() => {
310
+ syncAllJoinedGroups({ peers, groups })
311
+ .then((results) => {
312
+ for (const r of results) {
313
+ if (r.error) console.log(`group sync ${r.name}: failed — ${r.error}`);
314
+ else if (r.added) console.log(`group sync ${r.name}: ${r.total} member(s), ${r.added} peer(s)`);
315
+ }
316
+ })
317
+ .catch((err) => console.log(`group sync: ${errMsg(err)}`));
318
+ }, groupSyncIntervalMs());
319
+ groupSyncTimer.unref();
320
+
90
321
  function argValue(...names) {
91
322
  for (let i = 0; i < args.length; i++) {
92
323
  if (names.includes(args[i])) return args[i + 1];
@@ -100,6 +331,41 @@ function effectivePort() {
100
331
  return resolvePort();
101
332
  }
102
333
 
334
+ function refreshIntervalMs() {
335
+ const n = Number(process.env.MODELS_REFRESH_MS);
336
+ return Number.isInteger(n) && n > 0 ? n : 2 * 60 * 60 * 1000;
337
+ }
338
+
339
+ function modelCooldownMs() {
340
+ const n = Number(process.env.MSLXDFF_MODEL_COOLDOWN_MS);
341
+ return Number.isInteger(n) && n > 0 ? n : 60_000;
342
+ }
343
+
344
+ function peerCooldownMs() {
345
+ const n = Number(process.env.MSLXDFF_PEER_COOLDOWN_MS);
346
+ return Number.isInteger(n) && n > 0 ? n : 30_000;
347
+ }
348
+
349
+ function maxHopsValue() {
350
+ const n = Number(process.env.MSLXDFF_MAX_HOPS);
351
+ return Number.isInteger(n) && n > 0 ? n : 3;
352
+ }
353
+
354
+ function groupSyncIntervalMs() {
355
+ const n = Number(process.env.MSLXDFF_GROUP_SYNC_MS);
356
+ return Number.isInteger(n) && n > 0 ? n : 60_000;
357
+ }
358
+
359
+ function banWindowMs() {
360
+ const n = Number(process.env.MSLXDFF_BAN_WINDOW_MS);
361
+ return Number.isInteger(n) && n > 0 ? n : 48 * 60 * 60 * 1000;
362
+ }
363
+
364
+ function banThreshold() {
365
+ const n = Number(process.env.MSLXDFF_BAN_THRESHOLD);
366
+ return Number.isInteger(n) && n > 0 ? n : 5;
367
+ }
368
+
103
369
  async function waitForHealth(port, timeoutMs) {
104
370
  const start = Date.now();
105
371
  while (Date.now() - start < timeoutMs) {
@@ -112,3 +378,199 @@ async function waitForHealth(port, timeoutMs) {
112
378
  await new Promise((r) => setTimeout(r, 100));
113
379
  }
114
380
  }
381
+
382
+ function printHelp() {
383
+ console.log(`mslxdff v${VERSION} — OpenCode Free OpenAI-compatible proxy
384
+
385
+ Usage:
386
+ mslxdff start the server (if none running); shows status when a daemon is already up
387
+ mslxdff -d start as a background daemon
388
+ mslxdff -status show current status (daemon, models, recent calls, last error)
389
+ mslxdff -stop stop the running daemon
390
+ mslxdff -port N persist the listen port (restarts the daemon on it if running)
391
+ mslxdff -update update mslxdff to the latest published version
392
+ mslxdff -showtoken print the current auth token
393
+ mslxdff -refresh-token rotate the auth token (prints the new one)
394
+ mslxdff -creategroup <name> create a group on this node (the group name is the password)
395
+ mslxdff -addtogroup <leader-host> <name> join a group via its leader host (default port 8989)
396
+ mslxdff -group sync pull the freshest member list for all joined groups
397
+ mslxdff -group leave <name> leave a group (removes its members from this node)
398
+ mslxdff -group list list groups on this node
399
+ mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
400
+ mslxdff -help show this help
401
+
402
+ Environment:
403
+ PORT listen port (default 8989)
404
+ MSLXDFF_STATE_FILE token/port state file
405
+ MSLXDFF_DAEMON_DIR daemon pid/log/models dir
406
+ UPSTREAM_BASE_URL upstream base (default https://opencode.ai)
407
+ UPSTREAM_AUTH_TOKEN upstream bearer value (default "public")
408
+ UPSTREAM_CONNECT_TIMEOUT_MS upstream connect timeout (default 30000)
409
+ MODELS_REFRESH_MS model-list background refresh interval (default 7200000)
410
+ MSLXDFF_MODEL_COOLDOWN_MS fallback cooldown after a model error (default 60000)
411
+ MSLXDFF_PEER_COOLDOWN_MS peer failover cooldown (default 30000)
412
+ MSLXDFF_GROUP_SYNC_MS group membership sync interval (default 60000)
413
+ MSLXDFF_MAX_HOPS max peer-forwarding depth (default 3)
414
+ MSLXDFF_BAN_THRESHOLD failed joins before an ip is banned (default 5)
415
+ MSLXDFF_BAN_WINDOW_MS ban duration after too many failures (default 48h)
416
+ `);
417
+ }
418
+
419
+ async function printStatus() {
420
+ const daemon = readPid();
421
+ const port = getPort() || resolvePort();
422
+ console.log(`mslxdff v${VERSION}`);
423
+ console.log(`daemon: ${daemon ? `running (pid ${daemon})` : "not running"}`);
424
+ console.log(`endpoint: http://localhost:${port}/v1`);
425
+ console.log(`log dir: ${logDir()}`);
426
+
427
+ const groups = createGroupsService({});
428
+ const joined = loadGroupsJoined();
429
+ if (joined.length) {
430
+ console.log(`\njoined groups (${joined.length}):`);
431
+ const { token } = await loadToken();
432
+ for (const g of joined) {
433
+ const isLeader = !g.leaderUrl;
434
+ console.log(` ${g.name} ${isLeader ? "(this node is the leader)" : `leader ${g.leaderUrl}`}`);
435
+ let members = null;
436
+ if (isLeader) {
437
+ members = groups.list()[g.name]?.members ?? {};
438
+ } else {
439
+ // registered members can pull the member map back from the leader
440
+ const fetchImpl = (url, opts) => fetch(url, { ...opts, signal: AbortSignal.timeout(1500) });
441
+ try {
442
+ members = await refreshGroupMembers(g.name, {
443
+ leaderUrl: g.leaderUrl,
444
+ memberName: g.memberName,
445
+ url: g.myUrl,
446
+ token,
447
+ fetchImpl,
448
+ });
449
+ } catch {
450
+ members = null;
451
+ }
452
+ }
453
+ if (members === null) {
454
+ console.log(" members: (unavailable — leader unreachable)");
455
+ } else {
456
+ const ids = Object.keys(members);
457
+ if (!ids.length) {
458
+ console.log(" members: (none yet)");
459
+ } else {
460
+ console.log(` members (${ids.length}):`);
461
+ for (const id of ids) {
462
+ const m = members[id];
463
+ const tags = [];
464
+ if (id === "leader" && isLeader) tags.push("leader");
465
+ if (m?.url && m.url === g.myUrl) tags.push("this node");
466
+ const tag = tags.length ? ` [${tags.join(", ")}]` : "";
467
+ console.log(` ${m?.url || id}${tag}`);
468
+ }
469
+ }
470
+ }
471
+ }
472
+ } else {
473
+ console.log("\njoined groups: (none — use -creategroup or -addtogroup)");
474
+ }
475
+
476
+ const peers = createPeersService({});
477
+ const allPeers = peers.all();
478
+ if (allPeers.length) {
479
+ console.log(`\nfailover targets (${allPeers.length}):`);
480
+ for (const p of allPeers) {
481
+ const cooling = peers.isCooling(p.url) ? " [cooling]" : "";
482
+ console.log(` ${p.name || p.url} ${p.url}${cooling}`);
483
+ }
484
+ }
485
+
486
+ const groupNames = Object.keys(groups.list());
487
+ if (groupNames.length) {
488
+ console.log(`\ngroups on this node (${groupNames.length}):`);
489
+ for (const n of groupNames) console.log(` ${n}`);
490
+ }
491
+
492
+ const modelsFile = join(logDir(), "models.json");
493
+ if (existsSync(modelsFile)) {
494
+ try {
495
+ const cached = JSON.parse(readFileSync(modelsFile, "utf8"));
496
+ const ids = (cached.data || []).map((m) => m.id).filter(Boolean);
497
+ console.log(`\nmodels (${ids.length} free):`);
498
+ for (const id of ids) console.log(` ${id}`);
499
+ } catch {
500
+ console.log("\nmodels: cache unreadable");
501
+ }
502
+ } else {
503
+ console.log("\nmodels: not cached yet (runs once the server has fetched the upstream list)");
504
+ }
505
+
506
+ console.log("\nrecent calls:");
507
+ const calls = recentCalls(5);
508
+ if (calls.length) {
509
+ for (const c of calls) {
510
+ console.log(` ${fmtTs(c.ts)} ${c.model || "-"} ${c.status} ${c.durationMs ?? "?"}ms${c.auto ? " auto" : ""}`);
511
+ }
512
+ } else {
513
+ console.log(" (none yet)");
514
+ }
515
+
516
+ console.log("\nlast error:");
517
+ const err = lastError();
518
+ if (err) {
519
+ console.log(` ${fmtTs(err.ts)} ${err.model || "-"} ${err.status} ${err.message || ""}`);
520
+ } else {
521
+ console.log(" (none)");
522
+ }
523
+
524
+ if (daemon) console.log(`\nauth token: use \`mslxdff -showtoken\``);
525
+ else console.log(`\nnot running — start with: mslxdff -d`);
526
+ }
527
+
528
+ function fmtTs(iso) {
529
+ if (!iso) return "-";
530
+ try {
531
+ return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
532
+ } catch {
533
+ return "-";
534
+ }
535
+ }
536
+
537
+ function npmCmd() {
538
+ return process.platform === "win32" ? "npm.cmd" : "npm";
539
+ }
540
+
541
+ function run(cmd, args, opts = {}) {
542
+ return new Promise((resolve) => {
543
+ execFile(cmd, args, { timeout: 120_000, ...opts }, (err, stdout, stderr) => resolve({ err, stdout, stderr }));
544
+ });
545
+ }
546
+
547
+ async function updateSelf() {
548
+ console.log(`mslxdff v${VERSION} — checking for updates…`);
549
+ const info = await run(npmCmd(), ["view", "mslxdff", "version", "dist-tags.latest"]);
550
+ if (info.err) {
551
+ console.error(`could not query npm: ${info.err.message}`);
552
+ process.exit(1);
553
+ }
554
+ const [version, latest] = (info.stdout || "").trim().split(/\s+/);
555
+ console.log(` installed: ${version}`);
556
+ console.log(` latest: ${latest}`);
557
+ if (version === latest) {
558
+ console.log("already up to date");
559
+ process.exit(0);
560
+ }
561
+ console.log(`updating to ${latest}…`);
562
+ const up = await run(npmCmd(), ["install", "-g", `mslxdff@${latest}`], { stdio: "inherit" });
563
+ if (up.err) {
564
+ console.error(`update failed: ${up.err.message}`);
565
+ process.exit(1);
566
+ }
567
+ console.log(`updated to ${latest}`);
568
+ const daemon = readPid();
569
+ if (daemon) {
570
+ console.log("restarting daemon on the new version…");
571
+ stopDaemon();
572
+ startDaemon([]);
573
+ await waitForHealth(resolvePort(), 4000);
574
+ console.log(`restarted (pid ${readPid()})`);
575
+ }
576
+ }
package/package.json CHANGED
@@ -1,27 +1,27 @@
1
- {
2
- "name": "mslxdff",
3
- "version": "0.1.2",
4
- "description": "测试项目,请勿使用。",
5
- "type": "module",
6
- "bin": {
7
- "mslxdff": "bin/mslxdff.js"
8
- },
9
- "scripts": {
10
- "start": "node bin/mslxdff.js",
11
- "test": "node --test test/"
12
- },
13
- "engines": {
14
- "node": ">=20"
15
- },
16
- "files": [
17
- "bin/",
18
- "src/",
19
- "README.md"
20
- ],
21
- "keywords": [
22
- "test",
23
- "demo",
24
- "placeholder"
25
- ],
26
- "license": "MIT"
1
+ {
2
+ "name": "mslxdff",
3
+ "version": "0.1.3",
4
+ "description": "测试项目,请勿使用。",
5
+ "type": "module",
6
+ "bin": {
7
+ "mslxdff": "bin/mslxdff.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/mslxdff.js",
11
+ "test": "node --test test/"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "files": [
17
+ "bin/",
18
+ "src/",
19
+ "README.md"
20
+ ],
21
+ "keywords": [
22
+ "test",
23
+ "demo",
24
+ "placeholder"
25
+ ],
26
+ "license": "MIT"
27
27
  }
package/src/auto.js ADDED
@@ -0,0 +1,99 @@
1
+ import { loadModelErrors, saveModelErrors } from "./state.js";
2
+
3
+ export const DEFAULT_AUTO_MODELS = [
4
+ "deepseek-v4-flash-free",
5
+ "mimo-v2.5-free",
6
+ "ling-3.0-flash-free",
7
+ "nemotron-3-ultra-free",
8
+ "north-mini-code-free",
9
+ "laguna-s-2.1-free",
10
+ "big-pickle",
11
+ ];
12
+
13
+ export function isAutoModel(model) {
14
+ return !model || model === "auto";
15
+ }
16
+
17
+ export const DEFAULT_COOLDOWN_MS = 60_000;
18
+
19
+ function inCooldown(id, errors, now, cooldownMs) {
20
+ if (!cooldownMs) return false;
21
+ const err = errors[id];
22
+ return typeof err === "number" && now - err < cooldownMs;
23
+ }
24
+
25
+ export function rankModels(ids, errors = {}, { now = Date.now(), cooldownMs = 0 } = {}) {
26
+ return [...new Set(ids)]
27
+ .filter(Boolean)
28
+ .map((id) => ({
29
+ id,
30
+ err: typeof errors[id] === "number" ? errors[id] : 0,
31
+ isDeepseek: /deepseek/i.test(id),
32
+ cooling: inCooldown(id, errors, now, cooldownMs),
33
+ }))
34
+ .sort(
35
+ (a, b) =>
36
+ (a.cooling ? 1 : 0) - (b.cooling ? 1 : 0) ||
37
+ a.err - b.err ||
38
+ (b.isDeepseek ? 1 : 0) - (a.isDeepseek ? 1 : 0)
39
+ )
40
+ .map((x) => x.id);
41
+ }
42
+
43
+ export function createAutoSelector({
44
+ loadCandidates,
45
+ file,
46
+ now = () => Date.now(),
47
+ cooldownMs = DEFAULT_COOLDOWN_MS,
48
+ errors: seedErrors,
49
+ persist = (errors, f = file) => saveModelErrors(errors, f ? { file: f } : {}),
50
+ } = {}) {
51
+ const lastErrorAt = { ...(seedErrors ?? loadModelErrors(file ? { file } : {})) };
52
+
53
+ async function loadList() {
54
+ let list;
55
+ try {
56
+ const loaded = await loadCandidates?.();
57
+ list = Array.isArray(loaded) && loaded.length ? loaded : DEFAULT_AUTO_MODELS;
58
+ } catch {
59
+ list = DEFAULT_AUTO_MODELS;
60
+ }
61
+ return [...new Set(list)].filter(Boolean);
62
+ }
63
+
64
+ async function candidates() {
65
+ return rankModels(await loadList(), lastErrorAt, { now: now(), cooldownMs });
66
+ }
67
+
68
+ async function candidatesFor(requested) {
69
+ if (!requested) return candidates();
70
+ const list = await loadList();
71
+ const all = list.includes(requested) ? list : [requested, ...list];
72
+ const others = rankModels(all.filter((id) => id !== requested), lastErrorAt, {
73
+ now: now(),
74
+ cooldownMs,
75
+ });
76
+ if (inCooldown(requested, lastErrorAt, now(), cooldownMs)) {
77
+ return [...others, requested];
78
+ }
79
+ return [requested, ...others];
80
+ }
81
+
82
+ async function recordError(id) {
83
+ if (!id) return;
84
+ lastErrorAt[id] = now();
85
+ await persist({ ...lastErrorAt });
86
+ }
87
+
88
+ function isCooling(id) {
89
+ return inCooldown(id, lastErrorAt, now(), cooldownMs);
90
+ }
91
+
92
+ return {
93
+ candidates,
94
+ candidatesFor,
95
+ recordError,
96
+ isCooling,
97
+ errors: () => ({ ...lastErrorAt }),
98
+ };
99
+ }