@librechat/agents 3.3.12 → 3.4.0
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 +10 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +1 -0
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +6 -3
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +35 -206
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +121 -241
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +19 -12
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/mistral/index.cjs +26 -0
- package/dist/cjs/llm/mistral/index.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +82 -80
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/providers.cjs +19 -19
- package/dist/cjs/llm/providers.cjs.map +1 -1
- package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
- package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
- package/dist/cjs/llm/stream/smoother.cjs +369 -0
- package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
- package/dist/cjs/llm/vertexai/index.cjs +13 -1
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -10
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +34 -2
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +76 -29
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +10 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +1 -0
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +6 -3
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +34 -205
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +120 -240
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +19 -12
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/mistral/index.mjs +26 -0
- package/dist/esm/llm/mistral/index.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +82 -80
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/providers.mjs +3 -3
- package/dist/esm/llm/providers.mjs.map +1 -1
- package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
- package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
- package/dist/esm/llm/stream/smoother.mjs +365 -0
- package/dist/esm/llm/stream/smoother.mjs.map +1 -0
- package/dist/esm/llm/vertexai/index.mjs +13 -1
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +4 -2
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +34 -2
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +76 -30
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +3 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/llm/google/index.d.ts +2 -0
- package/dist/types/llm/mistral/index.d.ts +11 -0
- package/dist/types/llm/openai/index.d.ts +20 -4
- package/dist/types/llm/openrouter/index.d.ts +4 -1
- package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
- package/dist/types/llm/stream/smoother.d.ts +95 -0
- package/dist/types/llm/vertexai/index.d.ts +2 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +3 -0
- package/dist/types/types/graph.d.ts +5 -0
- package/dist/types/types/llm.d.ts +21 -14
- package/dist/types/utils/tokens.d.ts +10 -0
- package/package.json +2 -2
- package/src/graphs/Graph.ts +11 -0
- package/src/index.ts +8 -0
- package/src/instrumentation.ts +1 -0
- package/src/langfuseSpanRegistry.ts +9 -0
- package/src/llm/anthropic/index.ts +85 -354
- package/src/llm/bedrock/index.ts +174 -390
- package/src/llm/bedrock/llm.spec.ts +2 -0
- package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
- package/src/llm/custom-chat-models.smoke.test.ts +16 -1
- package/src/llm/google/index.ts +17 -18
- package/src/llm/google/streamSmoothing.test.ts +121 -0
- package/src/llm/mistral/index.ts +33 -0
- package/src/llm/mistral/streamSmoothing.test.ts +97 -0
- package/src/llm/openai/deepseek.test.ts +56 -0
- package/src/llm/openai/index.ts +119 -126
- package/src/llm/openrouter/index.ts +4 -1
- package/src/llm/providers.ts +3 -3
- package/src/llm/stream/chunkAdapters.test.ts +202 -0
- package/src/llm/stream/chunkAdapters.ts +317 -0
- package/src/llm/stream/reassembly.test.ts +241 -0
- package/src/llm/stream/smoother.bench.test.ts +155 -0
- package/src/llm/stream/smoother.test.ts +519 -0
- package/src/llm/stream/smoother.ts +574 -0
- package/src/llm/vertexai/index.ts +17 -1
- package/src/llm/vertexai/streamSmoothing.test.ts +109 -0
- package/src/specs/langfuse-instrumentation.test.ts +26 -0
- package/src/specs/langfuse-span-registry.test.ts +17 -0
- package/src/specs/subagent.test.ts +44 -0
- package/src/specs/summarization.test.ts +5 -2
- package/src/specs/tokens.test.ts +159 -14
- package/src/tools/__tests__/SubagentExecutor.test.ts +48 -1
- package/src/tools/subagent/SubagentExecutor.ts +69 -5
- package/src/types/graph.ts +5 -0
- package/src/types/llm.ts +53 -36
- package/src/utils/tokens.ts +115 -30
package/src/llm/openai/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type { BindToolsInput } from '@langchain/core/language_models/chat_models
|
|
|
37
37
|
import type { ChatGeneration, ChatResult } from '@langchain/core/outputs';
|
|
38
38
|
import type { ChatXAIInput } from '@langchain/xai';
|
|
39
39
|
import type * as t from '@langchain/openai';
|
|
40
|
+
import type { SmoothItem, SmoothPiece } from '@/llm/stream/smoother';
|
|
40
41
|
import type { ResponsesReplayPosition } from '@/messages/core';
|
|
41
42
|
import type { SeenScalarMetadata } from './streamMetadata';
|
|
42
43
|
import type { HeaderValue, HeadersLike } from './types';
|
|
@@ -58,14 +59,17 @@ import {
|
|
|
58
59
|
} from '@/tools/streamedToolCallSeals';
|
|
59
60
|
import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
|
|
60
61
|
import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
|
|
62
|
+
import { smoothStream, resolveStreamDelay } from '@/llm/stream/smoother';
|
|
63
|
+
import {
|
|
64
|
+
hasReasoningKwargs,
|
|
65
|
+
hasToolCallChunks,
|
|
66
|
+
getReasoningKwargsText,
|
|
67
|
+
} from '@/llm/stream/chunkAdapters';
|
|
61
68
|
import { dropRepeatedScalarMetadata } from './streamMetadata';
|
|
62
69
|
|
|
63
70
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
64
71
|
const iife = <T>(fn: () => T) => fn();
|
|
65
72
|
|
|
66
|
-
const STREAM_CHUNK_MIN_SIZE = 4;
|
|
67
|
-
const STREAM_BOUNDARIES = new Set([' ', '.', ',', '!', '?', ';', ':']);
|
|
68
|
-
|
|
69
73
|
export function isHeaders(headers: unknown): headers is Headers {
|
|
70
74
|
return (
|
|
71
75
|
typeof Headers !== 'undefined' &&
|
|
@@ -1114,77 +1118,87 @@ function getCustomOpenAIClientOptions(
|
|
|
1114
1118
|
return requestOptions;
|
|
1115
1119
|
}
|
|
1116
1120
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
function
|
|
1132
|
-
const chunks: string[] = [];
|
|
1133
|
-
let currentIndex = 0;
|
|
1134
|
-
|
|
1135
|
-
while (currentIndex < text.length) {
|
|
1136
|
-
const remainingText = text.slice(currentIndex);
|
|
1137
|
-
const chunkSize = findStreamChunkBoundary(
|
|
1138
|
-
remainingText,
|
|
1139
|
-
STREAM_CHUNK_MIN_SIZE
|
|
1140
|
-
);
|
|
1141
|
-
chunks.push(text.slice(currentIndex, currentIndex + chunkSize));
|
|
1142
|
-
currentIndex += chunkSize;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
return chunks;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function splitTextGenerationChunk(
|
|
1121
|
+
/**
|
|
1122
|
+
* Classifies a generation chunk for the smoothing engine:
|
|
1123
|
+
* - splittable: plain visible text (string content equal to `chunk.text`, no
|
|
1124
|
+
* logprobs / finish_reason) — sliced adaptively at the pacing cadence.
|
|
1125
|
+
* ANY logprobs value blocks splitting here (this family only attaches
|
|
1126
|
+
* logprobs on request; the DeepSeek suite pins chunks with them staying
|
|
1127
|
+
* intact) — deliberately stricter than `stream/chunkAdapters.ts`, where
|
|
1128
|
+
* google-common's always-present empty logprobs must not block.
|
|
1129
|
+
* - atomic: text- or reasoning-bearing chunks whose metadata cannot survive
|
|
1130
|
+
* slicing — paced as one piece, never split (legacy parity: these were
|
|
1131
|
+
* emitted whole but still paced).
|
|
1132
|
+
* - passthrough: tool-call deltas, usage-only, finish_reason and other
|
|
1133
|
+
* metadata chunks — strict FIFO, zero delay.
|
|
1134
|
+
*/
|
|
1135
|
+
export function toSmoothItem(
|
|
1149
1136
|
chunk: ChatGenerationChunk
|
|
1150
|
-
): ChatGenerationChunk
|
|
1137
|
+
): SmoothItem<ChatGenerationChunk> {
|
|
1151
1138
|
const { message } = chunk;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
chunk.
|
|
1159
|
-
|
|
1160
|
-
|
|
1139
|
+
const isMessageChunk = message instanceof AIMessageChunk;
|
|
1140
|
+
/** Chunks pairing visible text with a reasoning delta (reasoning_content,
|
|
1141
|
+
* reasoning summary, or OpenRouter reasoning_details) or with tool-call
|
|
1142
|
+
* deltas must pace whole: split pieces would each clone the same kwargs /
|
|
1143
|
+
* tool_call_chunks and downstream accumulation duplicates them per piece. */
|
|
1144
|
+
const splittable =
|
|
1145
|
+
Boolean(chunk.text) &&
|
|
1146
|
+
isMessageChunk &&
|
|
1147
|
+
typeof message.content === 'string' &&
|
|
1148
|
+
message.content === chunk.text &&
|
|
1149
|
+
chunk.generationInfo?.logprobs == null &&
|
|
1150
|
+
chunk.generationInfo?.finish_reason == null &&
|
|
1151
|
+
!hasReasoningKwargs(message) &&
|
|
1152
|
+
!hasToolCallChunks(message);
|
|
1153
|
+
|
|
1154
|
+
if (splittable) {
|
|
1155
|
+
return {
|
|
1156
|
+
text: chunk.text,
|
|
1157
|
+
smooth: true,
|
|
1158
|
+
emit: (piece) => cloneGenerationChunkPiece(chunk, piece),
|
|
1159
|
+
};
|
|
1161
1160
|
}
|
|
1162
1161
|
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1162
|
+
const pacedText =
|
|
1163
|
+
chunk.text || (isMessageChunk ? getReasoningKwargsText(message) : '');
|
|
1164
|
+
if (pacedText !== '') {
|
|
1165
|
+
return {
|
|
1166
|
+
text: pacedText,
|
|
1167
|
+
smooth: true,
|
|
1168
|
+
atomic: true,
|
|
1169
|
+
emit: () => chunk,
|
|
1170
|
+
};
|
|
1166
1171
|
}
|
|
1167
1172
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const usageMetadata =
|
|
1171
|
-
emittedUsage && message.usage_metadata != null
|
|
1172
|
-
? undefined
|
|
1173
|
-
: message.usage_metadata;
|
|
1174
|
-
if (message.usage_metadata != null && !emittedUsage) {
|
|
1175
|
-
emittedUsage = true;
|
|
1176
|
-
}
|
|
1173
|
+
return { text: '', smooth: false, emit: () => chunk };
|
|
1174
|
+
}
|
|
1177
1175
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1176
|
+
/**
|
|
1177
|
+
* Usage metadata, additional kwargs and response metadata survive only on
|
|
1178
|
+
* the first piece: the aggregator's dict merge concatenates string fields
|
|
1179
|
+
* and sums usage, so replication across pieces corrupts them. Unlike the
|
|
1180
|
+
* generic adapter, `generationInfo` stays on every piece — per-piece token
|
|
1181
|
+
* indices ride in it and `dropRepeatedScalarMetadata` owns repetition there.
|
|
1182
|
+
*/
|
|
1183
|
+
function cloneGenerationChunkPiece(
|
|
1184
|
+
chunk: ChatGenerationChunk,
|
|
1185
|
+
piece: SmoothPiece
|
|
1186
|
+
): ChatGenerationChunk {
|
|
1187
|
+
if (piece.isFirst && piece.isLast) {
|
|
1188
|
+
return chunk;
|
|
1189
|
+
}
|
|
1190
|
+
const message = chunk.message as AIMessageChunk;
|
|
1191
|
+
return new ChatGenerationChunk({
|
|
1192
|
+
text: piece.text,
|
|
1193
|
+
generationInfo: chunk.generationInfo,
|
|
1194
|
+
message: new AIMessageChunk(
|
|
1195
|
+
Object.assign({}, message, {
|
|
1196
|
+
content: piece.text,
|
|
1197
|
+
usage_metadata: piece.isFirst ? message.usage_metadata : undefined,
|
|
1198
|
+
additional_kwargs: piece.isFirst ? message.additional_kwargs : {},
|
|
1199
|
+
response_metadata: piece.isFirst ? message.response_metadata : {},
|
|
1200
|
+
})
|
|
1201
|
+
),
|
|
1188
1202
|
});
|
|
1189
1203
|
}
|
|
1190
1204
|
|
|
@@ -1215,66 +1229,45 @@ function getStreamChunkTokenIndices(
|
|
|
1215
1229
|
return undefined;
|
|
1216
1230
|
}
|
|
1217
1231
|
|
|
1232
|
+
/**
|
|
1233
|
+
* Adaptive smoothing adapter for the OpenAI chat-model family, layered over
|
|
1234
|
+
* the shared `smoothStream` engine. Keeps the historical signature so every
|
|
1235
|
+
* `_streamResponseChunks` call site is unchanged.
|
|
1236
|
+
*
|
|
1237
|
+
* `seenScalarMetadata`: when provided, de-duplicates repeated scalar metadata
|
|
1238
|
+
* just before emitting, so token callbacks and the yielded chunk observe the
|
|
1239
|
+
* same cleaned data. Omitted by callers that wrap this stream and finalize
|
|
1240
|
+
* downstream (e.g. `ChatOpenRouter`, which needs the raw `finish_reason` as
|
|
1241
|
+
* its flush signal and de-duplicates after its own processing).
|
|
1242
|
+
*/
|
|
1218
1243
|
async function* delayStreamChunks(
|
|
1219
1244
|
chunks: AsyncGenerator<ChatGenerationChunk>,
|
|
1220
1245
|
delay?: number,
|
|
1221
1246
|
signal?: AbortSignal,
|
|
1222
1247
|
runManager?: CallbackManagerForLLMRun,
|
|
1223
|
-
// When provided, de-duplicate repeated scalar metadata just before emitting,
|
|
1224
|
-
// so token callbacks and the yielded chunk observe the same cleaned data.
|
|
1225
|
-
// Omitted by callers that wrap this stream and finalize downstream (e.g.
|
|
1226
|
-
// `ChatOpenRouter`, which needs the raw `finish_reason` as its flush signal
|
|
1227
|
-
// and de-duplicates after its own processing).
|
|
1228
1248
|
seenScalarMetadata?: SeenScalarMetadata
|
|
1229
1249
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
signal?.throwIfAborted();
|
|
1244
|
-
lastYieldedAt = Date.now();
|
|
1245
|
-
if (seenScalarMetadata != null) {
|
|
1246
|
-
dropRepeatedScalarMetadata(outputChunk, seenScalarMetadata);
|
|
1247
|
-
}
|
|
1248
|
-
await emitStreamChunkCallback(outputChunk, runManager);
|
|
1249
|
-
signal?.throwIfAborted();
|
|
1250
|
-
yield outputChunk;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1250
|
+
const source = (async function* (): AsyncGenerator<
|
|
1251
|
+
SmoothItem<ChatGenerationChunk>
|
|
1252
|
+
> {
|
|
1253
|
+
for await (const chunk of chunks) {
|
|
1254
|
+
yield toSmoothItem(chunk);
|
|
1255
|
+
}
|
|
1256
|
+
})();
|
|
1257
|
+
|
|
1258
|
+
const smoothed = smoothStream({
|
|
1259
|
+
source,
|
|
1260
|
+
delayMs: delay != null && delay > 0 ? delay : 0,
|
|
1261
|
+
signal,
|
|
1262
|
+
});
|
|
1254
1263
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
): Promise<void> {
|
|
1259
|
-
if (delay <= 0) {
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
signal?.throwIfAborted();
|
|
1263
|
-
await new Promise<void>((resolve, reject) => {
|
|
1264
|
-
const timeout = setTimeout(() => {
|
|
1265
|
-
signal?.removeEventListener('abort', onAbort);
|
|
1266
|
-
resolve();
|
|
1267
|
-
}, delay);
|
|
1268
|
-
const onAbort = (): void => {
|
|
1269
|
-
clearTimeout(timeout);
|
|
1270
|
-
signal?.removeEventListener('abort', onAbort);
|
|
1271
|
-
reject(signal?.reason ?? new Error('AbortError: User aborted request.'));
|
|
1272
|
-
};
|
|
1273
|
-
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1274
|
-
if (signal?.aborted === true) {
|
|
1275
|
-
onAbort();
|
|
1264
|
+
for await (const outputChunk of smoothed) {
|
|
1265
|
+
if (seenScalarMetadata != null) {
|
|
1266
|
+
dropRepeatedScalarMetadata(outputChunk, seenScalarMetadata);
|
|
1276
1267
|
}
|
|
1277
|
-
|
|
1268
|
+
await emitStreamChunkCallback(outputChunk, runManager);
|
|
1269
|
+
yield outputChunk;
|
|
1270
|
+
}
|
|
1278
1271
|
}
|
|
1279
1272
|
|
|
1280
1273
|
function createAbortHandler(controller: AbortController): () => void {
|
|
@@ -2410,13 +2403,13 @@ function withLibreChatOpenAIFields(
|
|
|
2410
2403
|
}
|
|
2411
2404
|
|
|
2412
2405
|
export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
|
|
2413
|
-
_lc_stream_delay
|
|
2406
|
+
_lc_stream_delay: number;
|
|
2414
2407
|
|
|
2415
2408
|
constructor(
|
|
2416
2409
|
fields?: LibreChatOpenAIFields & t.OpenAIChatInput['modelKwargs']
|
|
2417
2410
|
) {
|
|
2418
2411
|
super(withLibreChatOpenAIFields(fields));
|
|
2419
|
-
this._lc_stream_delay = fields?._lc_stream_delay;
|
|
2412
|
+
this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
|
|
2420
2413
|
}
|
|
2421
2414
|
|
|
2422
2415
|
public get exposedClient(): CustomOpenAIClient {
|
|
@@ -2507,13 +2500,13 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
|
|
|
2507
2500
|
}
|
|
2508
2501
|
|
|
2509
2502
|
export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
|
|
2510
|
-
_lc_stream_delay
|
|
2503
|
+
_lc_stream_delay: number;
|
|
2511
2504
|
|
|
2512
2505
|
constructor(fields?: LibreChatAzureOpenAIFields) {
|
|
2513
2506
|
super(fields);
|
|
2514
2507
|
this.completions = new LibreChatAzureOpenAICompletions(fields);
|
|
2515
2508
|
this.responses = new LibreChatAzureOpenAIResponses(fields);
|
|
2516
|
-
this._lc_stream_delay = fields?._lc_stream_delay;
|
|
2509
|
+
this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
|
|
2517
2510
|
}
|
|
2518
2511
|
|
|
2519
2512
|
public get exposedClient(): CustomOpenAIClient {
|
|
@@ -2619,7 +2612,7 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
|
|
|
2619
2612
|
}
|
|
2620
2613
|
}
|
|
2621
2614
|
export class ChatDeepSeek extends OriginalChatDeepSeek {
|
|
2622
|
-
_lc_stream_delay
|
|
2615
|
+
_lc_stream_delay: number;
|
|
2623
2616
|
|
|
2624
2617
|
constructor(
|
|
2625
2618
|
fields?: ConstructorParameters<typeof OriginalChatDeepSeek>[0] & {
|
|
@@ -2627,7 +2620,7 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
|
|
|
2627
2620
|
}
|
|
2628
2621
|
) {
|
|
2629
2622
|
super(fields);
|
|
2630
|
-
this._lc_stream_delay = fields?._lc_stream_delay;
|
|
2623
|
+
this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
|
|
2631
2624
|
}
|
|
2632
2625
|
|
|
2633
2626
|
public get exposedClient(): CustomOpenAIClient {
|
|
@@ -3140,7 +3133,7 @@ export class ChatMoonshot extends ChatOpenAI {
|
|
|
3140
3133
|
}
|
|
3141
3134
|
|
|
3142
3135
|
export class ChatXAI extends OriginalChatXAI {
|
|
3143
|
-
_lc_stream_delay
|
|
3136
|
+
_lc_stream_delay: number;
|
|
3144
3137
|
|
|
3145
3138
|
constructor(
|
|
3146
3139
|
fields?: Partial<ChatXAIInput> & {
|
|
@@ -3150,7 +3143,7 @@ export class ChatXAI extends OriginalChatXAI {
|
|
|
3150
3143
|
}
|
|
3151
3144
|
) {
|
|
3152
3145
|
super(fields);
|
|
3153
|
-
this._lc_stream_delay = fields?._lc_stream_delay;
|
|
3146
|
+
this._lc_stream_delay = resolveStreamDelay(fields?._lc_stream_delay);
|
|
3154
3147
|
const customBaseURL =
|
|
3155
3148
|
fields?.configuration?.baseURL ?? fields?.clientConfig?.baseURL;
|
|
3156
3149
|
if (customBaseURL != null && customBaseURL) {
|
|
@@ -45,7 +45,10 @@ export interface ChatOpenRouterCallOptions
|
|
|
45
45
|
|
|
46
46
|
export type ChatOpenRouterInput = Partial<
|
|
47
47
|
ChatOpenRouterCallOptions & OpenAIChatInput
|
|
48
|
-
|
|
48
|
+
> & {
|
|
49
|
+
/** Minimum delay in ms between visible streamed deltas (default 25; 0 disables). */
|
|
50
|
+
_lc_stream_delay?: number;
|
|
51
|
+
};
|
|
49
52
|
|
|
50
53
|
/** invocationParams return type extended with OpenRouter reasoning */
|
|
51
54
|
export type OpenRouterInvocationParams = Omit<
|
package/src/llm/providers.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/llm/providers.ts
|
|
2
|
-
import {
|
|
2
|
+
import { CustomChatMistralAI } from '@/llm/mistral';
|
|
3
3
|
import type {
|
|
4
4
|
ChatModelConstructorMap,
|
|
5
5
|
ProviderOptionsMap,
|
|
@@ -25,8 +25,8 @@ export const llmProviders: Partial<ChatModelConstructorMap> = {
|
|
|
25
25
|
[Providers.AZURE]: AzureChatOpenAI,
|
|
26
26
|
[Providers.VERTEXAI]: ChatVertexAI,
|
|
27
27
|
[Providers.DEEPSEEK]: ChatDeepSeek,
|
|
28
|
-
[Providers.MISTRALAI]:
|
|
29
|
-
[Providers.MISTRAL]:
|
|
28
|
+
[Providers.MISTRALAI]: CustomChatMistralAI,
|
|
29
|
+
[Providers.MISTRAL]: CustomChatMistralAI,
|
|
30
30
|
[Providers.ANTHROPIC]: CustomAnthropic,
|
|
31
31
|
[Providers.OPENROUTER]: ChatOpenRouter,
|
|
32
32
|
[Providers.BEDROCK]: CustomChatBedrockConverse,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { AIMessageChunk } from '@langchain/core/messages';
|
|
2
|
+
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
3
|
+
import {
|
|
4
|
+
toGenerationSmoothItem,
|
|
5
|
+
getReasoningKwargsText,
|
|
6
|
+
cloneGenerationChunkPiece,
|
|
7
|
+
} from './chunkAdapters';
|
|
8
|
+
|
|
9
|
+
type MessageChunkFields = {
|
|
10
|
+
usage_metadata?: AIMessageChunk['usage_metadata'];
|
|
11
|
+
additional_kwargs?: AIMessageChunk['additional_kwargs'];
|
|
12
|
+
response_metadata?: AIMessageChunk['response_metadata'];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function textChunk(
|
|
16
|
+
text: string,
|
|
17
|
+
extra: MessageChunkFields = {}
|
|
18
|
+
): ChatGenerationChunk {
|
|
19
|
+
return new ChatGenerationChunk({
|
|
20
|
+
text,
|
|
21
|
+
message: new AIMessageChunk({ content: text, ...extra }),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('toGenerationSmoothItem classification', () => {
|
|
26
|
+
it('splits plain string-content text chunks', () => {
|
|
27
|
+
const item = toGenerationSmoothItem(textChunk('alpha beta gamma'));
|
|
28
|
+
expect(item.smooth).toBe(true);
|
|
29
|
+
expect(item.atomic).toBeUndefined();
|
|
30
|
+
expect(item.text).toBe('alpha beta gamma');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps chunks carrying reasoning_content kwargs atomic', () => {
|
|
34
|
+
const chunk = textChunk('visible text here', {
|
|
35
|
+
additional_kwargs: { reasoning_content: 'hidden thought' },
|
|
36
|
+
});
|
|
37
|
+
const item = toGenerationSmoothItem(chunk);
|
|
38
|
+
expect(item.smooth).toBe(true);
|
|
39
|
+
expect(item.atomic).toBe(true);
|
|
40
|
+
expect(item.emit({ text: item.text, isFirst: true, isLast: true })).toBe(
|
|
41
|
+
chunk
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('keeps chunks carrying a reasoning summary object atomic', () => {
|
|
46
|
+
const chunk = textChunk('visible text here', {
|
|
47
|
+
additional_kwargs: { reasoning: { summary: [{ text: 'thought' }] } },
|
|
48
|
+
});
|
|
49
|
+
expect(toGenerationSmoothItem(chunk).atomic).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps chunks carrying OpenRouter reasoning_details atomic', () => {
|
|
53
|
+
const chunk = textChunk('visible text here', {
|
|
54
|
+
additional_kwargs: {
|
|
55
|
+
reasoning_details: [{ type: 'reasoning.text', text: 'thought' }],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
expect(toGenerationSmoothItem(chunk).atomic).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('keeps chunks carrying camelCase finishReason atomic', () => {
|
|
62
|
+
const chunk = new ChatGenerationChunk({
|
|
63
|
+
text: 'final text with several words here',
|
|
64
|
+
generationInfo: { finishReason: 'STOP' },
|
|
65
|
+
message: new AIMessageChunk({
|
|
66
|
+
content: 'final text with several words here',
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
const item = toGenerationSmoothItem(chunk);
|
|
70
|
+
expect(item.atomic).toBe(true);
|
|
71
|
+
expect(item.emit({ text: item.text, isFirst: true, isLast: true })).toBe(
|
|
72
|
+
chunk
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('keeps mixed text/tool-call chunks atomic', () => {
|
|
77
|
+
const chunk = new ChatGenerationChunk({
|
|
78
|
+
text: 'calling the weather tool for you now',
|
|
79
|
+
message: new AIMessageChunk({
|
|
80
|
+
content: 'calling the weather tool for you now',
|
|
81
|
+
tool_call_chunks: [
|
|
82
|
+
{ name: 'weather', args: '{"city":', id: 'call_1', index: 0 },
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
const item = toGenerationSmoothItem(chunk);
|
|
87
|
+
expect(item.atomic).toBe(true);
|
|
88
|
+
expect(item.emit({ text: item.text, isFirst: true, isLast: true })).toBe(
|
|
89
|
+
chunk
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('paces reasoning-only chunks atomically via the kwargs extractor', () => {
|
|
94
|
+
const thoughtOnly = new ChatGenerationChunk({
|
|
95
|
+
text: '',
|
|
96
|
+
message: new AIMessageChunk({
|
|
97
|
+
content: '',
|
|
98
|
+
additional_kwargs: { reasoning: 'a hidden gemini thought' },
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
const item = toGenerationSmoothItem(thoughtOnly, getReasoningKwargsText);
|
|
102
|
+
expect(item.smooth).toBe(true);
|
|
103
|
+
expect(item.atomic).toBe(true);
|
|
104
|
+
expect(item.text).toBe('a hidden gemini thought');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('paces reasoning_details-only chunks atomically via the kwargs extractor', () => {
|
|
108
|
+
const detailsOnly = new ChatGenerationChunk({
|
|
109
|
+
text: '',
|
|
110
|
+
message: new AIMessageChunk({
|
|
111
|
+
content: '',
|
|
112
|
+
additional_kwargs: {
|
|
113
|
+
reasoning_details: [
|
|
114
|
+
{ type: 'reasoning.text', text: 'first thought ' },
|
|
115
|
+
{ type: 'reasoning.text', text: 'second thought' },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
const item = toGenerationSmoothItem(detailsOnly, getReasoningKwargsText);
|
|
121
|
+
expect(item.smooth).toBe(true);
|
|
122
|
+
expect(item.atomic).toBe(true);
|
|
123
|
+
expect(item.text).toBe('first thought second thought');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('classifies usage-only chunks as passthrough', () => {
|
|
127
|
+
const chunk = new ChatGenerationChunk({
|
|
128
|
+
text: '',
|
|
129
|
+
message: new AIMessageChunk({
|
|
130
|
+
content: '',
|
|
131
|
+
usage_metadata: { input_tokens: 1, output_tokens: 2, total_tokens: 3 },
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
const item = toGenerationSmoothItem(chunk);
|
|
135
|
+
expect(item.smooth).toBe(false);
|
|
136
|
+
expect(item.text).toBe('');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('cloneGenerationChunkPiece metadata scoping', () => {
|
|
141
|
+
const chunk = textChunk('alpha beta gamma', {
|
|
142
|
+
usage_metadata: { input_tokens: 1, output_tokens: 2, total_tokens: 3 },
|
|
143
|
+
additional_kwargs: { annotation: 'once' },
|
|
144
|
+
response_metadata: { model_name: 'test-model' },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('keeps kwargs, response metadata and usage on the first piece only', () => {
|
|
148
|
+
const first = cloneGenerationChunkPiece(chunk, {
|
|
149
|
+
text: 'alpha ',
|
|
150
|
+
isFirst: true,
|
|
151
|
+
isLast: false,
|
|
152
|
+
});
|
|
153
|
+
const later = cloneGenerationChunkPiece(chunk, {
|
|
154
|
+
text: 'beta ',
|
|
155
|
+
isFirst: false,
|
|
156
|
+
isLast: false,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const firstMessage = first.message as AIMessageChunk;
|
|
160
|
+
const laterMessage = later.message as AIMessageChunk;
|
|
161
|
+
expect(firstMessage.additional_kwargs).toEqual({ annotation: 'once' });
|
|
162
|
+
expect(firstMessage.response_metadata).toEqual({
|
|
163
|
+
model_name: 'test-model',
|
|
164
|
+
});
|
|
165
|
+
expect(firstMessage.usage_metadata).toBeDefined();
|
|
166
|
+
expect(laterMessage.additional_kwargs).toEqual({});
|
|
167
|
+
expect(laterMessage.response_metadata).toEqual({});
|
|
168
|
+
expect(laterMessage.usage_metadata).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('keeps generationInfo on the first piece only', () => {
|
|
172
|
+
const infoChunk = new ChatGenerationChunk({
|
|
173
|
+
text: 'alpha beta gamma',
|
|
174
|
+
generationInfo: {
|
|
175
|
+
usage_metadata: { input_tokens: 1, output_tokens: 2, total_tokens: 3 },
|
|
176
|
+
},
|
|
177
|
+
message: new AIMessageChunk({ content: 'alpha beta gamma' }),
|
|
178
|
+
});
|
|
179
|
+
const first = cloneGenerationChunkPiece(infoChunk, {
|
|
180
|
+
text: 'alpha ',
|
|
181
|
+
isFirst: true,
|
|
182
|
+
isLast: false,
|
|
183
|
+
});
|
|
184
|
+
const later = cloneGenerationChunkPiece(infoChunk, {
|
|
185
|
+
text: 'beta ',
|
|
186
|
+
isFirst: false,
|
|
187
|
+
isLast: false,
|
|
188
|
+
});
|
|
189
|
+
expect(first.generationInfo).toBeDefined();
|
|
190
|
+
expect(later.generationInfo).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns the original chunk for unsplit pieces', () => {
|
|
194
|
+
expect(
|
|
195
|
+
cloneGenerationChunkPiece(chunk, {
|
|
196
|
+
text: 'alpha beta gamma',
|
|
197
|
+
isFirst: true,
|
|
198
|
+
isLast: true,
|
|
199
|
+
})
|
|
200
|
+
).toBe(chunk);
|
|
201
|
+
});
|
|
202
|
+
});
|