orquesta-cli 0.2.100 → 0.2.102

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.
@@ -44,7 +44,7 @@ export declare class LLMClient {
44
44
  private model;
45
45
  private modelName;
46
46
  private triedBatutaFallback;
47
- private currentAbortController;
47
+ private activeControllers;
48
48
  private isInterrupted;
49
49
  onStreamingContent: ((token: string) => void) | null;
50
50
  private static readonly DEFAULT_MAX_RETRIES;
@@ -123,7 +123,7 @@ export class LLMClient {
123
123
  model;
124
124
  modelName;
125
125
  triedBatutaFallback = false;
126
- currentAbortController = null;
126
+ activeControllers = new Set();
127
127
  isInterrupted = false;
128
128
  onStreamingContent = null;
129
129
  static DEFAULT_MAX_RETRIES = 3;
@@ -220,6 +220,8 @@ export class LLMClient {
220
220
  maxRetries
221
221
  });
222
222
  const url = '/chat/completions';
223
+ let controller = null;
224
+ let gapTimedOut = false;
223
225
  try {
224
226
  logger.flow('Starting message preprocessing');
225
227
  const modelId = options.model || this.model;
@@ -251,13 +253,14 @@ export class LLMClient {
251
253
  logger.llmRequest(processedMessages, modelId, options.tools);
252
254
  }
253
255
  logger.startTimer('llm-api-call');
254
- this.currentAbortController = new AbortController();
256
+ controller = new AbortController();
257
+ this.activeControllers.add(controller);
255
258
  let response;
256
259
  if (this.onStreamingContent) {
257
260
  const streamRequestBody = { ...requestBody, stream: true };
258
261
  const streamResp = await this.axiosInstance.post(url, streamRequestBody, {
259
262
  responseType: 'stream',
260
- signal: this.currentAbortController.signal,
263
+ signal: controller.signal,
261
264
  headers: buildPerRequestHeaders(),
262
265
  });
263
266
  captureBatutaHeaders(streamResp.headers);
@@ -270,62 +273,80 @@ export class LLMClient {
270
273
  const toolCallsMap = new Map();
271
274
  let responseId = '';
272
275
  let responseModel = '';
273
- for await (const chunk of stream) {
274
- if (this.isInterrupted) {
275
- throw new Error('INTERRUPTED');
276
- }
277
- buffer += chunk.toString();
278
- const lines = buffer.split('\n');
279
- buffer = lines.pop() || '';
280
- for (const line of lines) {
281
- const trimmed = line.trim();
282
- if (!trimmed || trimmed === 'data: [DONE]')
283
- continue;
284
- if (!trimmed.startsWith('data: '))
285
- continue;
276
+ const GAP_MS = Number(process.env['ORQUESTA_STREAM_GAP_MS']) || 45000;
277
+ let lastChunkAt = Date.now();
278
+ const gapTimer = setInterval(() => {
279
+ if (Date.now() - lastChunkAt > GAP_MS) {
280
+ gapTimedOut = true;
281
+ clearInterval(gapTimer);
286
282
  try {
287
- const data = JSON.parse(trimmed.slice(6));
288
- if (data.id)
289
- responseId = data.id;
290
- if (data.model)
291
- responseModel = data.model;
292
- const choice = data.choices?.[0];
293
- if (!choice)
283
+ controller?.abort();
284
+ }
285
+ catch { }
286
+ }
287
+ }, 5000);
288
+ gapTimer.unref?.();
289
+ try {
290
+ for await (const chunk of stream) {
291
+ lastChunkAt = Date.now();
292
+ if (this.isInterrupted) {
293
+ throw new Error('INTERRUPTED');
294
+ }
295
+ buffer += chunk.toString();
296
+ const lines = buffer.split('\n');
297
+ buffer = lines.pop() || '';
298
+ for (const line of lines) {
299
+ const trimmed = line.trim();
300
+ if (!trimmed || trimmed === 'data: [DONE]')
294
301
  continue;
295
- if (choice.finish_reason)
296
- finishReason = choice.finish_reason;
297
- const delta = choice.delta;
298
- if (!delta)
302
+ if (!trimmed.startsWith('data: '))
299
303
  continue;
300
- if (delta.role)
301
- role = delta.role;
302
- if (delta.content) {
303
- contentAccum += delta.content;
304
- this.onStreamingContent(delta.content);
305
- }
306
- if (delta.reasoning) {
307
- reasoningAccum += delta.reasoning;
308
- }
309
- if (delta.tool_calls) {
310
- for (const tc of delta.tool_calls) {
311
- const idx = tc.index ?? 0;
312
- if (!toolCallsMap.has(idx)) {
313
- toolCallsMap.set(idx, { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } });
304
+ try {
305
+ const data = JSON.parse(trimmed.slice(6));
306
+ if (data.id)
307
+ responseId = data.id;
308
+ if (data.model)
309
+ responseModel = data.model;
310
+ const choice = data.choices?.[0];
311
+ if (!choice)
312
+ continue;
313
+ if (choice.finish_reason)
314
+ finishReason = choice.finish_reason;
315
+ const delta = choice.delta;
316
+ if (!delta)
317
+ continue;
318
+ if (delta.role)
319
+ role = delta.role;
320
+ if (delta.content) {
321
+ contentAccum += delta.content;
322
+ this.onStreamingContent(delta.content);
323
+ }
324
+ if (delta.reasoning) {
325
+ reasoningAccum += delta.reasoning;
326
+ }
327
+ if (delta.tool_calls) {
328
+ for (const tc of delta.tool_calls) {
329
+ const idx = tc.index ?? 0;
330
+ if (!toolCallsMap.has(idx)) {
331
+ toolCallsMap.set(idx, { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } });
332
+ }
333
+ const existing = toolCallsMap.get(idx);
334
+ if (tc.id)
335
+ existing.id = tc.id;
336
+ if (tc.function?.name)
337
+ existing.function.name += tc.function.name;
338
+ if (tc.function?.arguments)
339
+ existing.function.arguments += tc.function.arguments;
314
340
  }
315
- const existing = toolCallsMap.get(idx);
316
- if (tc.id)
317
- existing.id = tc.id;
318
- if (tc.function?.name)
319
- existing.function.name += tc.function.name;
320
- if (tc.function?.arguments)
321
- existing.function.arguments += tc.function.arguments;
322
341
  }
323
342
  }
343
+ catch { }
324
344
  }
325
- catch { }
326
345
  }
327
346
  }
328
- this.currentAbortController = null;
347
+ finally {
348
+ clearInterval(gapTimer);
349
+ }
329
350
  const toolCalls = Array.from(toolCallsMap.values())
330
351
  .filter(tc => tc.id && tc.function.name)
331
352
  .map(tc => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } }));
@@ -350,10 +371,9 @@ export class LLMClient {
350
371
  }
351
372
  else {
352
373
  const httpResp = await this.axiosInstance.post(url, requestBody, {
353
- signal: this.currentAbortController.signal,
374
+ signal: controller.signal,
354
375
  headers: buildPerRequestHeaders(),
355
376
  });
356
- this.currentAbortController = null;
357
377
  response = { data: httpResp.data, status: httpResp.status, statusText: httpResp.statusText, headers: httpResp.headers };
358
378
  captureBatutaHeaders(response.headers);
359
379
  }
@@ -397,7 +417,11 @@ export class LLMClient {
397
417
  return response.data;
398
418
  }
399
419
  catch (error) {
400
- this.currentAbortController = null;
420
+ if (gapTimedOut) {
421
+ logger.flow('Stream stalled — gap watchdog aborted the request');
422
+ logger.exit('chatCompletion', { success: false, stalled: true });
423
+ throw new Error('Upstream stalled: no streaming data received within the gap window. Try again or switch endpoint.');
424
+ }
401
425
  if (axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError')) {
402
426
  logger.flow('API call canceled (user interrupt)');
403
427
  logger.exit('chatCompletion', { success: false, aborted: true });
@@ -444,14 +468,21 @@ export class LLMClient {
444
468
  body: options,
445
469
  });
446
470
  }
471
+ finally {
472
+ if (controller)
473
+ this.activeControllers.delete(controller);
474
+ }
447
475
  }
448
476
  abort() {
449
477
  logger.flow('LLM interrupt - Stopping all operations');
450
478
  this.isInterrupted = true;
451
- if (this.currentAbortController) {
452
- this.currentAbortController.abort();
453
- this.currentAbortController = null;
479
+ for (const controller of this.activeControllers) {
480
+ try {
481
+ controller.abort();
482
+ }
483
+ catch { }
454
484
  }
485
+ this.activeControllers.clear();
455
486
  }
456
487
  checkInterrupted() {
457
488
  return this.isInterrupted;
@@ -460,7 +491,7 @@ export class LLMClient {
460
491
  this.isInterrupted = false;
461
492
  }
462
493
  isRequestActive() {
463
- return this.currentAbortController !== null;
494
+ return this.activeControllers.size > 0;
464
495
  }
465
496
  isConnectionError(error) {
466
497
  if (!axios.isAxiosError(error))
@@ -521,6 +552,7 @@ export class LLMClient {
521
552
  }
522
553
  async *chatCompletionStream(options) {
523
554
  const url = '/chat/completions';
555
+ let controller = null;
524
556
  try {
525
557
  const modelId = options.model || this.model;
526
558
  const processedMessages = options.messages ?
@@ -543,10 +575,11 @@ export class LLMClient {
543
575
  max_tokens: requestBody.max_tokens,
544
576
  });
545
577
  logger.verbose('Full Streaming Request Body', requestBody);
546
- this.currentAbortController = new AbortController();
578
+ controller = new AbortController();
579
+ this.activeControllers.add(controller);
547
580
  const response = await this.axiosInstance.post(url, requestBody, {
548
581
  responseType: 'stream',
549
- signal: this.currentAbortController.signal,
582
+ signal: controller.signal,
550
583
  headers: buildPerRequestHeaders(),
551
584
  });
552
585
  captureBatutaHeaders(response.headers);
@@ -594,7 +627,8 @@ export class LLMClient {
594
627
  }
595
628
  }
596
629
  finally {
597
- this.currentAbortController = null;
630
+ if (controller)
631
+ this.activeControllers.delete(controller);
598
632
  }
599
633
  logger.debug('Streaming response completed', { chunkCount });
600
634
  }
@@ -605,6 +639,10 @@ export class LLMClient {
605
639
  body: options,
606
640
  });
607
641
  }
642
+ finally {
643
+ if (controller)
644
+ this.activeControllers.delete(controller);
645
+ }
608
646
  }
609
647
  async sendMessage(userMessage, systemPrompt) {
610
648
  logger.enter('sendMessage', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquesta-cli",
3
- "version": "0.2.100",
3
+ "version": "0.2.102",
4
4
  "description": "Orquesta CLI - AI-powered coding assistant with team collaboration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",