@librechat/agents 3.1.86 → 3.1.88
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/README.md +69 -0
- package/dist/cjs/events.cjs +23 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +133 -18
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +251 -53
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -5
- package/dist/cjs/llm/init.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +113 -24
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +3 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -5
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/openai/index.cjs +253 -0
- package/dist/cjs/openai/index.cjs.map +1 -0
- package/dist/cjs/responses/index.cjs +448 -0
- package/dist/cjs/responses/index.cjs.map +1 -0
- package/dist/cjs/run.cjs +108 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1057 -0
- package/dist/cjs/session/AgentSession.cjs.map +1 -0
- package/dist/cjs/session/JsonlSessionStore.cjs +425 -0
- package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -0
- package/dist/cjs/session/handlers.cjs +221 -0
- package/dist/cjs/session/handlers.cjs.map +1 -0
- package/dist/cjs/session/ids.cjs +22 -0
- package/dist/cjs/session/ids.cjs.map +1 -0
- package/dist/cjs/session/messageSerialization.cjs +179 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -0
- package/dist/cjs/stream.cjs +475 -11
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +1 -1
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +177 -59
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +113 -0
- package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -0
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/streamedToolCallSeals.cjs +42 -0
- package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -0
- package/dist/esm/events.mjs +23 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +133 -18
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +251 -53
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -5
- package/dist/esm/llm/init.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +113 -25
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -2
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/openai/index.mjs +246 -0
- package/dist/esm/openai/index.mjs.map +1 -0
- package/dist/esm/responses/index.mjs +440 -0
- package/dist/esm/responses/index.mjs.map +1 -0
- package/dist/esm/run.mjs +108 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1054 -0
- package/dist/esm/session/AgentSession.mjs.map +1 -0
- package/dist/esm/session/JsonlSessionStore.mjs +422 -0
- package/dist/esm/session/JsonlSessionStore.mjs.map +1 -0
- package/dist/esm/session/handlers.mjs +219 -0
- package/dist/esm/session/handlers.mjs.map +1 -0
- package/dist/esm/session/ids.mjs +17 -0
- package/dist/esm/session/ids.mjs.map +1 -0
- package/dist/esm/session/messageSerialization.mjs +173 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -0
- package/dist/esm/stream.mjs +476 -12
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +1 -1
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +177 -59
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +107 -0
- package/dist/esm/tools/eagerEventExecution.mjs.map +1 -0
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/streamedToolCallSeals.mjs +36 -0
- package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -0
- package/dist/types/events.d.ts +1 -0
- package/dist/types/graphs/Graph.d.ts +24 -9
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +1 -0
- package/dist/types/openai/index.d.ts +75 -0
- package/dist/types/responses/index.d.ts +97 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/session/AgentSession.d.ts +32 -0
- package/dist/types/session/JsonlSessionStore.d.ts +67 -0
- package/dist/types/session/handlers.d.ts +8 -0
- package/dist/types/session/ids.d.ts +4 -0
- package/dist/types/session/index.d.ts +5 -0
- package/dist/types/session/messageSerialization.d.ts +7 -0
- package/dist/types/session/types.d.ts +191 -0
- package/dist/types/tools/ToolNode.d.ts +12 -1
- package/dist/types/tools/eagerEventExecution.d.ts +23 -0
- package/dist/types/tools/streamedToolCallSeals.d.ts +13 -0
- package/dist/types/types/hitl.d.ts +4 -0
- package/dist/types/types/run.d.ts +11 -1
- package/dist/types/types/tools.d.ts +36 -0
- package/package.json +19 -2
- package/src/__tests__/stream.eagerEventExecution.test.ts +2571 -0
- package/src/events.ts +29 -0
- package/src/graphs/Graph.ts +224 -50
- package/src/graphs/MultiAgentGraph.ts +1 -1
- package/src/graphs/__tests__/composition.smoke.test.ts +30 -0
- package/src/index.ts +3 -0
- package/src/llm/anthropic/index.ts +356 -84
- package/src/llm/anthropic/llm.spec.ts +64 -0
- package/src/llm/custom-chat-models.smoke.test.ts +175 -4
- package/src/llm/openai/contentBlocks.test.ts +35 -0
- package/src/llm/openai/deepseek.test.ts +201 -2
- package/src/llm/openai/index.ts +171 -26
- package/src/llm/openai/utils/index.ts +22 -0
- package/src/llm/openrouter/index.ts +4 -2
- package/src/openai/__tests__/openai.test.ts +337 -0
- package/src/openai/index.ts +404 -0
- package/src/responses/__tests__/responses.test.ts +652 -0
- package/src/responses/index.ts +677 -0
- package/src/run.ts +158 -8
- package/src/scripts/compare_pi_vs_ours.ts +592 -173
- package/src/scripts/session_live.ts +548 -0
- package/src/session/AgentSession.ts +1432 -0
- package/src/session/JsonlSessionStore.ts +572 -0
- package/src/session/__tests__/JsonlSessionStore.test.ts +1410 -0
- package/src/session/__tests__/handlers.test.ts +161 -0
- package/src/session/handlers.ts +272 -0
- package/src/session/ids.ts +17 -0
- package/src/session/index.ts +44 -0
- package/src/session/messageSerialization.ts +207 -0
- package/src/session/types.ts +275 -0
- package/src/specs/custom-event-await.test.ts +89 -0
- package/src/specs/summarization.test.ts +1 -1
- package/src/stream.ts +756 -48
- package/src/summarization/node.ts +1 -1
- package/src/tools/ToolNode.ts +299 -126
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +373 -0
- package/src/tools/__tests__/handlers.test.ts +2 -1
- package/src/tools/__tests__/hitl.test.ts +206 -110
- package/src/tools/eagerEventExecution.ts +153 -0
- package/src/tools/handlers.ts +8 -4
- package/src/tools/streamedToolCallSeals.ts +57 -0
- package/src/types/hitl.ts +4 -0
- package/src/types/run.ts +11 -0
- package/src/types/tools.ts +36 -0
- package/dist/cjs/llm/text.cjs +0 -69
- package/dist/cjs/llm/text.cjs.map +0 -1
- package/dist/esm/llm/text.mjs +0 -67
- package/dist/esm/llm/text.mjs.map +0 -1
package/src/llm/openai/index.ts
CHANGED
|
@@ -35,11 +35,13 @@ import type { ChatGeneration, ChatResult } from '@langchain/core/outputs';
|
|
|
35
35
|
import type { ChatXAIInput } from '@langchain/xai';
|
|
36
36
|
import type * as t from '@langchain/openai';
|
|
37
37
|
import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
|
|
38
|
-
import { sleep } from '@/utils';
|
|
39
38
|
|
|
40
39
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
41
40
|
const iife = <T>(fn: () => T) => fn();
|
|
42
41
|
|
|
42
|
+
const STREAM_CHUNK_MIN_SIZE = 4;
|
|
43
|
+
const STREAM_BOUNDARIES = new Set([' ', '.', ',', '!', '?', ';', ':']);
|
|
44
|
+
|
|
43
45
|
export function isHeaders(headers: unknown): headers is Headers {
|
|
44
46
|
return (
|
|
45
47
|
typeof Headers !== 'undefined' &&
|
|
@@ -403,18 +405,160 @@ function getCustomOpenAIClientOptions(
|
|
|
403
405
|
return requestOptions;
|
|
404
406
|
}
|
|
405
407
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
function findStreamChunkBoundary(text: string, minSize: number): number {
|
|
409
|
+
if (minSize >= text.length) {
|
|
410
|
+
return text.length;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (let position = minSize; position < text.length; position++) {
|
|
414
|
+
if (STREAM_BOUNDARIES.has(text[position])) {
|
|
415
|
+
return position + 1;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return text.length;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function splitStreamToken(text: string): string[] {
|
|
423
|
+
const chunks: string[] = [];
|
|
424
|
+
let currentIndex = 0;
|
|
425
|
+
|
|
426
|
+
while (currentIndex < text.length) {
|
|
427
|
+
const remainingText = text.slice(currentIndex);
|
|
428
|
+
const chunkSize = findStreamChunkBoundary(
|
|
429
|
+
remainingText,
|
|
430
|
+
STREAM_CHUNK_MIN_SIZE
|
|
431
|
+
);
|
|
432
|
+
chunks.push(text.slice(currentIndex, currentIndex + chunkSize));
|
|
433
|
+
currentIndex += chunkSize;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return chunks;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function splitTextGenerationChunk(
|
|
440
|
+
chunk: ChatGenerationChunk
|
|
441
|
+
): ChatGenerationChunk[] {
|
|
442
|
+
const { message } = chunk;
|
|
443
|
+
if (
|
|
444
|
+
!chunk.text ||
|
|
445
|
+
!(message instanceof AIMessageChunk) ||
|
|
446
|
+
typeof message.content !== 'string' ||
|
|
447
|
+
message.content !== chunk.text ||
|
|
448
|
+
chunk.generationInfo?.logprobs != null ||
|
|
449
|
+
chunk.generationInfo?.finish_reason != null
|
|
450
|
+
) {
|
|
451
|
+
return [chunk];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const tokenChunks = splitStreamToken(chunk.text);
|
|
455
|
+
if (tokenChunks.length <= 1) {
|
|
456
|
+
return [chunk];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let emittedUsage = false;
|
|
460
|
+
return tokenChunks.map((token) => {
|
|
461
|
+
const usageMetadata =
|
|
462
|
+
emittedUsage && message.usage_metadata != null
|
|
463
|
+
? undefined
|
|
464
|
+
: message.usage_metadata;
|
|
465
|
+
if (message.usage_metadata != null && !emittedUsage) {
|
|
466
|
+
emittedUsage = true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return new ChatGenerationChunk({
|
|
470
|
+
text: token,
|
|
471
|
+
generationInfo: chunk.generationInfo,
|
|
472
|
+
message: new AIMessageChunk(
|
|
473
|
+
Object.assign({}, message, {
|
|
474
|
+
content: token,
|
|
475
|
+
usage_metadata: usageMetadata,
|
|
476
|
+
})
|
|
477
|
+
),
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function emitStreamChunkCallback(
|
|
483
|
+
chunk: ChatGenerationChunk,
|
|
484
|
+
runManager?: CallbackManagerForLLMRun
|
|
485
|
+
): Promise<void> {
|
|
486
|
+
await runManager?.handleLLMNewToken(
|
|
487
|
+
chunk.text,
|
|
488
|
+
getStreamChunkTokenIndices(chunk),
|
|
489
|
+
undefined,
|
|
490
|
+
undefined,
|
|
491
|
+
undefined,
|
|
492
|
+
{ chunk }
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function getStreamChunkTokenIndices(
|
|
497
|
+
chunk: ChatGenerationChunk
|
|
498
|
+
): { prompt: number; completion: number } | undefined {
|
|
499
|
+
const prompt = chunk.generationInfo?.prompt;
|
|
500
|
+
const completion = chunk.generationInfo?.completion;
|
|
501
|
+
|
|
502
|
+
if (typeof prompt === 'number' && typeof completion === 'number') {
|
|
503
|
+
return { prompt, completion };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function* delayStreamChunks(
|
|
510
|
+
chunks: AsyncGenerator<ChatGenerationChunk>,
|
|
511
|
+
delay?: number,
|
|
512
|
+
signal?: AbortSignal,
|
|
513
|
+
runManager?: CallbackManagerForLLMRun
|
|
514
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
515
|
+
let lastYieldedAt: number | undefined;
|
|
410
516
|
for await (const chunk of chunks) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
517
|
+
const outputChunks =
|
|
518
|
+
delay != null && delay > 0 ? splitTextGenerationChunk(chunk) : [chunk];
|
|
519
|
+
for (const outputChunk of outputChunks) {
|
|
520
|
+
signal?.throwIfAborted();
|
|
521
|
+
if (delay != null && delay > 0 && lastYieldedAt != null) {
|
|
522
|
+
const timeSinceLastYield = Date.now() - lastYieldedAt;
|
|
523
|
+
const timeToWait = Math.max(0, delay - timeSinceLastYield);
|
|
524
|
+
if (timeToWait > 0) {
|
|
525
|
+
await sleepWithAbort(timeToWait, signal);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
signal?.throwIfAborted();
|
|
529
|
+
lastYieldedAt = Date.now();
|
|
530
|
+
await emitStreamChunkCallback(outputChunk, runManager);
|
|
531
|
+
signal?.throwIfAborted();
|
|
532
|
+
yield outputChunk;
|
|
414
533
|
}
|
|
415
534
|
}
|
|
416
535
|
}
|
|
417
536
|
|
|
537
|
+
async function sleepWithAbort(
|
|
538
|
+
delay: number,
|
|
539
|
+
signal?: AbortSignal
|
|
540
|
+
): Promise<void> {
|
|
541
|
+
if (delay <= 0) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
signal?.throwIfAborted();
|
|
545
|
+
await new Promise<void>((resolve, reject) => {
|
|
546
|
+
const timeout = setTimeout(() => {
|
|
547
|
+
signal?.removeEventListener('abort', onAbort);
|
|
548
|
+
resolve();
|
|
549
|
+
}, delay);
|
|
550
|
+
const onAbort = (): void => {
|
|
551
|
+
clearTimeout(timeout);
|
|
552
|
+
signal?.removeEventListener('abort', onAbort);
|
|
553
|
+
reject(signal?.reason ?? new Error('AbortError: User aborted request.'));
|
|
554
|
+
};
|
|
555
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
556
|
+
if (signal?.aborted === true) {
|
|
557
|
+
onAbort();
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
418
562
|
function createAbortHandler(controller: AbortController): () => void {
|
|
419
563
|
return function (): void {
|
|
420
564
|
controller.abort();
|
|
@@ -468,7 +612,7 @@ export class CustomOpenAIClient extends OpenAIClient {
|
|
|
468
612
|
this.abortHandler = handler;
|
|
469
613
|
if (signal) signal.addEventListener('abort', handler, { once: true });
|
|
470
614
|
|
|
471
|
-
const timeout = setTimeout(
|
|
615
|
+
const timeout = setTimeout(handler, ms);
|
|
472
616
|
|
|
473
617
|
const fetchOptions = {
|
|
474
618
|
signal: controller.signal as AbortSignal,
|
|
@@ -503,7 +647,7 @@ export class CustomAzureOpenAIClient extends AzureOpenAIClient {
|
|
|
503
647
|
this.abortHandler = handler;
|
|
504
648
|
if (signal) signal.addEventListener('abort', handler, { once: true });
|
|
505
649
|
|
|
506
|
-
const timeout = setTimeout(
|
|
650
|
+
const timeout = setTimeout(handler, ms);
|
|
507
651
|
|
|
508
652
|
const fetchOptions = {
|
|
509
653
|
signal: controller.signal as AbortSignal,
|
|
@@ -1184,8 +1328,10 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
|
|
|
1184
1328
|
runManager?: CallbackManagerForLLMRun
|
|
1185
1329
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
1186
1330
|
yield* delayStreamChunks(
|
|
1187
|
-
super._streamResponseChunks(messages, options,
|
|
1188
|
-
this._lc_stream_delay
|
|
1331
|
+
super._streamResponseChunks(messages, options, undefined),
|
|
1332
|
+
this._lc_stream_delay,
|
|
1333
|
+
options.signal,
|
|
1334
|
+
runManager
|
|
1189
1335
|
);
|
|
1190
1336
|
}
|
|
1191
1337
|
}
|
|
@@ -1294,8 +1440,10 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
|
|
|
1294
1440
|
runManager?: CallbackManagerForLLMRun
|
|
1295
1441
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
1296
1442
|
yield* delayStreamChunks(
|
|
1297
|
-
super._streamResponseChunks(messages, options,
|
|
1298
|
-
this._lc_stream_delay
|
|
1443
|
+
super._streamResponseChunks(messages, options, undefined),
|
|
1444
|
+
this._lc_stream_delay,
|
|
1445
|
+
options.signal,
|
|
1446
|
+
runManager
|
|
1299
1447
|
);
|
|
1300
1448
|
}
|
|
1301
1449
|
}
|
|
@@ -1425,8 +1573,10 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
|
|
|
1425
1573
|
runManager?: CallbackManagerForLLMRun
|
|
1426
1574
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
1427
1575
|
yield* delayStreamChunks(
|
|
1428
|
-
this._streamResponseChunksWithReasoning(messages, options,
|
|
1429
|
-
this._lc_stream_delay
|
|
1576
|
+
this._streamResponseChunksWithReasoning(messages, options, undefined),
|
|
1577
|
+
this._lc_stream_delay,
|
|
1578
|
+
options.signal,
|
|
1579
|
+
runManager
|
|
1430
1580
|
);
|
|
1431
1581
|
}
|
|
1432
1582
|
|
|
@@ -1767,14 +1917,7 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
|
|
|
1767
1917
|
protected _getDeepSeekTokenIndices(
|
|
1768
1918
|
chunk: ChatGenerationChunk
|
|
1769
1919
|
): { prompt: number; completion: number } | undefined {
|
|
1770
|
-
|
|
1771
|
-
const completion = chunk.generationInfo?.completion;
|
|
1772
|
-
|
|
1773
|
-
if (typeof prompt === 'number' && typeof completion === 'number') {
|
|
1774
|
-
return { prompt, completion };
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
return undefined;
|
|
1920
|
+
return getStreamChunkTokenIndices(chunk);
|
|
1778
1921
|
}
|
|
1779
1922
|
|
|
1780
1923
|
protected _getDeepSeekPartialTagSplitIndex(
|
|
@@ -1891,8 +2034,10 @@ export class ChatXAI extends OriginalChatXAI {
|
|
|
1891
2034
|
runManager?: CallbackManagerForLLMRun
|
|
1892
2035
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
1893
2036
|
yield* delayStreamChunks(
|
|
1894
|
-
super._streamResponseChunks(messages, options,
|
|
1895
|
-
this._lc_stream_delay
|
|
2037
|
+
super._streamResponseChunks(messages, options, undefined),
|
|
2038
|
+
this._lc_stream_delay,
|
|
2039
|
+
options.signal,
|
|
2040
|
+
runManager
|
|
1896
2041
|
);
|
|
1897
2042
|
}
|
|
1898
2043
|
}
|
|
@@ -38,6 +38,11 @@ import type {
|
|
|
38
38
|
ChatOpenAIReasoningSummary,
|
|
39
39
|
} from '@langchain/openai';
|
|
40
40
|
import { toLangChainContent } from '@/messages/langchain';
|
|
41
|
+
import {
|
|
42
|
+
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
43
|
+
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
44
|
+
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
|
|
45
|
+
} from '@/tools/streamedToolCallSeals';
|
|
41
46
|
|
|
42
47
|
export type { OpenAICallOptions, OpenAIChatInput };
|
|
43
48
|
|
|
@@ -948,6 +953,8 @@ export function _convertOpenAIResponsesDeltaToBaseMessageChunk(
|
|
|
948
953
|
chunk.type === 'response.output_item.added' &&
|
|
949
954
|
chunk.item.type === 'function_call'
|
|
950
955
|
) {
|
|
956
|
+
response_metadata[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY] =
|
|
957
|
+
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER;
|
|
951
958
|
tool_call_chunks.push({
|
|
952
959
|
type: 'tool_call_chunk',
|
|
953
960
|
name: chunk.item.name,
|
|
@@ -988,11 +995,26 @@ export function _convertOpenAIResponsesDeltaToBaseMessageChunk(
|
|
|
988
995
|
if (key !== 'id') response_metadata[key] = value;
|
|
989
996
|
}
|
|
990
997
|
} else if (chunk.type === 'response.function_call_arguments.delta') {
|
|
998
|
+
response_metadata[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY] =
|
|
999
|
+
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER;
|
|
991
1000
|
tool_call_chunks.push({
|
|
992
1001
|
type: 'tool_call_chunk',
|
|
993
1002
|
args: chunk.delta,
|
|
994
1003
|
index: chunk.output_index,
|
|
995
1004
|
});
|
|
1005
|
+
} else if (chunk.type === 'response.function_call_arguments.done') {
|
|
1006
|
+
response_metadata[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY] =
|
|
1007
|
+
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER;
|
|
1008
|
+
response_metadata[STREAMED_TOOL_CALL_SEAL_METADATA_KEY] = {
|
|
1009
|
+
kind: 'single',
|
|
1010
|
+
index: chunk.output_index,
|
|
1011
|
+
};
|
|
1012
|
+
tool_call_chunks.push({
|
|
1013
|
+
type: 'tool_call_chunk',
|
|
1014
|
+
name: chunk.name,
|
|
1015
|
+
args: chunk.arguments,
|
|
1016
|
+
index: chunk.output_index,
|
|
1017
|
+
});
|
|
996
1018
|
} else if (
|
|
997
1019
|
chunk.type === 'response.web_search_call.completed' ||
|
|
998
1020
|
chunk.type === 'response.file_search_call.completed'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatOpenAI } from '@/llm/openai';
|
|
1
|
+
import { ChatOpenAI, emitStreamChunkCallback } from '@/llm/openai';
|
|
2
2
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
3
3
|
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
4
4
|
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
@@ -237,7 +237,7 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|
|
237
237
|
for await (const generationChunk of super._streamResponseChunks(
|
|
238
238
|
messages,
|
|
239
239
|
options,
|
|
240
|
-
|
|
240
|
+
undefined
|
|
241
241
|
)) {
|
|
242
242
|
let currentReasoningText = '';
|
|
243
243
|
const reasoningDetails = getReasoningDetails(
|
|
@@ -283,11 +283,13 @@ export class ChatOpenRouter extends ChatOpenAI {
|
|
|
283
283
|
} else {
|
|
284
284
|
delete generationChunk.message.additional_kwargs.reasoning_details;
|
|
285
285
|
}
|
|
286
|
+
await emitStreamChunkCallback(generationChunk, runManager);
|
|
286
287
|
yield generationChunk;
|
|
287
288
|
continue;
|
|
288
289
|
}
|
|
289
290
|
|
|
290
291
|
delete generationChunk.message.additional_kwargs.reasoning_details;
|
|
292
|
+
await emitStreamChunkCallback(generationChunk, runManager);
|
|
291
293
|
yield generationChunk;
|
|
292
294
|
}
|
|
293
295
|
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { GraphEvents } from '@/common';
|
|
2
|
+
import {
|
|
3
|
+
createChatCompletionChunk,
|
|
4
|
+
createOpenAIHandlers,
|
|
5
|
+
createOpenAIStreamTracker,
|
|
6
|
+
sendOpenAIFinalChunk,
|
|
7
|
+
} from '@/openai';
|
|
8
|
+
import type * as t from '@/types';
|
|
9
|
+
|
|
10
|
+
describe('OpenAI-compatible adapters', () => {
|
|
11
|
+
it('creates chunks and streams message deltas as SSE data', async () => {
|
|
12
|
+
const writes: string[] = [];
|
|
13
|
+
const handlers = createOpenAIHandlers({
|
|
14
|
+
writer: { write: (data) => void writes.push(data) },
|
|
15
|
+
context: { requestId: 'chatcmpl_1', model: 'agent', created: 1 },
|
|
16
|
+
tracker: createOpenAIStreamTracker(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await handlers[GraphEvents.ON_MESSAGE_DELTA].handle(
|
|
20
|
+
GraphEvents.ON_MESSAGE_DELTA,
|
|
21
|
+
{
|
|
22
|
+
id: 'msg',
|
|
23
|
+
delta: { content: [{ type: 'text', text: 'hello' }] },
|
|
24
|
+
} satisfies t.MessageDeltaEvent
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(writes).toHaveLength(2);
|
|
28
|
+
expect(writes[0]).toContain('"role":"assistant"');
|
|
29
|
+
expect(writes[1]).toContain('"content":"hello"');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('sends a final usage chunk and done marker', async () => {
|
|
33
|
+
const writes: string[] = [];
|
|
34
|
+
const tracker = createOpenAIStreamTracker();
|
|
35
|
+
tracker.usage.promptTokens = 3;
|
|
36
|
+
tracker.usage.completionTokens = 5;
|
|
37
|
+
|
|
38
|
+
await sendOpenAIFinalChunk({
|
|
39
|
+
writer: { write: (data) => void writes.push(data) },
|
|
40
|
+
context: { requestId: 'chatcmpl_2', model: 'agent', created: 1 },
|
|
41
|
+
tracker,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(writes).toHaveLength(4);
|
|
45
|
+
expect(writes[0]).toContain('"role":"assistant"');
|
|
46
|
+
expect(writes[1]).toContain('"finish_reason":"stop"');
|
|
47
|
+
expect(writes[1]).not.toContain('"usage"');
|
|
48
|
+
expect(writes[2]).toContain('"choices":[]');
|
|
49
|
+
expect(writes[2]).toContain('"total_tokens":8');
|
|
50
|
+
expect(writes[3]).toBe('data: [DONE]\n\n');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('uses tool_calls finish reason after streaming tool deltas', async () => {
|
|
54
|
+
const writes: string[] = [];
|
|
55
|
+
const tracker = createOpenAIStreamTracker();
|
|
56
|
+
const handlers = createOpenAIHandlers({
|
|
57
|
+
writer: { write: (data) => void writes.push(data) },
|
|
58
|
+
context: { requestId: 'chatcmpl_tools', model: 'agent', created: 1 },
|
|
59
|
+
tracker,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await handlers[GraphEvents.ON_RUN_STEP_DELTA].handle(
|
|
63
|
+
GraphEvents.ON_RUN_STEP_DELTA,
|
|
64
|
+
{
|
|
65
|
+
id: 'step_1',
|
|
66
|
+
delta: {
|
|
67
|
+
type: 'tool_calls',
|
|
68
|
+
tool_calls: [
|
|
69
|
+
{
|
|
70
|
+
index: 0,
|
|
71
|
+
id: 'call_1',
|
|
72
|
+
name: 'search',
|
|
73
|
+
args: '{"query":"sessions"}',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
} as t.RunStepDeltaEvent
|
|
78
|
+
);
|
|
79
|
+
await sendOpenAIFinalChunk({
|
|
80
|
+
writer: { write: (data) => void writes.push(data) },
|
|
81
|
+
context: { requestId: 'chatcmpl_tools', model: 'agent', created: 1 },
|
|
82
|
+
tracker,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(writes[0]).toContain('"role":"assistant"');
|
|
86
|
+
expect(writes[1]).toContain('"tool_calls"');
|
|
87
|
+
expect(writes.at(-3)).toContain('"finish_reason":"tool_calls"');
|
|
88
|
+
expect(writes.at(-2)).toContain('"choices":[]');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('uses stop finish reason when assistant text follows tool calls', async () => {
|
|
92
|
+
const writes: string[] = [];
|
|
93
|
+
const tracker = createOpenAIStreamTracker();
|
|
94
|
+
const handlers = createOpenAIHandlers({
|
|
95
|
+
writer: { write: (data) => void writes.push(data) },
|
|
96
|
+
context: { requestId: 'chatcmpl_tools_done', model: 'agent', created: 1 },
|
|
97
|
+
tracker,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await handlers[GraphEvents.ON_RUN_STEP_DELTA].handle(
|
|
101
|
+
GraphEvents.ON_RUN_STEP_DELTA,
|
|
102
|
+
{
|
|
103
|
+
id: 'step_1',
|
|
104
|
+
delta: {
|
|
105
|
+
type: 'tool_calls',
|
|
106
|
+
tool_calls: [{ index: 0, id: 'call_1', name: 'search' }],
|
|
107
|
+
},
|
|
108
|
+
} as t.RunStepDeltaEvent
|
|
109
|
+
);
|
|
110
|
+
await handlers[GraphEvents.ON_MESSAGE_DELTA].handle(
|
|
111
|
+
GraphEvents.ON_MESSAGE_DELTA,
|
|
112
|
+
{
|
|
113
|
+
id: 'msg',
|
|
114
|
+
delta: { content: [{ type: 'text', text: 'done' }] },
|
|
115
|
+
} satisfies t.MessageDeltaEvent
|
|
116
|
+
);
|
|
117
|
+
await handlers[GraphEvents.ON_RUN_STEP_DELTA].handle(
|
|
118
|
+
GraphEvents.ON_RUN_STEP_DELTA,
|
|
119
|
+
{
|
|
120
|
+
id: 'step_1',
|
|
121
|
+
delta: {
|
|
122
|
+
type: 'tool_calls',
|
|
123
|
+
tool_calls: [{ index: 0, id: 'call_1', name: 'search' }],
|
|
124
|
+
},
|
|
125
|
+
} as t.RunStepDeltaEvent
|
|
126
|
+
);
|
|
127
|
+
await sendOpenAIFinalChunk({
|
|
128
|
+
writer: { write: (data) => void writes.push(data) },
|
|
129
|
+
context: { requestId: 'chatcmpl_tools_done', model: 'agent', created: 1 },
|
|
130
|
+
tracker,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(writes.at(-3)).toContain('"finish_reason":"stop"');
|
|
134
|
+
expect(writes.at(-2)).toContain('"choices":[]');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('scopes tool-call argument state by run step', async () => {
|
|
138
|
+
const writes: string[] = [];
|
|
139
|
+
const tracker = createOpenAIStreamTracker();
|
|
140
|
+
const handlers = createOpenAIHandlers({
|
|
141
|
+
writer: { write: (data) => void writes.push(data) },
|
|
142
|
+
context: { requestId: 'chatcmpl_step_tools', model: 'agent', created: 1 },
|
|
143
|
+
tracker,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await handlers[GraphEvents.ON_RUN_STEP_DELTA].handle(
|
|
147
|
+
GraphEvents.ON_RUN_STEP_DELTA,
|
|
148
|
+
{
|
|
149
|
+
id: 'step_1',
|
|
150
|
+
delta: {
|
|
151
|
+
type: 'tool_calls',
|
|
152
|
+
tool_calls: [
|
|
153
|
+
{
|
|
154
|
+
index: 0,
|
|
155
|
+
id: 'call_1',
|
|
156
|
+
name: 'search',
|
|
157
|
+
args: '{"query":"first"}',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
} as t.RunStepDeltaEvent
|
|
162
|
+
);
|
|
163
|
+
await handlers[GraphEvents.ON_RUN_STEP_DELTA].handle(
|
|
164
|
+
GraphEvents.ON_RUN_STEP_DELTA,
|
|
165
|
+
{
|
|
166
|
+
id: 'step_2',
|
|
167
|
+
delta: {
|
|
168
|
+
type: 'tool_calls',
|
|
169
|
+
tool_calls: [
|
|
170
|
+
{
|
|
171
|
+
index: 0,
|
|
172
|
+
id: 'call_2',
|
|
173
|
+
name: 'search',
|
|
174
|
+
args: '{"query":"second"}',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
} as t.RunStepDeltaEvent
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const toolCallDeltas = writes
|
|
182
|
+
.map(
|
|
183
|
+
(data) =>
|
|
184
|
+
JSON.parse(data.slice(6)) as {
|
|
185
|
+
choices: Array<{
|
|
186
|
+
delta: {
|
|
187
|
+
tool_calls?: Array<{
|
|
188
|
+
id?: string;
|
|
189
|
+
function?: { name?: string; arguments?: string };
|
|
190
|
+
}>;
|
|
191
|
+
};
|
|
192
|
+
}>;
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
.flatMap((chunk) => chunk.choices[0].delta.tool_calls ?? []);
|
|
196
|
+
|
|
197
|
+
expect(toolCallDeltas).toHaveLength(2);
|
|
198
|
+
expect(toolCallDeltas[1]).toMatchObject({
|
|
199
|
+
id: 'call_2',
|
|
200
|
+
function: { name: 'search', arguments: '{"query":"second"}' },
|
|
201
|
+
});
|
|
202
|
+
expect(tracker.toolCalls.get(0)?.function.arguments).toBe(
|
|
203
|
+
'{"query":"second"}'
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('streams completed tool-call run steps without deltas', async () => {
|
|
208
|
+
const writes: string[] = [];
|
|
209
|
+
const tracker = createOpenAIStreamTracker();
|
|
210
|
+
const handlers = createOpenAIHandlers({
|
|
211
|
+
writer: { write: (data) => void writes.push(data) },
|
|
212
|
+
context: {
|
|
213
|
+
requestId: 'chatcmpl_complete_tools',
|
|
214
|
+
model: 'agent',
|
|
215
|
+
created: 1,
|
|
216
|
+
},
|
|
217
|
+
tracker,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await handlers[GraphEvents.ON_RUN_STEP].handle(GraphEvents.ON_RUN_STEP, {
|
|
221
|
+
id: 'step_complete',
|
|
222
|
+
index: 2,
|
|
223
|
+
type: 'tool_calls',
|
|
224
|
+
stepDetails: {
|
|
225
|
+
type: 'tool_calls',
|
|
226
|
+
tool_calls: [
|
|
227
|
+
{
|
|
228
|
+
id: 'call_complete',
|
|
229
|
+
type: 'function',
|
|
230
|
+
function: {
|
|
231
|
+
name: 'search',
|
|
232
|
+
arguments: { query: 'sessions' },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
} as t.RunStep);
|
|
238
|
+
await sendOpenAIFinalChunk({
|
|
239
|
+
writer: { write: (data) => void writes.push(data) },
|
|
240
|
+
context: {
|
|
241
|
+
requestId: 'chatcmpl_complete_tools',
|
|
242
|
+
model: 'agent',
|
|
243
|
+
created: 1,
|
|
244
|
+
},
|
|
245
|
+
tracker,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(writes[0]).toContain('"role":"assistant"');
|
|
249
|
+
expect(writes[1]).toContain('"tool_calls"');
|
|
250
|
+
expect(writes[1]).toContain('"id":"call_complete"');
|
|
251
|
+
expect(writes[1]).toContain('"name":"search"');
|
|
252
|
+
expect(writes[1]).toContain('"{\\"query\\":\\"sessions\\"}"');
|
|
253
|
+
expect(writes.at(-3)).toContain('"finish_reason":"tool_calls"');
|
|
254
|
+
expect(writes.at(-2)).toContain('"choices":[]');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('tracks partial usage metadata without NaN totals', async () => {
|
|
258
|
+
const tracker = createOpenAIStreamTracker();
|
|
259
|
+
const handlers = createOpenAIHandlers({
|
|
260
|
+
writer: { write: jest.fn() },
|
|
261
|
+
context: { requestId: 'chatcmpl_usage', model: 'agent', created: 1 },
|
|
262
|
+
tracker,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await handlers[GraphEvents.CHAT_MODEL_END].handle(
|
|
266
|
+
GraphEvents.CHAT_MODEL_END,
|
|
267
|
+
{
|
|
268
|
+
output: { usage_metadata: { input_tokens: 3 } },
|
|
269
|
+
} as t.ModelEndData
|
|
270
|
+
);
|
|
271
|
+
await handlers[GraphEvents.CHAT_MODEL_END].handle(
|
|
272
|
+
GraphEvents.CHAT_MODEL_END,
|
|
273
|
+
{
|
|
274
|
+
output: { usage_metadata: { output_tokens: 5 } },
|
|
275
|
+
} as t.ModelEndData
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(tracker.usage.promptTokens).toBe(3);
|
|
279
|
+
expect(tracker.usage.completionTokens).toBe(5);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('includes reasoning token usage in the final chunk', async () => {
|
|
283
|
+
const writes: string[] = [];
|
|
284
|
+
const tracker = createOpenAIStreamTracker();
|
|
285
|
+
const handlers = createOpenAIHandlers({
|
|
286
|
+
writer: { write: (data) => void writes.push(data) },
|
|
287
|
+
context: {
|
|
288
|
+
requestId: 'chatcmpl_reasoning_usage',
|
|
289
|
+
model: 'agent',
|
|
290
|
+
created: 1,
|
|
291
|
+
},
|
|
292
|
+
tracker,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await handlers[GraphEvents.CHAT_MODEL_END].handle(
|
|
296
|
+
GraphEvents.CHAT_MODEL_END,
|
|
297
|
+
{
|
|
298
|
+
output: {
|
|
299
|
+
usage_metadata: {
|
|
300
|
+
input_tokens: 3,
|
|
301
|
+
output_tokens: 5,
|
|
302
|
+
output_token_details: { reasoning: 2 },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
} as t.ModelEndData
|
|
306
|
+
);
|
|
307
|
+
await sendOpenAIFinalChunk({
|
|
308
|
+
writer: { write: (data) => void writes.push(data) },
|
|
309
|
+
context: {
|
|
310
|
+
requestId: 'chatcmpl_reasoning_usage',
|
|
311
|
+
model: 'agent',
|
|
312
|
+
created: 1,
|
|
313
|
+
},
|
|
314
|
+
tracker,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(writes.at(-2)).toContain('"choices":[]');
|
|
318
|
+
expect(writes.at(-2)).toContain(
|
|
319
|
+
'"completion_tokens_details":{"reasoning_tokens":2}'
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('builds a chat completion chunk without transport dependencies', () => {
|
|
324
|
+
expect(
|
|
325
|
+
createChatCompletionChunk(
|
|
326
|
+
{ requestId: 'chatcmpl_3', model: 'agent', created: 1 },
|
|
327
|
+
{ content: 'x' }
|
|
328
|
+
)
|
|
329
|
+
).toEqual({
|
|
330
|
+
id: 'chatcmpl_3',
|
|
331
|
+
object: 'chat.completion.chunk',
|
|
332
|
+
created: 1,
|
|
333
|
+
model: 'agent',
|
|
334
|
+
choices: [{ index: 0, delta: { content: 'x' }, finish_reason: null }],
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|