mslxdff 0.1.87 → 0.1.89
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/cline-bench.js +21 -33
- package/src/bench/probe.js +4 -11
- package/src/bench/runner.js +8 -23
- package/src/bench/via-probe.js +12 -30
- package/src/bench/workbuddy-bench.js +15 -31
- package/src/chat-pipeline/engine.js +241 -0
- package/src/chat-pipeline/index.js +86 -0
- package/src/chat-pipeline/planner.js +31 -0
- package/src/chat-pipeline/policy.js +73 -0
- package/src/providers/cline/chat.js +26 -53
- package/src/providers/workbuddy/chat.js +109 -221
- package/src/routes/chat/gateway.js +23 -286
- package/src/routes/groups-relay.js +85 -1
- package/src/routes/index.js +7 -1
- package/src/routes/relay-queue.js +53 -0
- package/src/runtime/bootstrap.js +116 -34
- package/src/transport/index.js +248 -0
- package/src/transport/pool.js +60 -0
- package/src/transport/retry.js +24 -0
- package/src/transport/sse.js +93 -0
- package/src/upstream.js +113 -287
package/src/upstream.js
CHANGED
|
@@ -2,37 +2,24 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { appendFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
6
|
import { isFreeModel } from "./models.js";
|
|
7
|
-
import { defaultStateFile } from "./state.js";
|
|
8
7
|
import { fmtShanghaiYMDHMS } from "./time.js";
|
|
9
|
-
|
|
10
|
-
let UndiciAgent = null;
|
|
11
|
-
let UndiciFetch = null;
|
|
12
|
-
try {
|
|
13
|
-
const mod = await import("undici");
|
|
14
|
-
UndiciAgent = mod.Agent;
|
|
15
|
-
UndiciFetch = mod.fetch;
|
|
16
|
-
} catch {
|
|
17
|
-
// undici not installed — fallback to no dispatcher (still works, just no keep-alive tuning)
|
|
18
|
-
}
|
|
8
|
+
import { createTransport } from "./transport/index.js";
|
|
19
9
|
|
|
20
10
|
function genId(prefix) {
|
|
21
11
|
return `${prefix}${crypto.randomUUID().replace(/-/g, "")}`;
|
|
22
12
|
}
|
|
23
|
-
|
|
24
13
|
function envInt(name, fallback) {
|
|
25
14
|
const v = Number(process.env[name]);
|
|
26
15
|
return Number.isInteger(v) && v > 0 ? v : fallback;
|
|
27
16
|
}
|
|
28
|
-
|
|
29
17
|
function isPreheatDisabled() {
|
|
30
18
|
const raw = process.env.MSLXDFF_PREHEAT;
|
|
31
19
|
if (raw === undefined || raw === null || raw === "") return false;
|
|
32
20
|
const s = String(raw).trim().toLowerCase();
|
|
33
21
|
return s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled";
|
|
34
22
|
}
|
|
35
|
-
|
|
36
23
|
export function createUpstreamClient({
|
|
37
24
|
baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
38
25
|
authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
|
|
@@ -48,17 +35,6 @@ export function createUpstreamClient({
|
|
|
48
35
|
hooks,
|
|
49
36
|
keepAlive = true,
|
|
50
37
|
} = {}) {
|
|
51
|
-
// 使用 undici 的 fetch 与 Agent 配对,避免 global fetch 与 npm undici Agent 不兼容
|
|
52
|
-
if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
|
|
53
|
-
// hooks: async (name, ctx) => ({ value, changed }) | null — 由 bin 用 runHook 绑定;错误已在上层隔离
|
|
54
|
-
const applyHook = async (name, ctx) => {
|
|
55
|
-
if (!hooks) return null;
|
|
56
|
-
try {
|
|
57
|
-
return await hooks(name, ctx);
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
38
|
function isAnonFirst() {
|
|
63
39
|
const raw = process.env.MSLXDFF_OPENCOD_ANON_FIRST ?? process.env.MSLXDFF_ANON_FIRST ?? process.env.MSLXDFF_OPENCOD_ANON;
|
|
64
40
|
if (raw === undefined || raw === null || raw === "") return authToken === "public";
|
|
@@ -70,7 +46,7 @@ export function createUpstreamClient({
|
|
|
70
46
|
const baseHeaders = anonFirst
|
|
71
47
|
? {
|
|
72
48
|
"Content-Type": "application/json",
|
|
73
|
-
|
|
49
|
+
Authorization: "",
|
|
74
50
|
"x-opencode-client": "desktop",
|
|
75
51
|
"User-Agent": "opencode",
|
|
76
52
|
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
|
|
@@ -78,53 +54,40 @@ export function createUpstreamClient({
|
|
|
78
54
|
}
|
|
79
55
|
: {
|
|
80
56
|
"Content-Type": "application/json",
|
|
81
|
-
|
|
57
|
+
Authorization: `Bearer ${authToken}`,
|
|
82
58
|
"x-opencode-client": "desktop",
|
|
83
59
|
};
|
|
84
|
-
|
|
85
60
|
function buildHeaders(body, { anonymous = anonFirst } = {}) {
|
|
86
61
|
const isStream = body?.stream !== false;
|
|
87
62
|
const base = {
|
|
88
63
|
...baseHeaders,
|
|
89
|
-
|
|
64
|
+
Accept: isStream ? "text/event-stream" : "*/*",
|
|
90
65
|
"User-Agent": "opencode",
|
|
91
66
|
"x-opencode-session": genId("ses_"),
|
|
92
67
|
"x-opencode-request": genId("msg_"),
|
|
93
68
|
"x-opencode-project": "global",
|
|
94
69
|
};
|
|
95
70
|
if (anonymous) {
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
...base,
|
|
99
|
-
"Authorization": "",
|
|
100
|
-
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
|
|
101
|
-
"X-Title": "Hermes Agent",
|
|
102
|
-
};
|
|
71
|
+
return { ...base, Authorization: "", "HTTP-Referer": "https://hermes-agent.nousresearch.com", "X-Title": "Hermes Agent" };
|
|
103
72
|
}
|
|
104
|
-
// 显式 public 回退(anonFirst 时去掉 hermes 头)
|
|
105
73
|
if (anonFirst) {
|
|
106
74
|
const { "HTTP-Referer": _a, "X-Title": _b, ...rest } = base;
|
|
107
|
-
return { ...rest,
|
|
75
|
+
return { ...rest, Authorization: `Bearer ${authToken}` };
|
|
108
76
|
}
|
|
109
77
|
return base;
|
|
110
78
|
}
|
|
111
|
-
|
|
112
79
|
function shouldTryAnonFree() {
|
|
113
80
|
const raw = process.env.MSLXDFF_FREE_ANON;
|
|
114
81
|
if (raw === "0" || raw === "off" || raw === "false" || raw === "no") return false;
|
|
115
82
|
return true;
|
|
116
83
|
}
|
|
117
|
-
|
|
118
84
|
function isResponsesModel(model) {
|
|
119
|
-
|
|
120
|
-
return m.startsWith("muse-spark");
|
|
85
|
+
return String(model || "").toLowerCase().startsWith("muse-spark");
|
|
121
86
|
}
|
|
122
|
-
|
|
123
87
|
function chatToResponsesBody(chatBody) {
|
|
124
88
|
const msgs = Array.isArray(chatBody?.messages) ? chatBody.messages : [];
|
|
125
89
|
const system = msgs.filter((m) => m.role === "system").map((m) => String(m.content || "")).join("\n");
|
|
126
90
|
const nonSystem = msgs.filter((m) => m.role !== "system");
|
|
127
|
-
// 取最后一条 user 内容作为 input,其余拼到 input 前(保留上下文)
|
|
128
91
|
const inputParts = nonSystem.map((m) => {
|
|
129
92
|
const c = m.content;
|
|
130
93
|
if (typeof c === "string") return `${m.role}: ${c}`;
|
|
@@ -132,11 +95,7 @@ export function createUpstreamClient({
|
|
|
132
95
|
return `${m.role}: ${String(c || "")}`;
|
|
133
96
|
});
|
|
134
97
|
const input = inputParts.join("\n\n") || "hi";
|
|
135
|
-
const out = {
|
|
136
|
-
model: chatBody.model,
|
|
137
|
-
input,
|
|
138
|
-
stream: false, // muse-spark 的 chat 流式经 responses 的 SSE 格式不同,先以非流式 200 保证可用
|
|
139
|
-
};
|
|
98
|
+
const out = { model: chatBody.model, input, stream: false };
|
|
140
99
|
if (system) out.instructions = system;
|
|
141
100
|
if (chatBody.tools) out.tools = chatBody.tools;
|
|
142
101
|
if (chatBody.tool_choice) out.tool_choice = chatBody.tool_choice;
|
|
@@ -144,9 +103,7 @@ export function createUpstreamClient({
|
|
|
144
103
|
if (chatBody.max_tokens != null) out.max_output_tokens = chatBody.max_tokens;
|
|
145
104
|
return out;
|
|
146
105
|
}
|
|
147
|
-
|
|
148
106
|
function responsesToChatJson(respJson) {
|
|
149
|
-
// responses: {id, model, output:[{type:reasoning},{type:message, content:[{type:output_text,text}]}]}
|
|
150
107
|
let text = "";
|
|
151
108
|
for (const item of respJson.output || []) {
|
|
152
109
|
if (item.type === "message" && item.role === "assistant") {
|
|
@@ -157,7 +114,6 @@ export function createUpstreamClient({
|
|
|
157
114
|
}
|
|
158
115
|
}
|
|
159
116
|
if (!text) {
|
|
160
|
-
// 兜底:取 output_text 的第一个
|
|
161
117
|
for (const item of respJson.output || []) {
|
|
162
118
|
if (item.type === "message") {
|
|
163
119
|
const t = item.content?.[0]?.text;
|
|
@@ -165,288 +121,158 @@ export function createUpstreamClient({
|
|
|
165
121
|
}
|
|
166
122
|
}
|
|
167
123
|
}
|
|
168
|
-
if (!text) text = "";
|
|
169
124
|
const chatJson = {
|
|
170
125
|
id: respJson.id || `resp_${Date.now()}`,
|
|
171
126
|
object: "chat.completion",
|
|
172
127
|
created: Math.floor((respJson.created_at || Date.now() / 1000)),
|
|
173
128
|
model: respJson.model,
|
|
174
|
-
choices: [
|
|
175
|
-
{
|
|
176
|
-
index: 0,
|
|
177
|
-
finish_reason: respJson.status === "completed" ? "stop" : "length",
|
|
178
|
-
message: { role: "assistant", content: text },
|
|
179
|
-
},
|
|
180
|
-
],
|
|
129
|
+
choices: [{ index: 0, finish_reason: respJson.status === "completed" ? "stop" : "length", message: { role: "assistant", content: text } }],
|
|
181
130
|
usage: respJson.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
182
131
|
};
|
|
183
132
|
return chatJson;
|
|
184
133
|
}
|
|
185
|
-
|
|
186
134
|
function freeAnonLogFile() {
|
|
187
|
-
// 按用户要求:放在当前项目目录,方便一眼看到;可用 env 覆盖
|
|
188
135
|
return process.env.MSLXDFF_FREE_ANON_LOG || join(process.cwd(), "free-anon-extra.txt");
|
|
189
136
|
}
|
|
190
|
-
|
|
191
137
|
function logFreeAnon({ model, publicStatus, anonStatus, anonAttempts, hit, totalMs }) {
|
|
192
138
|
const file = freeAnonLogFile();
|
|
193
139
|
const line = `${fmtShanghaiYMDHMS(new Date())} model=${model} public=429 anonTries=${anonAttempts} anonStatus=${anonStatus ?? "none"} hit=${hit ? "YES额外额度" : "NO无额外"} totalMs=${totalMs} count=${hit ? "1" : "0"}\n`;
|
|
194
|
-
try {
|
|
195
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
196
|
-
} catch {}
|
|
140
|
+
try { mkdirSync(dirname(file), { recursive: true }); } catch {}
|
|
197
141
|
appendFile(file, line).catch(() => {});
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
if (process.env.MSLXDFF_DEBUG === "1") console.log(`[free-anon] ${line.trim()}`);
|
|
201
|
-
} catch {}
|
|
142
|
+
try { if (process.env.MSLXDFF_DEBUG === "1") console.log(`[free-anon] ${line.trim()}`); } catch {}
|
|
202
143
|
}
|
|
203
|
-
|
|
204
|
-
// 连续命中计数(内存,方便 txt 里看出“连续几次都有”)
|
|
205
144
|
let consecutiveHits = 0;
|
|
206
145
|
|
|
207
|
-
//
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
agent = new UndiciAgent({
|
|
216
|
-
keepAliveTimeout,
|
|
217
|
-
keepAliveMaxTimeout,
|
|
218
|
-
connections: keepAliveConnections,
|
|
219
|
-
pipelining: 1,
|
|
220
|
-
});
|
|
221
|
-
dispatcher = agent;
|
|
222
|
-
} catch {
|
|
223
|
-
agent = null;
|
|
224
|
-
dispatcher = null;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
146
|
+
// 深模块:网络层委托 StreamingTransport
|
|
147
|
+
const transport = createTransport({
|
|
148
|
+
keepAlive,
|
|
149
|
+
fetchImpl,
|
|
150
|
+
timeoutMs: connectTimeoutMs,
|
|
151
|
+
retry,
|
|
152
|
+
hooks,
|
|
153
|
+
});
|
|
227
154
|
|
|
228
155
|
async function chat(body) {
|
|
229
156
|
const isResp = isResponsesModel(body?.model);
|
|
230
157
|
const url = isResp ? `${baseUrl}/zen/v1/responses` : `${baseUrl}/zen/v1/chat/completions`;
|
|
231
158
|
const reqBody = isResp ? chatToResponsesBody(body) : body;
|
|
232
159
|
const t0 = performance.now();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
160
|
+
|
|
161
|
+
// 首发请求(transport 已处理 network/429 等重试)
|
|
162
|
+
let res;
|
|
163
|
+
try {
|
|
164
|
+
res = await transport.request({
|
|
165
|
+
url,
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: buildHeaders(reqBody),
|
|
168
|
+
body: reqBody,
|
|
169
|
+
stream: body?.stream !== false,
|
|
170
|
+
timeoutMs: connectTimeoutMs,
|
|
171
|
+
retry,
|
|
242
172
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
173
|
+
} catch (e) {
|
|
174
|
+
e._t = e._t || { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0) };
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 匿名兜底:仅非 anonFirst 时,public 429 + free 模型才走 hermes 空头重试
|
|
179
|
+
if (!anonFirst && res.status === 429 && isFreeModel(body?.model) && shouldTryAnonFree()) {
|
|
180
|
+
const anonRetries = envInt("MSLXDFF_FREE_ANON_RETRIES", 3);
|
|
181
|
+
const anonDelay = envInt("MSLXDFF_FREE_ANON_DELAY_MS", 1000);
|
|
182
|
+
let anonRes = null;
|
|
183
|
+
for (let i = 0; i < anonRetries; i++) {
|
|
184
|
+
await new Promise((r) => setTimeout(r, anonDelay));
|
|
185
|
+
try {
|
|
186
|
+
anonRes = await transport.request({
|
|
187
|
+
url,
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: buildHeaders(reqBody, { anonymous: true }),
|
|
190
|
+
body: reqBody,
|
|
191
|
+
stream: body?.stream !== false,
|
|
192
|
+
timeoutMs: connectTimeoutMs,
|
|
193
|
+
retry,
|
|
194
|
+
});
|
|
195
|
+
} catch (e) {
|
|
248
196
|
continue;
|
|
249
197
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
};
|
|
255
|
-
throw result;
|
|
256
|
-
}
|
|
257
|
-
const entry = retry?.[result.status];
|
|
258
|
-
if (entry && attempt < entry.attempts) {
|
|
259
|
-
await sleep(entry.delayMs);
|
|
260
|
-
waitMs += entry.delayMs;
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
// 额外额度探测:仅当非 anon 优先时,public 429 且为 free 模型时,用空头(hermes 方式)每秒重试 3 次;anon 优先时首发即高额度,无需再 public 撞墙
|
|
264
|
-
if (!anonFirst && result.status === 429 && isFreeModel(body?.model) && shouldTryAnonFree()) {
|
|
265
|
-
const anonRetries = envInt("MSLXDFF_FREE_ANON_RETRIES", 3);
|
|
266
|
-
const anonDelay = envInt("MSLXDFF_FREE_ANON_DELAY_MS", 1000);
|
|
267
|
-
let anonResult = null;
|
|
268
|
-
let hit = false;
|
|
269
|
-
let hitAt = -1;
|
|
270
|
-
for (let i = 0; i < anonRetries; i++) {
|
|
271
|
-
await sleep(anonDelay);
|
|
272
|
-
waitMs += anonDelay;
|
|
273
|
-
const t2 = performance.now();
|
|
274
|
-
anonResult = await attemptOnce(url, reqBody, { anonymous: true });
|
|
275
|
-
attempts.push({
|
|
276
|
-
attempt: `anon-${i}`,
|
|
277
|
-
type: anonResult instanceof Error ? "network" : `http${anonResult.status}`,
|
|
278
|
-
ms: Math.round(performance.now() - t2),
|
|
279
|
-
});
|
|
280
|
-
if (anonResult instanceof Error) continue;
|
|
281
|
-
if (anonResult.status !== 429) {
|
|
282
|
-
// responses 模型的 anon 命中也需转回 chat 形状
|
|
283
|
-
let outAnon = anonResult;
|
|
284
|
-
if (isResp && anonResult.ok) {
|
|
285
|
-
try {
|
|
286
|
-
const txt = await anonResult.text();
|
|
287
|
-
const j = JSON.parse(txt);
|
|
288
|
-
if (j && Array.isArray(j.output)) {
|
|
289
|
-
const chatJson = responsesToChatJson(j);
|
|
290
|
-
const newHeaders = new Headers(anonResult.headers);
|
|
291
|
-
newHeaders.set("content-type", "application/json");
|
|
292
|
-
outAnon = new Response(JSON.stringify(chatJson), { status: anonResult.status, headers: newHeaders });
|
|
293
|
-
} else {
|
|
294
|
-
outAnon = new Response(txt, { status: anonResult.status, headers: anonResult.headers });
|
|
295
|
-
}
|
|
296
|
-
} catch { outAnon = anonResult; }
|
|
297
|
-
}
|
|
298
|
-
outAnon._t = {
|
|
299
|
-
attempts,
|
|
300
|
-
waitMs,
|
|
301
|
-
totalMs: Math.round(performance.now() - t0),
|
|
302
|
-
anonTried: true,
|
|
303
|
-
anonAttempts: i + 1,
|
|
304
|
-
};
|
|
305
|
-
hit = true;
|
|
306
|
-
hitAt = i + 1;
|
|
307
|
-
consecutiveHits += 1;
|
|
308
|
-
logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonResult.status, anonAttempts: i + 1, hit: true, totalMs: outAnon._t.totalMs });
|
|
309
|
-
// 额外在 txt 里强调连续命中
|
|
198
|
+
if (anonRes.status !== 429) {
|
|
199
|
+
// responses 模型需转回 chat 形状
|
|
200
|
+
let outAnon = anonRes;
|
|
201
|
+
if (isResp && anonRes.ok) {
|
|
310
202
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
203
|
+
const txt = await anonRes.text();
|
|
204
|
+
const j = JSON.parse(txt);
|
|
205
|
+
if (j && Array.isArray(j.output)) {
|
|
206
|
+
const chatJson = responsesToChatJson(j);
|
|
207
|
+
outAnon = new Response(JSON.stringify(chatJson), { status: anonRes.status, headers: new Headers(anonRes.headers) });
|
|
208
|
+
outAnon.headers.set("content-type", "application/json");
|
|
209
|
+
} else {
|
|
210
|
+
outAnon = new Response(txt, { status: anonRes.status, headers: anonRes.headers });
|
|
314
211
|
}
|
|
315
|
-
} catch {}
|
|
316
|
-
if (process.env.MSLXDFF_DEBUG === "1") try { console.log(`[free-anon] ${body?.model} public 429 -> anon ${anonResult.status} after ${i + 1} try HIT`); } catch {}
|
|
317
|
-
return outAnon;
|
|
212
|
+
} catch { outAnon = anonRes; }
|
|
318
213
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
waitMs,
|
|
325
|
-
totalMs: Math.round(performance.now() - t0),
|
|
326
|
-
anonTried: true,
|
|
327
|
-
anonAttempts: anonRetries,
|
|
328
|
-
};
|
|
329
|
-
consecutiveHits = 0;
|
|
330
|
-
logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonResult.status, anonAttempts: anonRetries, hit: false, totalMs: anonResult._t.totalMs });
|
|
331
|
-
if (process.env.MSLXDFF_DEBUG === "1") try { console.log(`[free-anon] ${body?.model} public 429 -> anon 429 x${anonRetries} MISS`); } catch {}
|
|
332
|
-
return anonResult;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// Muse Spark 走 /responses,需把 responses 的 JSON 转回 chat.completions 形状,保持上层无感
|
|
336
|
-
if (isResp && result.ok) {
|
|
337
|
-
try {
|
|
338
|
-
const txt = await result.text();
|
|
339
|
-
const j = JSON.parse(txt);
|
|
340
|
-
// responses 成功态:{output:[...]};chat 态:{choices:[...]}。非 responses 形状则原样回
|
|
341
|
-
if (j && Array.isArray(j.output)) {
|
|
342
|
-
const chatJson = responsesToChatJson(j);
|
|
343
|
-
const newBody = JSON.stringify(chatJson);
|
|
344
|
-
const newHeaders = new Headers(result.headers);
|
|
345
|
-
newHeaders.set("content-type", "application/json");
|
|
346
|
-
const transformed = new Response(newBody, { status: result.status, headers: newHeaders });
|
|
347
|
-
transformed._t = {
|
|
348
|
-
attempts,
|
|
349
|
-
waitMs,
|
|
350
|
-
totalMs: Math.round(performance.now() - t0),
|
|
351
|
-
};
|
|
352
|
-
// 保留原始 timing 供日志
|
|
353
|
-
if (result._t) transformed._t = { ...transformed._t, ...result._t };
|
|
354
|
-
return transformed;
|
|
214
|
+
outAnon._t = { ...(outAnon._t || {}), anonTried: true, anonAttempts: i + 1, totalMs: Math.round(performance.now() - t0) };
|
|
215
|
+
consecutiveHits += 1;
|
|
216
|
+
logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonRes.status, anonAttempts: i + 1, hit: true, totalMs: outAnon._t.totalMs });
|
|
217
|
+
if (consecutiveHits >= 2) {
|
|
218
|
+
try { appendFile(freeAnonLogFile(), ` -> 连续额外额度 ${consecutiveHits} 次\n`).catch(() => {}); } catch {}
|
|
355
219
|
}
|
|
356
|
-
|
|
357
|
-
const fallback = new Response(txt, { status: result.status, headers: result.headers });
|
|
358
|
-
fallback._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
359
|
-
return fallback;
|
|
360
|
-
} catch {
|
|
361
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
362
|
-
return result;
|
|
220
|
+
return outAnon;
|
|
363
221
|
}
|
|
364
222
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
totalMs:
|
|
369
|
-
|
|
370
|
-
|
|
223
|
+
if (anonRes) {
|
|
224
|
+
anonRes._t = { ...(anonRes._t || {}), anonTried: true, anonAttempts: anonRetries, totalMs: Math.round(performance.now() - t0) };
|
|
225
|
+
consecutiveHits = 0;
|
|
226
|
+
logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonRes.status, anonAttempts: anonRetries, hit: false, totalMs: anonRes._t.totalMs });
|
|
227
|
+
return anonRes;
|
|
228
|
+
}
|
|
371
229
|
}
|
|
372
|
-
}
|
|
373
230
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
231
|
+
// responses 模型成功态转 chat
|
|
232
|
+
if (isResp && res.ok) {
|
|
233
|
+
try {
|
|
234
|
+
const txt = await res.text();
|
|
235
|
+
const j = JSON.parse(txt);
|
|
236
|
+
if (j && Array.isArray(j.output)) {
|
|
237
|
+
const chatJson = responsesToChatJson(j);
|
|
238
|
+
const headers = new Headers(res.headers);
|
|
239
|
+
headers.set("content-type", "application/json");
|
|
240
|
+
const transformed = new Response(JSON.stringify(chatJson), { status: res.status, headers });
|
|
241
|
+
transformed._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
|
|
242
|
+
return transformed;
|
|
243
|
+
}
|
|
244
|
+
const fallback = new Response(txt, { status: res.status, headers: res.headers });
|
|
245
|
+
fallback._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
|
|
246
|
+
return fallback;
|
|
247
|
+
} catch {
|
|
248
|
+
res._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
|
|
249
|
+
return res;
|
|
391
250
|
}
|
|
392
|
-
const opts = {
|
|
393
|
-
method: "POST",
|
|
394
|
-
headers,
|
|
395
|
-
body: JSON.stringify(body),
|
|
396
|
-
signal: controller.signal,
|
|
397
|
-
};
|
|
398
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
399
|
-
const res = await fetchImpl(reqUrl, opts);
|
|
400
|
-
return res;
|
|
401
|
-
} catch (err) {
|
|
402
|
-
return err;
|
|
403
|
-
} finally {
|
|
404
|
-
clearTimeout(timer);
|
|
405
251
|
}
|
|
252
|
+
res._t = { ...(res._t || {}), totalMs: res._t?.totalMs ?? Math.round(performance.now() - t0) };
|
|
253
|
+
return res;
|
|
406
254
|
}
|
|
407
255
|
|
|
408
256
|
async function preheat() {
|
|
409
257
|
if (isPreheatDisabled()) return { ok: false, skipped: true, reason: "disabled" };
|
|
410
|
-
const url = `${baseUrl}/zen/v1/models`;
|
|
411
|
-
const controller = new AbortController();
|
|
412
|
-
const timer = setTimeout(() => controller.abort(new Error("preheat timed out after 3000ms")), 3000);
|
|
413
258
|
const t0 = performance.now();
|
|
414
259
|
try {
|
|
415
|
-
const
|
|
416
|
-
|
|
260
|
+
const res = await transport.request({
|
|
261
|
+
url: `${baseUrl}/zen/v1/models`,
|
|
417
262
|
method: "GET",
|
|
418
|
-
headers,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return { ok: res.ok, status: res.status, ms };
|
|
427
|
-
} catch (err) {
|
|
428
|
-
return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
|
|
429
|
-
} finally {
|
|
430
|
-
clearTimeout(timer);
|
|
263
|
+
headers: buildHeaders({ stream: false }),
|
|
264
|
+
stream: false,
|
|
265
|
+
timeoutMs: 3000,
|
|
266
|
+
});
|
|
267
|
+
try { await res.text().catch(() => {}); } catch {}
|
|
268
|
+
return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return { ok: false, error: String(e?.message || e), ms: Math.round(performance.now() - t0) };
|
|
431
271
|
}
|
|
432
272
|
}
|
|
433
|
-
|
|
434
|
-
let closed = false;
|
|
435
273
|
async function close() {
|
|
436
|
-
|
|
437
|
-
closed = true;
|
|
438
|
-
if (agent && typeof agent.close === "function") {
|
|
439
|
-
try { await agent.close(); } catch {}
|
|
440
|
-
} else if (dispatcher && typeof dispatcher.close === "function" && dispatcher !== agent) {
|
|
441
|
-
try { await dispatcher.close(); } catch {}
|
|
442
|
-
}
|
|
274
|
+
await transport.close();
|
|
443
275
|
}
|
|
444
|
-
|
|
445
|
-
// 兼容旧调用:headers 为动态生成,暴露 getter 快照(用于测试/展示)
|
|
446
276
|
const headers = buildHeaders({});
|
|
447
|
-
return { chat, preheat, close, headers, buildHeaders, dispatcher, agent, [Symbol.asyncDispose]: close };
|
|
277
|
+
return { chat, preheat, close, headers, buildHeaders, get dispatcher() { return transport.dispatcher; }, get agent() { return transport.agent; }, [Symbol.asyncDispose]: close };
|
|
448
278
|
}
|
|
449
|
-
|
|
450
|
-
function sleep(ms) {
|
|
451
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
452
|
-
}
|