@aj-archipelago/cortex 1.4.32 → 1.4.33
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/config.js +72 -0
- package/helper-apps/cortex-file-handler/Dockerfile +1 -1
- package/lib/fileUtils.js +24 -5
- package/lib/pathwayManager.js +6 -6
- package/lib/pathwayTools.js +21 -1
- package/lib/requestExecutor.js +49 -5
- package/package.json +1 -1
- package/pathways/system/entity/sys_compress_context.js +82 -0
- package/pathways/system/entity/sys_entity_agent.js +65 -15
- package/pathways/transcribe_gemini.js +1 -1
- package/server/modelExecutor.js +4 -0
- package/server/pathwayResolver.js +102 -12
- package/server/plugins/claudeAnthropicPlugin.js +84 -0
- package/server/plugins/gemini15ChatPlugin.js +17 -0
- package/server/plugins/gemini15VisionPlugin.js +51 -5
- package/server/plugins/grokResponsesPlugin.js +2 -0
- package/server/plugins/openAiVisionPlugin.js +4 -2
- package/test.log +42834 -0
- package/tests/integration/rest/vendors/claude_anthropic_direct.test.js +197 -0
- package/tests/unit/plugins/claudeAnthropicPlugin.test.js +236 -0
- package/tests/unit/sys_entity_agent_errors.test.js +792 -0
|
@@ -274,6 +274,14 @@ class PathwayResolver {
|
|
|
274
274
|
|
|
275
275
|
async handleStream(response) {
|
|
276
276
|
let streamErrorOccurred = false;
|
|
277
|
+
let streamErrorMessage = null;
|
|
278
|
+
let completionSent = false;
|
|
279
|
+
let receivedSSEData = false; // Track if we actually received SSE events
|
|
280
|
+
let receivedAnyData = false; // Track if we received ANY data from the stream
|
|
281
|
+
let toolCallbackInvoked = false; // Track if a tool callback was invoked (stream close is expected)
|
|
282
|
+
// Accumulate streamed content for continuity memory
|
|
283
|
+
this.streamedContent = '';
|
|
284
|
+
const requestId = this.rootRequestId || this.requestId;
|
|
277
285
|
|
|
278
286
|
if (response && typeof response.on === 'function') {
|
|
279
287
|
try {
|
|
@@ -282,7 +290,7 @@ class PathwayResolver {
|
|
|
282
290
|
|
|
283
291
|
const onParse = (event) => {
|
|
284
292
|
let requestProgress = {
|
|
285
|
-
requestId
|
|
293
|
+
requestId
|
|
286
294
|
};
|
|
287
295
|
|
|
288
296
|
logger.debug(`Received event: ${event.type}`);
|
|
@@ -292,15 +300,34 @@ class PathwayResolver {
|
|
|
292
300
|
logger.debug(`id: ${event.id || '<none>'}`)
|
|
293
301
|
logger.debug(`name: ${event.name || '<none>'}`)
|
|
294
302
|
logger.debug(`data: ${event.data}`)
|
|
303
|
+
|
|
304
|
+
receivedSSEData = true; // Only mark SSE data when we get actual 'event' type
|
|
305
|
+
|
|
306
|
+
// Check for error events in the stream data
|
|
307
|
+
try {
|
|
308
|
+
const eventData = JSON.parse(event.data);
|
|
309
|
+
if (eventData.error) {
|
|
310
|
+
streamErrorOccurred = true;
|
|
311
|
+
streamErrorMessage = eventData.error.message || JSON.stringify(eventData.error);
|
|
312
|
+
logger.error(`Stream contained error event: ${streamErrorMessage}`);
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
// Not JSON or no error field, continue normal processing
|
|
316
|
+
}
|
|
295
317
|
} else if (event.type === 'reconnect-interval') {
|
|
296
318
|
logger.debug(`We should set reconnect interval to ${event.value} milliseconds`)
|
|
297
319
|
}
|
|
298
320
|
|
|
299
321
|
try {
|
|
300
322
|
requestProgress = this.modelExecutor.plugin.processStreamEvent(event, requestProgress);
|
|
323
|
+
// Check if plugin signaled a tool callback was invoked
|
|
324
|
+
if (requestProgress.toolCallbackInvoked) {
|
|
325
|
+
toolCallbackInvoked = true;
|
|
326
|
+
}
|
|
301
327
|
} catch (error) {
|
|
302
328
|
streamErrorOccurred = true;
|
|
303
|
-
|
|
329
|
+
streamErrorMessage = error instanceof Error ? error.message : String(error);
|
|
330
|
+
logger.error(`Stream processing error: ${error instanceof Error ? error.stack || error.message : JSON.stringify(error)}`);
|
|
304
331
|
incomingMessage.off('data', processStream);
|
|
305
332
|
return;
|
|
306
333
|
}
|
|
@@ -309,6 +336,9 @@ class PathwayResolver {
|
|
|
309
336
|
if (!streamEnded && requestProgress.data) {
|
|
310
337
|
this.publishNestedRequestProgress(requestProgress);
|
|
311
338
|
streamEnded = requestProgress.progress === 1;
|
|
339
|
+
if (streamEnded) {
|
|
340
|
+
completionSent = true;
|
|
341
|
+
}
|
|
312
342
|
}
|
|
313
343
|
} catch (error) {
|
|
314
344
|
logger.error(`Could not publish the stream message: "${event.data}", ${error instanceof Error ? error.stack || error.message : JSON.stringify(error)}`);
|
|
@@ -319,7 +349,7 @@ class PathwayResolver {
|
|
|
319
349
|
const sseParser = createParser(onParse);
|
|
320
350
|
|
|
321
351
|
const processStream = (data) => {
|
|
322
|
-
//
|
|
352
|
+
receivedAnyData = true; // Track that we got data from the stream
|
|
323
353
|
sseParser.feed(data.toString());
|
|
324
354
|
}
|
|
325
355
|
|
|
@@ -327,27 +357,71 @@ class PathwayResolver {
|
|
|
327
357
|
await new Promise((resolve, reject) => {
|
|
328
358
|
incomingMessage.on('data', processStream);
|
|
329
359
|
incomingMessage.on('end', resolve);
|
|
330
|
-
incomingMessage.on('error',
|
|
360
|
+
incomingMessage.on('error', (err) => {
|
|
361
|
+
streamErrorOccurred = true;
|
|
362
|
+
streamErrorMessage = err instanceof Error ? err.message : String(err);
|
|
363
|
+
reject(err);
|
|
364
|
+
});
|
|
365
|
+
incomingMessage.on('close', () => {
|
|
366
|
+
// Stream closed - detect various incomplete states
|
|
367
|
+
if (!receivedAnyData && !streamErrorOccurred && !toolCallbackInvoked) {
|
|
368
|
+
// Stream opened but closed with NO data at all - this is likely a provider issue
|
|
369
|
+
logger.warn('Stream closed with no data received (empty stream)');
|
|
370
|
+
} else if (receivedSSEData && !completionSent && !streamErrorOccurred && !toolCallbackInvoked) {
|
|
371
|
+
// Got SSE data but no completion signal
|
|
372
|
+
logger.warn('Stream closed without completion signal');
|
|
373
|
+
}
|
|
374
|
+
resolve();
|
|
375
|
+
});
|
|
331
376
|
});
|
|
332
377
|
}
|
|
333
378
|
|
|
334
379
|
} catch (error) {
|
|
380
|
+
streamErrorOccurred = true;
|
|
381
|
+
if (!streamErrorMessage) {
|
|
382
|
+
streamErrorMessage = error instanceof Error ? error.message : String(error);
|
|
383
|
+
}
|
|
335
384
|
logger.error(`Could not subscribe to stream: ${error instanceof Error ? error.stack || error.message : JSON.stringify(error)}`);
|
|
336
385
|
}
|
|
337
386
|
|
|
338
|
-
|
|
339
|
-
|
|
387
|
+
// Detect empty stream (opened but closed with no data) - this should be retried
|
|
388
|
+
const emptyStream = !receivedAnyData && !streamErrorOccurred && !toolCallbackInvoked;
|
|
389
|
+
|
|
390
|
+
// Ensure completion is sent if not already done
|
|
391
|
+
// Send completion if:
|
|
392
|
+
// 1. Stream error occurred (always notify client of errors)
|
|
393
|
+
// 2. OR we received SSE data but no completion was sent (and no tool callback)
|
|
394
|
+
// 3. OR empty stream (will only happen if retry logic exhausted - see executePathway)
|
|
395
|
+
// Don't send completion if a tool callback was invoked (stream will resume)
|
|
396
|
+
const shouldSendCompletion = !toolCallbackInvoked && !completionSent &&
|
|
397
|
+
(streamErrorOccurred || receivedSSEData);
|
|
398
|
+
|
|
399
|
+
if (shouldSendCompletion) {
|
|
400
|
+
if (streamErrorOccurred) {
|
|
401
|
+
logger.error(`Stream read failed: ${streamErrorMessage}`);
|
|
402
|
+
}
|
|
403
|
+
const errorMessage = streamErrorOccurred
|
|
404
|
+
? (streamErrorMessage || this.errors.join(', ') || 'Stream read failed')
|
|
405
|
+
: '';
|
|
340
406
|
publishRequestProgress({
|
|
341
|
-
requestId
|
|
407
|
+
requestId,
|
|
342
408
|
progress: 1,
|
|
343
409
|
data: '',
|
|
344
|
-
info:
|
|
345
|
-
error:
|
|
410
|
+
info: JSON.stringify(this.pathwayResultData || {}),
|
|
411
|
+
error: errorMessage
|
|
346
412
|
});
|
|
347
|
-
} else {
|
|
348
|
-
return;
|
|
349
413
|
}
|
|
414
|
+
|
|
415
|
+
// Return stream result for retry logic
|
|
416
|
+
return {
|
|
417
|
+
success: completionSent || toolCallbackInvoked,
|
|
418
|
+
emptyStream,
|
|
419
|
+
error: streamErrorOccurred ? streamErrorMessage : null
|
|
420
|
+
};
|
|
350
421
|
}
|
|
422
|
+
|
|
423
|
+
// Non-stream response
|
|
424
|
+
return { success: true, emptyStream: false, error: null };
|
|
351
425
|
}
|
|
352
426
|
|
|
353
427
|
async resolve(args) {
|
|
@@ -479,7 +553,23 @@ class PathwayResolver {
|
|
|
479
553
|
|
|
480
554
|
// if data is a stream, handle it
|
|
481
555
|
if (data && typeof data.on === 'function') {
|
|
482
|
-
await this.handleStream(data);
|
|
556
|
+
const streamResult = await this.handleStream(data);
|
|
557
|
+
// Check if stream was empty (opened but closed with no data) - retry if so
|
|
558
|
+
if (streamResult?.emptyStream) {
|
|
559
|
+
logger.warn(`Empty stream received - retrying. Attempt ${retries + 1} of ${MAX_RETRIES}`);
|
|
560
|
+
if (retries === MAX_RETRIES - 1) {
|
|
561
|
+
// Last retry - send error completion so client doesn't hang
|
|
562
|
+
logger.error('All stream retries exhausted - empty stream from provider');
|
|
563
|
+
publishRequestProgress({
|
|
564
|
+
requestId: this.rootRequestId || this.requestId,
|
|
565
|
+
progress: 1,
|
|
566
|
+
data: '',
|
|
567
|
+
info: JSON.stringify(this.pathwayResultData || {}),
|
|
568
|
+
error: 'Provider returned empty stream - please try again'
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
continue; // Retry
|
|
572
|
+
}
|
|
483
573
|
return data;
|
|
484
574
|
}
|
|
485
575
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import Claude4VertexPlugin from "./claude4VertexPlugin.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin for direct Anthropic API access (api.anthropic.com)
|
|
5
|
+
*
|
|
6
|
+
* This plugin extends Claude4VertexPlugin and reuses all the message/content
|
|
7
|
+
* conversion logic, but uses direct Anthropic API authentication and endpoints
|
|
8
|
+
* instead of Google Vertex AI.
|
|
9
|
+
*
|
|
10
|
+
* Key differences from Vertex AI:
|
|
11
|
+
* - Uses x-api-key header instead of Bearer token
|
|
12
|
+
* - Uses https://api.anthropic.com/v1/messages endpoint
|
|
13
|
+
* - Model specified in request body, not URL
|
|
14
|
+
* - anthropic-version specified in header, not body
|
|
15
|
+
* - Streaming via stream:true in body, not URL suffix
|
|
16
|
+
*/
|
|
17
|
+
class ClaudeAnthropicPlugin extends Claude4VertexPlugin {
|
|
18
|
+
|
|
19
|
+
constructor(pathway, model) {
|
|
20
|
+
super(pathway, model);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async getRequestParameters(text, parameters, prompt) {
|
|
24
|
+
// Get base request parameters from parent (includes message conversion)
|
|
25
|
+
const requestParameters = await super.getRequestParameters(
|
|
26
|
+
text,
|
|
27
|
+
parameters,
|
|
28
|
+
prompt
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Remove Vertex-specific anthropic_version from body
|
|
32
|
+
delete requestParameters.anthropic_version;
|
|
33
|
+
|
|
34
|
+
// Add model to request body (required for direct Anthropic API)
|
|
35
|
+
// The model name should come from the endpoint params or model config
|
|
36
|
+
const modelName = this.model.params?.model ||
|
|
37
|
+
this.model.endpoints?.[0]?.params?.model ||
|
|
38
|
+
this.modelName;
|
|
39
|
+
requestParameters.model = modelName;
|
|
40
|
+
|
|
41
|
+
return requestParameters;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async execute(text, parameters, prompt, cortexRequest) {
|
|
45
|
+
const requestParameters = await this.getRequestParameters(
|
|
46
|
+
text,
|
|
47
|
+
parameters,
|
|
48
|
+
prompt,
|
|
49
|
+
cortexRequest
|
|
50
|
+
);
|
|
51
|
+
const { stream } = parameters;
|
|
52
|
+
|
|
53
|
+
// Add stream parameter to request body for Anthropic API
|
|
54
|
+
if (stream) {
|
|
55
|
+
requestParameters.stream = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cortexRequest.data = {
|
|
59
|
+
...(cortexRequest.data || {}),
|
|
60
|
+
...requestParameters,
|
|
61
|
+
};
|
|
62
|
+
cortexRequest.params = {}; // query params
|
|
63
|
+
cortexRequest.stream = stream;
|
|
64
|
+
|
|
65
|
+
// Direct Anthropic API doesn't use URL suffix for streaming
|
|
66
|
+
cortexRequest.urlSuffix = "";
|
|
67
|
+
|
|
68
|
+
// Set Anthropic-specific headers
|
|
69
|
+
// The x-api-key should already be in the model config headers
|
|
70
|
+
// but we need to add the anthropic-version header
|
|
71
|
+
cortexRequest.headers = {
|
|
72
|
+
...(cortexRequest.headers || {}),
|
|
73
|
+
"anthropic-version": "2023-06-01"
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// For direct Anthropic API, authentication is handled via headers in config
|
|
77
|
+
// (x-api-key: {{CLAUDE_API_KEY}})
|
|
78
|
+
// No need for GCP auth token
|
|
79
|
+
|
|
80
|
+
return this.executeRequest(cortexRequest);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default ClaudeAnthropicPlugin;
|
|
@@ -178,6 +178,16 @@ class Gemini15ChatPlugin extends ModelPlugin {
|
|
|
178
178
|
const { content, finishReason, safetyRatings } = data.candidates[0];
|
|
179
179
|
if (finishReason === 'STOP' || finishReason === 'MAX_TOKENS') {
|
|
180
180
|
return content?.parts?.[0]?.text ?? '';
|
|
181
|
+
} else if (finishReason === 'MALFORMED_FUNCTION_CALL') {
|
|
182
|
+
// Model attempted a function call but generated invalid JSON
|
|
183
|
+
// Return any partial text content if available, otherwise return an error message
|
|
184
|
+
const textContent = content?.parts?.[0]?.text;
|
|
185
|
+
if (textContent) {
|
|
186
|
+
logger.warn(`Gemini returned MALFORMED_FUNCTION_CALL but had text content, returning text`);
|
|
187
|
+
return textContent;
|
|
188
|
+
}
|
|
189
|
+
logger.warn(`Gemini returned MALFORMED_FUNCTION_CALL with no text content`);
|
|
190
|
+
return 'I encountered an issue processing that request. Please try rephrasing your question.';
|
|
181
191
|
} else {
|
|
182
192
|
const returnString = `Response was not completed. Finish reason: ${finishReason}, Safety ratings: ${JSON.stringify(safetyRatings, null, 2)}`;
|
|
183
193
|
throw new Error(returnString);
|
|
@@ -259,6 +269,13 @@ class Gemini15ChatPlugin extends ModelPlugin {
|
|
|
259
269
|
// Only send DONE if there was no content in this message
|
|
260
270
|
requestProgress.data = '[DONE]';
|
|
261
271
|
requestProgress.progress = 1;
|
|
272
|
+
} else if (eventData.candidates?.[0]?.finishReason === "MALFORMED_FUNCTION_CALL") {
|
|
273
|
+
// Model attempted a function call but generated invalid JSON
|
|
274
|
+
logger.warn(`Gemini streaming returned MALFORMED_FUNCTION_CALL`);
|
|
275
|
+
requestProgress.data = JSON.stringify(createChunk({
|
|
276
|
+
content: '\n\nI encountered an issue processing that request. Please try rephrasing your question.'
|
|
277
|
+
}));
|
|
278
|
+
requestProgress.progress = 1;
|
|
262
279
|
}
|
|
263
280
|
|
|
264
281
|
// Handle safety blocks
|
|
@@ -2,6 +2,7 @@ import Gemini15ChatPlugin from './gemini15ChatPlugin.js';
|
|
|
2
2
|
import CortexResponse from '../../lib/cortexResponse.js';
|
|
3
3
|
import { requestState } from '../requestState.js';
|
|
4
4
|
import { addCitationsToResolver } from '../../lib/pathwayTools.js';
|
|
5
|
+
import logger from '../../lib/logger.js';
|
|
5
6
|
import mime from 'mime-types';
|
|
6
7
|
|
|
7
8
|
class Gemini15VisionPlugin extends Gemini15ChatPlugin {
|
|
@@ -414,6 +415,18 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
|
|
|
414
415
|
|
|
415
416
|
return cortexResponse;
|
|
416
417
|
}
|
|
418
|
+
|
|
419
|
+
// Handle MALFORMED_FUNCTION_CALL - model tried to call a function but generated invalid JSON
|
|
420
|
+
if (finishReason === 'MALFORMED_FUNCTION_CALL') {
|
|
421
|
+
const textContent = content?.parts?.[0]?.text || '';
|
|
422
|
+
logger.warn(`Gemini returned MALFORMED_FUNCTION_CALL, returning graceful response`);
|
|
423
|
+
return new CortexResponse({
|
|
424
|
+
output_text: textContent || 'I encountered an issue processing that request. Please try rephrasing your question.',
|
|
425
|
+
finishReason: "stop",
|
|
426
|
+
usage: data.usageMetadata || null,
|
|
427
|
+
metadata: { model: this.modelName }
|
|
428
|
+
});
|
|
429
|
+
}
|
|
417
430
|
}
|
|
418
431
|
|
|
419
432
|
// Fallback to parent implementation
|
|
@@ -500,20 +513,38 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
|
|
|
500
513
|
const finishReason = this.hadToolCalls ? "tool_calls" : "stop";
|
|
501
514
|
|
|
502
515
|
// Check if there's any remaining content in the final chunk that needs to be published
|
|
516
|
+
let sentFinalChunk = false;
|
|
503
517
|
if (eventData.candidates?.[0]?.content?.parts) {
|
|
504
518
|
const parts = eventData.candidates[0].content.parts;
|
|
505
519
|
for (const part of parts) {
|
|
506
520
|
if (part.text && part.text.trim()) {
|
|
507
521
|
// Send the final content chunk with finish reason
|
|
508
|
-
requestProgress.data = JSON.stringify(createChunk({
|
|
509
|
-
content: part.text
|
|
522
|
+
requestProgress.data = JSON.stringify(createChunk({
|
|
523
|
+
content: part.text
|
|
510
524
|
}, finishReason));
|
|
525
|
+
sentFinalChunk = true;
|
|
511
526
|
break; // Only process the first text part
|
|
512
527
|
}
|
|
513
528
|
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
529
|
+
}
|
|
530
|
+
if (!sentFinalChunk) {
|
|
531
|
+
// If we have tool calls, include them in the finish chunk
|
|
532
|
+
// (Gemini often sends functionCall and finishReason in the same event)
|
|
533
|
+
// Filter out undefined elements before mapping
|
|
534
|
+
const validToolCallsForChunk = this.toolCallsBuffer.filter(tc => tc && tc.function);
|
|
535
|
+
if (this.hadToolCalls && validToolCallsForChunk.length > 0) {
|
|
536
|
+
requestProgress.data = JSON.stringify(createChunk({
|
|
537
|
+
tool_calls: validToolCallsForChunk.map((tc, index) => ({
|
|
538
|
+
index,
|
|
539
|
+
id: tc.id,
|
|
540
|
+
type: tc.type,
|
|
541
|
+
function: tc.function
|
|
542
|
+
}))
|
|
543
|
+
}, finishReason));
|
|
544
|
+
} else {
|
|
545
|
+
// No final text content, just send finish chunk
|
|
546
|
+
requestProgress.data = JSON.stringify(createChunk({}, finishReason));
|
|
547
|
+
}
|
|
517
548
|
}
|
|
518
549
|
|
|
519
550
|
const pathwayResolver = requestState[this.requestId]?.pathwayResolver;
|
|
@@ -528,6 +559,8 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
|
|
|
528
559
|
tool_calls: validToolCalls,
|
|
529
560
|
};
|
|
530
561
|
this.pathwayToolCallback(pathwayResolver?.args, toolMessage, pathwayResolver);
|
|
562
|
+
// Signal to pathwayResolver that stream close is expected (tool callback invoked)
|
|
563
|
+
requestProgress.toolCallbackInvoked = true;
|
|
531
564
|
// Clear tool buffer after processing; keep content for citations/continuations
|
|
532
565
|
this.toolCallsBuffer = [];
|
|
533
566
|
} else {
|
|
@@ -539,6 +572,19 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
|
|
|
539
572
|
}
|
|
540
573
|
}
|
|
541
574
|
|
|
575
|
+
// Handle MALFORMED_FUNCTION_CALL - model tried to call a function but generated invalid JSON
|
|
576
|
+
if (eventData.candidates?.[0]?.finishReason === "MALFORMED_FUNCTION_CALL") {
|
|
577
|
+
logger.warn(`Gemini streaming returned MALFORMED_FUNCTION_CALL`);
|
|
578
|
+
requestProgress.data = JSON.stringify(createChunk({
|
|
579
|
+
content: '\n\nI encountered an issue processing that request. Please try rephrasing your question.'
|
|
580
|
+
}, "stop"));
|
|
581
|
+
requestProgress.progress = 1;
|
|
582
|
+
// Clear buffers
|
|
583
|
+
this.toolCallsBuffer = [];
|
|
584
|
+
this.contentBuffer = '';
|
|
585
|
+
return requestProgress;
|
|
586
|
+
}
|
|
587
|
+
|
|
542
588
|
// Handle safety blocks
|
|
543
589
|
if (eventData.candidates?.[0]?.safetyRatings?.some(rating => rating.blocked)) {
|
|
544
590
|
requestProgress.data = JSON.stringify(createChunk({
|
|
@@ -642,6 +642,8 @@ class GrokResponsesPlugin extends OpenAIVisionPlugin {
|
|
|
642
642
|
tool_calls: validToolCalls,
|
|
643
643
|
};
|
|
644
644
|
this.pathwayToolCallback(pathwayResolver?.args, toolMessage, pathwayResolver);
|
|
645
|
+
// Signal to pathwayResolver that tool callback was invoked - prevents [DONE] from ending stream
|
|
646
|
+
requestProgress.toolCallbackInvoked = true;
|
|
645
647
|
}
|
|
646
648
|
this.toolCallsBuffer = [];
|
|
647
649
|
break;
|
|
@@ -374,14 +374,16 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
|
|
|
374
374
|
const validToolCalls = this.toolCallsBuffer.filter(tc => tc && tc.function && tc.function.name);
|
|
375
375
|
const toolMessage = {
|
|
376
376
|
role: 'assistant',
|
|
377
|
-
content: delta?.content || '',
|
|
377
|
+
content: delta?.content || '',
|
|
378
378
|
tool_calls: validToolCalls,
|
|
379
379
|
};
|
|
380
380
|
this.pathwayToolCallback(pathwayResolver?.args, toolMessage, pathwayResolver);
|
|
381
|
+
// Signal to pathwayResolver that tool callback was invoked - prevents [DONE] from ending stream
|
|
382
|
+
requestProgress.toolCallbackInvoked = true;
|
|
381
383
|
}
|
|
382
384
|
// Don't set progress to 1 for tool calls to keep stream open
|
|
383
385
|
// Clear tool buffer after processing, but keep content buffer
|
|
384
|
-
this.toolCallsBuffer = [];
|
|
386
|
+
this.toolCallsBuffer = [];
|
|
385
387
|
break;
|
|
386
388
|
case 'safety':
|
|
387
389
|
const safetyRatings = JSON.stringify(parsedMessage?.candidates?.[0]?.safetyRatings) || '';
|