@librechat/agents 3.3.10 → 3.3.11
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/dist/cjs/graphs/Graph.cjs +2 -2
- package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +14 -7
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +188 -11
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -3
- package/dist/cjs/messages/core.cjs +592 -27
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/run.cjs +1 -1
- package/dist/cjs/stream.cjs +2 -2
- package/dist/cjs/tools/ToolNode.cjs +1 -1
- package/dist/cjs/tools/search/tool.cjs +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/esm/graphs/Graph.mjs +2 -2
- package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +14 -7
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +190 -13
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -5
- package/dist/esm/messages/core.mjs +592 -28
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/run.mjs +1 -1
- package/dist/esm/stream.mjs +2 -2
- package/dist/esm/tools/ToolNode.mjs +1 -1
- package/dist/esm/tools/search/tool.mjs +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/types/langfuseToolOutputTracing.d.ts +1 -0
- package/dist/types/llm/invoke.d.ts +1 -1
- package/dist/types/messages/core.d.ts +11 -6
- package/package.json +1 -1
- package/src/langfuseToolOutputTracing.ts +410 -14
- package/src/llm/custom-chat-models.smoke.test.ts +747 -0
- package/src/llm/invoke.test.ts +98 -0
- package/src/llm/invoke.ts +34 -23
- package/src/llm/openai/index.ts +334 -25
- package/src/llm/openai/llm.spec.ts +107 -6
- package/src/messages/core.ts +1290 -42
- package/src/messages/formatAgentMessages.test.ts +2623 -0
- package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
- package/src/specs/preemptSeal.test.ts +374 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { convertMessagesToResponsesInput } from '@langchain/openai';
|
|
1
2
|
import {
|
|
2
3
|
AIMessage,
|
|
3
4
|
AIMessageChunk,
|
|
@@ -21,6 +22,10 @@ import {
|
|
|
21
22
|
CustomOpenAIClient,
|
|
22
23
|
CustomAzureOpenAIClient,
|
|
23
24
|
} from '@/llm/openai';
|
|
25
|
+
import {
|
|
26
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY,
|
|
27
|
+
projectOpenAIResponsesToolMessageContent,
|
|
28
|
+
} from '@/messages/core';
|
|
24
29
|
import { CustomChatGoogleGenerativeAI } from '@/llm/google';
|
|
25
30
|
import { CustomChatBedrockConverse } from '@/llm/bedrock';
|
|
26
31
|
import { ChatOpenRouter } from '@/llm/openrouter';
|
|
@@ -37,6 +42,29 @@ type OpenAIResponsesDelegate = {
|
|
|
37
42
|
client?: unknown;
|
|
38
43
|
_getClientOptions: (options?: OpenAIRequestOptions) => OpenAIRequestOptions;
|
|
39
44
|
};
|
|
45
|
+
type StreamingOpenAIResponsesDelegate = OpenAIResponsesDelegate & {
|
|
46
|
+
completionWithRetry: (
|
|
47
|
+
request: OpenAIClient.Responses.ResponseCreateParamsStreaming
|
|
48
|
+
) => Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
|
|
49
|
+
};
|
|
50
|
+
type MockableResponsesDelegate = OpenAIResponsesDelegate & {
|
|
51
|
+
completionWithRetry: (request: {
|
|
52
|
+
input?: unknown;
|
|
53
|
+
stream?: boolean;
|
|
54
|
+
}) => Promise<unknown>;
|
|
55
|
+
_generate: (
|
|
56
|
+
messages: AIMessage[],
|
|
57
|
+
options: Record<string, unknown>
|
|
58
|
+
) => Promise<unknown>;
|
|
59
|
+
_streamChatModelEvents: (
|
|
60
|
+
messages: AIMessage[],
|
|
61
|
+
options: Record<string, unknown>
|
|
62
|
+
) => AsyncGenerator<unknown>;
|
|
63
|
+
_streamResponseChunks: (
|
|
64
|
+
messages: AIMessage[],
|
|
65
|
+
options: Record<string, unknown>
|
|
66
|
+
) => AsyncGenerator<ChatGenerationChunk>;
|
|
67
|
+
};
|
|
40
68
|
type AnthropicCallOptions = Parameters<
|
|
41
69
|
CustomAnthropic['invocationParams']
|
|
42
70
|
>[0] & {
|
|
@@ -183,6 +211,107 @@ const baseAzureFields = {
|
|
|
183
211
|
azureOpenAIApiDeploymentName: 'test-deployment',
|
|
184
212
|
};
|
|
185
213
|
|
|
214
|
+
function createPersistedPreemptedAzureMessage(): AIMessage {
|
|
215
|
+
return new AIMessage({
|
|
216
|
+
id: 'msg_persisted_azure',
|
|
217
|
+
content: [{ type: 'text', text: 'Partial Azure answer.' }],
|
|
218
|
+
additional_kwargs: {
|
|
219
|
+
reasoning: {
|
|
220
|
+
id: 'rs_persisted_azure',
|
|
221
|
+
type: 'reasoning',
|
|
222
|
+
status: 'in_progress',
|
|
223
|
+
summary: [],
|
|
224
|
+
},
|
|
225
|
+
tool_outputs: [
|
|
226
|
+
{
|
|
227
|
+
id: 'local_persisted_azure',
|
|
228
|
+
type: 'local_shell_call_output',
|
|
229
|
+
status: 'completed',
|
|
230
|
+
output: 'AZURE_SERVER_RESULT',
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
|
|
234
|
+
{
|
|
235
|
+
itemId: 'local_persisted_azure',
|
|
236
|
+
kind: 'output',
|
|
237
|
+
outputIndex: 0,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
itemId: 'rs_persisted_azure',
|
|
241
|
+
kind: 'reasoning',
|
|
242
|
+
outputIndex: 1,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
itemId: 'msg_persisted_azure',
|
|
246
|
+
kind: 'text',
|
|
247
|
+
outputIndex: 2,
|
|
248
|
+
contentIndex: 0,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
response_metadata: {
|
|
253
|
+
id: 'resp_persisted_azure',
|
|
254
|
+
model_provider: 'openai',
|
|
255
|
+
preempted: true,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function expectNeutralizedAzureResponsesRequest(request: unknown): void {
|
|
261
|
+
const serialized = JSON.stringify(request);
|
|
262
|
+
expect(serialized).toContain('AZURE_SERVER_RESULT');
|
|
263
|
+
expect(serialized.match(/AZURE_SERVER_RESULT/g)).toHaveLength(1);
|
|
264
|
+
expect(serialized).toContain('serverToolResult');
|
|
265
|
+
expect(serialized).not.toContain('msg_persisted_azure');
|
|
266
|
+
expect(serialized).not.toContain('rs_persisted_azure');
|
|
267
|
+
expect(serialized).not.toContain('local_persisted_azure');
|
|
268
|
+
expect(serialized).not.toContain('resp_persisted_azure');
|
|
269
|
+
expect(serialized).not.toContain(OPENAI_RESPONSES_REPLAY_POSITIONS_KEY);
|
|
270
|
+
expect(serialized).not.toContain('local_shell_call_output');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function createCompletedAzureResponse(): OpenAIClient.Responses.Response {
|
|
274
|
+
return {
|
|
275
|
+
id: 'resp_fresh_azure',
|
|
276
|
+
created_at: 0,
|
|
277
|
+
error: null,
|
|
278
|
+
incomplete_details: null,
|
|
279
|
+
instructions: null,
|
|
280
|
+
metadata: {},
|
|
281
|
+
model: 'gpt-5',
|
|
282
|
+
object: 'response',
|
|
283
|
+
output: [
|
|
284
|
+
{
|
|
285
|
+
id: 'msg_fresh_azure',
|
|
286
|
+
type: 'message',
|
|
287
|
+
role: 'assistant',
|
|
288
|
+
status: 'completed',
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: 'output_text',
|
|
292
|
+
text: 'Fresh Azure answer.',
|
|
293
|
+
annotations: [],
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
output_text: 'Fresh Azure answer.',
|
|
299
|
+
parallel_tool_calls: true,
|
|
300
|
+
status: 'completed',
|
|
301
|
+
temperature: null,
|
|
302
|
+
tool_choice: 'auto',
|
|
303
|
+
tools: [],
|
|
304
|
+
top_p: null,
|
|
305
|
+
usage: {
|
|
306
|
+
input_tokens: 1,
|
|
307
|
+
input_tokens_details: { cached_tokens: 0, cache_write_tokens: 0 },
|
|
308
|
+
output_tokens: 1,
|
|
309
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
310
|
+
total_tokens: 2,
|
|
311
|
+
},
|
|
312
|
+
} as OpenAIClient.Responses.Response;
|
|
313
|
+
}
|
|
314
|
+
|
|
186
315
|
const waitForFetchOutcome = (
|
|
187
316
|
promise: Promise<Response>,
|
|
188
317
|
timeoutMs = 100
|
|
@@ -401,6 +530,569 @@ describe('custom chat model class smoke tests', () => {
|
|
|
401
530
|
);
|
|
402
531
|
});
|
|
403
532
|
|
|
533
|
+
it('retains replayable Responses output items before a terminal event', async () => {
|
|
534
|
+
const model = new ChatOpenAI({
|
|
535
|
+
model: 'gpt-5',
|
|
536
|
+
apiKey: 'test-key',
|
|
537
|
+
useResponsesApi: true,
|
|
538
|
+
});
|
|
539
|
+
const responses = (
|
|
540
|
+
model as unknown as { responses: StreamingOpenAIResponsesDelegate }
|
|
541
|
+
).responses;
|
|
542
|
+
const items = [
|
|
543
|
+
{
|
|
544
|
+
id: 'local_output_item',
|
|
545
|
+
type: 'local_shell_call_output',
|
|
546
|
+
status: 'completed',
|
|
547
|
+
output: 'local output',
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
id: 'shell_output_item',
|
|
551
|
+
call_id: 'shell_call_id',
|
|
552
|
+
type: 'shell_call_output',
|
|
553
|
+
status: 'completed',
|
|
554
|
+
max_output_length: 1_000,
|
|
555
|
+
output: [
|
|
556
|
+
{
|
|
557
|
+
stdout: 'shell output',
|
|
558
|
+
stderr: '',
|
|
559
|
+
outcome: { type: 'exit', exit_code: 0 },
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: 'patch_output_item',
|
|
565
|
+
call_id: 'patch_call_id',
|
|
566
|
+
type: 'apply_patch_call_output',
|
|
567
|
+
status: 'completed',
|
|
568
|
+
output: 'patch output',
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
id: 'program_output_item',
|
|
572
|
+
call_id: 'program_call_id',
|
|
573
|
+
type: 'program_output',
|
|
574
|
+
status: 'completed',
|
|
575
|
+
result: 'program output',
|
|
576
|
+
},
|
|
577
|
+
] as const;
|
|
578
|
+
responses.completionWithRetry = async () =>
|
|
579
|
+
(async function* () {
|
|
580
|
+
for (let i = 0; i < items.length; i++) {
|
|
581
|
+
yield {
|
|
582
|
+
type: 'response.output_item.done',
|
|
583
|
+
sequence_number: i,
|
|
584
|
+
output_index: i,
|
|
585
|
+
item: items[i],
|
|
586
|
+
} as unknown as OpenAIClient.Responses.ResponseStreamEvent;
|
|
587
|
+
}
|
|
588
|
+
yield {
|
|
589
|
+
type: 'response.output_item.added',
|
|
590
|
+
sequence_number: items.length,
|
|
591
|
+
output_index: items.length,
|
|
592
|
+
item: {
|
|
593
|
+
id: 'msg_partial',
|
|
594
|
+
type: 'message',
|
|
595
|
+
role: 'assistant',
|
|
596
|
+
status: 'in_progress',
|
|
597
|
+
content: [],
|
|
598
|
+
},
|
|
599
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
600
|
+
yield {
|
|
601
|
+
type: 'response.output_text.delta',
|
|
602
|
+
sequence_number: items.length + 1,
|
|
603
|
+
output_index: items.length,
|
|
604
|
+
content_index: 0,
|
|
605
|
+
item_id: 'msg_partial',
|
|
606
|
+
delta: 'Partial ',
|
|
607
|
+
logprobs: [],
|
|
608
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
609
|
+
yield {
|
|
610
|
+
type: 'response.output_text.delta',
|
|
611
|
+
sequence_number: items.length + 2,
|
|
612
|
+
output_index: items.length,
|
|
613
|
+
content_index: 0,
|
|
614
|
+
item_id: 'msg_partial',
|
|
615
|
+
delta: 'answer.',
|
|
616
|
+
logprobs: [],
|
|
617
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
618
|
+
})();
|
|
619
|
+
|
|
620
|
+
const chunks: AIMessageChunk[] = [];
|
|
621
|
+
const stream = await model.stream([new HumanMessage('run tools')]);
|
|
622
|
+
for await (const chunk of stream) {
|
|
623
|
+
chunks.push(chunk as AIMessageChunk);
|
|
624
|
+
}
|
|
625
|
+
const combined = chunks.reduce<AIMessageChunk | undefined>(
|
|
626
|
+
(current, chunk) =>
|
|
627
|
+
current == null ? chunk : (current.concat(chunk) as AIMessageChunk),
|
|
628
|
+
undefined
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
expect(chunks).toHaveLength(items.length + 3);
|
|
632
|
+
expect(combined?.text).toBe('Partial answer.');
|
|
633
|
+
expect(combined?.additional_kwargs.tool_outputs).toEqual(items);
|
|
634
|
+
expect(
|
|
635
|
+
combined?.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]
|
|
636
|
+
).toEqual([
|
|
637
|
+
...items.map((item, outputIndex) => ({
|
|
638
|
+
itemId: item.id,
|
|
639
|
+
kind: 'output',
|
|
640
|
+
outputIndex,
|
|
641
|
+
})),
|
|
642
|
+
{
|
|
643
|
+
itemId: 'msg_partial',
|
|
644
|
+
kind: 'message',
|
|
645
|
+
outputIndex: items.length,
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
contentIndex: 0,
|
|
649
|
+
itemId: 'msg_partial',
|
|
650
|
+
kind: 'text',
|
|
651
|
+
outputIndex: items.length,
|
|
652
|
+
},
|
|
653
|
+
]);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('rehydrates one-chunk Responses replay ordering from constructor kwargs', async () => {
|
|
657
|
+
const model = new ChatOpenAI({
|
|
658
|
+
model: 'gpt-5',
|
|
659
|
+
apiKey: 'test-key',
|
|
660
|
+
useResponsesApi: true,
|
|
661
|
+
});
|
|
662
|
+
const responses = (
|
|
663
|
+
model as unknown as { responses: StreamingOpenAIResponsesDelegate }
|
|
664
|
+
).responses;
|
|
665
|
+
responses.completionWithRetry = async () =>
|
|
666
|
+
(async function* () {
|
|
667
|
+
yield {
|
|
668
|
+
type: 'response.output_text.delta',
|
|
669
|
+
sequence_number: 0,
|
|
670
|
+
output_index: 3,
|
|
671
|
+
content_index: 2,
|
|
672
|
+
item_id: 'msg_one_chunk',
|
|
673
|
+
delta: 'Partial answer.',
|
|
674
|
+
logprobs: [],
|
|
675
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
676
|
+
})();
|
|
677
|
+
|
|
678
|
+
const stream = await model.stream([new HumanMessage('answer')]);
|
|
679
|
+
const result = await stream.next();
|
|
680
|
+
expect(result.done).toBe(false);
|
|
681
|
+
const chunk = result.value as AIMessageChunk;
|
|
682
|
+
const serializedFields = JSON.parse(
|
|
683
|
+
JSON.stringify(chunk.lc_kwargs)
|
|
684
|
+
) as ConstructorParameters<typeof AIMessageChunk>[0];
|
|
685
|
+
const rehydrated = new AIMessageChunk(serializedFields);
|
|
686
|
+
|
|
687
|
+
expect(chunk.content).toEqual([
|
|
688
|
+
{ type: 'text', text: 'Partial answer.', index: 0 },
|
|
689
|
+
]);
|
|
690
|
+
expect(rehydrated.content).toEqual([
|
|
691
|
+
{ type: 'text', text: 'Partial answer.', index: 0 },
|
|
692
|
+
]);
|
|
693
|
+
expect(
|
|
694
|
+
rehydrated.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]
|
|
695
|
+
).toEqual([
|
|
696
|
+
{
|
|
697
|
+
contentIndex: 2,
|
|
698
|
+
itemId: 'msg_one_chunk',
|
|
699
|
+
kind: 'text',
|
|
700
|
+
outputIndex: 3,
|
|
701
|
+
},
|
|
702
|
+
]);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it.each(['response.completed', 'response.incomplete'] as const)(
|
|
706
|
+
'removes provisional replay outputs after %s supplies the authoritative output',
|
|
707
|
+
async (terminalType) => {
|
|
708
|
+
const model = new ChatOpenAI({
|
|
709
|
+
model: 'gpt-5',
|
|
710
|
+
apiKey: 'test-key',
|
|
711
|
+
useResponsesApi: true,
|
|
712
|
+
});
|
|
713
|
+
const responses = (
|
|
714
|
+
model as unknown as { responses: StreamingOpenAIResponsesDelegate }
|
|
715
|
+
).responses;
|
|
716
|
+
const streamedToolOutput = {
|
|
717
|
+
id: 'web_search_item',
|
|
718
|
+
type: 'web_search_call',
|
|
719
|
+
status: 'completed',
|
|
720
|
+
} as const;
|
|
721
|
+
const provisionalReplayOutput = {
|
|
722
|
+
id: 'local_output_item',
|
|
723
|
+
type: 'local_shell_call_output',
|
|
724
|
+
status: 'completed',
|
|
725
|
+
output: 'stale provisional local output',
|
|
726
|
+
} as const;
|
|
727
|
+
const authoritativeReplayOutput = {
|
|
728
|
+
...provisionalReplayOutput,
|
|
729
|
+
output: 'unique authoritative local output',
|
|
730
|
+
} as const;
|
|
731
|
+
responses.completionWithRetry = async () =>
|
|
732
|
+
(async function* () {
|
|
733
|
+
yield {
|
|
734
|
+
type: 'response.output_item.done',
|
|
735
|
+
sequence_number: 0,
|
|
736
|
+
output_index: 0,
|
|
737
|
+
item: streamedToolOutput,
|
|
738
|
+
} as unknown as OpenAIClient.Responses.ResponseStreamEvent;
|
|
739
|
+
yield {
|
|
740
|
+
type: 'response.output_item.done',
|
|
741
|
+
sequence_number: 1,
|
|
742
|
+
output_index: 1,
|
|
743
|
+
item: provisionalReplayOutput,
|
|
744
|
+
} as unknown as OpenAIClient.Responses.ResponseStreamEvent;
|
|
745
|
+
yield {
|
|
746
|
+
type: terminalType,
|
|
747
|
+
sequence_number: 2,
|
|
748
|
+
response: {
|
|
749
|
+
id: 'resp_terminal',
|
|
750
|
+
model: 'gpt-5',
|
|
751
|
+
output: [streamedToolOutput, authoritativeReplayOutput],
|
|
752
|
+
status:
|
|
753
|
+
terminalType === 'response.completed'
|
|
754
|
+
? 'completed'
|
|
755
|
+
: 'incomplete',
|
|
756
|
+
usage: null,
|
|
757
|
+
},
|
|
758
|
+
} as unknown as OpenAIClient.Responses.ResponseStreamEvent;
|
|
759
|
+
})();
|
|
760
|
+
|
|
761
|
+
let tracedResult: unknown;
|
|
762
|
+
const chunks: AIMessageChunk[] = [];
|
|
763
|
+
const stream = await model.stream([new HumanMessage('run tools')], {
|
|
764
|
+
callbacks: [
|
|
765
|
+
{
|
|
766
|
+
handleLLMEnd(result: unknown): void {
|
|
767
|
+
tracedResult = result;
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
],
|
|
771
|
+
});
|
|
772
|
+
for await (const chunk of stream) {
|
|
773
|
+
chunks.push(chunk as AIMessageChunk);
|
|
774
|
+
}
|
|
775
|
+
const combined = chunks.reduce<AIMessageChunk | undefined>(
|
|
776
|
+
(current, chunk) =>
|
|
777
|
+
current == null ? chunk : (current.concat(chunk) as AIMessageChunk),
|
|
778
|
+
undefined
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
expect(combined?.additional_kwargs.tool_outputs).toEqual([
|
|
782
|
+
streamedToolOutput,
|
|
783
|
+
]);
|
|
784
|
+
expect(combined?.response_metadata.output).toEqual([
|
|
785
|
+
streamedToolOutput,
|
|
786
|
+
authoritativeReplayOutput,
|
|
787
|
+
]);
|
|
788
|
+
expect(combined?.toJSON()).toEqual(
|
|
789
|
+
expect.objectContaining({
|
|
790
|
+
id: ['langchain_core', 'messages', 'AIMessageChunk'],
|
|
791
|
+
})
|
|
792
|
+
);
|
|
793
|
+
expect(
|
|
794
|
+
JSON.stringify(combined).match(/unique authoritative local output/g)
|
|
795
|
+
).toHaveLength(1);
|
|
796
|
+
expect(JSON.stringify(combined)).not.toContain(
|
|
797
|
+
'stale provisional local output'
|
|
798
|
+
);
|
|
799
|
+
expect(JSON.stringify(combined?.lc_kwargs)).not.toContain(
|
|
800
|
+
'stale provisional local output'
|
|
801
|
+
);
|
|
802
|
+
expect(combined?.lc_kwargs.additional_kwargs).toEqual(
|
|
803
|
+
expect.objectContaining({ tool_outputs: [streamedToolOutput] })
|
|
804
|
+
);
|
|
805
|
+
expect(combined?.additional_kwargs).not.toHaveProperty(
|
|
806
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
|
|
807
|
+
);
|
|
808
|
+
expect(combined?.lc_kwargs.additional_kwargs).not.toHaveProperty(
|
|
809
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
|
|
810
|
+
);
|
|
811
|
+
expect(
|
|
812
|
+
JSON.stringify(tracedResult).match(/unique authoritative local output/g)
|
|
813
|
+
).toHaveLength(1);
|
|
814
|
+
expect(JSON.stringify(tracedResult)).not.toContain(
|
|
815
|
+
'stale provisional local output'
|
|
816
|
+
);
|
|
817
|
+
expect(JSON.stringify(tracedResult)).not.toContain(
|
|
818
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
it('removes replay positions on a text-only terminal response', async () => {
|
|
824
|
+
const model = new ChatOpenAI({
|
|
825
|
+
model: 'gpt-5',
|
|
826
|
+
apiKey: 'test-key',
|
|
827
|
+
useResponsesApi: true,
|
|
828
|
+
});
|
|
829
|
+
const responses = (
|
|
830
|
+
model as unknown as { responses: StreamingOpenAIResponsesDelegate }
|
|
831
|
+
).responses;
|
|
832
|
+
const messageItem = {
|
|
833
|
+
id: 'msg_terminal',
|
|
834
|
+
type: 'message',
|
|
835
|
+
role: 'assistant',
|
|
836
|
+
status: 'completed',
|
|
837
|
+
content: [
|
|
838
|
+
{
|
|
839
|
+
type: 'output_text',
|
|
840
|
+
text: 'Complete answer.',
|
|
841
|
+
annotations: [],
|
|
842
|
+
logprobs: [],
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
} as const;
|
|
846
|
+
responses.completionWithRetry = async () =>
|
|
847
|
+
(async function* () {
|
|
848
|
+
yield {
|
|
849
|
+
type: 'response.output_text.delta',
|
|
850
|
+
sequence_number: 0,
|
|
851
|
+
output_index: 0,
|
|
852
|
+
content_index: 0,
|
|
853
|
+
item_id: messageItem.id,
|
|
854
|
+
delta: messageItem.content[0].text,
|
|
855
|
+
logprobs: [],
|
|
856
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
857
|
+
yield {
|
|
858
|
+
type: 'response.completed',
|
|
859
|
+
sequence_number: 1,
|
|
860
|
+
response: {
|
|
861
|
+
id: 'resp_text_terminal',
|
|
862
|
+
model: 'gpt-5',
|
|
863
|
+
output: [messageItem],
|
|
864
|
+
status: 'completed',
|
|
865
|
+
usage: null,
|
|
866
|
+
},
|
|
867
|
+
} as unknown as OpenAIClient.Responses.ResponseStreamEvent;
|
|
868
|
+
})();
|
|
869
|
+
|
|
870
|
+
let combined: AIMessageChunk | undefined;
|
|
871
|
+
const stream = await model.stream([new HumanMessage('answer')]);
|
|
872
|
+
for await (const chunk of stream) {
|
|
873
|
+
combined =
|
|
874
|
+
combined == null
|
|
875
|
+
? (chunk as AIMessageChunk)
|
|
876
|
+
: (combined.concat(chunk as AIMessageChunk) as AIMessageChunk);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
expect(combined?.text).toBe('Complete answer.');
|
|
880
|
+
expect(combined?.response_metadata.output).toEqual([messageItem]);
|
|
881
|
+
expect(combined?.additional_kwargs).not.toHaveProperty(
|
|
882
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
|
|
883
|
+
);
|
|
884
|
+
expect(combined?.lc_kwargs.additional_kwargs).not.toHaveProperty(
|
|
885
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('keeps a raw server result between distinct Responses output messages', async () => {
|
|
890
|
+
const model = new ChatOpenAI({
|
|
891
|
+
model: 'gpt-5',
|
|
892
|
+
apiKey: 'test-key',
|
|
893
|
+
useResponsesApi: true,
|
|
894
|
+
});
|
|
895
|
+
const responses = (
|
|
896
|
+
model as unknown as { responses: StreamingOpenAIResponsesDelegate }
|
|
897
|
+
).responses;
|
|
898
|
+
responses.completionWithRetry = async () =>
|
|
899
|
+
(async function* () {
|
|
900
|
+
yield {
|
|
901
|
+
type: 'response.output_text.delta',
|
|
902
|
+
sequence_number: 0,
|
|
903
|
+
output_index: 0,
|
|
904
|
+
content_index: 0,
|
|
905
|
+
item_id: 'msg_first',
|
|
906
|
+
delta: 'First narration.',
|
|
907
|
+
logprobs: [],
|
|
908
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
909
|
+
yield {
|
|
910
|
+
type: 'response.output_item.done',
|
|
911
|
+
sequence_number: 1,
|
|
912
|
+
output_index: 1,
|
|
913
|
+
item: {
|
|
914
|
+
id: 'local_output_item',
|
|
915
|
+
type: 'local_shell_call_output',
|
|
916
|
+
status: 'completed',
|
|
917
|
+
output: 'middle server result',
|
|
918
|
+
},
|
|
919
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
920
|
+
yield {
|
|
921
|
+
type: 'response.output_text.delta',
|
|
922
|
+
sequence_number: 2,
|
|
923
|
+
output_index: 2,
|
|
924
|
+
content_index: 0,
|
|
925
|
+
item_id: 'msg_second',
|
|
926
|
+
delta: 'Second narration.',
|
|
927
|
+
logprobs: [],
|
|
928
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
929
|
+
})();
|
|
930
|
+
|
|
931
|
+
let combined: AIMessageChunk | undefined;
|
|
932
|
+
const stream = await model.stream([new HumanMessage('run a server tool')]);
|
|
933
|
+
for await (const chunk of stream) {
|
|
934
|
+
combined =
|
|
935
|
+
combined == null
|
|
936
|
+
? (chunk as AIMessageChunk)
|
|
937
|
+
: (combined.concat(chunk as AIMessageChunk) as AIMessageChunk);
|
|
938
|
+
}
|
|
939
|
+
expect(combined).toBeDefined();
|
|
940
|
+
combined!.response_metadata.preempted = true;
|
|
941
|
+
|
|
942
|
+
const [projected] = projectOpenAIResponsesToolMessageContent([combined!]);
|
|
943
|
+
const providerInput = convertMessagesToResponsesInput({
|
|
944
|
+
messages: [projected],
|
|
945
|
+
model: 'gpt-5',
|
|
946
|
+
zdrEnabled: false,
|
|
947
|
+
});
|
|
948
|
+
const serialized = JSON.stringify(providerInput);
|
|
949
|
+
const firstIndex = serialized.indexOf('First narration.');
|
|
950
|
+
const resultIndex = serialized.indexOf('middle server result');
|
|
951
|
+
const secondIndex = serialized.indexOf('Second narration.');
|
|
952
|
+
|
|
953
|
+
expect(firstIndex).toBeGreaterThanOrEqual(0);
|
|
954
|
+
expect(firstIndex).toBeLessThan(resultIndex);
|
|
955
|
+
expect(resultIndex).toBeLessThan(secondIndex);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('keeps streamed generated images and server results in cross-item order', async () => {
|
|
959
|
+
const model = new ChatOpenAI({
|
|
960
|
+
model: 'gpt-5',
|
|
961
|
+
apiKey: 'test-key',
|
|
962
|
+
useResponsesApi: true,
|
|
963
|
+
});
|
|
964
|
+
const responses = (
|
|
965
|
+
model as unknown as { responses: StreamingOpenAIResponsesDelegate }
|
|
966
|
+
).responses;
|
|
967
|
+
responses.completionWithRetry = async () =>
|
|
968
|
+
(async function* () {
|
|
969
|
+
yield {
|
|
970
|
+
type: 'response.output_text.delta',
|
|
971
|
+
sequence_number: 0,
|
|
972
|
+
output_index: 0,
|
|
973
|
+
content_index: 0,
|
|
974
|
+
item_id: 'msg_first',
|
|
975
|
+
delta: 'First narration.',
|
|
976
|
+
logprobs: [],
|
|
977
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
978
|
+
yield {
|
|
979
|
+
type: 'response.output_item.done',
|
|
980
|
+
sequence_number: 1,
|
|
981
|
+
output_index: 1,
|
|
982
|
+
item: {
|
|
983
|
+
id: 'ig_middle',
|
|
984
|
+
type: 'image_generation_call',
|
|
985
|
+
status: 'completed',
|
|
986
|
+
result: 'AA==',
|
|
987
|
+
},
|
|
988
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
989
|
+
yield {
|
|
990
|
+
type: 'response.output_item.done',
|
|
991
|
+
sequence_number: 2,
|
|
992
|
+
output_index: 2,
|
|
993
|
+
item: {
|
|
994
|
+
id: 'local_middle',
|
|
995
|
+
type: 'local_shell_call_output',
|
|
996
|
+
status: 'completed',
|
|
997
|
+
output: 'middle server result',
|
|
998
|
+
},
|
|
999
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
1000
|
+
yield {
|
|
1001
|
+
type: 'response.output_item.added',
|
|
1002
|
+
sequence_number: 3,
|
|
1003
|
+
output_index: 3,
|
|
1004
|
+
item: {
|
|
1005
|
+
id: 'rs_middle',
|
|
1006
|
+
type: 'reasoning',
|
|
1007
|
+
status: 'in_progress',
|
|
1008
|
+
summary: [],
|
|
1009
|
+
},
|
|
1010
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
1011
|
+
yield {
|
|
1012
|
+
type: 'response.output_item.done',
|
|
1013
|
+
sequence_number: 4,
|
|
1014
|
+
output_index: 3,
|
|
1015
|
+
item: {
|
|
1016
|
+
id: 'rs_middle',
|
|
1017
|
+
type: 'reasoning',
|
|
1018
|
+
status: 'incomplete',
|
|
1019
|
+
summary: [],
|
|
1020
|
+
encrypted_content: 'opaque-middle-reasoning',
|
|
1021
|
+
},
|
|
1022
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
1023
|
+
yield {
|
|
1024
|
+
type: 'response.output_text.delta',
|
|
1025
|
+
sequence_number: 5,
|
|
1026
|
+
output_index: 4,
|
|
1027
|
+
content_index: 0,
|
|
1028
|
+
item_id: 'msg_second',
|
|
1029
|
+
delta: 'Second narration.',
|
|
1030
|
+
logprobs: [],
|
|
1031
|
+
} as OpenAIClient.Responses.ResponseStreamEvent;
|
|
1032
|
+
})();
|
|
1033
|
+
|
|
1034
|
+
let combined: AIMessageChunk | undefined;
|
|
1035
|
+
const stream = await model.stream([new HumanMessage('generate and run')]);
|
|
1036
|
+
for await (const chunk of stream) {
|
|
1037
|
+
combined =
|
|
1038
|
+
combined == null
|
|
1039
|
+
? (chunk as AIMessageChunk)
|
|
1040
|
+
: (combined.concat(chunk as AIMessageChunk) as AIMessageChunk);
|
|
1041
|
+
}
|
|
1042
|
+
expect(combined).toBeDefined();
|
|
1043
|
+
expect(combined!.additional_kwargs.reasoning).toEqual({
|
|
1044
|
+
id: 'rs_middle',
|
|
1045
|
+
type: 'reasoning',
|
|
1046
|
+
status: 'incomplete',
|
|
1047
|
+
summary: [],
|
|
1048
|
+
encrypted_content: 'opaque-middle-reasoning',
|
|
1049
|
+
});
|
|
1050
|
+
expect(
|
|
1051
|
+
combined!.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]
|
|
1052
|
+
).toEqual([
|
|
1053
|
+
{
|
|
1054
|
+
contentIndex: 0,
|
|
1055
|
+
itemId: 'msg_first',
|
|
1056
|
+
kind: 'text',
|
|
1057
|
+
outputIndex: 0,
|
|
1058
|
+
},
|
|
1059
|
+
{ itemId: 'ig_middle', kind: 'output', outputIndex: 1 },
|
|
1060
|
+
{ itemId: 'local_middle', kind: 'output', outputIndex: 2 },
|
|
1061
|
+
{ itemId: 'rs_middle', kind: 'reasoning', outputIndex: 3 },
|
|
1062
|
+
{
|
|
1063
|
+
contentIndex: 0,
|
|
1064
|
+
itemId: 'msg_second',
|
|
1065
|
+
kind: 'text',
|
|
1066
|
+
outputIndex: 4,
|
|
1067
|
+
},
|
|
1068
|
+
]);
|
|
1069
|
+
combined!.response_metadata.preempted = true;
|
|
1070
|
+
|
|
1071
|
+
const [projected] = projectOpenAIResponsesToolMessageContent([combined!]);
|
|
1072
|
+
const providerInput = convertMessagesToResponsesInput({
|
|
1073
|
+
messages: [projected],
|
|
1074
|
+
model: 'gpt-5',
|
|
1075
|
+
zdrEnabled: false,
|
|
1076
|
+
});
|
|
1077
|
+
const serialized = JSON.stringify(providerInput);
|
|
1078
|
+
const firstIndex = serialized.indexOf('First narration.');
|
|
1079
|
+
const imageIndex = serialized.indexOf('data:image/png;base64,AA==');
|
|
1080
|
+
const resultIndex = serialized.indexOf('middle server result');
|
|
1081
|
+
const reasoningIndex = serialized.indexOf('opaque-middle-reasoning');
|
|
1082
|
+
const secondIndex = serialized.indexOf('Second narration.');
|
|
1083
|
+
|
|
1084
|
+
expect(firstIndex).toBeGreaterThanOrEqual(0);
|
|
1085
|
+
expect(firstIndex).toBeLessThan(imageIndex);
|
|
1086
|
+
expect(imageIndex).toBeLessThan(resultIndex);
|
|
1087
|
+
expect(resultIndex).toBeLessThan(reasoningIndex);
|
|
1088
|
+
expect(reasoningIndex).toBeLessThan(secondIndex);
|
|
1089
|
+
expect(serialized).not.toContain('ig_middle');
|
|
1090
|
+
expect(serialized).not.toContain('local_middle');
|
|
1091
|
+
expect(projected.additional_kwargs).not.toHaveProperty(
|
|
1092
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
|
|
1093
|
+
);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
404
1096
|
it('keeps Azure client customization and gates reasoning to reasoning models', () => {
|
|
405
1097
|
const model = new AzureChatOpenAI({
|
|
406
1098
|
...baseAzureFields,
|
|
@@ -444,6 +1136,61 @@ describe('custom chat model class smoke tests', () => {
|
|
|
444
1136
|
expect(nonReasoningModel.getReasoningParams()).toBeUndefined();
|
|
445
1137
|
});
|
|
446
1138
|
|
|
1139
|
+
it('sanitizes persisted preempted history on direct Azure Responses streams', async () => {
|
|
1140
|
+
const model = new AzureChatOpenAI({
|
|
1141
|
+
...baseAzureFields,
|
|
1142
|
+
});
|
|
1143
|
+
model.model = 'gpt-5';
|
|
1144
|
+
const responses = (
|
|
1145
|
+
model as unknown as { responses: MockableResponsesDelegate }
|
|
1146
|
+
).responses;
|
|
1147
|
+
const requests: unknown[] = [];
|
|
1148
|
+
responses.completionWithRetry = async (request) => {
|
|
1149
|
+
requests.push(request);
|
|
1150
|
+
return (async function* () {})();
|
|
1151
|
+
};
|
|
1152
|
+
const message = createPersistedPreemptedAzureMessage();
|
|
1153
|
+
const originalSerialized = JSON.stringify(message.toJSON());
|
|
1154
|
+
|
|
1155
|
+
for await (const _chunk of responses._streamResponseChunks([message], {})) {
|
|
1156
|
+
// The empty mock stream intentionally yields no chunks.
|
|
1157
|
+
}
|
|
1158
|
+
for await (const _event of responses._streamChatModelEvents(
|
|
1159
|
+
[message],
|
|
1160
|
+
{}
|
|
1161
|
+
)) {
|
|
1162
|
+
// The empty mock stream intentionally yields no events.
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
expect(requests).toHaveLength(2);
|
|
1166
|
+
for (const request of requests) {
|
|
1167
|
+
expectNeutralizedAzureResponsesRequest(request);
|
|
1168
|
+
}
|
|
1169
|
+
expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it('sanitizes persisted preempted history on direct Azure Responses generation', async () => {
|
|
1173
|
+
const model = new AzureChatOpenAI({
|
|
1174
|
+
...baseAzureFields,
|
|
1175
|
+
});
|
|
1176
|
+
model.model = 'gpt-5';
|
|
1177
|
+
const responses = (
|
|
1178
|
+
model as unknown as { responses: MockableResponsesDelegate }
|
|
1179
|
+
).responses;
|
|
1180
|
+
let request: unknown;
|
|
1181
|
+
responses.completionWithRetry = async (nextRequest) => {
|
|
1182
|
+
request = nextRequest;
|
|
1183
|
+
return createCompletedAzureResponse();
|
|
1184
|
+
};
|
|
1185
|
+
const message = createPersistedPreemptedAzureMessage();
|
|
1186
|
+
const originalSerialized = JSON.stringify(message.toJSON());
|
|
1187
|
+
|
|
1188
|
+
await responses._generate([message], {});
|
|
1189
|
+
|
|
1190
|
+
expectNeutralizedAzureResponsesRequest(request);
|
|
1191
|
+
expect(JSON.stringify(message.toJSON())).toBe(originalSerialized);
|
|
1192
|
+
});
|
|
1193
|
+
|
|
447
1194
|
it('keeps DeepSeek, Moonshot, and xAI on LibreChat wrapper semantics', () => {
|
|
448
1195
|
const deepSeek = new ChatDeepSeek({
|
|
449
1196
|
model: 'deepseek-chat',
|