@noobdemon/noob-cli 1.10.8 → 1.10.10
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/api.js +5 -80
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -116,7 +116,7 @@ function hasUnclosedToolBlock(text) {
|
|
|
116
116
|
*
|
|
117
117
|
* @returns {Promise<{text:string, reasoning:string}>}
|
|
118
118
|
*/
|
|
119
|
-
export async function stream({ mode = "chat", message, model, system, conversation, effort, signal, onDelta, onReasoning, onStatus,
|
|
119
|
+
export async function stream({ mode = "chat", message, model, system, conversation, effort, signal, onDelta, onReasoning, onStatus, maxContinues = Infinity }) {
|
|
120
120
|
const endpoint = mode === "search" ? "/api/search" : mode === "merge" ? "/api/merge" : "/api/chat";
|
|
121
121
|
|
|
122
122
|
let fullText = "";
|
|
@@ -126,7 +126,7 @@ export async function stream({ mode = "chat", message, model, system, conversati
|
|
|
126
126
|
let emptyStreak = 0; // số lần liên tiếp stream rỗng (chống loop vô tận khi upstream chết hẳn)
|
|
127
127
|
|
|
128
128
|
for (let attempt = 0; ; attempt++) {
|
|
129
|
-
const r = await streamOnce({ endpoint, mode, message: prompt, model, system, conversation, effort, signal,
|
|
129
|
+
const r = await streamOnce({ endpoint, mode, message: prompt, model, system, conversation, effort, signal, onStatus, onDelta, onReasoning });
|
|
130
130
|
fullText = mode === "chat" ? fullText + r.text : r.text; // chat: ghép các đoạn nối tiếp; mode khác: thay thế
|
|
131
131
|
if (r.reasoning) reasoning = r.reasoning;
|
|
132
132
|
|
|
@@ -175,7 +175,7 @@ function continuationPrompt(message, partial) {
|
|
|
175
175
|
* One network attempt of the stream. Returns this attempt's accumulated text +
|
|
176
176
|
* a `truncated` flag telling the caller whether the reply was cut short.
|
|
177
177
|
*/
|
|
178
|
-
async function streamOnce({ endpoint, mode, message, model, system, conversation, effort, signal,
|
|
178
|
+
async function streamOnce({ endpoint, mode, message, model, system, conversation, effort, signal, onStatus, onDelta, onReasoning }) {
|
|
179
179
|
// chat body: gửi system + conversation riêng để gateway forward đúng tới upstream.
|
|
180
180
|
// Worker gateway (handleChat) + upstream đều nhận shape này.
|
|
181
181
|
let body;
|
|
@@ -188,77 +188,14 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
188
188
|
if (effort) body.effort = effort;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// Idle-timeout: nếu KHÔNG nhận được byte nào trong idleMs (kết nối treo), tự
|
|
192
|
-
// huỷ và báo lỗi rõ ràng — thay vì spinner quay vô tận. Vẫn tôn trọng signal
|
|
193
|
-
// của người dùng (Ctrl+C). Phân biệt 2 trường hợp qua cờ `timedOut`.
|
|
194
191
|
const ctrl = new AbortController();
|
|
195
|
-
let timedOut = false;
|
|
196
192
|
const onUserAbort = () => ctrl.abort();
|
|
197
193
|
signal?.addEventListener("abort", onUserAbort, { once: true });
|
|
198
|
-
// Hai loại timer tách rời (xem comment dài bên dưới):
|
|
199
|
-
// - WIRE: kết nối TCP còn thở không? Reset mỗi khi reader.read() trả bytes
|
|
200
|
-
// (kể cả comment SSE `: keepalive` từ worker). Ngưỡng cao (idleMs*2) — chỉ
|
|
201
|
-
// để bắt trường hợp socket chết cứng không còn cả heartbeat.
|
|
202
|
-
// - CONTENT: upstream có thực sự đang nghĩ/nói không? Reset chỉ khi parser
|
|
203
|
-
// thấy delta/status/reasoning/done/truncated thật. WARN/PROBE/idleMs gắn
|
|
204
|
-
// vào timer này. Đây là fix cho bug: worker phát `: keepalive\n\n` mỗi 15s
|
|
205
|
-
// bất kể upstream Vercel còn sống hay đã treo → nếu reset idle theo wire,
|
|
206
|
-
// status đếm thời gian sẽ chạy mãi không bao giờ trigger probe/abort.
|
|
207
|
-
let wireIdle = null;
|
|
208
|
-
let contentIdle = null;
|
|
209
|
-
let warnTimer = null;
|
|
210
|
-
let probeTimer = null;
|
|
211
|
-
let probeInFlight = false;
|
|
212
|
-
const WARN_MS = Math.min(15000, idleMs / 3);
|
|
213
|
-
const PROBE_MS = Math.min(25000, (idleMs * 2) / 3);
|
|
214
|
-
const WIRE_MS = idleMs * 3; // socket dead-cứng (mất cả heartbeat) — ngưỡng rất rộng
|
|
215
|
-
const clearContentTimers = () => {
|
|
216
|
-
clearTimeout(warnTimer); warnTimer = null;
|
|
217
|
-
clearTimeout(probeTimer); probeTimer = null;
|
|
218
|
-
clearTimeout(contentIdle); contentIdle = null;
|
|
219
|
-
};
|
|
220
|
-
const armContent = () => {
|
|
221
|
-
clearContentTimers();
|
|
222
|
-
warnTimer = setTimeout(() => {
|
|
223
|
-
if (onStatus) onStatus("Model đang suy nghĩ… (đợi thêm nếu task phức tạp)");
|
|
224
|
-
}, WARN_MS);
|
|
225
|
-
probeTimer = setTimeout(async () => {
|
|
226
|
-
if (probeInFlight || ctrl.signal.aborted) return;
|
|
227
|
-
probeInFlight = true;
|
|
228
|
-
try {
|
|
229
|
-
const probeCtl = new AbortController();
|
|
230
|
-
const probeT = setTimeout(() => probeCtl.abort(), 3000);
|
|
231
|
-
const r = await fetch(config.gatewayUrl + "/api/usage", { headers: authHeaders(), signal: probeCtl.signal });
|
|
232
|
-
clearTimeout(probeT);
|
|
233
|
-
if (!r.ok && r.status >= 500) throw new Error("proxy 5xx");
|
|
234
|
-
} catch {
|
|
235
|
-
if (!ctrl.signal.aborted) {
|
|
236
|
-
timedOut = true;
|
|
237
|
-
if (onStatus) onStatus("Model không phản hồi — đang kết nối lại…");
|
|
238
|
-
ctrl.abort();
|
|
239
|
-
}
|
|
240
|
-
} finally {
|
|
241
|
-
probeInFlight = false;
|
|
242
|
-
}
|
|
243
|
-
}, PROBE_MS);
|
|
244
|
-
contentIdle = setTimeout(() => {
|
|
245
|
-
timedOut = true;
|
|
246
|
-
ctrl.abort();
|
|
247
|
-
}, idleMs);
|
|
248
|
-
};
|
|
249
|
-
const armWire = () => {
|
|
250
|
-
clearTimeout(wireIdle);
|
|
251
|
-
wireIdle = setTimeout(() => {
|
|
252
|
-
// Mất cả heartbeat → socket chết cứng. Abort, lớp trên sẽ retry.
|
|
253
|
-
timedOut = true;
|
|
254
|
-
ctrl.abort();
|
|
255
|
-
}, WIRE_MS);
|
|
256
|
-
};
|
|
257
194
|
|
|
258
195
|
let text = "";
|
|
259
196
|
let reasoning = "";
|
|
260
197
|
let sawDone = false; // thấy {done} = stream kết thúc tử tế (không bị cắt)
|
|
261
|
-
let truncated = false; // gateway báo upstream bị cắt giữa chừng
|
|
198
|
+
let truncated = false; // gateway báo upstream bị cắt giữa chừng
|
|
262
199
|
|
|
263
200
|
// Một dòng SSE → cập nhật text/reasoning. Tách ra để dùng lại khi flush dòng cuối.
|
|
264
201
|
const processLine = (rawLine) => {
|
|
@@ -272,12 +209,6 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
272
209
|
} catch {
|
|
273
210
|
return;
|
|
274
211
|
}
|
|
275
|
-
// Có ít nhất 1 field nội dung thực → upstream đang nghĩ/nói. Reset content
|
|
276
|
-
// idle/warn/probe. JSON rỗng `{}` (ping) hoặc comment `: keepalive` của
|
|
277
|
-
// worker KHÔNG khớp điều kiện này → không che mất idle detection.
|
|
278
|
-
if (p.status || p.delta || p.reasoning || p.done || p.truncated || p.error) {
|
|
279
|
-
armContent();
|
|
280
|
-
}
|
|
281
212
|
if (p.status && onStatus) onStatus(p.status);
|
|
282
213
|
if (p.delta) {
|
|
283
214
|
text += p.delta;
|
|
@@ -294,8 +225,6 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
294
225
|
};
|
|
295
226
|
|
|
296
227
|
try {
|
|
297
|
-
armWire();
|
|
298
|
-
armContent();
|
|
299
228
|
const resp = await fetch(config.gatewayUrl + endpoint, {
|
|
300
229
|
method: "POST",
|
|
301
230
|
headers: authHeaders(),
|
|
@@ -309,7 +238,6 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
309
238
|
let buf = "";
|
|
310
239
|
while (true) {
|
|
311
240
|
const { done, value } = await reader.read();
|
|
312
|
-
armWire(); // bất kỳ byte nào tới → socket còn thở, gia hạn wire idle
|
|
313
241
|
if (done) break;
|
|
314
242
|
buf += decoder.decode(value, { stream: true });
|
|
315
243
|
let nl;
|
|
@@ -328,14 +256,11 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
328
256
|
return { text, reasoning, truncated };
|
|
329
257
|
} catch (err) {
|
|
330
258
|
if (signal?.aborted) throw err; // người dùng bấm Ctrl+C → huỷ thật, không nối tiếp
|
|
331
|
-
|
|
332
|
-
// Rớt mạng giữa chừng (không phải huỷ, không phải treo): với chat, nếu đã có
|
|
259
|
+
// Rớt mạng giữa chừng (không phải huỷ): với chat, nếu đã có
|
|
333
260
|
// chữ thì trả phần đã nhận + cờ truncated để lớp trên nối tiếp.
|
|
334
261
|
if (mode === "chat" && text) return { text, reasoning, truncated: true };
|
|
335
262
|
throw err;
|
|
336
263
|
} finally {
|
|
337
|
-
clearTimeout(wireIdle);
|
|
338
|
-
clearContentTimers();
|
|
339
264
|
signal?.removeEventListener("abort", onUserAbort);
|
|
340
265
|
}
|
|
341
266
|
}
|