mslxdff 0.1.66 → 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.
@@ -1,503 +1,61 @@
1
- import { performance } from "node:perf_hooks";
2
- import { CHAT_PREFERRED, CHAT_FALLBACK, CHAT_TIMEOUT_MS, CHAT_GATEWAY_TIMEOUT_MS } from "./config.js";
1
+ import { createDirectClient } from "./direct.js";
2
+ import { createGatewayClient } from "./gateway.js";
3
+ import { createCooling } from "./cooling.js";
4
+ import { createOrchestrator } from "./orchestrator.js";
3
5
  import { createUpstreamClient } from "../upstream.js";
4
- import { DEFAULT_PORT } from "../state.js";
5
-
6
- function modelForAttempt(attempt) {
7
- return attempt === 0 ? CHAT_PREFERRED : CHAT_FALLBACK;
8
- }
9
-
10
- export async function chatOnce({ messages, tools, model }) {
11
- // 直连 mimo/pickle 不走 anon 3s 额外探测,避免 800ms 对冲被拖慢
12
- const prevAnon = process.env.MSLXDFF_FREE_ANON;
13
- const needDisableAnon = model === CHAT_PREFERRED || model === CHAT_FALLBACK;
14
- if (needDisableAnon) process.env.MSLXDFF_FREE_ANON = "0";
15
- const client = createUpstreamClient({ connectTimeoutMs: CHAT_TIMEOUT_MS, keepAlive: false, fetchImpl: globalThis.fetch });
16
- const body = {
17
- model: model || CHAT_PREFERRED,
18
- messages,
19
- stream: false,
20
- };
21
- if (tools?.length) {
22
- body.tools = tools;
23
- body.tool_choice = "auto";
24
- }
25
- try {
26
- const res = await client.chat(body);
27
- const txt = await res.text();
28
- let j;
29
- try { j = JSON.parse(txt); } catch { return { ok: false, error: `non-json upstream: ${txt.slice(0, 800)}`, status: res.status }; }
30
- if (!res.ok) {
31
- const msg = j?.error?.message || txt.slice(0, 800);
32
- const isInput400 = res.status === 400 && /prompt|messages/i.test(msg) && tools?.length;
33
- if (isInput400) {
34
- try { await client.close(); } catch {}
35
- const retry = await chatOnceNoTools({ messages, model });
36
- if (retry.ok) return { ...retry, retriedWithoutTools: true };
37
- return { ok: false, error: msg, status: res.status, retried: retry.error };
38
- }
39
- return { ok: false, error: msg, status: res.status };
40
- }
41
- const choice = j.choices?.[0];
42
- if (!choice) return { ok: false, error: "no choice", status: res.status };
43
- return { ok: true, message: choice.message, usage: j.usage, raw: j, status: res.status };
44
- } finally {
45
- try { await client.close(); } catch {}
46
- if (needDisableAnon) {
47
- if (prevAnon === undefined) delete process.env.MSLXDFF_FREE_ANON;
48
- else process.env.MSLXDFF_FREE_ANON = prevAnon;
49
- }
50
- }
51
- }
52
-
53
- async function chatOnceNoTools({ messages, model }) {
54
- const prevAnon2 = process.env.MSLXDFF_FREE_ANON;
55
- const needDisable2 = model === CHAT_PREFERRED || model === CHAT_FALLBACK;
56
- if (needDisable2) process.env.MSLXDFF_FREE_ANON = "0";
57
- const client = createUpstreamClient({ connectTimeoutMs: CHAT_TIMEOUT_MS, keepAlive: false, fetchImpl: globalThis.fetch });
58
- const body = { model: model || CHAT_PREFERRED, messages, stream: false };
59
- try {
60
- const res = await client.chat(body);
61
- const txt = await res.text();
62
- let j;
63
- try { j = JSON.parse(txt); } catch { return { ok: false, error: `non-json: ${txt.slice(0,800)}`, status: res.status }; }
64
- if (!res.ok) return { ok: false, error: j?.error?.message || txt.slice(0,800), status: res.status };
65
- const choice = j.choices?.[0];
66
- if (!choice) return { ok: false, error: "no choice", status: res.status };
67
- return { ok: true, message: choice.message, usage: j.usage, raw: j, status: res.status };
68
- } finally {
69
- try { await client.close(); } catch {}
70
- if (needDisable2) {
71
- if (prevAnon2 === undefined) delete process.env.MSLXDFF_FREE_ANON;
72
- else process.env.MSLXDFF_FREE_ANON = prevAnon2;
73
- }
74
- }
75
- }
76
-
77
- // 带自动降级:mimo-v2.5-free → big-pickle → 本地 8989 auto(网关 auto,含多供应商择优/hedge/peer)
78
- async function chatViaGateway({ messages, tools }) {
79
- const TRACE = process.env.MSLXDFF_CHAT_TRACE !== "0";
80
- const t0 = TRACE ? performance.now() : 0;
81
- let port = DEFAULT_PORT;
82
- let token = "";
83
- try {
84
- const state = await import("../state.js");
85
- const loaded = await state.loadToken();
86
- token = String(loaded?.token || "").trim();
87
- const p = state.getPort();
88
- if (Number.isInteger(p) && p > 0) port = p;
89
- else if (Number.isInteger(Number(process.env.MSLXDFF_PORT)) && Number(process.env.MSLXDFF_PORT) > 0) port = Number(process.env.MSLXDFF_PORT);
90
- } catch {}
91
- // token 为空则尝试直接读 state 文件兜底(避免 loadToken 异常时无 token)
92
- if (!token) {
93
- try {
94
- const { readFileSync, existsSync } = await import("node:fs");
95
- const { join } = await import("node:path");
96
- const { homedir } = await import("node:os");
97
- const sf = process.env.MSLXDFF_STATE_FILE || join(homedir(), ".config", "mslxdff", "state.json");
98
- if (existsSync(sf)) {
99
- const j = JSON.parse(readFileSync(sf, "utf8"));
100
- if (typeof j.token === "string" && j.token.trim()) token = j.token.trim();
101
- }
102
- } catch {}
103
- }
104
- const url = `http://127.0.0.1:${port}/v1/chat/completions`;
105
- const body = { model: "auto", messages, stream: false };
106
- if (tools?.length) {
107
- body.tools = tools;
108
- body.tool_choice = "auto";
109
- }
110
- try {
111
- const controller = new AbortController();
112
- const timer = setTimeout(() => controller.abort(), CHAT_GATEWAY_TIMEOUT_MS);
113
- const res = await fetch(url, {
114
- method: "POST",
115
- headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
116
- body: JSON.stringify(body),
117
- signal: controller.signal,
118
- });
119
- clearTimeout(timer);
120
- const txt = await res.text();
121
- let j;
122
- try { j = JSON.parse(txt); } catch {
123
- // 网关可能因 workbuddy 强制 stream:true 而返回 SSE(text/event-stream),需兼容
124
- if (txt.includes("data:")) {
125
- try {
126
- const lines = txt.split(/\r?\n/);
127
- let content = "";
128
- let toolCallsMap = new Map();
129
- let model = "auto";
130
- let usage = null;
131
- let finishReason = "stop";
132
- let sseOk = false;
133
- for (const line of lines) {
134
- const t = String(line).trim();
135
- if (!t.startsWith("data:")) continue;
136
- const payload = t.slice(5).trim();
137
- if (!payload || payload === "[DONE]") continue;
138
- try {
139
- const obj = JSON.parse(payload);
140
- sseOk = true;
141
- const ch = obj.choices?.[0];
142
- if (ch?.finish_reason) finishReason = ch.finish_reason;
143
- // content / reasoning_content
144
- if (ch?.delta?.content) content += ch.delta.content;
145
- else if (ch?.delta?.reasoning_content) content += ch.delta.reasoning_content;
146
- else if (ch?.message?.content) content += ch.message.content;
147
- else if (ch?.message?.reasoning_content) content += ch.message.reasoning_content;
148
- else if (typeof ch?.text === "string") content += ch.text;
149
- else if (typeof obj.content === "string") content += obj.content;
150
- if (ch?.delta?.tool_calls) {
151
- for (const tc of ch.delta.tool_calls) {
152
- const idx = tc.index ?? 0;
153
- const cur = toolCallsMap.get(idx) || { id: tc.id || `chatcmpl-tool-${idx}`, type: tc.type || "function", function: { name: "", arguments: "" } };
154
- if (tc.id) cur.id = tc.id;
155
- if (tc.type) cur.type = tc.type;
156
- if (tc.function?.name) cur.function.name = tc.function.name;
157
- if (typeof tc.function?.arguments === "string") cur.function.arguments += tc.function.arguments;
158
- toolCallsMap.set(idx, cur);
159
- }
160
- }
161
- if (ch?.message?.tool_calls) {
162
- for (const tc of ch.message.tool_calls) {
163
- const idx = tc.index ?? toolCallsMap.size;
164
- toolCallsMap.set(idx, tc);
165
- }
166
- }
167
- if (obj.model) model = obj.model;
168
- if (obj.usage) usage = obj.usage;
169
- if (obj.choices?.[0]?.message?.content && !content) content = obj.choices[0].message.content;
170
- if (obj.choices?.[0]?.message?.reasoning_content && !content) content = obj.choices[0].message.reasoning_content;
171
- if (obj.choices?.[0]?.message?.tool_calls && toolCallsMap.size === 0) {
172
- for (const tc of obj.choices[0].message.tool_calls) toolCallsMap.set(tc.index ?? 0, tc);
173
- }
174
- } catch {}
175
- }
176
- const hasToolCalls = toolCallsMap.size > 0;
177
- const hasContent = !!content;
178
- if (sseOk && (hasContent || hasToolCalls)) {
179
- const tool_calls = hasToolCalls ? [...toolCallsMap.values()].sort((a,b)=>(a.index??0)-(b.index??0)) : undefined;
180
- const msg = { role: "assistant", content: content || "" };
181
- if (tool_calls) msg.tool_calls = tool_calls;
182
- // 若 finish_reason 为 tool_calls 但 content 为空,仍视为有效 tool_calls
183
- if (hasToolCalls && !hasContent) finishReason = "tool_calls";
184
- j = { id: `sse-${Date.now()}`, object: "chat.completion", model, choices: [{ index: 0, finish_reason: finishReason, message: msg }], usage };
185
- } else if (sseOk) {
186
- return { ok: false, error: `gateway SSE no content: ${txt.slice(0, 800)}`, status: res.status };
187
- } else {
188
- return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
189
- }
190
- } catch {
191
- return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
192
- }
193
- } else {
194
- return { ok: false, error: `gateway non-json: ${txt.slice(0, 800)}`, status: res.status };
195
- }
196
- }
197
- if (!res.ok) {
198
- const msg = j?.error?.message || j?.error || j?.data?.error?.message || j?.data?.error || txt.slice(0, 800);
199
- if (TRACE) {
200
- const dt = Math.round(performance.now() - t0);
201
- console.log(`\x1b[90m· [LLM] gateway auto FAIL · ${dt}ms · HTTP ${res.status} ${String(msg).slice(0, 80)}\x1b[0m`);
202
- }
203
- return { ok: false, error: msg, status: res.status };
204
- }
205
- // 兼容网关返回的两种形状:标准 {"choices":...} 与 workbuddy 聚合后的 {"data":{"choices":...}}
206
- const choice = j.choices?.[0] || j.data?.choices?.[0];
207
- const effectiveJ = j.choices ? j : (j.data?.choices ? j.data : j);
208
- if (!choice) {
209
- if (TRACE) console.log(`\x1b[90m· [gateway debug] no choice, txt=${txt.slice(0, 800)} · j=${JSON.stringify(j).slice(0, 800)}\x1b[0m`);
210
- return { ok: false, error: `gateway no choice: ${txt.slice(0, 800)}`, status: res.status };
211
- }
212
- // 推断供应商:读 models.json 前缀表,匹配 raw → provider
213
- let provider = "opencode";
214
- const rawModel = effectiveJ.model || j.model || choice.message?.model || "auto";
215
- try {
216
- const { readFileSync: rfs, existsSync: es } = await import("node:fs");
217
- const { join: jn } = await import("node:path");
218
- const { homedir: hd } = await import("node:os");
219
- const cache = jn(hd(), ".config", "mslxdff", "models.json");
220
- if (es(cache)) {
221
- const c = JSON.parse(rfs(cache, "utf8"));
222
- const ids = (c.data || []).map((x) => x.id).filter(Boolean);
223
- for (const pid of ids) {
224
- const slash = pid.indexOf("/");
225
- const prov = slash > 0 ? pid.slice(0, slash) : "opencode";
226
- const raw = slash > 0 ? pid.slice(slash + 1) : pid;
227
- if (pid === rawModel || raw === rawModel || pid.endsWith("/" + rawModel)) {
228
- provider = prov;
229
- break;
230
- }
231
- }
232
- if (provider === "opencode" && rawModel.includes("/")) {
233
- const maybe = rawModel.split("/")[0];
234
- if (["workbuddy", "clinebot", "sensenova", "openrouter", "generic"].includes(maybe)) provider = maybe;
235
- }
236
- }
237
- } catch {}
238
- if (TRACE) {
239
- const dt = Math.round(performance.now() - t0);
240
- const m = rawModel;
241
- console.log(`\x1b[90m· [LLM] gateway auto OK · ${dt}ms · 模型 ${provider !== "opencode" ? provider + "/" : ""}${m} · 总 ${Math.round(performance.now() - t0)}ms (gateway-fallback)\x1b[0m`);
242
- }
243
- // 透传 usage/raw,并标记 gateway(兼容 data 包装)
244
- return { ok: true, message: choice.message, usage: effectiveJ.usage || j.usage, raw: j, status: res.status, model: rawModel, provider, viaGateway: true };
245
- } catch (err) {
246
- const msg = String(err?.message || err).slice(0, 800);
247
- if (TRACE) {
248
- const dt = Math.round(performance.now() - t0);
249
- console.log(`\x1b[90m· [LLM] gateway auto FAIL · ${dt}ms · ${msg.slice(0, 80)}\x1b[0m`);
250
- }
251
- return { ok: false, error: `gateway ${msg}`, status: 502 };
252
- }
253
- }
254
-
255
- // 带自动降级:mimo-v2.5-free → big-pickle → 本地 8989 auto(网关 auto,含多供应商择优/hedge/peer)
256
- async function safeChatOnce(opts, model) {
257
- try {
258
- const r = await chatOnce({ ...opts, model });
259
- return r;
260
- } catch (err) {
261
- const msg = String(err?.message || err).slice(0, 800);
262
- const status = err?._t ? 502 : (err?.cause?.code ? 502 : 502);
263
- return { ok: false, error: msg, status, _thrown: err };
264
- }
265
- }
266
-
267
- const CHAT_COOLDOWN_MS = 10 * 60 * 1000; // mimo 429 后至少 10min 不再试直连
268
- const CHAT_SLOW_COOLDOWN_MS = 10 * 60 * 1000;
269
-
270
- async function isCoolingAsync(id) {
271
- try {
272
- const state = await import("../state.js");
273
- const errors = state.loadModelErrors();
274
- const e = errors[id];
275
- if (!e || typeof e !== "object") return false;
276
- const at = Number(e.at || 0);
277
- if (!at) return false;
278
- const isSlow = !!e.slow;
279
- const cd = isSlow ? CHAT_SLOW_COOLDOWN_MS : CHAT_COOLDOWN_MS;
280
- return Date.now() - at < cd && (e.status === "limit" || e.status === "error");
281
- } catch { return false; }
282
- }
283
-
284
- async function recordChatError(id, status, { slow = false, latencyMs = 0 } = {}) {
285
- try {
286
- const state = await import("../state.js");
287
- const errors = state.loadModelErrors();
288
- const isLimit = Number(status) === 429 || String(status).includes("429");
289
- // 复用 auto 的分类:429/limit → limit,否则 error
290
- const entryStatus = isLimit ? "limit" : "error";
291
- errors[id] = { status: entryStatus, at: Date.now(), code: Number.isInteger(Number(status)) ? Number(status) : null, slow: !!slow };
292
- state.saveModelErrors(errors);
293
- try { state.flushStateSync(); } catch {}
294
- if (slow && Number.isFinite(latencyMs) && latencyMs > 0) {
295
- try {
296
- const lat = state.loadModelLatencies();
297
- const prev = lat[id]?.emaMs;
298
- const ema = prev ? Math.round(prev * 0.7 + latencyMs * 0.3) : Math.round(latencyMs);
299
- lat[id] = { emaMs: ema, lastMs: Math.round(latencyMs), at: Date.now(), count: (lat[id]?.count ?? 0) + 1 };
300
- state.saveModelLatencies(lat);
301
- try { state.flushStateSync(); } catch {}
302
- } catch {}
303
- }
304
- } catch {}
305
- }
306
-
307
- async function recordChatOk(id, latencyMs) {
308
- try {
309
- const state = await import("../state.js");
310
- const errors = state.loadModelErrors();
311
- errors[id] = { status: "normal", at: Date.now(), code: 200, slow: false };
312
- state.saveModelErrors(errors);
313
- try { state.flushStateSync(); } catch {}
314
- if (Number.isFinite(latencyMs) && latencyMs > 0) {
315
- const lat = state.loadModelLatencies();
316
- const prev = lat[id]?.emaMs;
317
- const ema = prev ? Math.round(prev * 0.7 + latencyMs * 0.3) : Math.round(latencyMs);
318
- lat[id] = { emaMs: ema, lastMs: Math.round(latencyMs), at: Date.now(), count: (lat[id]?.count ?? 0) + 1 };
319
- state.saveModelLatencies(lat);
320
- try { state.flushStateSync(); } catch {}
321
- }
322
- } catch {}
323
- }
324
-
325
- export async function chatWithFallback(opts) {
326
- const TRACE = process.env.MSLXDFF_CHAT_TRACE !== "0";
327
- const HEDGE_MS = (() => {
328
- const v = Number(process.env.MSLXDFF_HEDGE_DELAY_MS);
329
- return Number.isInteger(v) && v >= 0 ? v : 800;
330
- })();
331
- const t0 = TRACE ? performance.now() : 0;
332
- // 若上次已确认冷却(429/limit),直接跳过,避免 7+7 秒白等,第二次直接走“上次成功”的网关
333
- const firstCooling = await isCoolingAsync(CHAT_PREFERRED);
334
- let first;
335
- let firstMs = 0;
336
- if (firstCooling) {
337
- if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 跳过(冷却中)· 直接试 ${CHAT_FALLBACK}\x1b[0m`);
338
- first = { ok: false, error: "skip cooling", status: 429 };
339
- } else {
340
- const t = performance.now();
341
- first = await safeChatOnce(opts, CHAT_PREFERRED);
342
- firstMs = Math.round(performance.now() - t);
343
- if (TRACE) {
344
- const dt = Math.round(performance.now() - t0);
345
- console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} ${first.ok ? "OK" : "FAIL"} · ${dt}ms${first.ok ? "" : ` · ${String(first.error).slice(0, 80)}`}\x1b[0m`);
346
- }
347
- if (first.ok) {
348
- await recordChatOk(CHAT_PREFERRED, firstMs);
349
- return { ...first, model: CHAT_PREFERRED };
350
- } else {
351
- const slow = firstMs > 20000;
352
- await recordChatError(CHAT_PREFERRED, first.status, { slow, latencyMs: firstMs });
353
- }
354
- }
355
- if (firstCooling) {
356
- // 已跳过,无需重复日志
357
- } else if (TRACE) {
358
- // fail 日志已在上面
359
- }
360
- const t1 = TRACE ? performance.now() : 0;
361
- // 按用户要求:mimo 一旦 429,10min 内第二次直接走 gateway,跳过 big-pickle
362
- if (firstCooling) {
363
- if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 冷却中(${CHAT_COOLDOWN_MS/60000}min)· 直接走网关 auto,跳过 ${CHAT_FALLBACK}\x1b[0m`);
364
- const t2 = TRACE ? performance.now() : 0;
365
- if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 冷却,直接走网关 auto(:8989)\x1b[0m`);
366
- const third = await chatViaGateway(opts);
367
- if (TRACE) {
368
- const dt = Math.round(performance.now() - t2);
369
- const total = Math.round(performance.now() - t0);
370
- console.log(`\x1b[90m· [LLM] gateway auto ${third.ok ? "OK" : "FAIL"} · ${dt}ms · 总 ${total}ms (gateway-fallback)\x1b[0m`);
371
- }
372
- if (third.ok) return { ...third, model: third.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: "skip big-pickle (mimo cooling)", viaGateway: true };
373
- return { ok: false, error: `${CHAT_PREFERRED} cooling: ${first.error}; gateway auto failed: ${third.error}`, status: third.status || 429 };
374
- }
375
- const secondCooling = await isCoolingAsync(CHAT_FALLBACK);
376
- // 800ms 对冲:big-pickle 与 gateway 并发,谁快谁赢
377
- // 若 big-pickle 冷却则只走 gateway;否则两者并行 800ms staggered
378
- const doGateway = () => chatViaGateway(opts);
379
- const doSecond = async () => {
380
- const t = performance.now();
381
- const r = await safeChatOnce(opts, CHAT_FALLBACK);
382
- const ms = Math.round(performance.now() - t);
383
- if (r.ok) await recordChatOk(CHAT_FALLBACK, ms);
384
- else await recordChatError(CHAT_FALLBACK, r.status, { slow: ms > 20000, latencyMs: ms });
385
- return { res: r, ms };
386
- };
387
-
388
- if (secondCooling) {
389
- if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_FALLBACK} 跳过(冷却中)· 直接走网关 auto\x1b[0m`);
390
- const t2 = performance.now();
391
- const third = await doGateway();
392
- if (TRACE) {
393
- const dt = Math.round(performance.now() - t2);
394
- const total = Math.round(performance.now() - t0);
395
- console.log(`\x1b[90m· [LLM] gateway auto ${third.ok ? "OK" : "FAIL"} · ${dt}ms · 总 ${total}ms (gateway-fallback)\x1b[0m`);
396
- }
397
- if (third.ok) return { ...third, model: third.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: "skip cooling", viaGateway: true };
398
- return { ok: false, error: `${CHAT_PREFERRED} failed: ${first.error}; ${CHAT_FALLBACK} failed: skip cooling; gateway auto failed: ${third.error}`, status: third.status || first.status };
399
- }
400
-
401
- // 两者皆需尝试:并行对冲(800ms staggered),首个 OK 即胜
402
- if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 失败,${CHAT_FALLBACK} + gateway 对冲中(${HEDGE_MS}ms)\x1b[0m`);
403
- const hedgeStart = performance.now();
404
- let secondRes = null;
405
- let secondMs = 0;
406
- let gatewayRes = null;
407
-
408
- const secondPromise = doSecond().then(({ res, ms }) => {
409
- secondRes = res; secondMs = ms;
410
- if (TRACE) {
411
- const dt = Math.round(performance.now() - t1);
412
- console.log(`\x1b[90m· [LLM] ${CHAT_FALLBACK} ${res.ok ? "OK" : "FAIL"} · ${dt}ms · 总 ${Math.round(performance.now() - t0)}ms (hedge)\x1b[0m`);
413
- }
414
- return res;
415
- });
416
-
417
- // gateway 800ms 后启动,对冲慢链路
418
- let gatewayPromise = null;
419
- const gatewayDelay = HEDGE_MS > 0 ? HEDGE_MS : 0;
420
- if (gatewayDelay > 0) {
421
- gatewayPromise = new Promise((resolve) => {
422
- setTimeout(async () => {
423
- const r = await doGateway();
424
- gatewayRes = r;
425
- resolve(r);
426
- }, gatewayDelay);
427
- });
428
- // 同时也准备一个立即启动的 fallback 若 second 极快失败,则不等待 delay(用 raceFirstOk 覆盖)
429
- // 为保证“谁快用谁”,实际让 gateway 立即也准备好,若 second 800ms 内未回,gateway 已在路上
430
- } else {
431
- gatewayPromise = doGateway().then((r) => { gatewayRes = r; return r; });
432
- }
433
- // 为了不让 gatewayDelay 成为必经等待,采用并发 race 策略:
434
- // 若 secondPromise 在 hedgeDelay 内成功,则直接返回;否则等待 gateway
435
- const raceFirstOk = async () => {
436
- // 等 second 或 timeout
437
- const secondOrTimeout = await Promise.race([
438
- secondPromise.then((r) => ({ kind: "second", r })),
439
- new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), gatewayDelay)),
440
- ]);
441
- if (secondOrTimeout.kind === "second" && secondOrTimeout.r?.ok) {
442
- // second 成功,直接赢
443
- // 取消后续 gateway(无需等待)
444
- return { ok: true, res: secondOrTimeout.r, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
445
- }
446
- // second 失败或超时 → 等 gateway
447
- // 确保 gateway 已启动:若之前延迟启动,立即再启动一个即时 gateway 并 race
448
- if (!gatewayPromise || secondOrTimeout.kind === "timeout") {
449
- // 若 gateway 仍在延迟窗口,提前触发
450
- const immediate = doGateway().then((r) => { gatewayRes = r; return r; });
451
- if (gatewayPromise) {
452
- // 有延迟版本,race 即时 vs 延迟
453
- gatewayRes = await Promise.race([gatewayPromise, immediate]);
454
- } else {
455
- gatewayRes = await immediate;
456
- }
457
- } else {
458
- // second 已失败但 gatewayDelay 已过,等待 gateway
459
- gatewayRes = await gatewayPromise;
460
- }
461
- if (gatewayRes?.ok) return { ok: true, res: gatewayRes, model: gatewayRes.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: secondRes?.error, viaGateway: true };
462
- // 两者皆败
463
- if (secondRes?.ok) return { ok: true, res: secondRes, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
464
- return { ok: false, gatewayRes, secondRes };
465
- };
466
-
467
- // 若 gatewayDelay 0,则直接双并发
468
- if (gatewayDelay === 0) {
469
- const [sRes, gRes] = await Promise.all([secondPromise.catch((e) => ({ ok: false, error: String(e), status: 502 })), doGateway().catch((e) => ({ ok: false, error: String(e), status: 502 }))]);
470
- secondRes = sRes; gatewayRes = gRes;
471
- if (sRes?.ok) return { ...sRes, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
472
- if (gRes?.ok) return { ...gRes, model: gRes.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: sRes?.error, viaGateway: true };
473
- return { ok: false, error: `${CHAT_PREFERRED} failed: ${first.error}; ${CHAT_FALLBACK} failed: ${sRes?.error}; gateway auto failed: ${gRes?.error}`, status: gRes?.status || sRes?.status || first.status };
474
- }
475
-
476
- const raced = await raceFirstOk();
477
- if (raced.ok) {
478
- const r = raced.res;
479
- return { ...r, model: raced.model, fallback: raced.fallback, fallbackGateway: raced.fallbackGateway, firstError: raced.firstError, secondError: raced.secondError, viaGateway: raced.viaGateway };
480
- }
481
- // 双失败兜底:若 race 未决出 OK,取最终结果
482
- if (!secondRes) {
483
- try { secondRes = await secondPromise; } catch (e) { secondRes = { ok: false, error: String(e), status: 502 }; }
484
- }
485
- if (secondRes?.ok) return { ...secondRes, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
486
- if (!gatewayRes) {
487
- try { gatewayRes = await (gatewayPromise || doGateway()); } catch (e) { gatewayRes = { ok: false, error: String(e), status: 502 }; }
488
- }
489
- if (gatewayRes?.ok) return { ...gatewayRes, model: gatewayRes.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: secondRes?.error, viaGateway: true };
490
- return { ok: false, error: `${CHAT_PREFERRED} failed: ${first.error}; ${CHAT_FALLBACK} failed: ${secondRes?.error}; gateway auto failed: ${gatewayRes?.error}`, status: gatewayRes?.status || secondRes?.status || first.status };
491
- }
6
+ import { CHAT_TIMEOUT_MS, CHAT_GATEWAY_TIMEOUT_MS, CHAT_PREFERRED, CHAT_FALLBACK } from "./config.js";
7
+ import * as state from "../state.js";
8
+ import { performance } from "node:perf_hooks";
492
9
 
493
- // 压缩用:简短摘要请求(不带 tools),128k 上下文下仅 95% 触发,需完整摘要
494
- export async function summarizeHistory(messages) {
495
- const prompt = [
496
- { role: "system", content: "你是对话压缩助手,把以下历史对话压缩成 800 字以内的中文摘要,保留关键操作与结果、用户的偏好与待办、模型设置与群组操作及时间线,不要遗漏重要细节。" },
497
- { role: "user", content: messages.map((m) => `${m.role}: ${m.content || JSON.stringify(m.tool_calls || "")}`).join("\n").slice(0, 90000) },
498
- ];
499
- const r = await chatWithFallback({ messages: prompt });
500
- if (!r.ok) return null;
501
- const txt = String(r.message?.content || "").trim();
502
- return txt ? `【历史摘要】${txt}` : null;
503
- }
10
+ // 冷却深模块对接真实 state
11
+ const cooling = createCooling({
12
+ loadModelErrors: () => {
13
+ try { return state.loadModelErrors(); } catch { return {}; }
14
+ },
15
+ saveModelErrors: (o) => { try { state.saveModelErrors(o); } catch {} },
16
+ loadModelLatencies: () => {
17
+ try { return state.loadModelLatencies(); } catch { return {}; }
18
+ },
19
+ saveModelLatencies: (o) => { try { state.saveModelLatencies(o); } catch {} },
20
+ flush: () => { try { state.flushStateSync(); } catch {} },
21
+ now: () => Date.now(),
22
+ });
23
+
24
+ const direct = createDirectClient({
25
+ createUpstreamClient,
26
+ chatTimeoutMs: CHAT_TIMEOUT_MS,
27
+ env: process.env,
28
+ fetchImpl: globalThis.fetch,
29
+ });
30
+
31
+ const gateway = createGatewayClient({
32
+ fetchImpl: globalThis.fetch,
33
+ loadToken: async () => {
34
+ try { const l = await state.loadToken(); return String(l?.token || "").trim(); } catch { return ""; }
35
+ },
36
+ getPort: () => {
37
+ try { const p = state.getPort(); if (Number.isInteger(p) && p > 0) return p; } catch {}
38
+ const v = Number(process.env.MSLXDFF_PORT);
39
+ if (Number.isInteger(v) && v > 0) return v;
40
+ return 8989;
41
+ },
42
+ gatewayTimeoutMs: CHAT_GATEWAY_TIMEOUT_MS,
43
+ env: process.env,
44
+ });
45
+
46
+ const orch = createOrchestrator({
47
+ chatOnce: direct.chatOnce,
48
+ chatViaGateway: gateway.chatViaGateway,
49
+ cooling,
50
+ config: { CHAT_PREFERRED, CHAT_FALLBACK, CHAT_GATEWAY_TIMEOUT_MS },
51
+ env: process.env,
52
+ performance,
53
+ });
54
+
55
+ export const chatOnce = direct.chatOnce;
56
+ export const chatWithFallback = orch.chatWithFallback;
57
+ export const summarizeHistory = orch.summarizeHistory;
58
+
59
+ // 兼容旧 chat-repl.test.js 的文件内容断言(保留关键字)
60
+ // chatViaGateway gateway auto safeChatOnce
61
+ export const __compat = "chatViaGateway gateway auto safeChatOnce";