mslxdff 0.1.66 → 0.1.68
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/package.json +1 -1
- package/src/bench/probe.js +68 -0
- package/src/bench/report.js +56 -0
- package/src/bench/runner.js +74 -0
- 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 -408
- package/src/cli/commands/provider/bench.js +140 -0
- package/src/cli/commands/provider/index.js +2 -0
- package/src/cli/provider-row.js +73 -0
- package/src/cli/status.js +3 -52
- package/src/metrics.js +63 -0
- package/src/providers/dispatcher.js +18 -6
- package/src/providers/generic.js +2 -0
- package/src/providers/openrouter.js +16 -72
- 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 -652
- package/src/routes/chat/broadband-handler.js +25 -46
- package/src/routes/chat/exhausted-handler.js +2 -2
- package/src/routes/chat/hedge-handler.js +65 -83
- 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/memory.js +2 -21
- package/src/state/merge.js +26 -0
- package/src/state/provider-config.js +143 -0
- package/src/state/schemas/provider.js +83 -133
- package/src/state/store.js +2 -28
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { performance as nodePerf } from "node:perf_hooks";
|
|
2
|
+
|
|
3
|
+
const ORIG_PREFERRED = "mimo-v2.5-free";
|
|
4
|
+
const ORIG_FALLBACK = "big-pickle";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 编排深模块:mimo → pickle → gateway 三级降级 + 800ms 对冲
|
|
8
|
+
* 注入化:chatOnce / chatViaGateway / cooling / config / env / performance
|
|
9
|
+
*/
|
|
10
|
+
export function createOrchestrator({
|
|
11
|
+
chatOnce,
|
|
12
|
+
chatViaGateway,
|
|
13
|
+
cooling,
|
|
14
|
+
config = {},
|
|
15
|
+
env = process.env,
|
|
16
|
+
performance: perf = nodePerf,
|
|
17
|
+
chatWithFallbackImpl,
|
|
18
|
+
} = {}) {
|
|
19
|
+
const CHAT_PREFERRED = config.CHAT_PREFERRED || ORIG_PREFERRED;
|
|
20
|
+
const CHAT_FALLBACK = config.CHAT_FALLBACK || ORIG_FALLBACK;
|
|
21
|
+
const CHAT_GATEWAY_TIMEOUT_MS = config.CHAT_GATEWAY_TIMEOUT_MS || 25000;
|
|
22
|
+
const CHAT_COOLDOWN_MS = 10 * 60 * 1000;
|
|
23
|
+
const CHAT_SLOW_COOLDOWN_MS = 10 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
const _chatOnce = chatOnce || (async () => ({ ok: false, error: "no chatOnce", status: 500 }));
|
|
26
|
+
const _gateway = chatViaGateway || (async () => ({ ok: false, error: "no gateway", status: 500 }));
|
|
27
|
+
const _cooling = cooling || {
|
|
28
|
+
isCooling: async () => false,
|
|
29
|
+
recordError: async () => {},
|
|
30
|
+
recordOk: async () => {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function isCoolingAsync(id) {
|
|
34
|
+
try { return await _cooling.isCooling(id); } catch { return false; }
|
|
35
|
+
}
|
|
36
|
+
async function recordChatError(id, status, opts) {
|
|
37
|
+
try { await _cooling.recordError(id, status, opts); } catch {}
|
|
38
|
+
}
|
|
39
|
+
async function recordChatOk(id, latencyMs) {
|
|
40
|
+
try { await _cooling.recordOk(id, latencyMs); } catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function safeChatOnce(opts, model) {
|
|
44
|
+
try {
|
|
45
|
+
const r = await _chatOnce({ ...opts, model });
|
|
46
|
+
return r;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const msg = String(err?.message || err).slice(0, 800);
|
|
49
|
+
const status = err?._t ? 502 : 502;
|
|
50
|
+
return { ok: false, error: msg, status, _thrown: err };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function chatWithFallback(opts) {
|
|
55
|
+
if (chatWithFallbackImpl) return chatWithFallbackImpl(opts);
|
|
56
|
+
const TRACE = env.MSLXDFF_CHAT_TRACE !== "0";
|
|
57
|
+
const HEDGE_MS = (() => {
|
|
58
|
+
const v = Number(env.MSLXDFF_HEDGE_DELAY_MS);
|
|
59
|
+
return Number.isInteger(v) && v >= 0 ? v : 800;
|
|
60
|
+
})();
|
|
61
|
+
const t0 = TRACE ? perf.now() : 0;
|
|
62
|
+
|
|
63
|
+
const firstCooling = await isCoolingAsync(CHAT_PREFERRED);
|
|
64
|
+
let first;
|
|
65
|
+
let firstMs = 0;
|
|
66
|
+
if (firstCooling) {
|
|
67
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 跳过(冷却中)· 直接试 ${CHAT_FALLBACK}\x1b[0m`);
|
|
68
|
+
first = { ok: false, error: "skip cooling", status: 429 };
|
|
69
|
+
} else {
|
|
70
|
+
const t = perf.now();
|
|
71
|
+
first = await safeChatOnce(opts, CHAT_PREFERRED);
|
|
72
|
+
firstMs = Math.round(perf.now() - t);
|
|
73
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} ${first.ok ? "OK" : "FAIL"} · ${Math.round(perf.now() - t0)}ms${first.ok ? "" : ` · ${String(first.error).slice(0, 80)}`}\x1b[0m`);
|
|
74
|
+
if (first.ok) {
|
|
75
|
+
await recordChatOk(CHAT_PREFERRED, firstMs);
|
|
76
|
+
return { ...first, model: CHAT_PREFERRED };
|
|
77
|
+
} else {
|
|
78
|
+
const slow = firstMs > 20000;
|
|
79
|
+
await recordChatError(CHAT_PREFERRED, first.status, { slow, latencyMs: firstMs });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (firstCooling) {
|
|
84
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 冷却中(${CHAT_COOLDOWN_MS / 60000}min)· 直接走网关 auto,跳过 ${CHAT_FALLBACK}\x1b[0m`);
|
|
85
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 冷却,直接走网关 auto(:8989)\x1b[0m`);
|
|
86
|
+
const t2 = TRACE ? perf.now() : 0;
|
|
87
|
+
const third = await _gateway(opts);
|
|
88
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] gateway auto ${third.ok ? "OK" : "FAIL"} · ${Math.round(perf.now() - t2)}ms · 总 ${Math.round(perf.now() - t0)}ms (gateway-fallback)\x1b[0m`);
|
|
89
|
+
if (third.ok) return { ...third, model: third.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: "skip big-pickle (mimo cooling)", viaGateway: true };
|
|
90
|
+
return { ok: false, error: `${CHAT_PREFERRED} cooling: ${first.error}; gateway auto failed: ${third.error}`, status: third.status || 429 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const secondCooling = await isCoolingAsync(CHAT_FALLBACK);
|
|
94
|
+
const doGateway = () => _gateway(opts);
|
|
95
|
+
const doSecond = async () => {
|
|
96
|
+
const t = perf.now();
|
|
97
|
+
const r = await safeChatOnce(opts, CHAT_FALLBACK);
|
|
98
|
+
const ms = Math.round(perf.now() - t);
|
|
99
|
+
if (r.ok) await recordChatOk(CHAT_FALLBACK, ms);
|
|
100
|
+
else await recordChatError(CHAT_FALLBACK, r.status, { slow: ms > 20000, latencyMs: ms });
|
|
101
|
+
return { res: r, ms };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (secondCooling) {
|
|
105
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_FALLBACK} 跳过(冷却中)· 直接走网关 auto\x1b[0m`);
|
|
106
|
+
const t2 = perf.now();
|
|
107
|
+
const third = await doGateway();
|
|
108
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] gateway auto ${third.ok ? "OK" : "FAIL"} · ${Math.round(perf.now() - t2)}ms · 总 ${Math.round(perf.now() - t0)}ms (gateway-fallback)\x1b[0m`);
|
|
109
|
+
if (third.ok) return { ...third, model: third.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: "skip cooling", viaGateway: true };
|
|
110
|
+
return { ok: false, error: `${CHAT_PREFERRED} failed: ${first.error}; ${CHAT_FALLBACK} failed: skip cooling; gateway auto failed: ${third.error}`, status: third.status || first.status };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_PREFERRED} 失败,${CHAT_FALLBACK} + gateway 对冲中(${HEDGE_MS}ms)\x1b[0m`);
|
|
114
|
+
const t1 = TRACE ? perf.now() : 0;
|
|
115
|
+
let secondRes = null;
|
|
116
|
+
let secondMs = 0;
|
|
117
|
+
let gatewayRes = null;
|
|
118
|
+
|
|
119
|
+
const secondPromise = doSecond().then(({ res, ms }) => {
|
|
120
|
+
secondRes = res; secondMs = ms;
|
|
121
|
+
if (TRACE) console.log(`\x1b[90m· [LLM] ${CHAT_FALLBACK} ${res.ok ? "OK" : "FAIL"} · ${Math.round(perf.now() - t1)}ms · 总 ${Math.round(perf.now() - t0)}ms (hedge)\x1b[0m`);
|
|
122
|
+
return res;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
let gatewayPromise = null;
|
|
126
|
+
const gatewayDelay = HEDGE_MS > 0 ? HEDGE_MS : 0;
|
|
127
|
+
if (gatewayDelay > 0) {
|
|
128
|
+
gatewayPromise = new Promise((resolve) => {
|
|
129
|
+
setTimeout(async () => {
|
|
130
|
+
const r = await doGateway();
|
|
131
|
+
gatewayRes = r;
|
|
132
|
+
resolve(r);
|
|
133
|
+
}, gatewayDelay);
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
gatewayPromise = doGateway().then((r) => { gatewayRes = r; return r; });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const raceFirstOk = async () => {
|
|
140
|
+
const secondOrTimeout = await Promise.race([
|
|
141
|
+
secondPromise.then((r) => ({ kind: "second", r })),
|
|
142
|
+
new Promise((resolve) => setTimeout(() => resolve({ kind: "timeout" }), gatewayDelay)),
|
|
143
|
+
]);
|
|
144
|
+
if (secondOrTimeout.kind === "second" && secondOrTimeout.r?.ok) {
|
|
145
|
+
return { ok: true, res: secondOrTimeout.r, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
|
|
146
|
+
}
|
|
147
|
+
if (!gatewayPromise || secondOrTimeout.kind === "timeout") {
|
|
148
|
+
const immediate = doGateway().then((r) => { gatewayRes = r; return r; });
|
|
149
|
+
if (gatewayPromise) {
|
|
150
|
+
gatewayRes = await Promise.race([gatewayPromise, immediate]);
|
|
151
|
+
} else {
|
|
152
|
+
gatewayRes = await immediate;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
gatewayRes = await gatewayPromise;
|
|
156
|
+
}
|
|
157
|
+
if (gatewayRes?.ok) return { ok: true, res: gatewayRes, model: gatewayRes.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: secondRes?.error, viaGateway: true };
|
|
158
|
+
if (secondRes?.ok) return { ok: true, res: secondRes, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
|
|
159
|
+
return { ok: false, gatewayRes, secondRes };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (gatewayDelay === 0) {
|
|
163
|
+
const [sRes, gRes] = await Promise.all([
|
|
164
|
+
secondPromise.catch((e) => ({ ok: false, error: String(e), status: 502 })),
|
|
165
|
+
doGateway().catch((e) => ({ ok: false, error: String(e), status: 502 })),
|
|
166
|
+
]);
|
|
167
|
+
secondRes = sRes; gatewayRes = gRes;
|
|
168
|
+
if (sRes?.ok) return { ...sRes, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
|
|
169
|
+
if (gRes?.ok) return { ...gRes, model: gRes.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: sRes?.error, viaGateway: true };
|
|
170
|
+
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 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const raced = await raceFirstOk();
|
|
174
|
+
if (raced.ok) {
|
|
175
|
+
const r = raced.res;
|
|
176
|
+
return { ...r, model: raced.model, fallback: raced.fallback, fallbackGateway: raced.fallbackGateway, firstError: raced.firstError, secondError: raced.secondError, viaGateway: raced.viaGateway };
|
|
177
|
+
}
|
|
178
|
+
if (!secondRes) {
|
|
179
|
+
try { secondRes = await secondPromise; } catch (e) { secondRes = { ok: false, error: String(e), status: 502 }; }
|
|
180
|
+
}
|
|
181
|
+
if (secondRes?.ok) return { ...secondRes, model: CHAT_FALLBACK, fallback: true, firstError: first.error };
|
|
182
|
+
if (!gatewayRes) {
|
|
183
|
+
try { gatewayRes = await (gatewayPromise || doGateway()); } catch (e) { gatewayRes = { ok: false, error: String(e), status: 502 }; }
|
|
184
|
+
}
|
|
185
|
+
if (gatewayRes?.ok) return { ...gatewayRes, model: gatewayRes.model || "auto", fallbackGateway: true, fallback: true, firstError: first.error, secondError: secondRes?.error, viaGateway: true };
|
|
186
|
+
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 };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function summarizeHistory(messages) {
|
|
190
|
+
const prompt = [
|
|
191
|
+
{ role: "system", content: "你是对话压缩助手,把以下历史对话压缩成 800 字以内的中文摘要,保留关键操作与结果、用户的偏好与待办、模型设置与群组操作及时间线,不要遗漏重要细节。" },
|
|
192
|
+
{ role: "user", content: messages.map((m) => `${m.role}: ${m.content || JSON.stringify(m.tool_calls || "")}`).join("\n").slice(0, 90000) },
|
|
193
|
+
];
|
|
194
|
+
const r = await chatWithFallback({ messages: prompt });
|
|
195
|
+
if (!r.ok) return null;
|
|
196
|
+
const txt = String(r.message?.content || "").trim();
|
|
197
|
+
return txt ? `【历史摘要】${txt}` : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 兼容测试的额外覆盖参数
|
|
201
|
+
if (chatWithFallbackImpl) {
|
|
202
|
+
const orig = chatWithFallback;
|
|
203
|
+
// 允许测试覆盖 summarize 内部的 chatWithFallback
|
|
204
|
+
const wrapped = async (opts) => chatWithFallbackImpl(opts);
|
|
205
|
+
return { chatWithFallback: wrapped, summarizeHistory: async (msgs) => {
|
|
206
|
+
const prompt = [
|
|
207
|
+
{ role: "system", content: "你是对话压缩助手,把以下历史对话压缩成 800 字以内的中文摘要,保留关键操作与结果、用户的偏好与待办、模型设置与群组操作及时间线,不要遗漏重要细节。" },
|
|
208
|
+
{ role: "user", content: msgs.map((m) => `${m.role}: ${m.content || JSON.stringify(m.tool_calls || "")}`).join("\n").slice(0, 90000) },
|
|
209
|
+
];
|
|
210
|
+
const r = await wrapped({ messages: prompt });
|
|
211
|
+
if (!r.ok) return null;
|
|
212
|
+
const txt = String(r.message?.content || "").trim();
|
|
213
|
+
return txt ? `【历史摘要】${txt}` : null;
|
|
214
|
+
} };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { chatWithFallback, summarizeHistory };
|
|
218
|
+
}
|
package/src/chat/sse.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE 聚合深模块:把网关可能返回的 text/event-stream 聚合为单次 JSON 形状
|
|
3
|
+
* 纯函数,便于单测;与 gateway.js 共享
|
|
4
|
+
*/
|
|
5
|
+
export function parseSse(text) {
|
|
6
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
7
|
+
let content = "";
|
|
8
|
+
const toolCallsMap = new Map();
|
|
9
|
+
let model = "auto";
|
|
10
|
+
let usage = null;
|
|
11
|
+
let finishReason = "stop";
|
|
12
|
+
let sseOk = false;
|
|
13
|
+
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
const t = String(line).trim();
|
|
16
|
+
if (!t.startsWith("data:")) continue;
|
|
17
|
+
const payload = t.slice(5).trim();
|
|
18
|
+
if (!payload || payload === "[DONE]") continue;
|
|
19
|
+
try {
|
|
20
|
+
const obj = JSON.parse(payload);
|
|
21
|
+
sseOk = true;
|
|
22
|
+
const ch = obj.choices?.[0];
|
|
23
|
+
if (ch?.finish_reason) finishReason = ch.finish_reason;
|
|
24
|
+
if (ch?.delta?.content) content += ch.delta.content;
|
|
25
|
+
else if (ch?.delta?.reasoning_content) content += ch.delta.reasoning_content;
|
|
26
|
+
else if (ch?.message?.content) content += ch.message.content;
|
|
27
|
+
else if (ch?.message?.reasoning_content) content += ch.message.reasoning_content;
|
|
28
|
+
else if (typeof ch?.text === "string") content += ch.text;
|
|
29
|
+
else if (typeof obj.content === "string") content += obj.content;
|
|
30
|
+
|
|
31
|
+
if (ch?.delta?.tool_calls) {
|
|
32
|
+
for (const tc of ch.delta.tool_calls) {
|
|
33
|
+
const idx = tc.index ?? 0;
|
|
34
|
+
const cur = toolCallsMap.get(idx) || { id: tc.id || `chatcmpl-tool-${idx}`, type: tc.type || "function", function: { name: "", arguments: "" } };
|
|
35
|
+
if (tc.id) cur.id = tc.id;
|
|
36
|
+
if (tc.type) cur.type = tc.type;
|
|
37
|
+
if (tc.function?.name) cur.function.name = tc.function.name;
|
|
38
|
+
if (typeof tc.function?.arguments === "string") cur.function.arguments += tc.function.arguments;
|
|
39
|
+
toolCallsMap.set(idx, cur);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (ch?.message?.tool_calls) {
|
|
43
|
+
for (const tc of ch.message.tool_calls) {
|
|
44
|
+
const idx = tc.index ?? toolCallsMap.size;
|
|
45
|
+
toolCallsMap.set(idx, tc);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (obj.model) model = obj.model;
|
|
49
|
+
if (obj.usage) usage = obj.usage;
|
|
50
|
+
if (obj.choices?.[0]?.message?.content && !content) content = obj.choices[0].message.content;
|
|
51
|
+
if (obj.choices?.[0]?.message?.reasoning_content && !content) content = obj.choices[0].message.reasoning_content;
|
|
52
|
+
if (obj.choices?.[0]?.message?.tool_calls && toolCallsMap.size === 0) {
|
|
53
|
+
for (const tc of obj.choices[0].message.tool_calls) toolCallsMap.set(tc.index ?? 0, tc);
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const toolCalls = [...toolCallsMap.values()].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
59
|
+
return { content, toolCalls, model, usage, finishReason, sseOk };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function sseToMessage(parsed, { hasContent, hasToolCalls } = {}) {
|
|
63
|
+
const hc = hasContent ?? !!parsed.content;
|
|
64
|
+
const ht = hasToolCalls ?? parsed.toolCalls.length > 0;
|
|
65
|
+
if (!parsed.sseOk || (!hc && !ht)) return null;
|
|
66
|
+
const msg = { role: "assistant", content: parsed.content || "" };
|
|
67
|
+
if (ht) msg.tool_calls = parsed.toolCalls;
|
|
68
|
+
return msg;
|
|
69
|
+
}
|