mslxdff 0.1.65 → 0.1.67
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 +3 -3159
- package/package.json +1 -1
- package/src/chat/cooling.js +99 -0
- package/src/chat/direct.js +57 -0
- package/src/chat/gateway.js +148 -0
- package/src/chat/orchestrator.js +218 -0
- package/src/chat/sse.js +69 -0
- package/src/chat/upstream.js +59 -501
- package/src/cli/bootstrap.js +1 -0
- package/src/cli/commands/daemon.js +151 -0
- package/src/cli/commands/group.js +272 -0
- package/src/cli/commands/model.js +405 -0
- package/src/cli/commands/provider/add.js +106 -0
- package/src/cli/commands/provider/allowlist.js +99 -0
- package/src/cli/commands/provider/config.js +82 -0
- package/src/cli/commands/provider/index.js +193 -0
- package/src/cli/commands/provider/keys.js +135 -0
- package/src/cli/commands/provider/models.js +99 -0
- package/src/cli/commands/provider.js +1 -0
- package/src/cli/commands/sync.js +143 -0
- package/src/cli/commands/system.js +236 -0
- package/src/cli/commands/workbuddy.js +91 -0
- package/src/cli/format.js +119 -0
- package/src/cli/group-helpers.js +69 -0
- package/src/cli/help.js +66 -0
- package/src/cli/index.js +59 -0
- package/src/cli/interactive.js +84 -0
- package/src/cli/policy.js +119 -0
- package/src/cli/provider-row.js +73 -0
- package/src/cli/status.js +283 -0
- package/src/cli/util.js +24 -0
- package/src/providers/base.js +159 -0
- package/src/providers/dispatcher.js +18 -6
- package/src/providers/generic.js +28 -166
- package/src/providers/openrouter.js +19 -205
- package/src/providers/workbuddy/auth.js +175 -0
- package/src/providers/workbuddy/balance.js +84 -0
- package/src/providers/workbuddy/chat.js +310 -0
- package/src/providers/workbuddy/index.js +263 -0
- package/src/providers/workbuddy/models.js +111 -0
- package/src/providers/workbuddy/rotation-log.js +54 -0
- package/src/providers/workbuddy.js +2 -677
- package/src/routes/chat/broadband-handler.js +25 -46
- package/src/routes/chat/exhausted-handler.js +2 -2
- package/src/routes/chat/gateway.js +301 -0
- package/src/routes/chat/hedge-handler.js +65 -83
- package/src/routes/chat/index.js +1 -384
- package/src/routes/chat/local-handler.js +32 -61
- package/src/routes/chat/peer-handler.js +32 -23
- package/src/routes/chat/relay-pipeline.js +151 -0
- package/src/runtime/bootstrap.js +408 -0
- package/src/state/facade.js +57 -0
- package/src/state/memory.js +161 -0
- package/src/state/merge.js +26 -0
- package/src/state/persist.js +42 -0
- package/src/state/provider-config.js +143 -0
- package/src/state/schemas/allowlist.js +87 -0
- package/src/state/schemas/group.js +31 -0
- package/src/state/schemas/model.js +53 -0
- package/src/state/schemas/peer.js +31 -0
- package/src/state/schemas/port.js +10 -0
- package/src/state/schemas/provider.js +204 -0
- package/src/state/schemas/token.js +43 -0
- package/src/state/store.js +176 -0
- package/src/state.js +1 -711
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export async function handleProviderConfig(id, sub, rest) {
|
|
2
|
+
if (sub === "set-models-path" || sub === "setModelsPath" || sub === "models-path") {
|
|
3
|
+
const p = rest[1];
|
|
4
|
+
if (!p) {
|
|
5
|
+
console.error(`usage: mslxdff -provider ${id} set-models-path <path> (e.g. /v1/models)`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
if (!String(p).trim().startsWith("/")) {
|
|
9
|
+
console.error(`invalid modelsPath: ${p} (must start with /)`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const { loadProviderConfig, saveProviderConfig } = await import("../../../state.js");
|
|
13
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
14
|
+
const { normalizeProviderId } = await import("../../../providers/model-id.js");
|
|
15
|
+
const nid = normalizeProviderId(id);
|
|
16
|
+
saveProviderConfig(nid || id, { baseUrl: cur.baseUrl || "", keys: cur.keys || [], modelsPath: String(p).trim() });
|
|
17
|
+
console.log(`set ${nid || id} modelsPath: ${String(p).trim()} — restart daemon to activate`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
if (sub === "set-chat-path" || sub === "setChatPath" || sub === "chat-path") {
|
|
21
|
+
const p = rest[1];
|
|
22
|
+
if (!p) {
|
|
23
|
+
console.error(`usage: mslxdff -provider ${id} set-chat-path <path> (e.g. /v1/chat/completions)`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
if (!String(p).trim().startsWith("/")) {
|
|
27
|
+
console.error(`invalid chatPath: ${p} (must start with /)`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const { loadProviderConfig, saveProviderConfig } = await import("../../../state.js");
|
|
31
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
32
|
+
const { normalizeProviderId } = await import("../../../providers/model-id.js");
|
|
33
|
+
const nid = normalizeProviderId(id);
|
|
34
|
+
saveProviderConfig(nid || id, { baseUrl: cur.baseUrl || "", keys: cur.keys || [], chatPath: String(p).trim() });
|
|
35
|
+
console.log(`set ${nid || id} chatPath: ${String(p).trim()} — restart daemon to activate`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
if (sub === "clear") {
|
|
39
|
+
const { loadProviderConfigs, loadProviderConfig, saveProviderConfig, saveProviderKeys } = await import("../../../state.js");
|
|
40
|
+
const configs = loadProviderConfigs();
|
|
41
|
+
if (configs[id]) {
|
|
42
|
+
saveProviderConfig(id, { baseUrl: "", keys: [] });
|
|
43
|
+
} else {
|
|
44
|
+
saveProviderKeys(id, []);
|
|
45
|
+
}
|
|
46
|
+
console.log(`cleared ${id} API keys (provider disabled on next daemon start)`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
if (sub === "set-url" || sub === "setUrl" || sub === "url") {
|
|
50
|
+
const url = rest[1];
|
|
51
|
+
if (!url) {
|
|
52
|
+
console.error(`usage: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (!/^https?:\/\/.+/.test(String(url).trim())) {
|
|
56
|
+
console.error(`invalid baseUrl: ${url} (must start with http:// or https://)`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const { loadProviderConfig, saveProviderConfig } = await import("../../../state.js");
|
|
60
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
61
|
+
saveProviderConfig(id, { baseUrl: String(url).trim(), keys: cur.keys || [] });
|
|
62
|
+
console.log(`set ${id} baseUrl: ${String(url).trim().replace(/\/+$/, "")} — restart daemon to activate`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
if (sub === "share") {
|
|
66
|
+
const { loadProviderShareKeys, saveProviderShareKeys } = await import("../../../state.js");
|
|
67
|
+
const on = rest[1];
|
|
68
|
+
if (!on) {
|
|
69
|
+
console.log(`share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"}`);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
if (!["on", "off", "1", "0", "true", "false"].includes(String(on).toLowerCase())) {
|
|
73
|
+
console.error("usage: mslxdff -provider openrouter share on|off");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const state = ["on", "1", "true"].includes(String(on).toLowerCase());
|
|
77
|
+
saveProviderShareKeys(id, state);
|
|
78
|
+
console.log(`share keys to peers: ${state ? "ON" : "off"} — restart daemon to activate`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { defaultStateFile } from "../../../state.js";
|
|
3
|
+
|
|
4
|
+
export async function handleProviders(args) {
|
|
5
|
+
if (!(args.includes("-providers") || args.includes("--providers"))) return false;
|
|
6
|
+
const idx = args.findIndex((x) => x === "-providers" || x === "--providers");
|
|
7
|
+
const sub = args[idx + 1];
|
|
8
|
+
if (!sub || sub === "list" || sub === "status") {
|
|
9
|
+
const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl, loadProviderAllowedModels } = await import("../../../state.js");
|
|
10
|
+
const configs = loadProviderConfigs();
|
|
11
|
+
const opencodeEnabled = true;
|
|
12
|
+
const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
13
|
+
const orKeys = loadProviderKeys("openrouter");
|
|
14
|
+
const orBase = "https://openrouter.ai/api/v1";
|
|
15
|
+
const orShare = loadProviderShareKeys("openrouter");
|
|
16
|
+
const orAllowed = loadProviderAllowedModels("openrouter");
|
|
17
|
+
const genericIds = new Set(Object.keys(configs).filter((id) => id !== "opencode" && id !== "openrouter"));
|
|
18
|
+
try {
|
|
19
|
+
const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
|
|
20
|
+
const pk = raw.providerKeys || {};
|
|
21
|
+
for (const id of Object.keys(pk)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
|
|
22
|
+
const cfgRaw = raw.providerConfigs || {};
|
|
23
|
+
for (const id of Object.keys(cfgRaw)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
|
|
24
|
+
} catch {}
|
|
25
|
+
for (const k of Object.keys(process.env)) {
|
|
26
|
+
const m = k.match(/^MSLXDFF_(.+)_KEY$/);
|
|
27
|
+
if (m) {
|
|
28
|
+
const id = m[1].toLowerCase().replace(/__/g, "-");
|
|
29
|
+
if (id !== "openrouter" && id !== "opencode") genericIds.add(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
|
|
34
|
+
const cfgs = raw.providerConfigs || {};
|
|
35
|
+
for (const id of Object.keys(cfgs)) {
|
|
36
|
+
if (id === "opencode" || id === "openrouter") continue;
|
|
37
|
+
const am = cfgs[id]?.allowedModels;
|
|
38
|
+
if (Array.isArray(am) && am.length) genericIds.add(id);
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
const list = [];
|
|
42
|
+
const { loadProviderAllowAnyModels: _la0 } = await import("../../../state.js");
|
|
43
|
+
const opAllowAny = _la0("opencode");
|
|
44
|
+
const orAllowAny = _la0("openrouter");
|
|
45
|
+
const opAllowed = loadProviderAllowedModels("opencode");
|
|
46
|
+
list.push({ id: "opencode", enabled: opencodeEnabled, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, allowAny: opAllowAny, note: "built-in, no key, cannot share" });
|
|
47
|
+
list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, allowAny: orAllowAny, note: orKeys.length ? "" : "no keys" });
|
|
48
|
+
for (const gid of [...genericIds].sort()) {
|
|
49
|
+
const cfg = configs[gid];
|
|
50
|
+
const keys = loadProviderKeys(gid);
|
|
51
|
+
const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
|
|
52
|
+
const share = loadProviderShareKeys(gid);
|
|
53
|
+
const allowed = loadProviderAllowedModels(gid);
|
|
54
|
+
const allowAny = _la0(gid);
|
|
55
|
+
const enabled = Boolean(baseUrl && keys.length);
|
|
56
|
+
let note = "";
|
|
57
|
+
if (!baseUrl && !keys.length && !allowed.length && allowAny === false) note = "no baseUrl, no keys, BLOCKED (allowAny OFF)";
|
|
58
|
+
else if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
|
|
59
|
+
else if (!baseUrl && !allowed.length && !allowAny) note = "missing baseUrl, BLOCKED";
|
|
60
|
+
else if (!keys.length && !allowed.length && !allowAny) note = "no keys, BLOCKED";
|
|
61
|
+
else if (!baseUrl) note = "missing baseUrl";
|
|
62
|
+
else if (!keys.length) note = "no keys";
|
|
63
|
+
list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, allowed, allowAny, note });
|
|
64
|
+
}
|
|
65
|
+
console.log(`providers (${list.length}):`);
|
|
66
|
+
for (const p of list) {
|
|
67
|
+
const state = p.enabled ? "enabled " : "disabled";
|
|
68
|
+
const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
|
|
69
|
+
const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
|
|
70
|
+
const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 3).join(",")}${p.allowed.length > 3 ? "..." : ""})` : (p.allowAny ? "allow=all" : "allow=none(BLOCKED)");
|
|
71
|
+
const note = p.note ? ` (${p.note})` : "";
|
|
72
|
+
console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} ${allowInfo.padEnd(22)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
|
|
73
|
+
}
|
|
74
|
+
console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider <id> allowlist set <model...> to restrict`);
|
|
75
|
+
console.log(` mslxdff -provider <id> allowAny on|off (empty allowlist = block or allow all)`);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
console.error("usage: mslxdff -providers list");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function handleProvider(args) {
|
|
83
|
+
if (!(args.includes("-provider") || args.includes("--provider"))) return false;
|
|
84
|
+
const idx = args.findIndex((x) => x === "-provider" || x === "--provider");
|
|
85
|
+
const id = args[idx + 1];
|
|
86
|
+
const sub = args[idx + 2];
|
|
87
|
+
const rest = args.slice(idx + 2);
|
|
88
|
+
if (!id) {
|
|
89
|
+
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|models|clear|share|set-url|allowlist|allowAny]");
|
|
90
|
+
console.error(" e.g. mslxdff -provider openrouter sk-1 sk-2 sk-3 set multiple keys (replaces all)");
|
|
91
|
+
console.error(" mslxdff -provider openrouter add sk-4 append one key");
|
|
92
|
+
console.error(" mslxdff -provider openrouter remove sk-1 remove a key by value");
|
|
93
|
+
console.error(" mslxdff -provider openrouter list list all keys (masked)");
|
|
94
|
+
console.error(" mslxdff -provider openrouter models [--json] list provider models (allowlist filtered)");
|
|
95
|
+
console.error(" mslxdff -provider openrouter share on|off share keys with peers on outgoing forward (ADR-0008)");
|
|
96
|
+
console.error(" mslxdff -provider openrouter set-url https://api.example.com/v1");
|
|
97
|
+
console.error(" mslxdff -provider openrouter set-models-path /v1/models");
|
|
98
|
+
console.error(" mslxdff -provider openrouter set-chat-path /v1/chat/completions");
|
|
99
|
+
console.error(" mslxdff -provider openrouter allowlist set gpt-4 gpt-3.5 manage allowed models (empty=block unless allowAny ON)");
|
|
100
|
+
console.error(" mslxdff -provider openrouter allowAny on|off empty allowlist = allow all or block all (default OFF, secure)");
|
|
101
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx add generic OpenAI-compatible provider");
|
|
102
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
|
|
103
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
|
|
104
|
+
console.error(" mslxdff -provider openrouter interactive hidden input (append)");
|
|
105
|
+
console.error(" mslxdff -provider openrouter clear remove all keys");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
if (id === "list" || id === "status") {
|
|
109
|
+
const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl } = await import("../../../state.js");
|
|
110
|
+
const configs = loadProviderConfigs();
|
|
111
|
+
const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
|
|
112
|
+
const orKeys = loadProviderKeys("openrouter");
|
|
113
|
+
const orBase = "https://openrouter.ai/api/v1";
|
|
114
|
+
const orShare = loadProviderShareKeys("openrouter");
|
|
115
|
+
const genericIds = new Set(Object.keys(configs).filter((x) => x !== "opencode" && x !== "openrouter"));
|
|
116
|
+
try {
|
|
117
|
+
const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
|
|
118
|
+
const pk = raw.providerKeys || {};
|
|
119
|
+
for (const k of Object.keys(pk)) if (k !== "opencode" && k !== "openrouter") genericIds.add(k);
|
|
120
|
+
const cfgRaw = raw.providerConfigs || {};
|
|
121
|
+
for (const k of Object.keys(cfgRaw)) if (k !== "opencode" && k !== "openrouter") genericIds.add(k);
|
|
122
|
+
} catch {}
|
|
123
|
+
for (const k of Object.keys(process.env)) {
|
|
124
|
+
const m = k.match(/^MSLXDFF_(.+)_KEY$/);
|
|
125
|
+
if (m) {
|
|
126
|
+
const gid = m[1].toLowerCase().replace(/__/g, "-");
|
|
127
|
+
if (gid !== "openrouter" && gid !== "opencode") genericIds.add(gid);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
|
|
132
|
+
const cfgs = raw.providerConfigs || {};
|
|
133
|
+
for (const gid of Object.keys(cfgs)) {
|
|
134
|
+
if (gid === "opencode" || gid === "openrouter") continue;
|
|
135
|
+
const am = cfgs[gid]?.allowedModels;
|
|
136
|
+
if (Array.isArray(am) && am.length) genericIds.add(gid);
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
139
|
+
const list = [];
|
|
140
|
+
const { loadProviderAllowAnyModels: _la0 } = await import("../../../state.js");
|
|
141
|
+
const { loadProviderAllowedModels } = await import("../../../state.js");
|
|
142
|
+
const opAllowed = loadProviderAllowedModels("opencode");
|
|
143
|
+
const opAllowAny = _la0("opencode");
|
|
144
|
+
const orAllowed = loadProviderAllowedModels("openrouter");
|
|
145
|
+
const orAllowAny = _la0("openrouter");
|
|
146
|
+
list.push({ id: "opencode", enabled: true, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, allowAny: opAllowAny, note: "built-in, no key, cannot share" });
|
|
147
|
+
list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, allowAny: orAllowAny, note: orKeys.length ? "" : "no keys" });
|
|
148
|
+
for (const gid of [...genericIds].sort()) {
|
|
149
|
+
const cfg = configs[gid];
|
|
150
|
+
const keys = loadProviderKeys(gid);
|
|
151
|
+
const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
|
|
152
|
+
const share = loadProviderShareKeys(gid);
|
|
153
|
+
const allowed = loadProviderAllowedModels(gid);
|
|
154
|
+
const allowAny = _la0(gid);
|
|
155
|
+
const enabled = Boolean(baseUrl && keys.length);
|
|
156
|
+
let note = "";
|
|
157
|
+
if (!baseUrl && !keys.length) note = "no baseUrl, no keys";
|
|
158
|
+
else if (!baseUrl) note = "missing baseUrl";
|
|
159
|
+
else if (!keys.length) note = "no keys";
|
|
160
|
+
list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, note });
|
|
161
|
+
}
|
|
162
|
+
console.log(`providers (${list.length}):`);
|
|
163
|
+
for (const p of list) {
|
|
164
|
+
const state = p.enabled ? "enabled " : "disabled";
|
|
165
|
+
const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
|
|
166
|
+
const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
|
|
167
|
+
const note = p.note ? ` (${p.note})` : "";
|
|
168
|
+
console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
|
|
169
|
+
}
|
|
170
|
+
console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider add <id> <baseUrl> <key> to add generic`);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
if (id === "add") {
|
|
174
|
+
const { handleProviderAdd } = await import("./add.js");
|
|
175
|
+
await handleProviderAdd(id, sub, rest);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if ((id === "opencode" || id === "oc") && sub !== "allowlist" && sub !== "allow" && sub !== "allowed" && sub !== "whitelist" && sub !== "list" && sub !== "status" && sub !== "allowAny" && sub !== "allow-any" && sub !== "allow_any" && sub !== "allowany") {
|
|
179
|
+
console.log("opencode is the default (bare) provider — it needs no API key and can never be shared with peers");
|
|
180
|
+
console.log("(its IP-based rate limit is spread by peer forwarding itself)");
|
|
181
|
+
console.log(` allowlist: mslxdff -provider opencode allowlist [list|set|add|remove|clear] — restrict models (empty=allow all)`);
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
const { handleProviderConfig } = await import("./config.js");
|
|
185
|
+
if (await handleProviderConfig(id, sub, rest)) return true;
|
|
186
|
+
const { handleProviderAllowlist } = await import("./allowlist.js");
|
|
187
|
+
if (await handleProviderAllowlist(id, sub, rest)) return true;
|
|
188
|
+
const { handleProviderModels } = await import("./models.js");
|
|
189
|
+
if (await handleProviderModels(id, sub, args, rest)) return true;
|
|
190
|
+
const { handleProviderKeys } = await import("./keys.js");
|
|
191
|
+
await handleProviderKeys(id, sub, rest, args);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export async function handleProviderKeys(id, sub, rest, args) {
|
|
2
|
+
if (sub === "list" || sub === "status") {
|
|
3
|
+
const { loadProviderKeys, loadProviderConfig, loadProviderShareKeys, loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("../../../state.js");
|
|
4
|
+
const keys = loadProviderKeys(id);
|
|
5
|
+
const cfg = loadProviderConfig(id);
|
|
6
|
+
const allowed = loadProviderAllowedModels(id);
|
|
7
|
+
const allowAny = loadProviderAllowAnyModels(id);
|
|
8
|
+
const baseUrl = cfg?.baseUrl || "";
|
|
9
|
+
if (baseUrl) console.log(`provider: ${id} baseUrl: ${baseUrl}`);
|
|
10
|
+
else console.log(`provider: ${id}${id === "openrouter" ? " (built-in baseUrl: https://openrouter.ai/api/v1)" : ""}`);
|
|
11
|
+
if (keys.length) {
|
|
12
|
+
console.log(` keys: ${keys.length} key${keys.length > 1 ? "s" : ""}`);
|
|
13
|
+
keys.forEach((k, i) => console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`));
|
|
14
|
+
console.log(` remove by: mslxdff -provider ${id} remove <seq> [seq...] | <key-value>`);
|
|
15
|
+
} else {
|
|
16
|
+
console.log(` keys: (no keys configured)`);
|
|
17
|
+
}
|
|
18
|
+
console.log(` share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"} (mslxdff -provider ${id} share on|off)`);
|
|
19
|
+
console.log(` allowAnyModels: ${allowAny ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = BLOCK ALL)"} (mslxdff -provider ${id} allowAny on|off)`);
|
|
20
|
+
if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
|
|
21
|
+
else console.log(` allowedModels: (none — ${allowAny ? "allow all" : "BLOCK ALL"}) (mslxdff -provider ${id} allowlist set <model...> or allowAny on)`);
|
|
22
|
+
if (baseUrl) console.log(` set url: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
23
|
+
console.log(` NOTE: opencode is the default provider and can never be shared`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
if (sub === "add") {
|
|
27
|
+
const key = rest[1];
|
|
28
|
+
if (!key) {
|
|
29
|
+
console.error("usage: mslxdff -provider openrouter add <key>");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const trimmed = String(key).trim();
|
|
33
|
+
const { loadProviderKeys, loadProviderConfig, saveProviderConfig, addProviderKey, loadProviderConfigs } = await import("../../../state.js");
|
|
34
|
+
const curKeys = loadProviderKeys(id);
|
|
35
|
+
if (curKeys.some((k) => String(k).trim() === trimmed)) {
|
|
36
|
+
console.log(`key already exists for ${id} (${trimmed.slice(0, 4)}…${trimmed.slice(-4)}), skipped — still ${curKeys.length} key(s)`);
|
|
37
|
+
console.log(` use: mslxdff -provider ${id} list to see keys`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
const configs = loadProviderConfigs();
|
|
41
|
+
if (configs[id]) {
|
|
42
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
43
|
+
const keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
|
|
44
|
+
saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
|
|
45
|
+
console.log(`added ${id} API key (now ${keys.length} total) — restart daemon to activate`);
|
|
46
|
+
} else {
|
|
47
|
+
const added = addProviderKey(id, key);
|
|
48
|
+
console.log(`added ${id} API key (now ${added.length} total) — restart daemon to activate`);
|
|
49
|
+
}
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
if (sub === "remove") {
|
|
53
|
+
const { loadProviderKeys, loadProviderConfig, saveProviderConfig, removeProviderKeys, loadProviderConfigs } = await import("../../../state.js");
|
|
54
|
+
const targets = rest.slice(1).filter((k) => !k.startsWith("-"));
|
|
55
|
+
if (!targets.length) {
|
|
56
|
+
console.error("usage: mslxdff -provider openrouter remove <seq|key> [seq|key ...] (seq = index shown by 'list')");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const current = loadProviderKeys(id);
|
|
60
|
+
const toRemove = [];
|
|
61
|
+
for (const raw of targets.flatMap((t) => String(t).split(","))) {
|
|
62
|
+
const t = raw.trim();
|
|
63
|
+
if (!t) continue;
|
|
64
|
+
if (/^\d+$/.test(t)) {
|
|
65
|
+
const seq = Number(t);
|
|
66
|
+
const idx2 = seq - 1;
|
|
67
|
+
if (Number.isInteger(seq) && idx2 >= 0 && idx2 < current.length) toRemove.push(current[idx2]);
|
|
68
|
+
else console.log(` ! no key at sequence ${seq} (provider has ${current.length}) — skipped`);
|
|
69
|
+
} else {
|
|
70
|
+
toRemove.push(t);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!toRemove.length) {
|
|
74
|
+
console.log("nothing to remove");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
const configs = loadProviderConfigs();
|
|
78
|
+
let remaining;
|
|
79
|
+
if (configs[id]) {
|
|
80
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
81
|
+
const set = new Set([...new Set(toRemove)].map((k) => String(k).trim()));
|
|
82
|
+
const keys = (cur.keys || []).filter((k) => !set.has(k));
|
|
83
|
+
saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
|
|
84
|
+
remaining = keys;
|
|
85
|
+
} else {
|
|
86
|
+
remaining = removeProviderKeys(id, [...new Set(toRemove)]);
|
|
87
|
+
}
|
|
88
|
+
console.log(`removed ${current.length - remaining.length} ${id} API key(s) (now ${remaining.length} total) — restart daemon to activate`);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
if (sub && !sub.startsWith("-")) {
|
|
92
|
+
const { loadProviderKeys, loadProviderConfig, saveProviderConfig, saveProviderKeys, loadProviderConfigs } = await import("../../../state.js");
|
|
93
|
+
const keys = rest.filter((k) => !k.startsWith("-"));
|
|
94
|
+
if (!keys.length) {
|
|
95
|
+
console.error("no key given");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const configs = loadProviderConfigs();
|
|
99
|
+
if (configs[id]) {
|
|
100
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
101
|
+
const clean = [...new Set(keys.map((k) => String(k).trim()).filter(Boolean))];
|
|
102
|
+
saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys: clean });
|
|
103
|
+
console.log(`set ${id} API keys (${clean.length}: ${clean.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}) — restart daemon to activate`);
|
|
104
|
+
} else {
|
|
105
|
+
saveProviderKeys(id, keys);
|
|
106
|
+
console.log(`set ${id} API keys (${keys.length}: ${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}) — restart daemon to activate`);
|
|
107
|
+
}
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
111
|
+
const { loadProviderKeys, addProviderKey } = await import("../../../state.js");
|
|
112
|
+
const readline = await import("node:readline/promises");
|
|
113
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
114
|
+
console.log(`Enter ${id} API keys, one per line (input hidden). Blank line to finish:`);
|
|
115
|
+
const existing = loadProviderKeys(id);
|
|
116
|
+
const collected = [];
|
|
117
|
+
for (;;) {
|
|
118
|
+
const key = await rl.question(existing.length || collected.length ? "" : "");
|
|
119
|
+
const clean = String(key || "").trim();
|
|
120
|
+
if (!clean) break;
|
|
121
|
+
collected.push(clean);
|
|
122
|
+
}
|
|
123
|
+
rl.close();
|
|
124
|
+
if (!collected.length) {
|
|
125
|
+
console.log("empty input — nothing changed");
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
for (const k of collected) addProviderKey(id, k);
|
|
129
|
+
const total = (await import("../../../state.js")).loadProviderKeys(id).length;
|
|
130
|
+
console.log(`added ${collected.length} ${id} API key(s) (now ${total} total) — restart daemon to activate`);
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
console.error("provide keys inline (non-TTY): mslxdff -provider openrouter <key1> [key2 ...]");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { defaultStateFile } from "../../../state.js";
|
|
3
|
+
import { logDir } from "../../../logs.js";
|
|
4
|
+
|
|
5
|
+
export async function handleProviderModels(id, sub, args, rest) {
|
|
6
|
+
if (!(sub === "models" || sub === "show-models" || sub === "list-models" || sub === "ls")) return false;
|
|
7
|
+
const { loadProviderConfig, loadProviderKeys, isModelAllowed } = await import("../../../state.js");
|
|
8
|
+
const wantsJson = args.includes("--json") || args.includes("-json");
|
|
9
|
+
const cfg = loadProviderConfig(id);
|
|
10
|
+
if (id === "opencode" || id === "oc") {
|
|
11
|
+
try {
|
|
12
|
+
const cacheFile = join(logDir(), "models.json");
|
|
13
|
+
const { readFileSync } = await import("node:fs");
|
|
14
|
+
const raw = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
15
|
+
const ids = (raw.data || []).map((m) => m.id).filter((x) => !String(x).includes("/"));
|
|
16
|
+
if (wantsJson) {
|
|
17
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`opencode models (${ids.length}):`);
|
|
20
|
+
for (const mid of ids) console.log(` ${mid}`);
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error(`could not read models cache: ${String(e?.message || e)}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const { createGenericProvider } = await import("../../../providers/generic.js");
|
|
30
|
+
const { createWorkbuddyProvider } = await import("../../../providers/workbuddy.js");
|
|
31
|
+
const baseUrl = cfg?.baseUrl || (id === "workbuddy" ? "https://copilot.tencent.com" : "");
|
|
32
|
+
const keys = loadProviderKeys(id);
|
|
33
|
+
const auths = cfg?.auths || [];
|
|
34
|
+
let provider;
|
|
35
|
+
if (id === "workbuddy") {
|
|
36
|
+
provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
|
|
37
|
+
} else {
|
|
38
|
+
if (!baseUrl) {
|
|
39
|
+
console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
provider = createGenericProvider({ id, baseUrl, apiKeys: keys, file: defaultStateFile() });
|
|
43
|
+
}
|
|
44
|
+
const all = await provider.listModels();
|
|
45
|
+
const markAllowed = (mid) => {
|
|
46
|
+
const raw = String(mid || "").includes("/") ? String(mid).split("/").slice(1).join("/") : String(mid);
|
|
47
|
+
const checkRaw = mid.startsWith(`${id}/`) ? mid.slice(id.length + 1) : raw;
|
|
48
|
+
return isModelAllowed(id, checkRaw);
|
|
49
|
+
};
|
|
50
|
+
if (wantsJson) {
|
|
51
|
+
const filtered = all.filter((m) => markAllowed(m.id));
|
|
52
|
+
console.log(JSON.stringify({ object: "list", data: filtered }, null, 2));
|
|
53
|
+
} else {
|
|
54
|
+
const allowedCount = all.filter((m) => markAllowed(m.id)).length;
|
|
55
|
+
console.log(`${id} models (${all.length} total, ${allowedCount} ✓ allowed${allowedCount !== all.length ? `, ${all.length - allowedCount} x blocked by allowlist` : ""}):`);
|
|
56
|
+
const fmtPrice = (m) => {
|
|
57
|
+
const c = String(m.credits || "").trim();
|
|
58
|
+
if (c) {
|
|
59
|
+
const m0 = c.match(/x\s*([\d.]+)/i);
|
|
60
|
+
if (m0) return `x${m0[1]}`;
|
|
61
|
+
return c.replace(/\s*credits\s*/gi, "").trim().replace(/\s+/g, " ");
|
|
62
|
+
}
|
|
63
|
+
if (m.pricing && typeof m.pricing === "object") {
|
|
64
|
+
const p = m.pricing.prompt ?? m.pricing.input ?? m.pricing.completion ?? "";
|
|
65
|
+
if (p) return String(p);
|
|
66
|
+
}
|
|
67
|
+
if (m.price != null && String(m.price).trim()) return String(m.price).trim();
|
|
68
|
+
if (String(m.id).endsWith("/auto")) return "浮动";
|
|
69
|
+
return "—";
|
|
70
|
+
};
|
|
71
|
+
const fmtBadge = (m) => {
|
|
72
|
+
const tags = Array.isArray(m.tags) ? m.tags : [];
|
|
73
|
+
const b = tags.find((t) => String(t).includes("限时免费") || String(t).toLowerCase().includes("free"));
|
|
74
|
+
if (!b) return "";
|
|
75
|
+
const part = String(b).split(":")[1];
|
|
76
|
+
return part ? ` [${part}]` : ` [${b}]`;
|
|
77
|
+
};
|
|
78
|
+
const idW = Math.max(22, ...all.map((m) => String(m.id).length)) + 2;
|
|
79
|
+
const priceW = Math.max(6, ...all.map((m) => fmtPrice(m).length)) + 2;
|
|
80
|
+
for (const m of all) {
|
|
81
|
+
const ok = markAllowed(m.id);
|
|
82
|
+
const price = fmtPrice(m);
|
|
83
|
+
const badge = fmtBadge(m);
|
|
84
|
+
const name = m.name ? ` ${m.name}` : "";
|
|
85
|
+
const blocked = ok ? "" : " [blocked — allowlist]";
|
|
86
|
+
const line = ` ${ok ? "✓" : "x"} ${String(m.id).padEnd(idW)}${String(price).padEnd(priceW)}${name}${badge}${blocked}`;
|
|
87
|
+
console.log(line);
|
|
88
|
+
}
|
|
89
|
+
if (!all.length) console.log(` (no models — check baseUrl/keys or try: curl ${baseUrl}/models)`);
|
|
90
|
+
else if (allowedCount === 0) console.log(` tip: all blocked — mslxdff -provider ${id} allowAny on 或 allowlist set <model...>`);
|
|
91
|
+
else if (allowedCount !== all.length) console.log(` tip: blocked 仅影响 /v1/chat 调用,展示已全量列出`);
|
|
92
|
+
}
|
|
93
|
+
try { await provider.close?.(); } catch {}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(`could not list ${id} models: ${String(e?.message || e)}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./provider/index.js";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { loadToken, getPort, savePreferredModel, loadPreferredModel } from "../../state.js";
|
|
3
|
+
import { getPreferredModel as getPref } from "../../auto.js";
|
|
4
|
+
import { normalizeModel } from "../../reasoning.js";
|
|
5
|
+
import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
|
|
6
|
+
import { syncToOpencode, opencodeConfigPath, toExternalAlias, toInternalId } from "../../sync-opencode.js";
|
|
7
|
+
import { createModelsService } from "../../models.js";
|
|
8
|
+
import { createUpstreamClient } from "../../upstream.js";
|
|
9
|
+
import { logDir } from "../../logs.js";
|
|
10
|
+
|
|
11
|
+
export async function handleSetto(args) {
|
|
12
|
+
if (!(args.includes("-setto") || args.includes("--setto"))) return false;
|
|
13
|
+
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
14
|
+
const target = args[idx + 1];
|
|
15
|
+
if (!["workbuddy", "opencode"].includes(target)) {
|
|
16
|
+
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId]");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
if (target === "opencode") {
|
|
20
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
21
|
+
let id;
|
|
22
|
+
let internal;
|
|
23
|
+
if (raw) {
|
|
24
|
+
if (raw === "auto" || !raw) {
|
|
25
|
+
console.error("modelId 不能为 auto 或空");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const norm = normalizeModel(raw);
|
|
29
|
+
if (!norm) {
|
|
30
|
+
console.error("modelId 不能为空");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
savePreferredModel(norm);
|
|
34
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
35
|
+
internal = toInternalId(norm);
|
|
36
|
+
id = toExternalAlias(internal);
|
|
37
|
+
} else {
|
|
38
|
+
const pref = loadPreferredModel() || getPref();
|
|
39
|
+
if (!pref) {
|
|
40
|
+
console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const norm = normalizeModel(pref);
|
|
44
|
+
if (!norm) {
|
|
45
|
+
console.error("modelId 不能为空");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
internal = toInternalId(norm);
|
|
49
|
+
id = toExternalAlias(internal);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const cacheFile = join(logDir(), "models.json");
|
|
53
|
+
const models = createModelsService({
|
|
54
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
55
|
+
headers: createUpstreamClient({}).headers,
|
|
56
|
+
refreshMs: 0,
|
|
57
|
+
cacheFile,
|
|
58
|
+
});
|
|
59
|
+
const fresh = await Promise.race([
|
|
60
|
+
models.get(),
|
|
61
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
62
|
+
]);
|
|
63
|
+
if (fresh?.data?.length) {
|
|
64
|
+
const ids = fresh.data.map((m) => m.id);
|
|
65
|
+
if (!ids.includes(internal) && !ids.includes(id)) {
|
|
66
|
+
console.log(`warn: "${internal}" not in current free list (${ids.length} models), still syncing to opencode (alias ${id})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
try {
|
|
71
|
+
const { token } = await loadToken();
|
|
72
|
+
const persisted = getPort();
|
|
73
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
74
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
75
|
+
const file = opencodeConfigPath();
|
|
76
|
+
const r = await syncToOpencode({ id, token, port, file });
|
|
77
|
+
const aliasLabel = r.alias && r.alias !== r.id ? ` (alias ${r.alias} 对应内部 ${r.internal})` : (r.id !== r.internal ? ` (alias for "${r.internal}", 原名仍兼容)` : ` (原名兼容)`);
|
|
78
|
+
console.log(`synced to opencode: ${r.action} "${r.id}"${aliasLabel} @ ${file}`);
|
|
79
|
+
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
80
|
+
if (r.id !== r.internal) console.log(` alias: ${r.id} -> ${r.internal} (opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
|
|
81
|
+
else console.log(` alias: ${r.internal} (原名直用,opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
89
|
+
let id;
|
|
90
|
+
if (raw) {
|
|
91
|
+
if (raw === "auto" || !raw) {
|
|
92
|
+
console.error("modelId 不能为 auto 或空");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const norm = normalizeModel(raw);
|
|
96
|
+
if (!norm) {
|
|
97
|
+
console.error("modelId 不能为空");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
savePreferredModel(norm);
|
|
101
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
102
|
+
id = norm;
|
|
103
|
+
} else {
|
|
104
|
+
id = loadPreferredModel() || getPref();
|
|
105
|
+
if (!id) {
|
|
106
|
+
console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const cacheFile = join(logDir(), "models.json");
|
|
112
|
+
const models = createModelsService({
|
|
113
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
114
|
+
headers: createUpstreamClient({}).headers,
|
|
115
|
+
refreshMs: 0,
|
|
116
|
+
cacheFile,
|
|
117
|
+
});
|
|
118
|
+
const fresh = await Promise.race([
|
|
119
|
+
models.get(),
|
|
120
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
121
|
+
]);
|
|
122
|
+
if (fresh?.data?.length) {
|
|
123
|
+
const ids = fresh.data.map((m) => m.id);
|
|
124
|
+
if (!ids.includes(id)) {
|
|
125
|
+
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
try {
|
|
130
|
+
const { token } = await loadToken();
|
|
131
|
+
const persisted = getPort();
|
|
132
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
133
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
134
|
+
const file = workbuddyModelsPath();
|
|
135
|
+
const r = await syncToWorkbuddy({ id, token, port, file });
|
|
136
|
+
console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
|
|
137
|
+
console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|