orquesta-cli 0.2.101 → 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.
@@ -221,6 +221,7 @@ export class LLMClient {
221
221
  });
222
222
  const url = '/chat/completions';
223
223
  let controller = null;
224
+ let gapTimedOut = false;
224
225
  try {
225
226
  logger.flow('Starting message preprocessing');
226
227
  const modelId = options.model || this.model;
@@ -272,61 +273,80 @@ export class LLMClient {
272
273
  const toolCallsMap = new Map();
273
274
  let responseId = '';
274
275
  let responseModel = '';
275
- for await (const chunk of stream) {
276
- if (this.isInterrupted) {
277
- throw new Error('INTERRUPTED');
278
- }
279
- buffer += chunk.toString();
280
- const lines = buffer.split('\n');
281
- buffer = lines.pop() || '';
282
- for (const line of lines) {
283
- const trimmed = line.trim();
284
- if (!trimmed || trimmed === 'data: [DONE]')
285
- continue;
286
- if (!trimmed.startsWith('data: '))
287
- 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);
288
282
  try {
289
- const data = JSON.parse(trimmed.slice(6));
290
- if (data.id)
291
- responseId = data.id;
292
- if (data.model)
293
- responseModel = data.model;
294
- const choice = data.choices?.[0];
295
- 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]')
296
301
  continue;
297
- if (choice.finish_reason)
298
- finishReason = choice.finish_reason;
299
- const delta = choice.delta;
300
- if (!delta)
302
+ if (!trimmed.startsWith('data: '))
301
303
  continue;
302
- if (delta.role)
303
- role = delta.role;
304
- if (delta.content) {
305
- contentAccum += delta.content;
306
- this.onStreamingContent(delta.content);
307
- }
308
- if (delta.reasoning) {
309
- reasoningAccum += delta.reasoning;
310
- }
311
- if (delta.tool_calls) {
312
- for (const tc of delta.tool_calls) {
313
- const idx = tc.index ?? 0;
314
- if (!toolCallsMap.has(idx)) {
315
- 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;
316
340
  }
317
- const existing = toolCallsMap.get(idx);
318
- if (tc.id)
319
- existing.id = tc.id;
320
- if (tc.function?.name)
321
- existing.function.name += tc.function.name;
322
- if (tc.function?.arguments)
323
- existing.function.arguments += tc.function.arguments;
324
341
  }
325
342
  }
343
+ catch { }
326
344
  }
327
- catch { }
328
345
  }
329
346
  }
347
+ finally {
348
+ clearInterval(gapTimer);
349
+ }
330
350
  const toolCalls = Array.from(toolCallsMap.values())
331
351
  .filter(tc => tc.id && tc.function.name)
332
352
  .map(tc => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } }));
@@ -397,6 +417,11 @@ export class LLMClient {
397
417
  return response.data;
398
418
  }
399
419
  catch (error) {
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
+ }
400
425
  if (axios.isCancel(error) || (error instanceof Error && error.name === 'CanceledError')) {
401
426
  logger.flow('API call canceled (user interrupt)');
402
427
  logger.exit('chatCompletion', { success: false, aborted: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquesta-cli",
3
- "version": "0.2.101",
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",