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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 (
|
|
296
|
-
finishReason = choice.finish_reason;
|
|
297
|
-
const delta = choice.delta;
|
|
298
|
-
if (!delta)
|
|
302
|
+
if (!trimmed.startsWith('data: '))
|
|
299
303
|
continue;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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', {
|