@librechat/agents 3.2.61 → 3.2.62

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.
Files changed (116) hide show
  1. package/dist/cjs/common/enum.cjs +2 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +32 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +6 -0
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs +12 -0
  8. package/dist/cjs/hooks/index.cjs.map +1 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/langfuse.cjs +62 -1
  11. package/dist/cjs/langfuse.cjs.map +1 -1
  12. package/dist/cjs/llm/bedrock/index.cjs +266 -43
  13. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  14. package/dist/cjs/llm/openai/index.cjs +274 -7
  15. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  16. package/dist/cjs/llm/openai/streamMetadata.cjs +69 -0
  17. package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -0
  18. package/dist/cjs/llm/openrouter/index.cjs +5 -6
  19. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  20. package/dist/cjs/main.cjs +6 -1
  21. package/dist/cjs/messages/format.cjs +61 -0
  22. package/dist/cjs/messages/format.cjs.map +1 -1
  23. package/dist/cjs/messages/prune.cjs +31 -19
  24. package/dist/cjs/messages/prune.cjs.map +1 -1
  25. package/dist/cjs/stream.cjs +13 -3
  26. package/dist/cjs/stream.cjs.map +1 -1
  27. package/dist/cjs/tools/ToolNode.cjs +75 -10
  28. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  30. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  31. package/dist/cjs/utils/tokens.cjs +111 -0
  32. package/dist/cjs/utils/tokens.cjs.map +1 -1
  33. package/dist/esm/common/enum.mjs +2 -0
  34. package/dist/esm/common/enum.mjs.map +1 -1
  35. package/dist/esm/hooks/HookRegistry.mjs +32 -0
  36. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  37. package/dist/esm/hooks/executeHooks.mjs +6 -0
  38. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  39. package/dist/esm/hooks/index.mjs +12 -1
  40. package/dist/esm/hooks/index.mjs.map +1 -0
  41. package/dist/esm/hooks/types.mjs.map +1 -1
  42. package/dist/esm/langfuse.mjs +62 -1
  43. package/dist/esm/langfuse.mjs.map +1 -1
  44. package/dist/esm/llm/bedrock/index.mjs +266 -43
  45. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  46. package/dist/esm/llm/openai/index.mjs +274 -7
  47. package/dist/esm/llm/openai/index.mjs.map +1 -1
  48. package/dist/esm/llm/openai/streamMetadata.mjs +69 -0
  49. package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -0
  50. package/dist/esm/llm/openrouter/index.mjs +5 -6
  51. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  52. package/dist/esm/main.mjs +3 -3
  53. package/dist/esm/messages/format.mjs +61 -0
  54. package/dist/esm/messages/format.mjs.map +1 -1
  55. package/dist/esm/messages/prune.mjs +31 -19
  56. package/dist/esm/messages/prune.mjs.map +1 -1
  57. package/dist/esm/stream.mjs +13 -3
  58. package/dist/esm/stream.mjs.map +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +75 -10
  60. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  61. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  62. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  63. package/dist/esm/utils/tokens.mjs +108 -1
  64. package/dist/esm/utils/tokens.mjs.map +1 -1
  65. package/dist/types/common/enum.d.ts +3 -1
  66. package/dist/types/hooks/HookRegistry.d.ts +10 -0
  67. package/dist/types/hooks/index.d.ts +7 -0
  68. package/dist/types/hooks/types.d.ts +18 -1
  69. package/dist/types/llm/bedrock/index.d.ts +5 -0
  70. package/dist/types/llm/openai/index.d.ts +19 -0
  71. package/dist/types/llm/openai/streamMetadata.d.ts +16 -0
  72. package/dist/types/messages/prune.d.ts +4 -3
  73. package/dist/types/tools/ToolNode.d.ts +1 -0
  74. package/dist/types/types/llm.d.ts +12 -2
  75. package/dist/types/types/tools.d.ts +1 -1
  76. package/dist/types/utils/tokens.d.ts +30 -0
  77. package/package.json +6 -6
  78. package/src/__tests__/stream.eagerEventExecution.test.ts +100 -2
  79. package/src/common/enum.ts +2 -0
  80. package/src/hooks/HookRegistry.ts +45 -0
  81. package/src/hooks/__tests__/HookRegistry.test.ts +48 -0
  82. package/src/hooks/__tests__/executeHooks.test.ts +85 -2
  83. package/src/hooks/executeHooks.ts +15 -0
  84. package/src/hooks/index.ts +7 -0
  85. package/src/hooks/types.ts +18 -1
  86. package/src/langfuse.ts +134 -1
  87. package/src/llm/bedrock/index.ts +434 -83
  88. package/src/llm/bedrock/streamSealDispatch.test.ts +97 -0
  89. package/src/llm/custom-chat-models.smoke.test.ts +7 -0
  90. package/src/llm/openai/index.ts +604 -6
  91. package/src/llm/openai/managedRequests.test.ts +182 -0
  92. package/src/llm/openai/streamMetadata.spec.ts +86 -0
  93. package/src/llm/openai/streamMetadata.ts +95 -0
  94. package/src/llm/openai/streamMetadataDedup.spec.ts +166 -0
  95. package/src/llm/openrouter/index.ts +9 -5
  96. package/src/messages/format.ts +96 -3
  97. package/src/messages/formatAgentMessages.steer.test.ts +326 -0
  98. package/src/messages/labelContentByAgent.test.ts +75 -0
  99. package/src/messages/prune.ts +56 -30
  100. package/src/specs/anthropic.simple.test.ts +4 -2
  101. package/src/specs/cache.simple.test.ts +17 -4
  102. package/src/specs/langfuse-callbacks.test.ts +61 -0
  103. package/src/specs/openai.simple.test.ts +4 -2
  104. package/src/specs/spec.utils.ts +12 -0
  105. package/src/specs/summarization.test.ts +9 -13
  106. package/src/specs/token-accounting-pipeline.test.ts +130 -4
  107. package/src/specs/tokens.test.ts +214 -0
  108. package/src/stream.ts +22 -3
  109. package/src/tools/ToolNode.ts +107 -12
  110. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +554 -0
  111. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +49 -2
  112. package/src/tools/__tests__/hitl.test.ts +112 -0
  113. package/src/tools/subagent/SubagentExecutor.ts +2 -0
  114. package/src/types/llm.ts +12 -2
  115. package/src/types/tools.ts +1 -1
  116. package/src/utils/tokens.ts +181 -3
@@ -33,12 +33,14 @@ import type { BindToolsInput } from '@langchain/core/language_models/chat_models
33
33
  import type { ChatGeneration, ChatResult } from '@langchain/core/outputs';
34
34
  import type { ChatXAIInput } from '@langchain/xai';
35
35
  import type * as t from '@langchain/openai';
36
+ import type { SeenScalarMetadata } from './streamMetadata';
36
37
  import type { HeaderValue, HeadersLike } from './types';
37
38
  import {
38
39
  STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
39
40
  OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
40
41
  } from '@/tools/streamedToolCallSeals';
41
42
  import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
43
+ import { dropRepeatedScalarMetadata } from './streamMetadata';
42
44
 
43
45
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
44
46
  const iife = <T>(fn: () => T) => fn();
@@ -100,9 +102,13 @@ type LibreChatOpenAIFields = t.ChatOpenAIFields & {
100
102
  includeReasoningContent?: boolean;
101
103
  includeReasoningDetails?: boolean;
102
104
  convertReasoningDetailsToContent?: boolean;
105
+ promptCacheExplicit?: boolean;
106
+ safety_identifier?: string;
103
107
  };
104
108
  type LibreChatAzureOpenAIFields = t.AzureOpenAIInput & {
105
109
  _lc_stream_delay?: number;
110
+ promptCacheExplicit?: boolean;
111
+ safety_identifier?: string;
106
112
  };
107
113
  type ReasoningCallOptions = {
108
114
  reasoning?: OpenAIClient.Reasoning;
@@ -151,6 +157,366 @@ type OpenAIChatCompletionRetry = (
151
157
  ) => Promise<
152
158
  AsyncIterable<OpenAIChatCompletionStreamItem> | OpenAIChatCompletion
153
159
  >;
160
+ type OpenAIManagedRequestFields = {
161
+ promptCacheExplicit?: boolean;
162
+ safetyIdentifier?: string;
163
+ };
164
+ type OpenAIManagedRequestParams = {
165
+ prompt_cache_options?: {
166
+ mode: 'explicit';
167
+ ttl: '30m';
168
+ };
169
+ safety_identifier?: string;
170
+ };
171
+ type ResponsesRequest =
172
+ | OpenAIClient.Responses.ResponseCreateParamsStreaming
173
+ | OpenAIClient.Responses.ResponseCreateParamsNonStreaming;
174
+ type ResponsesResult =
175
+ | AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>
176
+ | OpenAIClient.Responses.Response;
177
+ type CacheableChatPart = {
178
+ type: 'text' | 'image_url' | 'input_audio' | 'file' | 'refusal';
179
+ prompt_cache_breakpoint?: { mode: 'explicit' };
180
+ [key: string]: unknown;
181
+ };
182
+ type CacheableResponsePart = (
183
+ | OpenAIClient.Responses.ResponseInputText
184
+ | OpenAIClient.Responses.ResponseInputImage
185
+ | OpenAIClient.Responses.ResponseInputFile
186
+ ) & {
187
+ prompt_cache_breakpoint?: { mode: 'explicit' };
188
+ };
189
+ type ResponsesUsageWithCacheWrite = OpenAIClient.Responses.ResponseUsage & {
190
+ input_tokens_details?: OpenAIClient.Responses.ResponseUsage['input_tokens_details'] & {
191
+ cache_write_tokens?: number;
192
+ };
193
+ };
194
+ const CACHE_WRITE_METADATA_KEY = '__librechat_cache_write_tokens';
195
+
196
+ function applyManagedRequestParams<T extends object>(
197
+ params: T,
198
+ fields: OpenAIManagedRequestFields
199
+ ): T & OpenAIManagedRequestParams {
200
+ return {
201
+ ...params,
202
+ ...(fields.promptCacheExplicit === true && {
203
+ prompt_cache_options: {
204
+ mode: 'explicit' as const,
205
+ ttl: '30m' as const,
206
+ },
207
+ }),
208
+ ...(fields.safetyIdentifier != null && {
209
+ safety_identifier: fields.safetyIdentifier,
210
+ }),
211
+ };
212
+ }
213
+
214
+ function isCacheableChatPart(part: unknown): part is CacheableChatPart {
215
+ if (typeof part !== 'object' || part == null || !('type' in part)) {
216
+ return false;
217
+ }
218
+ return (
219
+ part.type === 'text' ||
220
+ part.type === 'image_url' ||
221
+ part.type === 'input_audio' ||
222
+ part.type === 'file' ||
223
+ part.type === 'refusal'
224
+ );
225
+ }
226
+
227
+ function canAddChatBreakpoint(
228
+ message: OpenAIClient.Chat.Completions.ChatCompletionMessageParam
229
+ ): boolean {
230
+ if (
231
+ message.role !== 'system' &&
232
+ message.role !== 'developer' &&
233
+ message.role !== 'user' &&
234
+ message.role !== 'assistant' &&
235
+ message.role !== 'tool'
236
+ ) {
237
+ return false;
238
+ }
239
+ if (typeof message.content === 'string') {
240
+ return message.content.length > 0;
241
+ }
242
+ return (
243
+ Array.isArray(message.content) &&
244
+ message.content.some((part) => isCacheableChatPart(part))
245
+ );
246
+ }
247
+
248
+ function addChatBreakpoint(
249
+ message: OpenAIClient.Chat.Completions.ChatCompletionMessageParam
250
+ ): OpenAIClient.Chat.Completions.ChatCompletionMessageParam {
251
+ if (!canAddChatBreakpoint(message)) {
252
+ return message;
253
+ }
254
+
255
+ if (typeof message.content === 'string') {
256
+ return {
257
+ ...message,
258
+ content: [
259
+ {
260
+ type: 'text',
261
+ text: message.content,
262
+ prompt_cache_breakpoint: { mode: 'explicit' },
263
+ },
264
+ ],
265
+ } as unknown as OpenAIClient.Chat.Completions.ChatCompletionMessageParam;
266
+ }
267
+ if (!Array.isArray(message.content) || message.content.length === 0) {
268
+ return message;
269
+ }
270
+
271
+ const content = [...message.content] as unknown as CacheableChatPart[];
272
+ let index = content.length - 1;
273
+ while (index >= 0 && !isCacheableChatPart(content[index])) {
274
+ index--;
275
+ }
276
+ if (index < 0) {
277
+ return message;
278
+ }
279
+ content[index] = {
280
+ ...content[index],
281
+ prompt_cache_breakpoint: { mode: 'explicit' },
282
+ };
283
+ return {
284
+ ...message,
285
+ content,
286
+ } as unknown as OpenAIClient.Chat.Completions.ChatCompletionMessageParam;
287
+ }
288
+
289
+ function selectCacheBreakpointIndexes(
290
+ roles: Array<string | undefined>,
291
+ cacheable: boolean[]
292
+ ): number[] {
293
+ let instructionIndex = -1;
294
+ let latestUserIndex = -1;
295
+ for (let index = 0; index < roles.length; index++) {
296
+ const role = roles[index];
297
+ if ((role === 'system' || role === 'developer') && cacheable[index]) {
298
+ instructionIndex = index;
299
+ }
300
+ if (role === 'user') {
301
+ latestUserIndex = index;
302
+ }
303
+ }
304
+
305
+ const indexes = new Set<number>();
306
+ if (instructionIndex >= 0) {
307
+ indexes.add(instructionIndex);
308
+ }
309
+ for (let index = latestUserIndex - 1; index >= 0; index--) {
310
+ if (cacheable[index]) {
311
+ indexes.add(index);
312
+ break;
313
+ }
314
+ }
315
+ return [...indexes];
316
+ }
317
+
318
+ /** @internal */
319
+ export function addChatCacheBreakpoints(
320
+ messages: OpenAIClient.Chat.Completions.ChatCompletionMessageParam[]
321
+ ): OpenAIClient.Chat.Completions.ChatCompletionMessageParam[] {
322
+ const indexes = new Set(
323
+ selectCacheBreakpointIndexes(
324
+ messages.map((message) => message.role),
325
+ messages.map((message) => canAddChatBreakpoint(message))
326
+ )
327
+ );
328
+ return messages.map((message, index) =>
329
+ indexes.has(index) ? addChatBreakpoint(message) : message
330
+ );
331
+ }
332
+
333
+ function isResponseMessage(
334
+ item: OpenAIClient.Responses.ResponseInputItem
335
+ ): item is OpenAIClient.Responses.ResponseInputItem.Message {
336
+ return item.type === 'message';
337
+ }
338
+
339
+ /** Only `input_text`/`input_image`/`input_file` accept a Responses breakpoint;
340
+ * `output_text`/`refusal` (replayed assistant blocks) are rejected with a 400. */
341
+ function isCacheableResponsePart(part: unknown): part is CacheableResponsePart {
342
+ if (typeof part !== 'object' || part == null || !('type' in part)) {
343
+ return false;
344
+ }
345
+ return (
346
+ part.type === 'input_text' ||
347
+ part.type === 'input_image' ||
348
+ part.type === 'input_file'
349
+ );
350
+ }
351
+
352
+ function addResponseBreakpoint(
353
+ item: OpenAIClient.Responses.ResponseInputItem
354
+ ): OpenAIClient.Responses.ResponseInputItem {
355
+ if (!isResponseMessage(item)) {
356
+ return item;
357
+ }
358
+ const rawContent = item.content as
359
+ | string
360
+ | OpenAIClient.Responses.ResponseInputMessageContentList;
361
+ if (typeof rawContent === 'string') {
362
+ if (rawContent.length === 0) {
363
+ return item;
364
+ }
365
+ return {
366
+ ...item,
367
+ content: [
368
+ {
369
+ type: 'input_text',
370
+ text: rawContent,
371
+ prompt_cache_breakpoint: { mode: 'explicit' },
372
+ },
373
+ ],
374
+ } as OpenAIClient.Responses.ResponseInputItem;
375
+ }
376
+ if (!Array.isArray(rawContent) || rawContent.length === 0) {
377
+ return item;
378
+ }
379
+
380
+ const content = [...rawContent];
381
+ let index = content.length - 1;
382
+ while (index >= 0 && !isCacheableResponsePart(content[index])) {
383
+ index--;
384
+ }
385
+ if (index < 0) {
386
+ return item;
387
+ }
388
+ content[index] = {
389
+ ...(content[index] as CacheableResponsePart),
390
+ prompt_cache_breakpoint: { mode: 'explicit' },
391
+ };
392
+ return { ...item, content };
393
+ }
394
+
395
+ /** @internal */
396
+ export function addResponseCacheBreakpoints(
397
+ input: OpenAIClient.Responses.ResponseCreateParams['input']
398
+ ): OpenAIClient.Responses.ResponseCreateParams['input'] {
399
+ if (!Array.isArray(input)) {
400
+ return input;
401
+ }
402
+ const indexes = new Set(
403
+ selectCacheBreakpointIndexes(
404
+ input.map((item) => (isResponseMessage(item) ? item.role : undefined)),
405
+ input.map((item) => {
406
+ if (!isResponseMessage(item)) {
407
+ return false;
408
+ }
409
+ /** Only input roles take a Responses breakpoint. Assistant/tool turns
410
+ * carry output content (string or output_text) that the API rejects
411
+ * under an input marker, so they're never eligible. */
412
+ if (
413
+ item.role !== 'system' &&
414
+ item.role !== 'developer' &&
415
+ item.role !== 'user'
416
+ ) {
417
+ return false;
418
+ }
419
+ const content = item.content as
420
+ | string
421
+ | OpenAIClient.Responses.ResponseInputMessageContentList;
422
+ return typeof content === 'string'
423
+ ? content.length > 0
424
+ : Array.isArray(content) &&
425
+ content.some((part) => isCacheableResponsePart(part));
426
+ })
427
+ )
428
+ );
429
+ return input.map((item, index) =>
430
+ indexes.has(index) ? addResponseBreakpoint(item) : item
431
+ );
432
+ }
433
+
434
+ /** @internal */
435
+ export function shouldIncludeEncryptedReasoning(
436
+ model: string,
437
+ params: {
438
+ store?: boolean | null;
439
+ reasoning?: unknown;
440
+ }
441
+ ): boolean {
442
+ const reasoningContext = (
443
+ params.reasoning as
444
+ | { context?: 'auto' | 'current_turn' | 'all_turns' }
445
+ | undefined
446
+ )?.context;
447
+ return (
448
+ /^gpt-5\.6(?:-|$)/i.test(model) &&
449
+ (params.store === false || reasoningContext !== 'current_turn')
450
+ );
451
+ }
452
+
453
+ function getCacheWriteTokens(message: BaseMessage): number | undefined {
454
+ const responseMetadata = message.response_metadata as {
455
+ usage?: ResponsesUsageWithCacheWrite;
456
+ metadata?: Record<string, string>;
457
+ };
458
+ const reported =
459
+ responseMetadata.usage?.input_tokens_details.cache_write_tokens;
460
+ if (reported != null) {
461
+ return reported;
462
+ }
463
+ const serialized = responseMetadata.metadata?.[CACHE_WRITE_METADATA_KEY];
464
+ if (serialized == null) {
465
+ return;
466
+ }
467
+ const parsed = Number(serialized);
468
+ return Number.isFinite(parsed) ? parsed : undefined;
469
+ }
470
+
471
+ function attachCacheWriteUsage(message: BaseMessage): void {
472
+ const cacheWriteTokens = getCacheWriteTokens(message);
473
+ if (
474
+ cacheWriteTokens == null ||
475
+ !isAIMessage(message) ||
476
+ message.usage_metadata == null
477
+ ) {
478
+ return;
479
+ }
480
+ message.usage_metadata.input_token_details = {
481
+ ...message.usage_metadata.input_token_details,
482
+ cache_creation: cacheWriteTokens,
483
+ };
484
+ const responseMetadata = message.response_metadata as {
485
+ metadata?: Record<string, string>;
486
+ };
487
+ if (responseMetadata.metadata?.[CACHE_WRITE_METADATA_KEY] == null) {
488
+ return;
489
+ }
490
+ const metadata = { ...responseMetadata.metadata };
491
+ delete metadata[CACHE_WRITE_METADATA_KEY];
492
+ message.response_metadata = {
493
+ ...message.response_metadata,
494
+ metadata,
495
+ };
496
+ }
497
+
498
+ function attachCacheWriteMetadata(
499
+ response: OpenAIClient.Responses.Response
500
+ ): OpenAIClient.Responses.Response {
501
+ const usage = response.usage as ResponsesUsageWithCacheWrite | undefined;
502
+ const cacheWriteTokens = usage?.input_tokens_details.cache_write_tokens;
503
+ if (cacheWriteTokens == null) {
504
+ return response;
505
+ }
506
+ return {
507
+ ...response,
508
+ metadata: {
509
+ ...(response.metadata ?? {}),
510
+ [CACHE_WRITE_METADATA_KEY]: String(cacheWriteTokens),
511
+ },
512
+ };
513
+ }
514
+
515
+ function isResponsesStream(
516
+ result: ResponsesResult
517
+ ): result is AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent> {
518
+ return Symbol.asyncIterator in result;
519
+ }
154
520
 
155
521
  function createUsageMetadata(
156
522
  usage?: OpenAIClient.Completions.CompletionUsage
@@ -514,7 +880,13 @@ async function* delayStreamChunks(
514
880
  chunks: AsyncGenerator<ChatGenerationChunk>,
515
881
  delay?: number,
516
882
  signal?: AbortSignal,
517
- runManager?: CallbackManagerForLLMRun
883
+ runManager?: CallbackManagerForLLMRun,
884
+ // When provided, de-duplicate repeated scalar metadata just before emitting,
885
+ // so token callbacks and the yielded chunk observe the same cleaned data.
886
+ // Omitted by callers that wrap this stream and finalize downstream (e.g.
887
+ // `ChatOpenRouter`, which needs the raw `finish_reason` as its flush signal
888
+ // and de-duplicates after its own processing).
889
+ seenScalarMetadata?: SeenScalarMetadata
518
890
  ): AsyncGenerator<ChatGenerationChunk> {
519
891
  let lastYieldedAt: number | undefined;
520
892
  for await (const chunk of chunks) {
@@ -531,6 +903,9 @@ async function* delayStreamChunks(
531
903
  }
532
904
  signal?.throwIfAborted();
533
905
  lastYieldedAt = Date.now();
906
+ if (seenScalarMetadata != null) {
907
+ dropRepeatedScalarMetadata(outputChunk, seenScalarMetadata);
908
+ }
534
909
  await emitStreamChunkCallback(outputChunk, runManager);
535
910
  signal?.throwIfAborted();
536
911
  yield outputChunk;
@@ -741,6 +1116,8 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
741
1116
  private includeReasoningContent?: boolean;
742
1117
  private includeReasoningDetails?: boolean;
743
1118
  private convertReasoningDetailsToContent?: boolean;
1119
+ private promptCacheExplicit?: boolean;
1120
+ private safetyIdentifier?: string;
744
1121
 
745
1122
  constructor(fields?: LibreChatOpenAIFields) {
746
1123
  super(fields);
@@ -748,6 +1125,18 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
748
1125
  this.includeReasoningDetails = fields?.includeReasoningDetails;
749
1126
  this.convertReasoningDetailsToContent =
750
1127
  fields?.convertReasoningDetailsToContent;
1128
+ this.promptCacheExplicit = fields?.promptCacheExplicit;
1129
+ this.safetyIdentifier = fields?.safety_identifier;
1130
+ }
1131
+
1132
+ invocationParams(
1133
+ options?: this['ParsedCallOptions'],
1134
+ extra?: { streaming?: boolean }
1135
+ ): ReturnType<OriginalChatOpenAICompletions['invocationParams']> {
1136
+ return applyManagedRequestParams(super.invocationParams(options, extra), {
1137
+ promptCacheExplicit: this.promptCacheExplicit,
1138
+ safetyIdentifier: this.safetyIdentifier,
1139
+ });
751
1140
  }
752
1141
 
753
1142
  protected _getReasoningParams(
@@ -777,7 +1166,9 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
777
1166
  requestOptions?: OpenAICoreRequestOptions
778
1167
  ): Promise<AsyncIterable<OpenAIChatCompletionChunk> | OpenAIChatCompletion> {
779
1168
  return completionWithFilteredOpenAIStream(
780
- request,
1169
+ this.promptCacheExplicit === true
1170
+ ? { ...request, messages: addChatCacheBreakpoints(request.messages) }
1171
+ : request,
781
1172
  requestOptions,
782
1173
  super.completionWithRetry.bind(this) as OpenAIChatCompletionRetry
783
1174
  );
@@ -1141,6 +1532,88 @@ class LibreChatOpenAICompletions extends OriginalChatOpenAICompletions {
1141
1532
  }
1142
1533
 
1143
1534
  class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
1535
+ private promptCacheExplicit?: boolean;
1536
+ private safetyIdentifier?: string;
1537
+
1538
+ constructor(fields?: LibreChatOpenAIFields) {
1539
+ super(fields);
1540
+ this.promptCacheExplicit = fields?.promptCacheExplicit;
1541
+ this.safetyIdentifier = fields?.safety_identifier;
1542
+ }
1543
+
1544
+ invocationParams(
1545
+ options?: this['ParsedCallOptions']
1546
+ ): ReturnType<OriginalChatOpenAIResponses['invocationParams']> {
1547
+ const params = applyManagedRequestParams(super.invocationParams(options), {
1548
+ promptCacheExplicit: this.promptCacheExplicit,
1549
+ safetyIdentifier: this.safetyIdentifier,
1550
+ });
1551
+ if (shouldIncludeEncryptedReasoning(this.model, params)) {
1552
+ params.include = [
1553
+ ...new Set([
1554
+ ...(params.include ?? []),
1555
+ 'reasoning.encrypted_content' as const,
1556
+ ]),
1557
+ ];
1558
+ }
1559
+ return params;
1560
+ }
1561
+
1562
+ async completionWithRetry(
1563
+ request: OpenAIClient.Responses.ResponseCreateParamsStreaming,
1564
+ requestOptions?: OpenAICoreRequestOptions
1565
+ ): Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
1566
+ async completionWithRetry(
1567
+ request: OpenAIClient.Responses.ResponseCreateParamsNonStreaming,
1568
+ requestOptions?: OpenAICoreRequestOptions
1569
+ ): Promise<OpenAIClient.Responses.Response>;
1570
+ async completionWithRetry(
1571
+ request: ResponsesRequest,
1572
+ requestOptions?: OpenAICoreRequestOptions
1573
+ ): Promise<ResponsesResult> {
1574
+ const managedRequest = {
1575
+ ...request,
1576
+ input:
1577
+ this.promptCacheExplicit === true
1578
+ ? addResponseCacheBreakpoints(request.input)
1579
+ : request.input,
1580
+ };
1581
+ const result = await super.completionWithRetry(
1582
+ managedRequest as OpenAIClient.Responses.ResponseCreateParamsStreaming,
1583
+ requestOptions
1584
+ );
1585
+ return isResponsesStream(result)
1586
+ ? result
1587
+ : attachCacheWriteMetadata(result);
1588
+ }
1589
+
1590
+ async _generate(
1591
+ messages: BaseMessage[],
1592
+ options: this['ParsedCallOptions'],
1593
+ runManager?: CallbackManagerForLLMRun
1594
+ ): Promise<ChatResult> {
1595
+ const result = await super._generate(messages, options, runManager);
1596
+ for (const generation of result.generations) {
1597
+ attachCacheWriteUsage(generation.message);
1598
+ }
1599
+ return result;
1600
+ }
1601
+
1602
+ async *_streamResponseChunks(
1603
+ messages: BaseMessage[],
1604
+ options: this['ParsedCallOptions'],
1605
+ runManager?: CallbackManagerForLLMRun
1606
+ ): AsyncGenerator<ChatGenerationChunk> {
1607
+ for await (const chunk of super._streamResponseChunks(
1608
+ messages,
1609
+ options,
1610
+ runManager
1611
+ )) {
1612
+ attachCacheWriteUsage(chunk.message);
1613
+ yield chunk;
1614
+ }
1615
+ }
1616
+
1144
1617
  protected _getReasoningParams(
1145
1618
  options?: this['ParsedCallOptions']
1146
1619
  ): OpenAIClient.Reasoning | undefined {
@@ -1155,6 +1628,25 @@ class LibreChatOpenAIResponses extends OriginalChatOpenAIResponses {
1155
1628
  }
1156
1629
 
1157
1630
  class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions {
1631
+ private promptCacheExplicit?: boolean;
1632
+ private safetyIdentifier?: string;
1633
+
1634
+ constructor(fields?: LibreChatAzureOpenAIFields) {
1635
+ super(fields);
1636
+ this.promptCacheExplicit = fields?.promptCacheExplicit;
1637
+ this.safetyIdentifier = fields?.safety_identifier;
1638
+ }
1639
+
1640
+ invocationParams(
1641
+ options?: this['ParsedCallOptions'],
1642
+ extra?: { streaming?: boolean }
1643
+ ): ReturnType<OriginalAzureChatOpenAICompletions['invocationParams']> {
1644
+ return applyManagedRequestParams(super.invocationParams(options, extra), {
1645
+ promptCacheExplicit: this.promptCacheExplicit,
1646
+ safetyIdentifier: this.safetyIdentifier,
1647
+ });
1648
+ }
1649
+
1158
1650
  protected _getReasoningParams(
1159
1651
  options?: this['ParsedCallOptions']
1160
1652
  ): OpenAIClient.Reasoning | undefined {
@@ -1262,7 +1754,9 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
1262
1754
  requestOptions?: OpenAICoreRequestOptions
1263
1755
  ): Promise<AsyncIterable<OpenAIChatCompletionChunk> | OpenAIChatCompletion> {
1264
1756
  return completionWithFilteredOpenAIStream(
1265
- request,
1757
+ this.promptCacheExplicit === true
1758
+ ? { ...request, messages: addChatCacheBreakpoints(request.messages) }
1759
+ : request,
1266
1760
  requestOptions,
1267
1761
  super.completionWithRetry.bind(this) as OpenAIChatCompletionRetry
1268
1762
  );
@@ -1270,6 +1764,88 @@ class LibreChatAzureOpenAICompletions extends OriginalAzureChatOpenAICompletions
1270
1764
  }
1271
1765
 
1272
1766
  class LibreChatAzureOpenAIResponses extends OriginalAzureChatOpenAIResponses {
1767
+ private promptCacheExplicit?: boolean;
1768
+ private safetyIdentifier?: string;
1769
+
1770
+ constructor(fields?: LibreChatAzureOpenAIFields) {
1771
+ super(fields);
1772
+ this.promptCacheExplicit = fields?.promptCacheExplicit;
1773
+ this.safetyIdentifier = fields?.safety_identifier;
1774
+ }
1775
+
1776
+ invocationParams(
1777
+ options?: this['ParsedCallOptions']
1778
+ ): ReturnType<OriginalAzureChatOpenAIResponses['invocationParams']> {
1779
+ const params = applyManagedRequestParams(super.invocationParams(options), {
1780
+ promptCacheExplicit: this.promptCacheExplicit,
1781
+ safetyIdentifier: this.safetyIdentifier,
1782
+ });
1783
+ if (shouldIncludeEncryptedReasoning(this.model, params)) {
1784
+ params.include = [
1785
+ ...new Set([
1786
+ ...(params.include ?? []),
1787
+ 'reasoning.encrypted_content' as const,
1788
+ ]),
1789
+ ];
1790
+ }
1791
+ return params;
1792
+ }
1793
+
1794
+ async completionWithRetry(
1795
+ request: OpenAIClient.Responses.ResponseCreateParamsStreaming,
1796
+ requestOptions?: OpenAICoreRequestOptions
1797
+ ): Promise<AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>>;
1798
+ async completionWithRetry(
1799
+ request: OpenAIClient.Responses.ResponseCreateParamsNonStreaming,
1800
+ requestOptions?: OpenAICoreRequestOptions
1801
+ ): Promise<OpenAIClient.Responses.Response>;
1802
+ async completionWithRetry(
1803
+ request: ResponsesRequest,
1804
+ requestOptions?: OpenAICoreRequestOptions
1805
+ ): Promise<ResponsesResult> {
1806
+ const managedRequest = {
1807
+ ...request,
1808
+ input:
1809
+ this.promptCacheExplicit === true
1810
+ ? addResponseCacheBreakpoints(request.input)
1811
+ : request.input,
1812
+ };
1813
+ const result = await super.completionWithRetry(
1814
+ managedRequest as OpenAIClient.Responses.ResponseCreateParamsStreaming,
1815
+ requestOptions
1816
+ );
1817
+ return isResponsesStream(result)
1818
+ ? result
1819
+ : attachCacheWriteMetadata(result);
1820
+ }
1821
+
1822
+ async _generate(
1823
+ messages: BaseMessage[],
1824
+ options: this['ParsedCallOptions'],
1825
+ runManager?: CallbackManagerForLLMRun
1826
+ ): Promise<ChatResult> {
1827
+ const result = await super._generate(messages, options, runManager);
1828
+ for (const generation of result.generations) {
1829
+ attachCacheWriteUsage(generation.message);
1830
+ }
1831
+ return result;
1832
+ }
1833
+
1834
+ async *_streamResponseChunks(
1835
+ messages: BaseMessage[],
1836
+ options: this['ParsedCallOptions'],
1837
+ runManager?: CallbackManagerForLLMRun
1838
+ ): AsyncGenerator<ChatGenerationChunk> {
1839
+ for await (const chunk of super._streamResponseChunks(
1840
+ messages,
1841
+ options,
1842
+ runManager
1843
+ )) {
1844
+ attachCacheWriteUsage(chunk.message);
1845
+ yield chunk;
1846
+ }
1847
+ }
1848
+
1273
1849
  protected _getReasoningParams(
1274
1850
  options?: this['ParsedCallOptions']
1275
1851
  ): OpenAIClient.Reasoning | undefined {
@@ -1422,6 +1998,25 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
1422
1998
  runManager?: CallbackManagerForLLMRun
1423
1999
  ): AsyncGenerator<ChatGenerationChunk> {
1424
2000
  yield* delayStreamChunks(
2001
+ super._streamResponseChunks(messages, options, undefined),
2002
+ this._lc_stream_delay,
2003
+ options.signal,
2004
+ runManager,
2005
+ new Map()
2006
+ );
2007
+ }
2008
+
2009
+ /**
2010
+ * Raw variant that skips scalar-metadata de-duplication. Used by subclasses
2011
+ * (e.g. `ChatOpenRouter`) that read `finish_reason` as a control signal and
2012
+ * must de-duplicate only after their own finalization.
2013
+ */
2014
+ protected _streamRawResponseChunks(
2015
+ messages: BaseMessage[],
2016
+ options: this['ParsedCallOptions'],
2017
+ runManager?: CallbackManagerForLLMRun
2018
+ ): AsyncGenerator<ChatGenerationChunk> {
2019
+ return delayStreamChunks(
1425
2020
  super._streamResponseChunks(messages, options, undefined),
1426
2021
  this._lc_stream_delay,
1427
2022
  options.signal,
@@ -1537,7 +2132,8 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
1537
2132
  super._streamResponseChunks(messages, options, undefined),
1538
2133
  this._lc_stream_delay,
1539
2134
  options.signal,
1540
- runManager
2135
+ runManager,
2136
+ new Map()
1541
2137
  );
1542
2138
  }
1543
2139
  }
@@ -1670,7 +2266,8 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
1670
2266
  this._streamResponseChunksWithReasoning(messages, options, undefined),
1671
2267
  this._lc_stream_delay,
1672
2268
  options.signal,
1673
- runManager
2269
+ runManager,
2270
+ new Map()
1674
2271
  );
1675
2272
  }
1676
2273
 
@@ -2131,7 +2728,8 @@ export class ChatXAI extends OriginalChatXAI {
2131
2728
  super._streamResponseChunks(messages, options, undefined),
2132
2729
  this._lc_stream_delay,
2133
2730
  options.signal,
2134
- runManager
2731
+ runManager,
2732
+ new Map()
2135
2733
  );
2136
2734
  }
2137
2735
  }