@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
package/src/llm/invoke.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type * as t from '@/types';
|
|
|
18
18
|
import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
|
|
19
19
|
import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
20
20
|
import { convertMessageContentToParts } from '@/llm/google/utils/common';
|
|
21
|
+
import { _convertMessagesToOpenAIParams } from '@/llm/openai/utils';
|
|
21
22
|
import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
|
|
22
23
|
import { toLangChainContent } from '@/messages/langchain';
|
|
23
24
|
import { Constants, Providers } from '@/common';
|
|
@@ -827,6 +828,103 @@ describe('tryFallbackProviders applies the same lazy annotation transform', () =
|
|
|
827
828
|
jest.dontMock('@/llm/init');
|
|
828
829
|
jest.resetModules();
|
|
829
830
|
});
|
|
831
|
+
|
|
832
|
+
it('neutralizes preempted Responses history before an OpenAI Chat fallback', async () => {
|
|
833
|
+
const reasoning = {
|
|
834
|
+
id: 'rs_fallback',
|
|
835
|
+
type: 'reasoning',
|
|
836
|
+
status: 'completed',
|
|
837
|
+
summary: [],
|
|
838
|
+
encrypted_content: 'opaque-fallback-reasoning',
|
|
839
|
+
};
|
|
840
|
+
const toolOutput = {
|
|
841
|
+
id: 'ci_fallback',
|
|
842
|
+
type: 'code_interpreter_call',
|
|
843
|
+
status: 'completed',
|
|
844
|
+
code: 'print("fallback result")',
|
|
845
|
+
outputs: [{ type: 'logs', logs: 'fallback result' }],
|
|
846
|
+
};
|
|
847
|
+
const translatedMessage = new AIMessage({
|
|
848
|
+
content: [{ type: 'text', text: 'Partial answer.' }],
|
|
849
|
+
additional_kwargs: {
|
|
850
|
+
reasoning,
|
|
851
|
+
tool_outputs: [toolOutput],
|
|
852
|
+
},
|
|
853
|
+
response_metadata: { model_provider: 'openai' },
|
|
854
|
+
});
|
|
855
|
+
const message = new AIMessage({
|
|
856
|
+
contentBlocks: [
|
|
857
|
+
...translatedMessage.contentBlocks,
|
|
858
|
+
{
|
|
859
|
+
type: 'non_standard',
|
|
860
|
+
value: {
|
|
861
|
+
id: 'rs_bare_fallback',
|
|
862
|
+
type: 'reasoning',
|
|
863
|
+
summary: [],
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
type: 'image',
|
|
868
|
+
mimeType: 'image/png',
|
|
869
|
+
data: 'AA==',
|
|
870
|
+
id: 'ig_fallback',
|
|
871
|
+
metadata: { status: 'completed' },
|
|
872
|
+
},
|
|
873
|
+
],
|
|
874
|
+
additional_kwargs: {
|
|
875
|
+
reasoning,
|
|
876
|
+
tool_outputs: [toolOutput],
|
|
877
|
+
},
|
|
878
|
+
response_metadata: {
|
|
879
|
+
model_provider: 'openai',
|
|
880
|
+
output_version: 'v1',
|
|
881
|
+
preempted: true,
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
const { invokeMessages, model } = buildCapturingModel();
|
|
885
|
+
model._useResponsesApi = () => false;
|
|
886
|
+
jest.doMock('@/llm/init', () => ({
|
|
887
|
+
initializeModel: (): unknown => model,
|
|
888
|
+
}));
|
|
889
|
+
jest.resetModules();
|
|
890
|
+
const { tryFallbackProviders: freshTry } = (await import(
|
|
891
|
+
'@/llm/invoke'
|
|
892
|
+
)) as { tryFallbackProviders: typeof tryFallbackProviders };
|
|
893
|
+
|
|
894
|
+
await freshTry({
|
|
895
|
+
fallbacks: [
|
|
896
|
+
{
|
|
897
|
+
provider: Providers.OPENAI,
|
|
898
|
+
clientOptions: { model: 'gpt-5.6' },
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
messages: [message],
|
|
902
|
+
primaryError: new Error('primary failed'),
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
const sent = invokeMessages[invokeMessages.length - 1][0] as AIMessage;
|
|
906
|
+
expect(sent).not.toBe(message);
|
|
907
|
+
expect(sent.content).toEqual([
|
|
908
|
+
{ type: 'text', text: 'Partial answer.' },
|
|
909
|
+
{ type: 'text', text: expect.stringContaining('fallback result') },
|
|
910
|
+
]);
|
|
911
|
+
expect(sent.additional_kwargs).toEqual({});
|
|
912
|
+
expect(JSON.stringify(sent.toJSON())).not.toMatch(/rs_|ci_|ig_/);
|
|
913
|
+
const chatPayload = _convertMessagesToOpenAIParams([sent], 'gpt-5.6');
|
|
914
|
+
expect(chatPayload).toEqual([
|
|
915
|
+
{
|
|
916
|
+
role: 'assistant',
|
|
917
|
+
content: sent.content,
|
|
918
|
+
},
|
|
919
|
+
]);
|
|
920
|
+
expect(JSON.stringify(chatPayload)).not.toMatch(/rs_|ci_|ig_/);
|
|
921
|
+
expect(JSON.stringify(message.toJSON())).toContain('rs_fallback');
|
|
922
|
+
expect(JSON.stringify(message.toJSON())).toContain('ci_fallback');
|
|
923
|
+
expect(JSON.stringify(message.toJSON())).toContain('ig_fallback');
|
|
924
|
+
|
|
925
|
+
jest.dontMock('@/llm/init');
|
|
926
|
+
jest.resetModules();
|
|
927
|
+
});
|
|
830
928
|
});
|
|
831
929
|
|
|
832
930
|
describe('invocation attribution metadata', () => {
|
package/src/llm/invoke.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { concat } from '@langchain/core/utils/stream';
|
|
2
2
|
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import { getCallbackManagerForConfig } from '@langchain/core/runnables';
|
|
3
4
|
import {
|
|
4
5
|
CallbackManager,
|
|
5
6
|
CallbackManagerForLLMRun,
|
|
6
7
|
type Callbacks,
|
|
7
8
|
} from '@langchain/core/callbacks/manager';
|
|
8
|
-
import { getCallbackManagerForConfig } from '@langchain/core/runnables';
|
|
9
9
|
import type { Serialized } from '@langchain/core/load/serializable';
|
|
10
|
-
import type { ChatGeneration } from '@langchain/core/outputs';
|
|
11
10
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
11
|
+
import type { ChatGeneration } from '@langchain/core/outputs';
|
|
12
12
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
13
13
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
14
14
|
import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
@@ -24,28 +24,28 @@ import {
|
|
|
24
24
|
projectStructuredToolOutputsToText,
|
|
25
25
|
projectToolStreamContentForProvider,
|
|
26
26
|
} from '@/messages/core';
|
|
27
|
+
import {
|
|
28
|
+
modifyDeltaProperties,
|
|
29
|
+
coalesceAdjacentUserTurns,
|
|
30
|
+
strictAlternationProviders,
|
|
31
|
+
appendPredecessorHandoffCue,
|
|
32
|
+
removePredecessorHandoffCue,
|
|
33
|
+
} from '@/messages';
|
|
27
34
|
import {
|
|
28
35
|
stripAnthropicCacheControl,
|
|
29
36
|
stripBedrockCacheControl,
|
|
30
37
|
} from '@/messages/cache';
|
|
38
|
+
import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
|
|
39
|
+
import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
|
|
31
40
|
import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
|
|
32
41
|
import { assertNotTruncatedToolCall } from '@/llm/truncation';
|
|
33
|
-
import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
|
|
34
42
|
import { manualToolStreamProviders } from '@/llm/providers';
|
|
35
|
-
import {
|
|
43
|
+
import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
|
|
36
44
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
37
45
|
import { getContextOverflowInfo } from '@/utils/errors';
|
|
38
|
-
import {
|
|
39
|
-
modifyDeltaProperties,
|
|
40
|
-
coalesceAdjacentUserTurns,
|
|
41
|
-
strictAlternationProviders,
|
|
42
|
-
appendPredecessorHandoffCue,
|
|
43
|
-
removePredecessorHandoffCue,
|
|
44
|
-
} from '@/messages';
|
|
46
|
+
import { appendCallbacks } from '@/utils/callbacks';
|
|
45
47
|
import { canSealPreempt } from '@/llm/preempt';
|
|
46
|
-
import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
|
|
47
48
|
import { initializeModel } from '@/llm/init';
|
|
48
|
-
import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Context passed to `attemptInvoke`. Matches the subset of Graph that
|
|
@@ -174,8 +174,17 @@ export function projectMessagesForProvider({
|
|
|
174
174
|
maxToolResultChars?: number;
|
|
175
175
|
callOptions?: unknown;
|
|
176
176
|
}): BaseMessage[] {
|
|
177
|
-
const
|
|
178
|
-
|
|
177
|
+
const nativeOpenAIResponses = usesNativeOpenAIResponses(
|
|
178
|
+
model,
|
|
179
|
+
provider,
|
|
180
|
+
callOptions
|
|
181
|
+
);
|
|
182
|
+
const providerInputMessages = projectToolStreamContentForProvider(
|
|
183
|
+
messages,
|
|
184
|
+
nativeOpenAIResponses ? 'native' : 'fallback',
|
|
185
|
+
maxToolResultChars
|
|
186
|
+
);
|
|
187
|
+
if (nativeOpenAIResponses) {
|
|
179
188
|
return projectOpenAIResponsesToolMessageContent(
|
|
180
189
|
stripAnthropicCacheControl(
|
|
181
190
|
stripBedrockCacheControl(providerInputMessages)
|
|
@@ -433,15 +442,19 @@ function synthesizeSealedUsage(
|
|
|
433
442
|
const inputTokens =
|
|
434
443
|
(countSealedTokens(context, metadata, prompt) ?? 0) +
|
|
435
444
|
sealedInstructionOverhead(context, metadata);
|
|
436
|
-
|
|
445
|
+
const usageMetadata = {
|
|
437
446
|
input_tokens: inputTokens,
|
|
438
447
|
output_tokens: outputTokens,
|
|
439
448
|
total_tokens: inputTokens + outputTokens,
|
|
440
449
|
};
|
|
441
|
-
chunk.
|
|
450
|
+
chunk.usage_metadata = usageMetadata;
|
|
451
|
+
chunk.lc_kwargs.usage_metadata = usageMetadata;
|
|
452
|
+
const responseMetadata = {
|
|
442
453
|
...chunk.response_metadata,
|
|
443
454
|
estimated_usage: true,
|
|
444
455
|
};
|
|
456
|
+
chunk.response_metadata = responseMetadata;
|
|
457
|
+
chunk.lc_kwargs.response_metadata = responseMetadata;
|
|
445
458
|
}
|
|
446
459
|
|
|
447
460
|
function getMessageText(chunk: AIMessageChunk): string {
|
|
@@ -677,11 +690,7 @@ export async function attemptInvoke(
|
|
|
677
690
|
});
|
|
678
691
|
const registry = context?.getOrCreateToolOutputRegistry();
|
|
679
692
|
const runId = config?.configurable?.run_id as string | undefined;
|
|
680
|
-
const annotated = annotateMessagesForLLM(
|
|
681
|
-
invocationMessages,
|
|
682
|
-
registry,
|
|
683
|
-
runId
|
|
684
|
-
);
|
|
693
|
+
const annotated = annotateMessagesForLLM(invocationMessages, registry, runId);
|
|
685
694
|
/**
|
|
686
695
|
* Keyed on the provider ACTUALLY serving this call, not the agent's primary.
|
|
687
696
|
* `createCallModel` normalizes for the primary, but `tryFallbackProviders`
|
|
@@ -860,10 +869,12 @@ export async function attemptInvoke(
|
|
|
860
869
|
}
|
|
861
870
|
|
|
862
871
|
if (preempted && finalChunk != null) {
|
|
863
|
-
|
|
872
|
+
const responseMetadata = {
|
|
864
873
|
...finalChunk.response_metadata,
|
|
865
874
|
preempted: true,
|
|
866
875
|
};
|
|
876
|
+
finalChunk.response_metadata = responseMetadata;
|
|
877
|
+
finalChunk.lc_kwargs.response_metadata = responseMetadata;
|
|
867
878
|
await endSealedModelRun(
|
|
868
879
|
context,
|
|
869
880
|
finalChunk,
|
package/src/llm/openai/index.ts
CHANGED
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
getEndpoint,
|
|
18
18
|
OpenAIClient,
|
|
19
|
+
wrapOpenAIClientError,
|
|
19
20
|
getHeadersWithUserAgent,
|
|
21
|
+
convertMessagesToResponsesInput,
|
|
22
|
+
convertResponsesDeltaToChatGenerationChunk,
|
|
20
23
|
ChatOpenAI as OriginalChatOpenAI,
|
|
21
24
|
ChatOpenAIResponses as OriginalChatOpenAIResponses,
|
|
22
25
|
ChatOpenAICompletions as OriginalChatOpenAICompletions,
|
|
@@ -34,9 +37,15 @@ import type { BindToolsInput } from '@langchain/core/language_models/chat_models
|
|
|
34
37
|
import type { ChatGeneration, ChatResult } from '@langchain/core/outputs';
|
|
35
38
|
import type { ChatXAIInput } from '@langchain/xai';
|
|
36
39
|
import type * as t from '@langchain/openai';
|
|
40
|
+
import type { ResponsesReplayPosition } from '@/messages/core';
|
|
37
41
|
import type { SeenScalarMetadata } from './streamMetadata';
|
|
38
42
|
import type { HeaderValue, HeadersLike } from './types';
|
|
39
43
|
import type { PromptCacheTtl } from '@/messages/cache';
|
|
44
|
+
import {
|
|
45
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY,
|
|
46
|
+
projectOpenAIResponsesToolMessageContent,
|
|
47
|
+
projectToolStreamContentForProvider,
|
|
48
|
+
} from '@/messages/core';
|
|
40
49
|
import {
|
|
41
50
|
buildAnthropicCacheControl,
|
|
42
51
|
resolvePromptCacheTtl,
|
|
@@ -47,12 +56,8 @@ import {
|
|
|
47
56
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
48
57
|
OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
|
|
49
58
|
} from '@/tools/streamedToolCallSeals';
|
|
50
|
-
import {
|
|
51
|
-
projectOpenAIResponsesToolMessageContent,
|
|
52
|
-
projectToolStreamContentForProvider,
|
|
53
|
-
} from '@/messages/core';
|
|
54
|
-
import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
|
|
55
59
|
import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
|
|
60
|
+
import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
|
|
56
61
|
import { dropRepeatedScalarMetadata } from './streamMetadata';
|
|
57
62
|
|
|
58
63
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
@@ -224,6 +229,10 @@ type ResponsesRequest =
|
|
|
224
229
|
type ResponsesResult =
|
|
225
230
|
| AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>
|
|
226
231
|
| OpenAIClient.Responses.Response;
|
|
232
|
+
type ResponsesStreamChunkOptions = {
|
|
233
|
+
promptIndex?: number;
|
|
234
|
+
signal?: AbortSignal;
|
|
235
|
+
};
|
|
227
236
|
type CacheableChatPart = {
|
|
228
237
|
type: 'text' | 'image_url' | 'input_audio' | 'file' | 'refusal';
|
|
229
238
|
prompt_cache_breakpoint?: { mode: 'explicit' };
|
|
@@ -386,6 +395,10 @@ function isResponseMessage(
|
|
|
386
395
|
return item.type === 'message';
|
|
387
396
|
}
|
|
388
397
|
|
|
398
|
+
function isResponseInputRole(role: string): boolean {
|
|
399
|
+
return role === 'system' || role === 'developer' || role === 'user';
|
|
400
|
+
}
|
|
401
|
+
|
|
389
402
|
/** Only `input_text`/`input_image`/`input_file` accept a Responses breakpoint;
|
|
390
403
|
* `output_text`/`refusal` (replayed assistant blocks) are rejected with a 400. */
|
|
391
404
|
function isCacheableResponsePart(part: unknown): part is CacheableResponsePart {
|
|
@@ -459,11 +472,7 @@ export function addResponseCacheBreakpoints(
|
|
|
459
472
|
/** Only input roles take a Responses breakpoint. Assistant/tool turns
|
|
460
473
|
* carry output content (string or output_text) that the API rejects
|
|
461
474
|
* under an input marker, so they're never eligible. */
|
|
462
|
-
if (
|
|
463
|
-
item.role !== 'system' &&
|
|
464
|
-
item.role !== 'developer' &&
|
|
465
|
-
item.role !== 'user'
|
|
466
|
-
) {
|
|
475
|
+
if (!isResponseInputRole(item.role)) {
|
|
467
476
|
return false;
|
|
468
477
|
}
|
|
469
478
|
const content = item.content as
|
|
@@ -568,6 +577,278 @@ function isResponsesStream(
|
|
|
568
577
|
return Symbol.asyncIterator in result;
|
|
569
578
|
}
|
|
570
579
|
|
|
580
|
+
const RESPONSES_REPLAY_OUTPUT_ITEM_TYPES = new Set([
|
|
581
|
+
'local_shell_call_output',
|
|
582
|
+
'shell_call_output',
|
|
583
|
+
'apply_patch_call_output',
|
|
584
|
+
'program_output',
|
|
585
|
+
]);
|
|
586
|
+
|
|
587
|
+
function isResponsesReplayOutputItem(item: unknown): boolean {
|
|
588
|
+
return (
|
|
589
|
+
typeof item === 'object' &&
|
|
590
|
+
item != null &&
|
|
591
|
+
'type' in item &&
|
|
592
|
+
typeof item.type === 'string' &&
|
|
593
|
+
RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(item.type)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* LangChain's Responses converter places the authoritative terminal output in
|
|
599
|
+
* response_metadata.output. Its chunk merge has no way to delete provisional
|
|
600
|
+
* tool_outputs or replay-position sidecars, so remove those preemption-only
|
|
601
|
+
* captures once that terminal output arrives. An interrupted stream has no
|
|
602
|
+
* terminal chunk and keeps the captures for replay.
|
|
603
|
+
*/
|
|
604
|
+
class ResponsesReplayAIMessageChunk extends AIMessageChunk {
|
|
605
|
+
override get lc_id(): string[] {
|
|
606
|
+
return [...this.lc_namespace, AIMessageChunk.lc_name()];
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
override concat(chunk: AIMessageChunk): this {
|
|
610
|
+
const combined = super.concat(chunk);
|
|
611
|
+
if (!Array.isArray(chunk.response_metadata.output)) {
|
|
612
|
+
return combined;
|
|
613
|
+
}
|
|
614
|
+
delete combined.additional_kwargs[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY];
|
|
615
|
+
const toolOutputs = combined.additional_kwargs.tool_outputs;
|
|
616
|
+
if (!Array.isArray(toolOutputs)) {
|
|
617
|
+
return combined;
|
|
618
|
+
}
|
|
619
|
+
const retainedToolOutputs = toolOutputs.filter(
|
|
620
|
+
(item) => !isResponsesReplayOutputItem(item)
|
|
621
|
+
);
|
|
622
|
+
if (retainedToolOutputs.length === toolOutputs.length) {
|
|
623
|
+
return combined;
|
|
624
|
+
}
|
|
625
|
+
if (retainedToolOutputs.length > 0) {
|
|
626
|
+
combined.additional_kwargs.tool_outputs = retainedToolOutputs;
|
|
627
|
+
} else {
|
|
628
|
+
delete combined.additional_kwargs.tool_outputs;
|
|
629
|
+
}
|
|
630
|
+
return combined;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function makeResponsesReplayAggregationSafe(
|
|
635
|
+
chunk: ChatGenerationChunk
|
|
636
|
+
): ChatGenerationChunk {
|
|
637
|
+
if (!AIMessageChunk.isInstance(chunk.message)) {
|
|
638
|
+
return chunk;
|
|
639
|
+
}
|
|
640
|
+
const message = chunk.message;
|
|
641
|
+
chunk.message = new ResponsesReplayAIMessageChunk({
|
|
642
|
+
id: message.id,
|
|
643
|
+
name: message.name,
|
|
644
|
+
content: message.content,
|
|
645
|
+
additional_kwargs: message.additional_kwargs,
|
|
646
|
+
response_metadata: message.response_metadata,
|
|
647
|
+
tool_calls: message.tool_calls,
|
|
648
|
+
invalid_tool_calls: message.invalid_tool_calls,
|
|
649
|
+
tool_call_chunks: message.tool_call_chunks,
|
|
650
|
+
usage_metadata: message.usage_metadata,
|
|
651
|
+
});
|
|
652
|
+
return chunk;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function remapResponsesTextBlockIndex(
|
|
656
|
+
chunk: ChatGenerationChunk,
|
|
657
|
+
event: OpenAIClient.Responses.ResponseStreamEvent,
|
|
658
|
+
textBlockIndices: Map<string, number>
|
|
659
|
+
): void {
|
|
660
|
+
const position = iife(() => {
|
|
661
|
+
if (
|
|
662
|
+
event.type === 'response.output_text.delta' ||
|
|
663
|
+
event.type === 'response.output_text.annotation.added'
|
|
664
|
+
) {
|
|
665
|
+
return {
|
|
666
|
+
contentIndex: event.content_index,
|
|
667
|
+
outputIndex: event.output_index,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (
|
|
671
|
+
event.type === 'response.output_item.added' &&
|
|
672
|
+
event.item.type === 'message'
|
|
673
|
+
) {
|
|
674
|
+
return { contentIndex: 0, outputIndex: event.output_index };
|
|
675
|
+
}
|
|
676
|
+
return undefined;
|
|
677
|
+
});
|
|
678
|
+
if (position == null || !Array.isArray(chunk.message.content)) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const key = `${position.outputIndex}:${position.contentIndex}`;
|
|
682
|
+
let blockIndex = textBlockIndices.get(key);
|
|
683
|
+
if (blockIndex == null) {
|
|
684
|
+
blockIndex = textBlockIndices.size;
|
|
685
|
+
textBlockIndices.set(key, blockIndex);
|
|
686
|
+
}
|
|
687
|
+
const content = chunk.message.content.map((block) =>
|
|
688
|
+
typeof block === 'object' && block.type === 'text'
|
|
689
|
+
? { ...block, index: blockIndex }
|
|
690
|
+
: block
|
|
691
|
+
);
|
|
692
|
+
chunk.message.content = content;
|
|
693
|
+
chunk.message.lc_kwargs.content = content;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function convertDroppedResponsesReplayOutput(
|
|
697
|
+
event: OpenAIClient.Responses.ResponseStreamEvent
|
|
698
|
+
): ChatGenerationChunk | null {
|
|
699
|
+
if (event.type !== 'response.output_item.done') {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
if (event.item.type === 'reasoning') {
|
|
703
|
+
// Added/summary events already stream id, type, and summary. Only merge
|
|
704
|
+
// terminal fields here so chunk concatenation does not duplicate summary.
|
|
705
|
+
return new ChatGenerationChunk({
|
|
706
|
+
text: '',
|
|
707
|
+
message: new AIMessageChunk({
|
|
708
|
+
content: [],
|
|
709
|
+
additional_kwargs: {
|
|
710
|
+
reasoning: {
|
|
711
|
+
status: event.item.status,
|
|
712
|
+
...(typeof event.item.encrypted_content === 'string'
|
|
713
|
+
? { encrypted_content: event.item.encrypted_content }
|
|
714
|
+
: {}),
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
response_metadata: { model_provider: 'openai' },
|
|
718
|
+
}),
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
if (!RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(event.item.type)) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
return new ChatGenerationChunk({
|
|
725
|
+
text: '',
|
|
726
|
+
message: new AIMessageChunk({
|
|
727
|
+
content: [],
|
|
728
|
+
additional_kwargs: { tool_outputs: [event.item] },
|
|
729
|
+
response_metadata: { model_provider: 'openai' },
|
|
730
|
+
}),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function attachResponsesReplayPosition(
|
|
735
|
+
chunk: ChatGenerationChunk,
|
|
736
|
+
event: OpenAIClient.Responses.ResponseStreamEvent,
|
|
737
|
+
seenPositions: Set<string>
|
|
738
|
+
): void {
|
|
739
|
+
let position: ResponsesReplayPosition | undefined;
|
|
740
|
+
if (event.type === 'response.output_text.delta' && event.delta.length > 0) {
|
|
741
|
+
position = {
|
|
742
|
+
contentIndex: event.content_index,
|
|
743
|
+
itemId: event.item_id,
|
|
744
|
+
kind: 'text',
|
|
745
|
+
outputIndex: event.output_index,
|
|
746
|
+
};
|
|
747
|
+
} else if (
|
|
748
|
+
event.type === 'response.output_item.added' &&
|
|
749
|
+
event.item.type === 'message' &&
|
|
750
|
+
typeof event.item.id === 'string' &&
|
|
751
|
+
event.item.id.length > 0
|
|
752
|
+
) {
|
|
753
|
+
position = {
|
|
754
|
+
itemId: event.item.id,
|
|
755
|
+
kind: 'message',
|
|
756
|
+
outputIndex: event.output_index,
|
|
757
|
+
};
|
|
758
|
+
} else if (
|
|
759
|
+
event.type === 'response.output_item.added' &&
|
|
760
|
+
event.item.type === 'reasoning' &&
|
|
761
|
+
typeof event.item.id === 'string' &&
|
|
762
|
+
event.item.id.length > 0
|
|
763
|
+
) {
|
|
764
|
+
position = {
|
|
765
|
+
itemId: event.item.id,
|
|
766
|
+
kind: 'reasoning',
|
|
767
|
+
outputIndex: event.output_index,
|
|
768
|
+
};
|
|
769
|
+
} else if (
|
|
770
|
+
event.type === 'response.output_item.done' &&
|
|
771
|
+
(RESPONSES_REPLAY_OUTPUT_ITEM_TYPES.has(event.item.type) ||
|
|
772
|
+
Array.isArray(chunk.message.additional_kwargs.tool_outputs))
|
|
773
|
+
) {
|
|
774
|
+
let itemId: string | undefined;
|
|
775
|
+
if (typeof event.item.id === 'string' && event.item.id.length > 0) {
|
|
776
|
+
itemId = event.item.id;
|
|
777
|
+
} else if (
|
|
778
|
+
'call_id' in event.item &&
|
|
779
|
+
typeof event.item.call_id === 'string' &&
|
|
780
|
+
event.item.call_id.length > 0
|
|
781
|
+
) {
|
|
782
|
+
itemId = event.item.call_id;
|
|
783
|
+
}
|
|
784
|
+
if (itemId != null) {
|
|
785
|
+
position = {
|
|
786
|
+
itemId,
|
|
787
|
+
kind: 'output',
|
|
788
|
+
outputIndex: event.output_index,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (position == null) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const positionKey = `${position.kind}:${position.itemId}:${position.outputIndex}:${position.contentIndex ?? ''}`;
|
|
796
|
+
if (seenPositions.has(positionKey)) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
seenPositions.add(positionKey);
|
|
800
|
+
const existing = chunk.message.additional_kwargs[
|
|
801
|
+
OPENAI_RESPONSES_REPLAY_POSITIONS_KEY
|
|
802
|
+
] as unknown;
|
|
803
|
+
const additionalKwargs = {
|
|
804
|
+
...chunk.message.additional_kwargs,
|
|
805
|
+
[OPENAI_RESPONSES_REPLAY_POSITIONS_KEY]: [
|
|
806
|
+
...(Array.isArray(existing) ? existing : []),
|
|
807
|
+
position,
|
|
808
|
+
],
|
|
809
|
+
};
|
|
810
|
+
chunk.message.additional_kwargs = additionalKwargs;
|
|
811
|
+
chunk.message.lc_kwargs.additional_kwargs = additionalKwargs;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function* convertLibreChatResponsesStream(
|
|
815
|
+
stream: AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>,
|
|
816
|
+
options: ResponsesStreamChunkOptions,
|
|
817
|
+
runManager?: CallbackManagerForLLMRun
|
|
818
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
819
|
+
const seenReplayPositions = new Set<string>();
|
|
820
|
+
const responsesTextBlockIndices = new Map<string, number>();
|
|
821
|
+
try {
|
|
822
|
+
for await (const event of stream) {
|
|
823
|
+
options.signal?.throwIfAborted();
|
|
824
|
+
const convertedChunk =
|
|
825
|
+
convertResponsesDeltaToChatGenerationChunk(event) ??
|
|
826
|
+
convertDroppedResponsesReplayOutput(event);
|
|
827
|
+
if (convertedChunk == null) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
const chunk = makeResponsesReplayAggregationSafe(convertedChunk);
|
|
831
|
+
remapResponsesTextBlockIndex(chunk, event, responsesTextBlockIndices);
|
|
832
|
+
attachResponsesReplayPosition(chunk, event, seenReplayPositions);
|
|
833
|
+
attachCacheWriteUsage(chunk.message);
|
|
834
|
+
await runManager?.handleLLMNewToken(
|
|
835
|
+
chunk.text || '',
|
|
836
|
+
{
|
|
837
|
+
prompt: options.promptIndex ?? 0,
|
|
838
|
+
completion: 0,
|
|
839
|
+
},
|
|
840
|
+
undefined,
|
|
841
|
+
undefined,
|
|
842
|
+
undefined,
|
|
843
|
+
{ chunk }
|
|
844
|
+
);
|
|
845
|
+
yield chunk;
|
|
846
|
+
}
|
|
847
|
+
} catch (e) {
|
|
848
|
+
throw wrapOpenAIClientError(e);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
571
852
|
function createUsageMetadata(
|
|
572
853
|
usage?: OpenAIClient.Completions.CompletionUsage
|
|
573
854
|
): UsageMetadata {
|
|
@@ -1754,14 +2035,20 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
|
|
|
1754
2035
|
options: this['ParsedCallOptions'],
|
|
1755
2036
|
runManager?: CallbackManagerForLLMRun
|
|
1756
2037
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
2038
|
+
const projectedMessages = projectOpenAIResponsesProviderMessages(messages);
|
|
2039
|
+
const stream = await this.completionWithRetry(
|
|
2040
|
+
{
|
|
2041
|
+
...this.invocationParams(options),
|
|
2042
|
+
input: convertMessagesToResponsesInput({
|
|
2043
|
+
messages: projectedMessages,
|
|
2044
|
+
zdrEnabled: this.zdrEnabled ?? false,
|
|
2045
|
+
model: this.model,
|
|
2046
|
+
}),
|
|
2047
|
+
stream: true,
|
|
2048
|
+
},
|
|
2049
|
+
options
|
|
2050
|
+
);
|
|
2051
|
+
yield* convertLibreChatResponsesStream(stream, options, runManager);
|
|
1765
2052
|
}
|
|
1766
2053
|
|
|
1767
2054
|
async *_streamChatModelEvents(
|
|
@@ -1988,7 +2275,11 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
|
|
|
1988
2275
|
options: this['ParsedCallOptions'],
|
|
1989
2276
|
runManager?: CallbackManagerForLLMRun
|
|
1990
2277
|
): Promise<ChatResult> {
|
|
1991
|
-
const result = await super._generate(
|
|
2278
|
+
const result = await super._generate(
|
|
2279
|
+
projectOpenAIResponsesProviderMessages(messages),
|
|
2280
|
+
options,
|
|
2281
|
+
runManager
|
|
2282
|
+
);
|
|
1992
2283
|
for (const generation of result.generations) {
|
|
1993
2284
|
attachCacheWriteUsage(generation.message);
|
|
1994
2285
|
}
|
|
@@ -2000,14 +2291,32 @@ class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
|
|
|
2000
2291
|
options: this['ParsedCallOptions'],
|
|
2001
2292
|
runManager?: CallbackManagerForLLMRun
|
|
2002
2293
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
2003
|
-
|
|
2004
|
-
|
|
2294
|
+
const projectedMessages = projectOpenAIResponsesProviderMessages(messages);
|
|
2295
|
+
const stream = await this.completionWithRetry(
|
|
2296
|
+
{
|
|
2297
|
+
...this.invocationParams(options),
|
|
2298
|
+
input: convertMessagesToResponsesInput({
|
|
2299
|
+
messages: projectedMessages,
|
|
2300
|
+
zdrEnabled: this.zdrEnabled ?? false,
|
|
2301
|
+
model: this.model,
|
|
2302
|
+
}),
|
|
2303
|
+
stream: true,
|
|
2304
|
+
},
|
|
2305
|
+
options
|
|
2306
|
+
);
|
|
2307
|
+
yield* convertLibreChatResponsesStream(stream, options, runManager);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
async *_streamChatModelEvents(
|
|
2311
|
+
messages: BaseMessage[],
|
|
2312
|
+
options: this['ParsedCallOptions'],
|
|
2313
|
+
runManager?: CallbackManagerForLLMRun
|
|
2314
|
+
): AsyncGenerator<ChatModelStreamEvent> {
|
|
2315
|
+
yield* super._streamChatModelEvents(
|
|
2316
|
+
projectOpenAIResponsesProviderMessages(messages),
|
|
2005
2317
|
options,
|
|
2006
2318
|
runManager
|
|
2007
|
-
)
|
|
2008
|
-
attachCacheWriteUsage(chunk.message);
|
|
2009
|
-
yield chunk;
|
|
2010
|
-
}
|
|
2319
|
+
);
|
|
2011
2320
|
}
|
|
2012
2321
|
|
|
2013
2322
|
protected _getReasoningParams(
|