mslxdff 0.1.159 → 0.1.160
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 +4 -4
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/auto-race.js +1 -1
- package/src/chat-pipeline/index.js +123 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +210 -210
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +124 -124
- package/src/cli/commands/provider/models.js +139 -139
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/stats.js +20 -5
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +96 -96
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +225 -225
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +183 -183
- package/src/providers/qoder/index.js +230 -230
- package/src/providers/qoder/sse.js +103 -103
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +327 -327
- package/src/providers/qwenwork/payload.js +142 -142
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/registry.js +66 -66
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/chat.js +11 -10
- package/src/providers/zcode/const.js +1 -1
- package/src/providers/zcode/context-shape.js +162 -0
- package/src/providers/zcode/headers.js +38 -0
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +3 -2
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/hedge-handler.js +3 -0
- package/src/routes/chat/local-handler.js +2 -0
- package/src/routes/chat/peer-handler.js +2 -0
- package/src/routes/chat/relay-pipeline.js +264 -250
- package/src/routes/chat/via-route-handler.js +146 -144
- package/src/routes/hedge.js +255 -255
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream-scan.js +86 -0
- package/src/routes/stream.js +393 -438
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/provider-gate.js +33 -33
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
- package/src/usage/record.js +8 -1
- package/src/usage/report.js +39 -2
- package/docs/cli_help_mini.md +0 -133
package/src/chat/gateway.js
CHANGED
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
import { performance } from "node:perf_hooks";
|
|
2
|
-
import { parseSse } from "./sse.js";
|
|
3
|
-
import { DEFAULT_PORT } from "../state.js";
|
|
4
|
-
import { compatFetch } from "../compat.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 网关深模块:POST 127.0.0.1:port/v1/chat/completions model:auto
|
|
8
|
-
* 注入化:fetch/loadToken/getPort/readModelsJson 均可伪,便于单测
|
|
9
|
-
*/
|
|
10
|
-
export function createGatewayClient({
|
|
11
|
-
fetchImpl = compatFetch,
|
|
12
|
-
loadToken,
|
|
13
|
-
getPort,
|
|
14
|
-
defaultPort = DEFAULT_PORT,
|
|
15
|
-
readModelsJson,
|
|
16
|
-
gatewayTimeoutMs = 25000,
|
|
17
|
-
env = process.env,
|
|
18
|
-
} = {}) {
|
|
19
|
-
const _fetch = fetchImpl;
|
|
20
|
-
const _loadToken = loadToken || (async () => {
|
|
21
|
-
try {
|
|
22
|
-
const state = await import("../state.js");
|
|
23
|
-
const loaded = await state.loadToken();
|
|
24
|
-
return String(loaded?.token || "").trim();
|
|
25
|
-
} catch { return ""; }
|
|
26
|
-
});
|
|
27
|
-
// 默认实现与 loadToken/readModelsJson 同款:懒加载 state(ESM 里没有 require)
|
|
28
|
-
// Note: 这里曾用 require 导致 state.port 被静默忽略 — 见 .agents/notes/implemented/bug-fix/2026-09-17-esm-dead-require-and-orphan-cooling.md
|
|
29
|
-
const _getPort = getPort || (async () => {
|
|
30
|
-
try {
|
|
31
|
-
const s = await import("../state.js");
|
|
32
|
-
const p = s.getPort();
|
|
33
|
-
if (Number.isInteger(p) && p > 0) return p;
|
|
34
|
-
} catch {}
|
|
35
|
-
const v = Number(env.MSLXDFF_PORT);
|
|
36
|
-
if (Number.isInteger(v) && v > 0) return v;
|
|
37
|
-
return defaultPort;
|
|
38
|
-
});
|
|
39
|
-
const _readModels = readModelsJson || (async () => {
|
|
40
|
-
try {
|
|
41
|
-
const { readFileSync, existsSync } = await import("node:fs");
|
|
42
|
-
const { join } = await import("node:path");
|
|
43
|
-
const { homedir } = await import("node:os");
|
|
44
|
-
const cache = join(homedir(), ".config", "mslxdff", "models.json");
|
|
45
|
-
if (existsSync(cache)) return JSON.parse(readFileSync(cache, "utf8"));
|
|
46
|
-
} catch {}
|
|
47
|
-
return { data: [] };
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
async function chatViaGateway({ messages, tools, autoProvider = "opencode" }) {
|
|
51
|
-
const TRACE = env.MSLXDFF_CHAT_TRACE !== "0";
|
|
52
|
-
const t0 = TRACE ? performance.now() : 0;
|
|
53
|
-
let port = defaultPort;
|
|
54
|
-
let token = "";
|
|
55
|
-
try {
|
|
56
|
-
token = String((await _loadToken()) || "").trim();
|
|
57
|
-
const p = await _getPort();
|
|
58
|
-
if (Number.isInteger(p) && p > 0) port = p;
|
|
59
|
-
} catch {}
|
|
60
|
-
if (!token) {
|
|
61
|
-
try {
|
|
62
|
-
const { readFileSync, existsSync } = await import("node:fs");
|
|
63
|
-
const { join } = await import("node:path");
|
|
64
|
-
const { homedir } = await import("node:os");
|
|
65
|
-
const sf = env.MSLXDFF_STATE_FILE || join(homedir(), ".config", "mslxdff", "state.json");
|
|
66
|
-
if (existsSync(sf)) {
|
|
67
|
-
const j = JSON.parse(readFileSync(sf, "utf8"));
|
|
68
|
-
if (typeof j.token === "string" && j.token.trim()) token = j.token.trim();
|
|
69
|
-
}
|
|
70
|
-
} catch {}
|
|
71
|
-
}
|
|
72
|
-
const url = `http://127.0.0.1:${port}/v1/chat/completions`;
|
|
73
|
-
const body = { model: "auto", messages, stream: false };
|
|
74
|
-
if (tools?.length) { body.tools = tools; body.tool_choice = "auto"; }
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const controller = new AbortController();
|
|
78
|
-
const timer = setTimeout(() => controller.abort(), gatewayTimeoutMs);
|
|
79
|
-
const res = await _fetch(url, {
|
|
80
|
-
method: "POST",
|
|
81
|
-
headers: {
|
|
82
|
-
"Content-Type": "application/json",
|
|
83
|
-
...(autoProvider ? { "x-mslxdff-auto-provider": autoProvider } : {}),
|
|
84
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
85
|
-
},
|
|
86
|
-
body: JSON.stringify(body),
|
|
87
|
-
signal: controller.signal,
|
|
88
|
-
});
|
|
89
|
-
clearTimeout(timer);
|
|
90
|
-
const txt = await res.text();
|
|
91
|
-
let j;
|
|
92
|
-
try { j = JSON.parse(txt); } catch {
|
|
93
|
-
if (txt.includes("data:")) {
|
|
94
|
-
try {
|
|
95
|
-
const parsed = parseSse(txt);
|
|
96
|
-
if (parsed.sseOk && (parsed.content || parsed.toolCalls.length)) {
|
|
97
|
-
const hasToolCalls = parsed.toolCalls.length > 0;
|
|
98
|
-
const hasContent = !!parsed.content;
|
|
99
|
-
let finishReason = parsed.finishReason;
|
|
100
|
-
if (hasToolCalls && !hasContent) finishReason = "tool_calls";
|
|
101
|
-
const tool_calls = hasToolCalls ? parsed.toolCalls : undefined;
|
|
102
|
-
const msg = { role: "assistant", content: parsed.content || "" };
|
|
103
|
-
if (tool_calls) msg.tool_calls = tool_calls;
|
|
104
|
-
j = { id: `sse-${Date.now()}`, object: "chat.completion", model: parsed.model, choices: [{ index: 0, finish_reason: finishReason, message: msg }], usage: parsed.usage };
|
|
105
|
-
} else if (parsed.sseOk) {
|
|
106
|
-
return { ok: false, error: `gateway SSE no content: ${txt.slice(0, 800)}`, status: res.status };
|
|
107
|
-
} else {
|
|
108
|
-
return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (!res.ok) {
|
|
118
|
-
const msg = j?.error?.message || j?.error || j?.data?.error?.message || j?.data?.error || txt.slice(0, 800);
|
|
119
|
-
if (TRACE) console.log(`\x1b[90m· [LLM] gateway auto FAIL · ${Math.round(performance.now() - t0)}ms · HTTP ${res.status} ${String(msg).slice(0, 80)}\x1b[0m`);
|
|
120
|
-
return { ok: false, error: msg, status: res.status };
|
|
121
|
-
}
|
|
122
|
-
const choice = j.choices?.[0] || j.data?.choices?.[0];
|
|
123
|
-
const effectiveJ = j.choices ? j : (j.data?.choices ? j.data : j);
|
|
124
|
-
if (!choice) {
|
|
125
|
-
if (TRACE) console.log(`\x1b[90m· [gateway debug] no choice, txt=${txt.slice(0, 800)} · j=${JSON.stringify(j).slice(0, 800)}\x1b[0m`);
|
|
126
|
-
return { ok: false, error: `gateway no choice: ${txt.slice(0, 800)}`, status: res.status };
|
|
127
|
-
}
|
|
128
|
-
let provider = "opencode";
|
|
129
|
-
const rawModel = effectiveJ.model || j.model || choice.message?.model || "auto";
|
|
130
|
-
try {
|
|
131
|
-
const c = await _readModels();
|
|
132
|
-
const ids = (c.data || []).map((x) => x.id).filter(Boolean);
|
|
133
|
-
for (const pid of ids) {
|
|
134
|
-
const slash = pid.indexOf("/");
|
|
135
|
-
const prov = slash > 0 ? pid.slice(0, slash) : "opencode";
|
|
136
|
-
const raw = slash > 0 ? pid.slice(slash + 1) : pid;
|
|
137
|
-
if (pid === rawModel || raw === rawModel || pid.endsWith("/" + rawModel)) { provider = prov; break; }
|
|
138
|
-
}
|
|
139
|
-
if (provider === "opencode" && rawModel.includes("/")) {
|
|
140
|
-
const maybe = rawModel.split("/")[0];
|
|
141
|
-
if (["workbuddy", "cline", "sensenova", "openrouter", "generic"].includes(maybe)) provider = maybe;
|
|
142
|
-
}
|
|
143
|
-
} catch {}
|
|
144
|
-
if (TRACE) {
|
|
145
|
-
const dt = Math.round(performance.now() - t0);
|
|
146
|
-
console.log(`\x1b[90m· [LLM] gateway auto OK · ${dt}ms · 模型 ${provider !== "opencode" ? provider + "/" : ""}${rawModel} · 总 ${dt}ms (gateway-fallback)\x1b[0m`);
|
|
147
|
-
}
|
|
148
|
-
return { ok: true, message: choice.message, usage: effectiveJ.usage || j.usage, raw: j, status: res.status, model: rawModel, provider, viaGateway: true };
|
|
149
|
-
} catch (err) {
|
|
150
|
-
const raw = String(err?.message || err);
|
|
151
|
-
const msg = raw.slice(0, 800);
|
|
152
|
-
const isConnRefused = /ECONNREFUSED|Failed to fetch|fetch failed|connect ECONNREFUSED|ECONNRESET|EADDRNOTAVAIL/i.test(raw);
|
|
153
|
-
if (isConnRefused) {
|
|
154
|
-
const friendly = `本地服务没有启动无法使用auto模式,请mslxdff -d 启动(本地网关 http://127.0.0.1:${port} 拒绝连接)— 3ms 内失败说明未触达任何模型(非模型额度问题);若已改端口请用 mslxdff -port N 或 MSLXDFF_PORT=${port} 保持一致`;
|
|
155
|
-
if (TRACE) console.log(`\x1b[90m· [LLM] gateway auto FAIL · ${Math.round(performance.now() - t0)}ms · ${friendly} · ${msg.slice(0, 80)}\x1b[0m`);
|
|
156
|
-
return { ok: false, error: friendly, status: 502, code: "GATEWAY_NOT_RUNNING", port, raw: msg };
|
|
157
|
-
}
|
|
158
|
-
return { ok: false, error: `gateway ${msg}`, status: 502 };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return { chatViaGateway };
|
|
163
|
-
}
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { parseSse } from "./sse.js";
|
|
3
|
+
import { DEFAULT_PORT } from "../state.js";
|
|
4
|
+
import { compatFetch } from "../compat.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 网关深模块:POST 127.0.0.1:port/v1/chat/completions model:auto
|
|
8
|
+
* 注入化:fetch/loadToken/getPort/readModelsJson 均可伪,便于单测
|
|
9
|
+
*/
|
|
10
|
+
export function createGatewayClient({
|
|
11
|
+
fetchImpl = compatFetch,
|
|
12
|
+
loadToken,
|
|
13
|
+
getPort,
|
|
14
|
+
defaultPort = DEFAULT_PORT,
|
|
15
|
+
readModelsJson,
|
|
16
|
+
gatewayTimeoutMs = 25000,
|
|
17
|
+
env = process.env,
|
|
18
|
+
} = {}) {
|
|
19
|
+
const _fetch = fetchImpl;
|
|
20
|
+
const _loadToken = loadToken || (async () => {
|
|
21
|
+
try {
|
|
22
|
+
const state = await import("../state.js");
|
|
23
|
+
const loaded = await state.loadToken();
|
|
24
|
+
return String(loaded?.token || "").trim();
|
|
25
|
+
} catch { return ""; }
|
|
26
|
+
});
|
|
27
|
+
// 默认实现与 loadToken/readModelsJson 同款:懒加载 state(ESM 里没有 require)
|
|
28
|
+
// Note: 这里曾用 require 导致 state.port 被静默忽略 — 见 .agents/notes/implemented/bug-fix/2026-09-17-esm-dead-require-and-orphan-cooling.md
|
|
29
|
+
const _getPort = getPort || (async () => {
|
|
30
|
+
try {
|
|
31
|
+
const s = await import("../state.js");
|
|
32
|
+
const p = s.getPort();
|
|
33
|
+
if (Number.isInteger(p) && p > 0) return p;
|
|
34
|
+
} catch {}
|
|
35
|
+
const v = Number(env.MSLXDFF_PORT);
|
|
36
|
+
if (Number.isInteger(v) && v > 0) return v;
|
|
37
|
+
return defaultPort;
|
|
38
|
+
});
|
|
39
|
+
const _readModels = readModelsJson || (async () => {
|
|
40
|
+
try {
|
|
41
|
+
const { readFileSync, existsSync } = await import("node:fs");
|
|
42
|
+
const { join } = await import("node:path");
|
|
43
|
+
const { homedir } = await import("node:os");
|
|
44
|
+
const cache = join(homedir(), ".config", "mslxdff", "models.json");
|
|
45
|
+
if (existsSync(cache)) return JSON.parse(readFileSync(cache, "utf8"));
|
|
46
|
+
} catch {}
|
|
47
|
+
return { data: [] };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function chatViaGateway({ messages, tools, autoProvider = "opencode" }) {
|
|
51
|
+
const TRACE = env.MSLXDFF_CHAT_TRACE !== "0";
|
|
52
|
+
const t0 = TRACE ? performance.now() : 0;
|
|
53
|
+
let port = defaultPort;
|
|
54
|
+
let token = "";
|
|
55
|
+
try {
|
|
56
|
+
token = String((await _loadToken()) || "").trim();
|
|
57
|
+
const p = await _getPort();
|
|
58
|
+
if (Number.isInteger(p) && p > 0) port = p;
|
|
59
|
+
} catch {}
|
|
60
|
+
if (!token) {
|
|
61
|
+
try {
|
|
62
|
+
const { readFileSync, existsSync } = await import("node:fs");
|
|
63
|
+
const { join } = await import("node:path");
|
|
64
|
+
const { homedir } = await import("node:os");
|
|
65
|
+
const sf = env.MSLXDFF_STATE_FILE || join(homedir(), ".config", "mslxdff", "state.json");
|
|
66
|
+
if (existsSync(sf)) {
|
|
67
|
+
const j = JSON.parse(readFileSync(sf, "utf8"));
|
|
68
|
+
if (typeof j.token === "string" && j.token.trim()) token = j.token.trim();
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
const url = `http://127.0.0.1:${port}/v1/chat/completions`;
|
|
73
|
+
const body = { model: "auto", messages, stream: false };
|
|
74
|
+
if (tools?.length) { body.tools = tools; body.tool_choice = "auto"; }
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timer = setTimeout(() => controller.abort(), gatewayTimeoutMs);
|
|
79
|
+
const res = await _fetch(url, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
...(autoProvider ? { "x-mslxdff-auto-provider": autoProvider } : {}),
|
|
84
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
const txt = await res.text();
|
|
91
|
+
let j;
|
|
92
|
+
try { j = JSON.parse(txt); } catch {
|
|
93
|
+
if (txt.includes("data:")) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = parseSse(txt);
|
|
96
|
+
if (parsed.sseOk && (parsed.content || parsed.toolCalls.length)) {
|
|
97
|
+
const hasToolCalls = parsed.toolCalls.length > 0;
|
|
98
|
+
const hasContent = !!parsed.content;
|
|
99
|
+
let finishReason = parsed.finishReason;
|
|
100
|
+
if (hasToolCalls && !hasContent) finishReason = "tool_calls";
|
|
101
|
+
const tool_calls = hasToolCalls ? parsed.toolCalls : undefined;
|
|
102
|
+
const msg = { role: "assistant", content: parsed.content || "" };
|
|
103
|
+
if (tool_calls) msg.tool_calls = tool_calls;
|
|
104
|
+
j = { id: `sse-${Date.now()}`, object: "chat.completion", model: parsed.model, choices: [{ index: 0, finish_reason: finishReason, message: msg }], usage: parsed.usage };
|
|
105
|
+
} else if (parsed.sseOk) {
|
|
106
|
+
return { ok: false, error: `gateway SSE no content: ${txt.slice(0, 800)}`, status: res.status };
|
|
107
|
+
} else {
|
|
108
|
+
return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const msg = j?.error?.message || j?.error || j?.data?.error?.message || j?.data?.error || txt.slice(0, 800);
|
|
119
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] gateway auto FAIL · ${Math.round(performance.now() - t0)}ms · HTTP ${res.status} ${String(msg).slice(0, 80)}\x1b[0m`);
|
|
120
|
+
return { ok: false, error: msg, status: res.status };
|
|
121
|
+
}
|
|
122
|
+
const choice = j.choices?.[0] || j.data?.choices?.[0];
|
|
123
|
+
const effectiveJ = j.choices ? j : (j.data?.choices ? j.data : j);
|
|
124
|
+
if (!choice) {
|
|
125
|
+
if (TRACE) console.log(`\x1b[90m· [gateway debug] no choice, txt=${txt.slice(0, 800)} · j=${JSON.stringify(j).slice(0, 800)}\x1b[0m`);
|
|
126
|
+
return { ok: false, error: `gateway no choice: ${txt.slice(0, 800)}`, status: res.status };
|
|
127
|
+
}
|
|
128
|
+
let provider = "opencode";
|
|
129
|
+
const rawModel = effectiveJ.model || j.model || choice.message?.model || "auto";
|
|
130
|
+
try {
|
|
131
|
+
const c = await _readModels();
|
|
132
|
+
const ids = (c.data || []).map((x) => x.id).filter(Boolean);
|
|
133
|
+
for (const pid of ids) {
|
|
134
|
+
const slash = pid.indexOf("/");
|
|
135
|
+
const prov = slash > 0 ? pid.slice(0, slash) : "opencode";
|
|
136
|
+
const raw = slash > 0 ? pid.slice(slash + 1) : pid;
|
|
137
|
+
if (pid === rawModel || raw === rawModel || pid.endsWith("/" + rawModel)) { provider = prov; break; }
|
|
138
|
+
}
|
|
139
|
+
if (provider === "opencode" && rawModel.includes("/")) {
|
|
140
|
+
const maybe = rawModel.split("/")[0];
|
|
141
|
+
if (["workbuddy", "cline", "sensenova", "openrouter", "generic"].includes(maybe)) provider = maybe;
|
|
142
|
+
}
|
|
143
|
+
} catch {}
|
|
144
|
+
if (TRACE) {
|
|
145
|
+
const dt = Math.round(performance.now() - t0);
|
|
146
|
+
console.log(`\x1b[90m· [LLM] gateway auto OK · ${dt}ms · 模型 ${provider !== "opencode" ? provider + "/" : ""}${rawModel} · 总 ${dt}ms (gateway-fallback)\x1b[0m`);
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, message: choice.message, usage: effectiveJ.usage || j.usage, raw: j, status: res.status, model: rawModel, provider, viaGateway: true };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const raw = String(err?.message || err);
|
|
151
|
+
const msg = raw.slice(0, 800);
|
|
152
|
+
const isConnRefused = /ECONNREFUSED|Failed to fetch|fetch failed|connect ECONNREFUSED|ECONNRESET|EADDRNOTAVAIL/i.test(raw);
|
|
153
|
+
if (isConnRefused) {
|
|
154
|
+
const friendly = `本地服务没有启动无法使用auto模式,请mslxdff -d 启动(本地网关 http://127.0.0.1:${port} 拒绝连接)— 3ms 内失败说明未触达任何模型(非模型额度问题);若已改端口请用 mslxdff -port N 或 MSLXDFF_PORT=${port} 保持一致`;
|
|
155
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] gateway auto FAIL · ${Math.round(performance.now() - t0)}ms · ${friendly} · ${msg.slice(0, 80)}\x1b[0m`);
|
|
156
|
+
return { ok: false, error: friendly, status: 502, code: "GATEWAY_NOT_RUNNING", port, raw: msg };
|
|
157
|
+
}
|
|
158
|
+
return { ok: false, error: `gateway ${msg}`, status: 502 };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { chatViaGateway };
|
|
163
|
+
}
|