@librechat/agents 3.2.0 → 3.2.2
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 +154 -67
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +3 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +93 -7
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/stream.cjs +10 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +155 -68
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +94 -8
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/stream.mjs +10 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +246 -104
- package/src/graphs/__tests__/Graph.reasoning.test.ts +747 -0
- package/src/index.ts +1 -0
- package/src/messages/core.ts +126 -11
- package/src/messages/formatAgentMessages.test.ts +122 -0
- package/src/specs/deepseek.simple.test.ts +8 -3
- package/src/specs/moonshot.simple.test.ts +8 -3
- package/src/splitStream.test.ts +64 -0
- package/src/stream.ts +12 -10
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { AIMessageChunk, HumanMessage } from '@langchain/core/messages';
|
|
2
|
+
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
3
|
+
import { FakeListChatModel } from '@langchain/core/utils/testing';
|
|
4
|
+
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
5
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
6
|
+
import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
|
|
7
|
+
import type * as t from '@/types';
|
|
8
|
+
import { ContentTypes, GraphEvents, Providers } from '@/common';
|
|
9
|
+
import { createContentAggregator } from '@/stream';
|
|
10
|
+
import { ModelEndHandler, ToolEndHandler } from '@/events';
|
|
11
|
+
import { Run } from '@/run';
|
|
12
|
+
|
|
13
|
+
type ReasoningKey = 'reasoning_content' | 'reasoning';
|
|
14
|
+
|
|
15
|
+
class InvokeOnlyReasoningModel implements t.ChatModel {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly response: {
|
|
18
|
+
content: string;
|
|
19
|
+
reasoningContent: string;
|
|
20
|
+
}
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async invoke(
|
|
24
|
+
_messages: BaseMessage[],
|
|
25
|
+
_config?: RunnableConfig
|
|
26
|
+
): Promise<AIMessageChunk> {
|
|
27
|
+
return new AIMessageChunk({
|
|
28
|
+
content: this.response.content,
|
|
29
|
+
additional_kwargs: {
|
|
30
|
+
reasoning_content: this.response.reasoningContent,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class InvokeOnlyMessageModel implements t.ChatModel {
|
|
37
|
+
constructor(private readonly message: AIMessageChunk) {}
|
|
38
|
+
|
|
39
|
+
async invoke(
|
|
40
|
+
_messages: BaseMessage[],
|
|
41
|
+
_config?: RunnableConfig
|
|
42
|
+
): Promise<AIMessageChunk> {
|
|
43
|
+
return this.message;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class StreamingReasoningModel implements t.ChatModel {
|
|
48
|
+
constructor(private readonly chunks: AIMessageChunk[]) {}
|
|
49
|
+
|
|
50
|
+
async invoke(
|
|
51
|
+
_messages: BaseMessage[],
|
|
52
|
+
_config?: RunnableConfig
|
|
53
|
+
): Promise<AIMessageChunk> {
|
|
54
|
+
return this.chunks[this.chunks.length - 1] ?? new AIMessageChunk('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async stream(
|
|
58
|
+
_messages: BaseMessage[],
|
|
59
|
+
_config?: RunnableConfig
|
|
60
|
+
): Promise<AsyncIterable<AIMessageChunk>> {
|
|
61
|
+
const chunks = this.chunks;
|
|
62
|
+
return (async function* streamChunks(): AsyncGenerator<AIMessageChunk> {
|
|
63
|
+
for (const chunk of chunks) {
|
|
64
|
+
yield chunk;
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class CallbackStreamingReasoningModel extends FakeListChatModel {
|
|
71
|
+
constructor(private readonly chunks: AIMessageChunk[]) {
|
|
72
|
+
super({ responses: [''] });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_llmType(): string {
|
|
76
|
+
return 'callback-streaming-reasoning';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async *_streamResponseChunks(
|
|
80
|
+
_messages: BaseMessage[],
|
|
81
|
+
_options: this['ParsedCallOptions'],
|
|
82
|
+
runManager?: CallbackManagerForLLMRun
|
|
83
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
84
|
+
for (const chunk of this.chunks) {
|
|
85
|
+
const text = typeof chunk.content === 'string' ? chunk.content : '';
|
|
86
|
+
yield new ChatGenerationChunk({
|
|
87
|
+
text,
|
|
88
|
+
generationInfo: {},
|
|
89
|
+
message: chunk,
|
|
90
|
+
});
|
|
91
|
+
void runManager?.handleLLMNewToken(text);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createReasoningChunk(
|
|
97
|
+
reasoningKey: ReasoningKey,
|
|
98
|
+
reasoningText: string
|
|
99
|
+
): AIMessageChunk {
|
|
100
|
+
return new AIMessageChunk({
|
|
101
|
+
content: '',
|
|
102
|
+
additional_kwargs: {
|
|
103
|
+
[reasoningKey]: reasoningText,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createOpenAIReasoningSummaryChunk(reasoningText: string): AIMessageChunk {
|
|
109
|
+
return new AIMessageChunk({
|
|
110
|
+
content: '',
|
|
111
|
+
additional_kwargs: {
|
|
112
|
+
reasoning: {
|
|
113
|
+
summary: [{ text: reasoningText }],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createReasoningHandlers(
|
|
120
|
+
aggregateContent: t.ContentAggregator,
|
|
121
|
+
reasoningDeltas: t.ReasoningDeltaEvent[],
|
|
122
|
+
messageDeltas?: t.MessageDeltaEvent[]
|
|
123
|
+
): Record<string | GraphEvents, t.EventHandler> {
|
|
124
|
+
return {
|
|
125
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
126
|
+
handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void => {
|
|
127
|
+
aggregateContent({ event, data: data as t.RunStep });
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
131
|
+
handle: (
|
|
132
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
133
|
+
data: t.StreamEventData
|
|
134
|
+
): void => {
|
|
135
|
+
const messageDelta = data as t.MessageDeltaEvent;
|
|
136
|
+
messageDeltas?.push(messageDelta);
|
|
137
|
+
aggregateContent({ event, data: messageDelta });
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
[GraphEvents.ON_REASONING_DELTA]: {
|
|
141
|
+
handle: (
|
|
142
|
+
event: GraphEvents.ON_REASONING_DELTA,
|
|
143
|
+
data: t.StreamEventData
|
|
144
|
+
): void => {
|
|
145
|
+
const reasoningDelta = data as t.ReasoningDeltaEvent;
|
|
146
|
+
reasoningDeltas.push(reasoningDelta);
|
|
147
|
+
aggregateContent({ event, data: reasoningDelta });
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function createLibreChatLikeHandlers({
|
|
154
|
+
aggregateContent,
|
|
155
|
+
collectedUsage,
|
|
156
|
+
emittedEvents,
|
|
157
|
+
}: {
|
|
158
|
+
aggregateContent: t.ContentAggregator;
|
|
159
|
+
collectedUsage: UsageMetadata[];
|
|
160
|
+
emittedEvents: Array<{ event: string; data: unknown }>;
|
|
161
|
+
}): Record<string | GraphEvents, t.EventHandler> {
|
|
162
|
+
const modelEndHandler = new ModelEndHandler(collectedUsage);
|
|
163
|
+
const toolEndHandler = new ToolEndHandler();
|
|
164
|
+
const aggregateAndEmit = (
|
|
165
|
+
event: GraphEvents,
|
|
166
|
+
data: t.StreamEventData
|
|
167
|
+
): void => {
|
|
168
|
+
aggregateContent({
|
|
169
|
+
event,
|
|
170
|
+
data: data as
|
|
171
|
+
| t.RunStep
|
|
172
|
+
| t.MessageDeltaEvent
|
|
173
|
+
| t.ReasoningDeltaEvent
|
|
174
|
+
| t.RunStepDeltaEvent
|
|
175
|
+
| { result: t.ToolEndEvent },
|
|
176
|
+
});
|
|
177
|
+
emittedEvents.push({
|
|
178
|
+
event,
|
|
179
|
+
data,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
[GraphEvents.CHAT_MODEL_END]: {
|
|
185
|
+
handle: async (event, data, metadata, graph): Promise<void> => {
|
|
186
|
+
await modelEndHandler.handle(
|
|
187
|
+
event,
|
|
188
|
+
data as t.ModelEndData,
|
|
189
|
+
metadata,
|
|
190
|
+
graph
|
|
191
|
+
);
|
|
192
|
+
emittedEvents.push({
|
|
193
|
+
event,
|
|
194
|
+
data,
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
[GraphEvents.TOOL_END]: toolEndHandler,
|
|
199
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
200
|
+
handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData): void =>
|
|
201
|
+
aggregateAndEmit(event, data),
|
|
202
|
+
},
|
|
203
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
204
|
+
handle: (
|
|
205
|
+
event: GraphEvents.ON_RUN_STEP_DELTA,
|
|
206
|
+
data: t.StreamEventData
|
|
207
|
+
): void => aggregateAndEmit(event, data),
|
|
208
|
+
},
|
|
209
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
210
|
+
handle: (
|
|
211
|
+
event: GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
212
|
+
data: t.StreamEventData
|
|
213
|
+
): void => aggregateAndEmit(event, data),
|
|
214
|
+
},
|
|
215
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
216
|
+
handle: (
|
|
217
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
218
|
+
data: t.StreamEventData
|
|
219
|
+
): void => aggregateAndEmit(event, data),
|
|
220
|
+
},
|
|
221
|
+
[GraphEvents.ON_REASONING_DELTA]: {
|
|
222
|
+
handle: (
|
|
223
|
+
event: GraphEvents.ON_REASONING_DELTA,
|
|
224
|
+
data: t.StreamEventData
|
|
225
|
+
): void => aggregateAndEmit(event, data),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
describe('StandardGraph final response reasoning fallback', () => {
|
|
231
|
+
const config = {
|
|
232
|
+
configurable: {
|
|
233
|
+
thread_id: 'reasoning-fallback-thread',
|
|
234
|
+
},
|
|
235
|
+
streamMode: 'values' as const,
|
|
236
|
+
version: 'v2' as const,
|
|
237
|
+
};
|
|
238
|
+
const llmConfig: t.LLMConfig = {
|
|
239
|
+
provider: Providers.OPENAI,
|
|
240
|
+
disableStreaming: true,
|
|
241
|
+
streamUsage: false,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
it('emits reasoning_content from invoke-only final responses', async () => {
|
|
245
|
+
const reasoningText = 'Need to inspect the Home Assistant tool state.';
|
|
246
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
247
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
248
|
+
const run = await Run.create<t.IState>({
|
|
249
|
+
runId: 'reasoning-fallback-empty-content',
|
|
250
|
+
graphConfig: {
|
|
251
|
+
type: 'standard',
|
|
252
|
+
llmConfig,
|
|
253
|
+
},
|
|
254
|
+
returnContent: true,
|
|
255
|
+
skipCleanup: true,
|
|
256
|
+
customHandlers: createReasoningHandlers(
|
|
257
|
+
aggregateContent,
|
|
258
|
+
reasoningDeltas
|
|
259
|
+
),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!run.Graph) {
|
|
263
|
+
throw new Error('Expected graph to be initialized');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
run.Graph.overrideModel = new InvokeOnlyReasoningModel({
|
|
267
|
+
content: '',
|
|
268
|
+
reasoningContent: reasoningText,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const finalContentParts = await run.processStream(
|
|
272
|
+
{ messages: [new HumanMessage('turn on the bedroom light')] },
|
|
273
|
+
config
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(finalContentParts).toEqual([
|
|
277
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
278
|
+
]);
|
|
279
|
+
expect(reasoningDeltas).toHaveLength(1);
|
|
280
|
+
expect(reasoningDeltas[0].delta.content?.[0]).toEqual({
|
|
281
|
+
type: ContentTypes.THINK,
|
|
282
|
+
think: reasoningText,
|
|
283
|
+
});
|
|
284
|
+
expect(contentParts).toContainEqual({
|
|
285
|
+
type: ContentTypes.THINK,
|
|
286
|
+
think: reasoningText,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('keeps final reasoning before final text when both are present', async () => {
|
|
291
|
+
const text = 'Done.';
|
|
292
|
+
const reasoningText = 'Decide whether a tool is needed first.';
|
|
293
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
294
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
295
|
+
const run = await Run.create<t.IState>({
|
|
296
|
+
runId: 'reasoning-fallback-with-text',
|
|
297
|
+
graphConfig: {
|
|
298
|
+
type: 'standard',
|
|
299
|
+
llmConfig,
|
|
300
|
+
},
|
|
301
|
+
returnContent: true,
|
|
302
|
+
skipCleanup: true,
|
|
303
|
+
customHandlers: createReasoningHandlers(
|
|
304
|
+
aggregateContent,
|
|
305
|
+
reasoningDeltas
|
|
306
|
+
),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (!run.Graph) {
|
|
310
|
+
throw new Error('Expected graph to be initialized');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
run.Graph.overrideModel = new InvokeOnlyReasoningModel({
|
|
314
|
+
content: text,
|
|
315
|
+
reasoningContent: reasoningText,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await run.processStream(
|
|
319
|
+
{ messages: [new HumanMessage('say done')] },
|
|
320
|
+
config
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(contentParts).toEqual([
|
|
324
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
325
|
+
{ type: ContentTypes.TEXT, text },
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('returns reasoning content without a custom aggregator', async () => {
|
|
330
|
+
const reasoningText = 'Reasoning should persist for returnContent.';
|
|
331
|
+
const run = await Run.create<t.IState>({
|
|
332
|
+
runId: 'reasoning-fallback-return-content',
|
|
333
|
+
graphConfig: {
|
|
334
|
+
type: 'standard',
|
|
335
|
+
llmConfig,
|
|
336
|
+
},
|
|
337
|
+
returnContent: true,
|
|
338
|
+
skipCleanup: true,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (!run.Graph) {
|
|
342
|
+
throw new Error('Expected graph to be initialized');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
run.Graph.overrideModel = new InvokeOnlyReasoningModel({
|
|
346
|
+
content: '',
|
|
347
|
+
reasoningContent: reasoningText,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const finalContentParts = await run.processStream(
|
|
351
|
+
{ messages: [new HumanMessage('return reasoning content')] },
|
|
352
|
+
{
|
|
353
|
+
...config,
|
|
354
|
+
configurable: {
|
|
355
|
+
thread_id: 'reasoning-fallback-return-content',
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect(finalContentParts).toEqual([
|
|
361
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('emits every OpenAI reasoning summary segment in invoke-only fallback', async () => {
|
|
366
|
+
const reasoningText = 'First summary. Second summary.';
|
|
367
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
368
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
369
|
+
const run = await Run.create<t.IState>({
|
|
370
|
+
runId: 'reasoning-fallback-openai-multi-summary',
|
|
371
|
+
graphConfig: {
|
|
372
|
+
type: 'standard',
|
|
373
|
+
llmConfig: {
|
|
374
|
+
provider: Providers.OPENAI,
|
|
375
|
+
disableStreaming: true,
|
|
376
|
+
streamUsage: false,
|
|
377
|
+
},
|
|
378
|
+
reasoningKey: 'reasoning',
|
|
379
|
+
},
|
|
380
|
+
returnContent: true,
|
|
381
|
+
skipCleanup: true,
|
|
382
|
+
customHandlers: createReasoningHandlers(
|
|
383
|
+
aggregateContent,
|
|
384
|
+
reasoningDeltas
|
|
385
|
+
),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (!run.Graph) {
|
|
389
|
+
throw new Error('Expected graph to be initialized');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
run.Graph.overrideModel = new InvokeOnlyMessageModel(
|
|
393
|
+
new AIMessageChunk({
|
|
394
|
+
content: '',
|
|
395
|
+
additional_kwargs: {
|
|
396
|
+
reasoning: {
|
|
397
|
+
summary: [{ text: 'First summary. ' }, { text: 'Second summary.' }],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const finalContentParts = await run.processStream(
|
|
404
|
+
{ messages: [new HumanMessage('return multi summary reasoning')] },
|
|
405
|
+
{
|
|
406
|
+
...config,
|
|
407
|
+
configurable: {
|
|
408
|
+
thread_id: 'reasoning-fallback-openai-multi-summary',
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(reasoningDeltas).toHaveLength(1);
|
|
414
|
+
expect(reasoningDeltas[0].delta.content?.[0]).toEqual({
|
|
415
|
+
type: ContentTypes.THINK,
|
|
416
|
+
think: reasoningText,
|
|
417
|
+
});
|
|
418
|
+
expect(finalContentParts).toEqual([
|
|
419
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
420
|
+
]);
|
|
421
|
+
expect(contentParts).toEqual([
|
|
422
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
423
|
+
]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('emits OpenRouter reasoning_details in invoke-only fallback', async () => {
|
|
427
|
+
const reasoningText = 'OpenRouter detail reasoning.';
|
|
428
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
429
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
430
|
+
const run = await Run.create<t.IState>({
|
|
431
|
+
runId: 'reasoning-fallback-openrouter-details',
|
|
432
|
+
graphConfig: {
|
|
433
|
+
type: 'standard',
|
|
434
|
+
llmConfig: {
|
|
435
|
+
provider: Providers.OPENROUTER,
|
|
436
|
+
disableStreaming: true,
|
|
437
|
+
streamUsage: false,
|
|
438
|
+
},
|
|
439
|
+
reasoningKey: 'reasoning',
|
|
440
|
+
},
|
|
441
|
+
returnContent: true,
|
|
442
|
+
skipCleanup: true,
|
|
443
|
+
customHandlers: createReasoningHandlers(
|
|
444
|
+
aggregateContent,
|
|
445
|
+
reasoningDeltas
|
|
446
|
+
),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (!run.Graph) {
|
|
450
|
+
throw new Error('Expected graph to be initialized');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
run.Graph.overrideModel = new InvokeOnlyMessageModel(
|
|
454
|
+
new AIMessageChunk({
|
|
455
|
+
content: '',
|
|
456
|
+
additional_kwargs: {
|
|
457
|
+
reasoning_details: [
|
|
458
|
+
{ type: 'reasoning.text', text: 'OpenRouter detail ' },
|
|
459
|
+
{ type: 'reasoning.encrypted', id: 'encrypted' },
|
|
460
|
+
{ type: 'reasoning.text', text: 'reasoning.' },
|
|
461
|
+
],
|
|
462
|
+
},
|
|
463
|
+
})
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const finalContentParts = await run.processStream(
|
|
467
|
+
{ messages: [new HumanMessage('return OpenRouter reasoning details')] },
|
|
468
|
+
{
|
|
469
|
+
...config,
|
|
470
|
+
configurable: {
|
|
471
|
+
thread_id: 'reasoning-fallback-openrouter-details',
|
|
472
|
+
},
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(reasoningDeltas).toHaveLength(1);
|
|
477
|
+
expect(reasoningDeltas[0].delta.content?.[0]).toEqual({
|
|
478
|
+
type: ContentTypes.THINK,
|
|
479
|
+
think: reasoningText,
|
|
480
|
+
});
|
|
481
|
+
expect(finalContentParts).toEqual([
|
|
482
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
483
|
+
]);
|
|
484
|
+
expect(contentParts).toEqual([
|
|
485
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
486
|
+
]);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it.each([
|
|
490
|
+
{
|
|
491
|
+
providerName: 'DeepSeek',
|
|
492
|
+
provider: Providers.DEEPSEEK,
|
|
493
|
+
reasoningKey: 'reasoning_content' as const,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
providerName: 'OpenRouter',
|
|
497
|
+
provider: Providers.OPENROUTER,
|
|
498
|
+
reasoningKey: 'reasoning' as const,
|
|
499
|
+
},
|
|
500
|
+
])(
|
|
501
|
+
'does not replay streamed $providerName reasoning from the final fallback',
|
|
502
|
+
async ({ provider, providerName, reasoningKey }) => {
|
|
503
|
+
const text = 'Done.';
|
|
504
|
+
const reasoningText = 'Check the provider reasoning stream first.';
|
|
505
|
+
const firstReasoningChunk = reasoningText.slice(0, 19);
|
|
506
|
+
const secondReasoningChunk = reasoningText.slice(19);
|
|
507
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
508
|
+
const messageDeltas: t.MessageDeltaEvent[] = [];
|
|
509
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
510
|
+
const run = await Run.create<t.IState>({
|
|
511
|
+
runId: `reasoning-fallback-${providerName.toLowerCase()}-stream`,
|
|
512
|
+
graphConfig: {
|
|
513
|
+
type: 'standard',
|
|
514
|
+
llmConfig: {
|
|
515
|
+
provider,
|
|
516
|
+
streamUsage: false,
|
|
517
|
+
},
|
|
518
|
+
reasoningKey,
|
|
519
|
+
},
|
|
520
|
+
returnContent: true,
|
|
521
|
+
skipCleanup: true,
|
|
522
|
+
customHandlers: createReasoningHandlers(
|
|
523
|
+
aggregateContent,
|
|
524
|
+
reasoningDeltas,
|
|
525
|
+
messageDeltas
|
|
526
|
+
),
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (!run.Graph) {
|
|
530
|
+
throw new Error('Expected graph to be initialized');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
run.Graph.overrideModel = new StreamingReasoningModel([
|
|
534
|
+
createReasoningChunk(reasoningKey, firstReasoningChunk),
|
|
535
|
+
createReasoningChunk(reasoningKey, secondReasoningChunk),
|
|
536
|
+
new AIMessageChunk({ content: text }),
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
await run.processStream(
|
|
540
|
+
{ messages: [new HumanMessage('stream provider reasoning')] },
|
|
541
|
+
{
|
|
542
|
+
...config,
|
|
543
|
+
configurable: {
|
|
544
|
+
thread_id: `reasoning-fallback-${providerName.toLowerCase()}-stream`,
|
|
545
|
+
},
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
expect(reasoningDeltas).toHaveLength(2);
|
|
550
|
+
expect(messageDeltas).toHaveLength(1);
|
|
551
|
+
expect(contentParts).toEqual([
|
|
552
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
553
|
+
{ type: ContentTypes.TEXT, text },
|
|
554
|
+
]);
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
it.each([
|
|
559
|
+
{
|
|
560
|
+
providerName: 'DeepSeek',
|
|
561
|
+
provider: Providers.DEEPSEEK,
|
|
562
|
+
reasoningKey: 'reasoning_content' as const,
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
providerName: 'OpenRouter',
|
|
566
|
+
provider: Providers.OPENROUTER,
|
|
567
|
+
reasoningKey: 'reasoning' as const,
|
|
568
|
+
},
|
|
569
|
+
])(
|
|
570
|
+
'does not replay streamed reasoning-only $providerName output',
|
|
571
|
+
async ({ provider, providerName, reasoningKey }) => {
|
|
572
|
+
const reasoningText = 'The answer is still being considered.';
|
|
573
|
+
const firstReasoningChunk = reasoningText.slice(0, 14);
|
|
574
|
+
const secondReasoningChunk = reasoningText.slice(14);
|
|
575
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
576
|
+
const messageDeltas: t.MessageDeltaEvent[] = [];
|
|
577
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
578
|
+
const run = await Run.create<t.IState>({
|
|
579
|
+
runId: `reasoning-only-${providerName.toLowerCase()}-stream`,
|
|
580
|
+
graphConfig: {
|
|
581
|
+
type: 'standard',
|
|
582
|
+
llmConfig: {
|
|
583
|
+
provider,
|
|
584
|
+
streamUsage: false,
|
|
585
|
+
},
|
|
586
|
+
reasoningKey,
|
|
587
|
+
},
|
|
588
|
+
returnContent: true,
|
|
589
|
+
skipCleanup: true,
|
|
590
|
+
customHandlers: createReasoningHandlers(
|
|
591
|
+
aggregateContent,
|
|
592
|
+
reasoningDeltas,
|
|
593
|
+
messageDeltas
|
|
594
|
+
),
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (!run.Graph) {
|
|
598
|
+
throw new Error('Expected graph to be initialized');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
run.Graph.overrideModel = new StreamingReasoningModel([
|
|
602
|
+
createReasoningChunk(reasoningKey, firstReasoningChunk),
|
|
603
|
+
createReasoningChunk(reasoningKey, secondReasoningChunk),
|
|
604
|
+
]);
|
|
605
|
+
|
|
606
|
+
await run.processStream(
|
|
607
|
+
{ messages: [new HumanMessage('stream provider reasoning only')] },
|
|
608
|
+
{
|
|
609
|
+
...config,
|
|
610
|
+
configurable: {
|
|
611
|
+
thread_id: `reasoning-only-${providerName.toLowerCase()}-stream`,
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
expect(reasoningDeltas).toHaveLength(2);
|
|
617
|
+
expect(messageDeltas).toHaveLength(0);
|
|
618
|
+
expect(contentParts).toEqual([
|
|
619
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
620
|
+
]);
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
it('does not replay streamed OpenAI reasoning summaries from the final fallback', async () => {
|
|
625
|
+
const text = 'Done.';
|
|
626
|
+
const reasoningText = 'Use the summary reasoning channel.';
|
|
627
|
+
const firstReasoningChunk = reasoningText.slice(0, 15);
|
|
628
|
+
const secondReasoningChunk = reasoningText.slice(15);
|
|
629
|
+
const reasoningDeltas: t.ReasoningDeltaEvent[] = [];
|
|
630
|
+
const messageDeltas: t.MessageDeltaEvent[] = [];
|
|
631
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
632
|
+
const run = await Run.create<t.IState>({
|
|
633
|
+
runId: 'reasoning-fallback-openai-summary-stream',
|
|
634
|
+
graphConfig: {
|
|
635
|
+
type: 'standard',
|
|
636
|
+
llmConfig: {
|
|
637
|
+
provider: Providers.OPENAI,
|
|
638
|
+
streamUsage: false,
|
|
639
|
+
},
|
|
640
|
+
reasoningKey: 'reasoning',
|
|
641
|
+
},
|
|
642
|
+
returnContent: true,
|
|
643
|
+
skipCleanup: true,
|
|
644
|
+
customHandlers: createReasoningHandlers(
|
|
645
|
+
aggregateContent,
|
|
646
|
+
reasoningDeltas,
|
|
647
|
+
messageDeltas
|
|
648
|
+
),
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (!run.Graph) {
|
|
652
|
+
throw new Error('Expected graph to be initialized');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
run.Graph.overrideModel = new StreamingReasoningModel([
|
|
656
|
+
createOpenAIReasoningSummaryChunk(firstReasoningChunk),
|
|
657
|
+
createOpenAIReasoningSummaryChunk(secondReasoningChunk),
|
|
658
|
+
new AIMessageChunk({ content: text }),
|
|
659
|
+
]);
|
|
660
|
+
|
|
661
|
+
await run.processStream(
|
|
662
|
+
{ messages: [new HumanMessage('stream OpenAI summary reasoning')] },
|
|
663
|
+
{
|
|
664
|
+
...config,
|
|
665
|
+
configurable: {
|
|
666
|
+
thread_id: 'reasoning-fallback-openai-summary-stream',
|
|
667
|
+
},
|
|
668
|
+
}
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
expect(reasoningDeltas).toHaveLength(2);
|
|
672
|
+
expect(messageDeltas).toHaveLength(1);
|
|
673
|
+
expect(contentParts).toEqual([
|
|
674
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
675
|
+
{ type: ContentTypes.TEXT, text },
|
|
676
|
+
]);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('preserves LibreChat-like callbacks including model_end usage collection', async () => {
|
|
680
|
+
const text = 'Visible answer.';
|
|
681
|
+
const reasoningText = 'Visible reasoning.';
|
|
682
|
+
const usage: UsageMetadata = {
|
|
683
|
+
input_tokens: 7,
|
|
684
|
+
output_tokens: 5,
|
|
685
|
+
total_tokens: 12,
|
|
686
|
+
output_token_details: {
|
|
687
|
+
reasoning: 3,
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
const collectedUsage: UsageMetadata[] = [];
|
|
691
|
+
const emittedEvents: Array<{ event: string; data: unknown }> = [];
|
|
692
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
693
|
+
const run = await Run.create<t.IState>({
|
|
694
|
+
runId: 'reasoning-fallback-librechat-callbacks',
|
|
695
|
+
graphConfig: {
|
|
696
|
+
type: 'standard',
|
|
697
|
+
llmConfig: {
|
|
698
|
+
provider: Providers.DEEPSEEK,
|
|
699
|
+
streamUsage: false,
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
returnContent: true,
|
|
703
|
+
skipCleanup: true,
|
|
704
|
+
customHandlers: createLibreChatLikeHandlers({
|
|
705
|
+
aggregateContent,
|
|
706
|
+
collectedUsage,
|
|
707
|
+
emittedEvents,
|
|
708
|
+
}),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
if (!run.Graph) {
|
|
712
|
+
throw new Error('Expected graph to be initialized');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
run.Graph.overrideModel = new CallbackStreamingReasoningModel([
|
|
716
|
+
createReasoningChunk('reasoning_content', reasoningText.slice(0, 8)),
|
|
717
|
+
createReasoningChunk('reasoning_content', reasoningText.slice(8)),
|
|
718
|
+
new AIMessageChunk({
|
|
719
|
+
content: text,
|
|
720
|
+
usage_metadata: usage,
|
|
721
|
+
}),
|
|
722
|
+
]);
|
|
723
|
+
|
|
724
|
+
await run.processStream(
|
|
725
|
+
{ messages: [new HumanMessage('stream with LibreChat handlers')] },
|
|
726
|
+
{
|
|
727
|
+
...config,
|
|
728
|
+
configurable: {
|
|
729
|
+
thread_id: 'reasoning-fallback-librechat-callbacks',
|
|
730
|
+
},
|
|
731
|
+
}
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
const countEvents = (event: GraphEvents): number =>
|
|
735
|
+
emittedEvents.filter((entry) => entry.event === event).length;
|
|
736
|
+
|
|
737
|
+
expect(countEvents(GraphEvents.ON_REASONING_DELTA)).toBe(2);
|
|
738
|
+
expect(countEvents(GraphEvents.ON_MESSAGE_DELTA)).toBe(1);
|
|
739
|
+
expect(countEvents(GraphEvents.CHAT_MODEL_END)).toBe(1);
|
|
740
|
+
expect(collectedUsage).toHaveLength(1);
|
|
741
|
+
expect(collectedUsage[0]).toMatchObject(usage);
|
|
742
|
+
expect(contentParts).toEqual([
|
|
743
|
+
{ type: ContentTypes.THINK, think: reasoningText },
|
|
744
|
+
{ type: ContentTypes.TEXT, text },
|
|
745
|
+
]);
|
|
746
|
+
});
|
|
747
|
+
});
|