@noobdemon/noob-cli 1.10.7 → 1.10.9

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/api.js +4 -79
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.10.7",
3
+ "version": "1.10.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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, idleMs = 45000, maxContinues = Infinity }) {
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 = "";
@@ -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, idleMs, onStatus, onDelta, onReasoning }) {
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 (Vercel 300s)
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
- if (timedOut) throw new ApiError("Kết nối tới máy chủ quá thời gian chờ (treo). Đang thử lại…", { code: "timeout" });
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 đã
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
  }