@librechat/agents 3.1.85 → 3.1.87
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/agents/AgentContext.cjs +7 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- 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 +472 -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/agents/AgentContext.mjs +7 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- 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 +473 -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 +2458 -0
- package/src/agents/AgentContext.ts +7 -2
- package/src/agents/__tests__/AgentContext.test.ts +254 -5
- 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 +755 -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
|
@@ -3,13 +3,15 @@ import {
|
|
|
3
3
|
AIMessageChunk,
|
|
4
4
|
HumanMessage,
|
|
5
5
|
} from '@langchain/core/messages';
|
|
6
|
+
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
6
7
|
import type { OpenAIChatInput, OpenAIClient } from '@langchain/openai';
|
|
7
|
-
import type {
|
|
8
|
-
import type { CustomAnthropicInput } from '@/llm/anthropic';
|
|
8
|
+
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
9
9
|
import type {
|
|
10
10
|
ChatAnthropicToolType,
|
|
11
11
|
AnthropicMCPServerURLDefinition,
|
|
12
12
|
} from '@/llm/anthropic/types';
|
|
13
|
+
import type { ChatOpenRouterCallOptions } from '@/llm/openrouter';
|
|
14
|
+
import type { CustomAnthropicInput } from '@/llm/anthropic';
|
|
13
15
|
import {
|
|
14
16
|
ChatXAI,
|
|
15
17
|
ChatOpenAI,
|
|
@@ -80,6 +82,19 @@ type OpenAIStreamEvent = {
|
|
|
80
82
|
type OpenAIStreamItem =
|
|
81
83
|
| OpenAIClient.Chat.Completions.ChatCompletionChunk
|
|
82
84
|
| OpenAIStreamEvent;
|
|
85
|
+
type FetchOutcome = 'resolved' | 'rejected' | 'pending';
|
|
86
|
+
type AbortableFetchCapture = {
|
|
87
|
+
fetch: typeof fetch;
|
|
88
|
+
getSignal: () => AbortSignal | undefined;
|
|
89
|
+
};
|
|
90
|
+
type FetchTimeoutClient = {
|
|
91
|
+
fetchWithTimeout: (
|
|
92
|
+
url: RequestInfo,
|
|
93
|
+
init: RequestInit | undefined,
|
|
94
|
+
ms: number,
|
|
95
|
+
controller: AbortController
|
|
96
|
+
) => Promise<Response>;
|
|
97
|
+
};
|
|
83
98
|
type MockableCompletionCreate = (
|
|
84
99
|
request: unknown,
|
|
85
100
|
options?: unknown
|
|
@@ -133,6 +148,18 @@ type CompletionUsageWithCacheWrite = Omit<
|
|
|
133
148
|
};
|
|
134
149
|
type OpenAIStreamModel = ChatOpenAI | AzureChatOpenAI;
|
|
135
150
|
|
|
151
|
+
class CallbackTestChatOpenRouter extends ChatOpenRouter {
|
|
152
|
+
streamChunksWithCallbacks(
|
|
153
|
+
runManager?: CallbackManagerForLLMRun
|
|
154
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
155
|
+
return this._streamResponseChunks(
|
|
156
|
+
[new HumanMessage('hi')],
|
|
157
|
+
{} as this['ParsedCallOptions'],
|
|
158
|
+
runManager
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
const baseAzureFields = {
|
|
137
164
|
azureOpenAIApiKey: 'test-azure-key',
|
|
138
165
|
azureOpenAIApiVersion: '2024-10-21',
|
|
@@ -140,6 +167,55 @@ const baseAzureFields = {
|
|
|
140
167
|
azureOpenAIApiDeploymentName: 'test-deployment',
|
|
141
168
|
};
|
|
142
169
|
|
|
170
|
+
const waitForFetchOutcome = (
|
|
171
|
+
promise: Promise<Response>,
|
|
172
|
+
timeoutMs = 100
|
|
173
|
+
): Promise<FetchOutcome> =>
|
|
174
|
+
Promise.race([
|
|
175
|
+
promise.then(
|
|
176
|
+
() => 'resolved' as const,
|
|
177
|
+
() => 'rejected' as const
|
|
178
|
+
),
|
|
179
|
+
new Promise<'pending'>((resolve) => {
|
|
180
|
+
setTimeout(() => resolve('pending'), timeoutMs);
|
|
181
|
+
}),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const createAbortableFetch = (): AbortableFetchCapture => {
|
|
185
|
+
let requestSignal: AbortSignal | undefined;
|
|
186
|
+
return {
|
|
187
|
+
fetch: async (_url, init): Promise<Response> =>
|
|
188
|
+
new Promise<Response>((_resolve, reject) => {
|
|
189
|
+
requestSignal = init?.signal ?? undefined;
|
|
190
|
+
requestSignal?.addEventListener(
|
|
191
|
+
'abort',
|
|
192
|
+
() => reject(new Error('Aborted')),
|
|
193
|
+
{ once: true }
|
|
194
|
+
);
|
|
195
|
+
}),
|
|
196
|
+
getSignal: () => requestSignal,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const expectFetchTimeoutAbort = async (
|
|
201
|
+
client: FetchTimeoutClient,
|
|
202
|
+
capturedFetch: AbortableFetchCapture,
|
|
203
|
+
url: string
|
|
204
|
+
): Promise<void> => {
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
|
|
207
|
+
const response = client.fetchWithTimeout(
|
|
208
|
+
url,
|
|
209
|
+
{ method: 'post' },
|
|
210
|
+
10,
|
|
211
|
+
controller
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await expect(waitForFetchOutcome(response)).resolves.toBe('rejected');
|
|
215
|
+
expect(controller.signal.aborted).toBe(true);
|
|
216
|
+
expect(capturedFetch.getSignal()?.aborted).toBe(true);
|
|
217
|
+
};
|
|
218
|
+
|
|
143
219
|
const baseBedrockFields = {
|
|
144
220
|
region: 'us-east-1',
|
|
145
221
|
credentials: {
|
|
@@ -711,8 +787,7 @@ describe('custom chat model class smoke tests', () => {
|
|
|
711
787
|
}
|
|
712
788
|
|
|
713
789
|
const usageChunk = chunks.find(
|
|
714
|
-
(chunk) =>
|
|
715
|
-
chunk.usage_metadata?.input_token_details?.cache_creation === 5
|
|
790
|
+
(chunk) => chunk.usage_metadata?.input_token_details?.cache_creation === 5
|
|
716
791
|
);
|
|
717
792
|
expect(usageChunk?.usage_metadata).toEqual({
|
|
718
793
|
input_tokens: 11,
|
|
@@ -730,6 +805,44 @@ describe('custom chat model class smoke tests', () => {
|
|
|
730
805
|
});
|
|
731
806
|
});
|
|
732
807
|
|
|
808
|
+
it('emits OpenRouter callbacks before an early stream break', async () => {
|
|
809
|
+
const model = new CallbackTestChatOpenRouter({
|
|
810
|
+
model: 'openai/gpt-4o-mini',
|
|
811
|
+
apiKey: 'test-key',
|
|
812
|
+
_lc_stream_delay: 1,
|
|
813
|
+
} as OpenRouterFields & { _lc_stream_delay: number });
|
|
814
|
+
const completions = (model as unknown as StreamingCompletionBackedModel)
|
|
815
|
+
.completions;
|
|
816
|
+
const textChunks: string[] = [];
|
|
817
|
+
const callbackTokens: string[] = [];
|
|
818
|
+
|
|
819
|
+
async function* streamChunks(): AsyncGenerator<OpenAIClient.Chat.Completions.ChatCompletionChunk> {
|
|
820
|
+
yield createOpenAIStreamChunk('alpha beta gamma');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
completions.completionWithRetry = async (): Promise<
|
|
824
|
+
AsyncIterable<OpenAIClient.Chat.Completions.ChatCompletionChunk>
|
|
825
|
+
> => streamChunks();
|
|
826
|
+
|
|
827
|
+
const runManager = {
|
|
828
|
+
handleLLMNewToken(token: string): void {
|
|
829
|
+
if (token !== '') {
|
|
830
|
+
callbackTokens.push(token);
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
} as unknown as CallbackManagerForLLMRun;
|
|
834
|
+
|
|
835
|
+
for await (const chunk of model.streamChunksWithCallbacks(runManager)) {
|
|
836
|
+
if (chunk.text !== '') {
|
|
837
|
+
textChunks.push(chunk.text);
|
|
838
|
+
}
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
expect(textChunks).toEqual(['alpha beta gamma']);
|
|
843
|
+
expect(callbackTokens).toEqual(textChunks);
|
|
844
|
+
});
|
|
845
|
+
|
|
733
846
|
it('keeps Anthropic output, residency, compaction, and stream-delay options', () => {
|
|
734
847
|
const contextManagement = {
|
|
735
848
|
edits: [
|
|
@@ -748,6 +861,10 @@ describe('custom chat model class smoke tests', () => {
|
|
|
748
861
|
contextManagement,
|
|
749
862
|
_lc_stream_delay: 8,
|
|
750
863
|
});
|
|
864
|
+
const defaultModel = new CustomAnthropic({
|
|
865
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
866
|
+
apiKey: 'test-key',
|
|
867
|
+
});
|
|
751
868
|
|
|
752
869
|
const params = model.invocationParams({
|
|
753
870
|
outputConfig: { effort: 'low' },
|
|
@@ -756,6 +873,7 @@ describe('custom chat model class smoke tests', () => {
|
|
|
756
873
|
|
|
757
874
|
expect(CustomAnthropic.lc_name()).toBe('LibreChatAnthropic');
|
|
758
875
|
expect(model._lc_stream_delay).toBe(8);
|
|
876
|
+
expect(defaultModel._lc_stream_delay).toBe(25);
|
|
759
877
|
expect(params.output_config).toEqual({ effort: 'low' });
|
|
760
878
|
expect(params.inference_geo).toBe('eu');
|
|
761
879
|
expect(params.context_management).toEqual(contextManagement);
|
|
@@ -909,4 +1027,57 @@ describe('custom chat model class smoke tests', () => {
|
|
|
909
1027
|
expect(method).toBe('PATCH');
|
|
910
1028
|
expect(client.abortHandler).toBeDefined();
|
|
911
1029
|
});
|
|
1030
|
+
|
|
1031
|
+
it('aborts custom OpenAI fetches when the request timeout elapses', async () => {
|
|
1032
|
+
const capturedFetch = createAbortableFetch();
|
|
1033
|
+
const client = new CustomOpenAIClient({
|
|
1034
|
+
apiKey: 'test-key',
|
|
1035
|
+
fetch: capturedFetch.fetch,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
await expectFetchTimeoutAbort(
|
|
1039
|
+
client,
|
|
1040
|
+
capturedFetch,
|
|
1041
|
+
'https://example.test/v1/chat/completions'
|
|
1042
|
+
);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('aborts custom Azure OpenAI fetches when the request timeout elapses', async () => {
|
|
1046
|
+
const capturedFetch = createAbortableFetch();
|
|
1047
|
+
const client = new CustomAzureOpenAIClient({
|
|
1048
|
+
apiKey: 'test-azure-key',
|
|
1049
|
+
apiVersion: '2024-10-21',
|
|
1050
|
+
baseURL: 'https://example.test/openai/deployments/test-deployment',
|
|
1051
|
+
fetch: capturedFetch.fetch,
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
await expectFetchTimeoutAbort(
|
|
1055
|
+
client,
|
|
1056
|
+
capturedFetch,
|
|
1057
|
+
'https://example.test/openai/deployments/test-deployment/chat/completions'
|
|
1058
|
+
);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it('propagates caller abort signals to custom OpenAI fetches', async () => {
|
|
1062
|
+
const capturedFetch = createAbortableFetch();
|
|
1063
|
+
const client = new CustomOpenAIClient({
|
|
1064
|
+
apiKey: 'test-key',
|
|
1065
|
+
fetch: capturedFetch.fetch,
|
|
1066
|
+
});
|
|
1067
|
+
const callerController = new AbortController();
|
|
1068
|
+
const requestController = new AbortController();
|
|
1069
|
+
|
|
1070
|
+
const response = client.fetchWithTimeout(
|
|
1071
|
+
'https://example.test/v1/chat/completions',
|
|
1072
|
+
{ method: 'post', signal: callerController.signal },
|
|
1073
|
+
1000,
|
|
1074
|
+
requestController
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
callerController.abort();
|
|
1078
|
+
|
|
1079
|
+
await expect(waitForFetchOutcome(response)).resolves.toBe('rejected');
|
|
1080
|
+
expect(requestController.signal.aborted).toBe(true);
|
|
1081
|
+
expect(capturedFetch.getSignal()?.aborted).toBe(true);
|
|
1082
|
+
});
|
|
912
1083
|
});
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
AIMessageChunk,
|
|
5
5
|
type ContentBlock,
|
|
6
6
|
} from '@langchain/core/messages';
|
|
7
|
+
import {
|
|
8
|
+
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
9
|
+
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
10
|
+
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
|
|
11
|
+
} from '@/tools/streamedToolCallSeals';
|
|
12
|
+
import { _convertOpenAIResponsesDeltaToBaseMessageChunk } from './utils';
|
|
7
13
|
|
|
8
14
|
describe('OpenAI content block translator compatibility', () => {
|
|
9
15
|
describe('Chat Completions', () => {
|
|
@@ -107,6 +113,35 @@ describe('OpenAI content block translator compatibility', () => {
|
|
|
107
113
|
});
|
|
108
114
|
|
|
109
115
|
describe('Responses', () => {
|
|
116
|
+
test('marks Responses function call arguments done as an explicit tool-call seal', () => {
|
|
117
|
+
const chunk = _convertOpenAIResponsesDeltaToBaseMessageChunk({
|
|
118
|
+
type: 'response.function_call_arguments.done',
|
|
119
|
+
sequence_number: 3,
|
|
120
|
+
item_id: 'fc_123',
|
|
121
|
+
output_index: 1,
|
|
122
|
+
name: 'search',
|
|
123
|
+
arguments: '{"query":"weather"}',
|
|
124
|
+
} as Parameters<typeof _convertOpenAIResponsesDeltaToBaseMessageChunk>[0]);
|
|
125
|
+
const message = chunk?.message as AIMessageChunk | undefined;
|
|
126
|
+
|
|
127
|
+
expect(message?.tool_call_chunks).toEqual([
|
|
128
|
+
{
|
|
129
|
+
type: 'tool_call_chunk',
|
|
130
|
+
name: 'search',
|
|
131
|
+
args: '{"query":"weather"}',
|
|
132
|
+
index: 1,
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
expect(message?.response_metadata).toMatchObject({
|
|
136
|
+
[STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY]:
|
|
137
|
+
OPENAI_RESPONSES_STREAMED_TOOL_CALL_ADAPTER,
|
|
138
|
+
[STREAMED_TOOL_CALL_SEAL_METADATA_KEY]: {
|
|
139
|
+
kind: 'single',
|
|
140
|
+
index: 1,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
110
145
|
test('translates Responses messages to v1 content blocks', () => {
|
|
111
146
|
const code = ['print(', 'hello', ')'].join(String.fromCharCode(39));
|
|
112
147
|
const responseTextBlock = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
2
3
|
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
3
4
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
5
|
import type { OpenAIClient } from '@langchain/openai';
|
|
@@ -66,6 +67,16 @@ class CapturingChatDeepSeek extends ChatDeepSeek {
|
|
|
66
67
|
signal,
|
|
67
68
|
} as this['ParsedCallOptions']);
|
|
68
69
|
}
|
|
70
|
+
|
|
71
|
+
streamChunksWithCallbacks(
|
|
72
|
+
runManager?: CallbackManagerForLLMRun
|
|
73
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
74
|
+
return this._streamResponseChunks(
|
|
75
|
+
[new HumanMessage('hi')],
|
|
76
|
+
{} as this['ParsedCallOptions'],
|
|
77
|
+
runManager
|
|
78
|
+
);
|
|
79
|
+
}
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
function createToolContextMessages(): BaseMessage[] {
|
|
@@ -111,7 +122,10 @@ function createCompletionStreamChunks(): OpenAIChatCompletionChunk[] {
|
|
|
111
122
|
];
|
|
112
123
|
}
|
|
113
124
|
|
|
114
|
-
function createContentChunk(
|
|
125
|
+
function createContentChunk(
|
|
126
|
+
content: string,
|
|
127
|
+
logprobs: OpenAIChatCompletionChunk['choices'][number]['logprobs'] = null
|
|
128
|
+
): OpenAIChatCompletionChunk {
|
|
115
129
|
return {
|
|
116
130
|
id: 'chatcmpl-deepseek-test',
|
|
117
131
|
object: 'chat.completion.chunk',
|
|
@@ -125,7 +139,7 @@ function createContentChunk(content: string): OpenAIChatCompletionChunk {
|
|
|
125
139
|
content,
|
|
126
140
|
},
|
|
127
141
|
finish_reason: null,
|
|
128
|
-
logprobs
|
|
142
|
+
logprobs,
|
|
129
143
|
},
|
|
130
144
|
],
|
|
131
145
|
};
|
|
@@ -489,4 +503,189 @@ describe('ChatDeepSeek', () => {
|
|
|
489
503
|
|
|
490
504
|
await expect(iterator.next()).rejects.toThrow('AbortError');
|
|
491
505
|
});
|
|
506
|
+
|
|
507
|
+
it('does not yield a delayed DeepSeek chunk after abort', async () => {
|
|
508
|
+
const controller = new AbortController();
|
|
509
|
+
const model = new CapturingChatDeepSeek(
|
|
510
|
+
{
|
|
511
|
+
apiKey: 'test-key',
|
|
512
|
+
model: 'deepseek-v4-pro',
|
|
513
|
+
streaming: true,
|
|
514
|
+
_lc_stream_delay: 1000,
|
|
515
|
+
},
|
|
516
|
+
[createContentChunk('first '), createContentChunk('second')]
|
|
517
|
+
);
|
|
518
|
+
const stream = model.streamChunksWithSignal(controller.signal);
|
|
519
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
520
|
+
|
|
521
|
+
await expect(iterator.next()).resolves.toEqual(
|
|
522
|
+
expect.objectContaining({
|
|
523
|
+
done: false,
|
|
524
|
+
value: expect.objectContaining({
|
|
525
|
+
text: 'first ',
|
|
526
|
+
}),
|
|
527
|
+
})
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const delayedChunk = iterator.next();
|
|
531
|
+
await Promise.resolve();
|
|
532
|
+
controller.abort(new Error('AbortError: User aborted request.'));
|
|
533
|
+
|
|
534
|
+
await expect(delayedChunk).rejects.toThrow('AbortError');
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('splits large delayed DeepSeek text chunks', async () => {
|
|
538
|
+
const model = new CapturingChatDeepSeek(
|
|
539
|
+
{
|
|
540
|
+
apiKey: 'test-key',
|
|
541
|
+
model: 'deepseek-v4-pro',
|
|
542
|
+
streaming: true,
|
|
543
|
+
_lc_stream_delay: 1,
|
|
544
|
+
},
|
|
545
|
+
[createContentChunk('alpha beta gamma')]
|
|
546
|
+
);
|
|
547
|
+
const textChunks: string[] = [];
|
|
548
|
+
|
|
549
|
+
for await (const chunk of model.streamChunksWithSignal(
|
|
550
|
+
new AbortController().signal
|
|
551
|
+
)) {
|
|
552
|
+
if (chunk.text) {
|
|
553
|
+
textChunks.push(chunk.text);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
expect(textChunks).toEqual(['alpha ', 'beta ', 'gamma']);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('keeps delayed DeepSeek logprob chunks intact', async () => {
|
|
561
|
+
const logprobs = { content: [], refusal: null } as NonNullable<
|
|
562
|
+
OpenAIChatCompletionChunk['choices'][number]['logprobs']
|
|
563
|
+
>;
|
|
564
|
+
const model = new CapturingChatDeepSeek(
|
|
565
|
+
{
|
|
566
|
+
apiKey: 'test-key',
|
|
567
|
+
model: 'deepseek-v4-pro',
|
|
568
|
+
streaming: true,
|
|
569
|
+
logprobs: true,
|
|
570
|
+
_lc_stream_delay: 1,
|
|
571
|
+
},
|
|
572
|
+
[createContentChunk('alpha beta gamma', logprobs)]
|
|
573
|
+
);
|
|
574
|
+
const chunks: ChatGenerationChunk[] = [];
|
|
575
|
+
|
|
576
|
+
for await (const chunk of model.streamChunksWithSignal(
|
|
577
|
+
new AbortController().signal
|
|
578
|
+
)) {
|
|
579
|
+
if (chunk.text !== '') {
|
|
580
|
+
chunks.push(chunk);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
expect(chunks.map((chunk) => chunk.text)).toEqual(['alpha beta gamma']);
|
|
585
|
+
expect(chunks[0].generationInfo?.logprobs).toBe(logprobs);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('emits callbacks for split delayed DeepSeek text chunks', async () => {
|
|
589
|
+
const model = new CapturingChatDeepSeek(
|
|
590
|
+
{
|
|
591
|
+
apiKey: 'test-key',
|
|
592
|
+
model: 'deepseek-v4-pro',
|
|
593
|
+
streaming: true,
|
|
594
|
+
_lc_stream_delay: 1,
|
|
595
|
+
},
|
|
596
|
+
[createContentChunk('alpha beta gamma')]
|
|
597
|
+
);
|
|
598
|
+
const textChunks: string[] = [];
|
|
599
|
+
const callbackTokens: string[] = [];
|
|
600
|
+
|
|
601
|
+
const stream = await model.stream([new HumanMessage('hi')], {
|
|
602
|
+
callbacks: [
|
|
603
|
+
{
|
|
604
|
+
handleLLMNewToken(token: string): void {
|
|
605
|
+
if (token !== '') {
|
|
606
|
+
callbackTokens.push(token);
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
for await (const chunk of stream) {
|
|
614
|
+
if (typeof chunk.content === 'string' && chunk.content !== '') {
|
|
615
|
+
textChunks.push(chunk.content);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
expect(textChunks).toEqual(['alpha ', 'beta ', 'gamma']);
|
|
620
|
+
expect(callbackTokens).toEqual(textChunks);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('emits a delayed DeepSeek callback before an early stream break', async () => {
|
|
624
|
+
const model = new CapturingChatDeepSeek(
|
|
625
|
+
{
|
|
626
|
+
apiKey: 'test-key',
|
|
627
|
+
model: 'deepseek-v4-pro',
|
|
628
|
+
streaming: true,
|
|
629
|
+
_lc_stream_delay: 1,
|
|
630
|
+
},
|
|
631
|
+
[createContentChunk('alpha beta gamma')]
|
|
632
|
+
);
|
|
633
|
+
const textChunks: string[] = [];
|
|
634
|
+
const callbackTokens: string[] = [];
|
|
635
|
+
const runManager = {
|
|
636
|
+
handleLLMNewToken(token: string): void {
|
|
637
|
+
if (token !== '') {
|
|
638
|
+
callbackTokens.push(token);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
} as unknown as CallbackManagerForLLMRun;
|
|
642
|
+
|
|
643
|
+
for await (const chunk of model.streamChunksWithCallbacks(runManager)) {
|
|
644
|
+
if (chunk.text !== '') {
|
|
645
|
+
textChunks.push(chunk.text);
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
expect(textChunks).toEqual(['alpha ']);
|
|
651
|
+
expect(callbackTokens).toEqual(textChunks);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('counts consumer work toward delayed DeepSeek cadence', async () => {
|
|
655
|
+
const model = new CapturingChatDeepSeek(
|
|
656
|
+
{
|
|
657
|
+
apiKey: 'test-key',
|
|
658
|
+
model: 'deepseek-v4-pro',
|
|
659
|
+
streaming: true,
|
|
660
|
+
_lc_stream_delay: 100,
|
|
661
|
+
},
|
|
662
|
+
[createContentChunk('first '), createContentChunk('second')]
|
|
663
|
+
);
|
|
664
|
+
const stream = model.streamChunksWithSignal(new AbortController().signal);
|
|
665
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
666
|
+
|
|
667
|
+
await expect(iterator.next()).resolves.toEqual(
|
|
668
|
+
expect.objectContaining({
|
|
669
|
+
done: false,
|
|
670
|
+
value: expect.objectContaining({
|
|
671
|
+
text: 'first ',
|
|
672
|
+
}),
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
await new Promise<void>((resolve) => {
|
|
677
|
+
setTimeout(resolve, 125);
|
|
678
|
+
});
|
|
679
|
+
const started = Date.now();
|
|
680
|
+
|
|
681
|
+
await expect(iterator.next()).resolves.toEqual(
|
|
682
|
+
expect.objectContaining({
|
|
683
|
+
done: false,
|
|
684
|
+
value: expect.objectContaining({
|
|
685
|
+
text: 'second',
|
|
686
|
+
}),
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
expect(Date.now() - started).toBeLessThan(50);
|
|
690
|
+
});
|
|
492
691
|
});
|