@librechat/agents 3.7.18 → 3.7.20

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 (53) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +5 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +2 -1
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +6 -2
  6. package/dist/cjs/langfuse.cjs.map +1 -1
  7. package/dist/cjs/llm/invoke.cjs +17 -2
  8. package/dist/cjs/llm/invoke.cjs.map +1 -1
  9. package/dist/cjs/llm/openai/index.cjs +110 -24
  10. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  11. package/dist/cjs/llm/prepareProviderRequest.cjs +102 -1
  12. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -1
  13. package/dist/cjs/main.cjs +1 -0
  14. package/dist/cjs/tools/search/format.cjs +7 -0
  15. package/dist/cjs/tools/search/format.cjs.map +1 -1
  16. package/dist/cjs/tools/search/tool.cjs +7 -2
  17. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  18. package/dist/esm/agents/AgentContext.mjs +5 -2
  19. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +2 -1
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/langfuse.mjs +6 -2
  23. package/dist/esm/langfuse.mjs.map +1 -1
  24. package/dist/esm/llm/invoke.mjs +17 -2
  25. package/dist/esm/llm/invoke.mjs.map +1 -1
  26. package/dist/esm/llm/openai/index.mjs +110 -25
  27. package/dist/esm/llm/openai/index.mjs.map +1 -1
  28. package/dist/esm/llm/prepareProviderRequest.mjs +104 -3
  29. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
  30. package/dist/esm/main.mjs +2 -2
  31. package/dist/esm/tools/search/format.mjs +7 -1
  32. package/dist/esm/tools/search/format.mjs.map +1 -1
  33. package/dist/esm/tools/search/tool.mjs +7 -3
  34. package/dist/esm/tools/search/tool.mjs.map +1 -1
  35. package/dist/types/agents/AgentContext.d.ts +4 -1
  36. package/dist/types/langfuse.d.ts +5 -1
  37. package/dist/types/llm/openai/index.d.ts +11 -1
  38. package/dist/types/llm/prepareProviderRequest.d.ts +1 -1
  39. package/dist/types/tools/search/format.d.ts +2 -1
  40. package/dist/types/tools/search/tool.d.ts +1 -0
  41. package/dist/types/types/graph.d.ts +2 -0
  42. package/dist/types/types/llm.d.ts +23 -0
  43. package/package.json +2 -1
  44. package/src/agents/AgentContext.ts +7 -0
  45. package/src/graphs/Graph.ts +2 -1
  46. package/src/langfuse.ts +12 -0
  47. package/src/llm/invoke.ts +23 -2
  48. package/src/llm/openai/index.ts +281 -27
  49. package/src/llm/prepareProviderRequest.ts +257 -6
  50. package/src/tools/search/format.ts +15 -1
  51. package/src/tools/search/tool.ts +13 -3
  52. package/src/types/graph.ts +2 -0
  53. package/src/types/llm.ts +23 -0
@@ -1,5 +1,10 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import type {
3
+ BaseMessage,
4
+ Data,
5
+ MessageContentComplex,
6
+ } from '@langchain/core/messages';
1
7
  import type { RunnableConfig } from '@langchain/core/runnables';
2
- import type { BaseMessage } from '@langchain/core/messages';
3
8
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
4
9
  import type * as t from '@/types';
5
10
  import {
@@ -20,14 +25,60 @@ import {
20
25
  import {
21
26
  stripAnthropicCacheControl,
22
27
  stripBedrockCacheControl,
28
+ cloneMessage,
23
29
  } from '@/messages/cache';
30
+ import {
31
+ isAnthropicLike,
32
+ isGoogleLike,
33
+ isOpenAILike,
34
+ } from '@/utils/llm';
24
35
  import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
25
36
  import { providerRequiresStrictAlternation } from '@/llm/providers';
26
- import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
27
37
  import { getProviderFamily } from '@/llm/providerRegistry';
28
38
  import { Providers } from '@/common';
29
39
 
30
40
  const preparedProviderRequestBrand = Symbol('PreparedProviderRequest');
41
+ const OMITTED_ATTACHMENT_TEXT =
42
+ '[Attachment omitted because its binary format is unsupported by this provider.]';
43
+
44
+ const BEDROCK_DOCUMENT_MIME_TYPES: Readonly<Partial<Record<string, string>>> = {
45
+ csv: 'text/csv',
46
+ doc: 'application/msword',
47
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
48
+ html: 'text/html',
49
+ md: 'text/markdown',
50
+ pdf: 'application/pdf',
51
+ txt: 'text/plain',
52
+ xls: 'application/vnd.ms-excel',
53
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
54
+ };
55
+
56
+ interface SerializedBuffer {
57
+ type: 'Buffer';
58
+ data: number[];
59
+ }
60
+
61
+ interface BedrockDocumentBlock {
62
+ type: 'document';
63
+ document: {
64
+ name: string;
65
+ format: string;
66
+ source: {
67
+ bytes: Uint8Array | SerializedBuffer;
68
+ };
69
+ };
70
+ }
71
+
72
+ type StandardBase64FileBlock = Data.StandardFileBlock & {
73
+ source_type: 'base64';
74
+ mime_type: string;
75
+ data: string;
76
+ };
77
+
78
+ type ProviderContentBlock =
79
+ | MessageContentComplex
80
+ | BedrockDocumentBlock
81
+ | StandardBase64FileBlock;
31
82
 
32
83
  export type ProviderMessageProjectionMode =
33
84
  | 'chat-messages'
@@ -159,16 +210,216 @@ interface ProjectMessagesForProviderParams {
159
210
  callOptions?: unknown;
160
211
  }
161
212
 
213
+ function isSerializedBuffer(value: object): value is SerializedBuffer {
214
+ return (
215
+ 'type' in value &&
216
+ value.type === 'Buffer' &&
217
+ 'data' in value &&
218
+ Array.isArray(value.data)
219
+ );
220
+ }
221
+
222
+ function isBedrockDocumentBlock(
223
+ block: ProviderContentBlock
224
+ ): block is BedrockDocumentBlock {
225
+ if (block.type !== 'document' || !('document' in block)) {
226
+ return false;
227
+ }
228
+ const document = block.document;
229
+ if (typeof document !== 'object' || document == null) {
230
+ return false;
231
+ }
232
+ if (
233
+ !('name' in document) ||
234
+ typeof document.name !== 'string' ||
235
+ !('format' in document) ||
236
+ typeof document.format !== 'string' ||
237
+ !('source' in document) ||
238
+ typeof document.source !== 'object' ||
239
+ document.source == null ||
240
+ !('bytes' in document.source)
241
+ ) {
242
+ return false;
243
+ }
244
+ const bytes = document.source.bytes;
245
+ return (
246
+ bytes instanceof Uint8Array ||
247
+ (typeof bytes === 'object' && bytes != null && isSerializedBuffer(bytes))
248
+ );
249
+ }
250
+
251
+ function toStandardFileBlock(
252
+ block: BedrockDocumentBlock
253
+ ): StandardBase64FileBlock | undefined {
254
+ const mimeType = BEDROCK_DOCUMENT_MIME_TYPES[block.document.format];
255
+ if (mimeType == null) {
256
+ return undefined;
257
+ }
258
+ const source = block.document.source.bytes;
259
+ const bytes = source instanceof Uint8Array ? source : source.data;
260
+ return {
261
+ type: 'file',
262
+ source_type: 'base64',
263
+ mime_type: mimeType,
264
+ data: Buffer.from(bytes).toString('base64'),
265
+ metadata: { name: block.document.name },
266
+ };
267
+ }
268
+
269
+ function isStandardFileBlock(
270
+ block: ProviderContentBlock
271
+ ): block is StandardBase64FileBlock {
272
+ return (
273
+ block.type === 'file' &&
274
+ 'source_type' in block &&
275
+ block.source_type === 'base64' &&
276
+ 'mime_type' in block &&
277
+ typeof block.mime_type === 'string' &&
278
+ 'data' in block &&
279
+ typeof block.data === 'string'
280
+ );
281
+ }
282
+
283
+ function shouldRetainStandardFile(
284
+ provider: t.ProviderName,
285
+ mimeType: string
286
+ ): boolean {
287
+ const normalizedMimeType = mimeType.split(';', 1)[0].trim().toLowerCase();
288
+ if (
289
+ provider === Providers.ANTHROPIC ||
290
+ getProviderFamily(provider) === 'anthropic'
291
+ ) {
292
+ return (
293
+ normalizedMimeType === 'application/pdf' ||
294
+ normalizedMimeType === 'image/jpeg' ||
295
+ normalizedMimeType === 'image/png' ||
296
+ normalizedMimeType === 'image/gif' ||
297
+ normalizedMimeType === 'image/webp'
298
+ );
299
+ }
300
+ if (isOpenAILike(provider)) {
301
+ return normalizedMimeType === 'application/pdf';
302
+ }
303
+ return true;
304
+ }
305
+
306
+ function canProjectBedrockDocument(
307
+ provider: t.ProviderName,
308
+ mimeType: string
309
+ ): boolean {
310
+ if (isGoogleLike(provider)) {
311
+ return true;
312
+ }
313
+ if (
314
+ provider === Providers.ANTHROPIC ||
315
+ getProviderFamily(provider) === 'anthropic' ||
316
+ isOpenAILike(provider)
317
+ ) {
318
+ return mimeType === 'application/pdf';
319
+ }
320
+ return false;
321
+ }
322
+
323
+ function isBlankTextBlock(block: ProviderContentBlock): boolean {
324
+ return (
325
+ block.type === 'text' &&
326
+ 'text' in block &&
327
+ typeof block.text === 'string' &&
328
+ block.text.trim() === ''
329
+ );
330
+ }
331
+
332
+ function copyUsableContentPrefix(
333
+ sourceContent: ProviderContentBlock[],
334
+ end: number
335
+ ): ProviderContentBlock[] {
336
+ const content: ProviderContentBlock[] = [];
337
+ for (let index = 0; index < end; index++) {
338
+ const block = sourceContent[index];
339
+ if (!isBlankTextBlock(block)) {
340
+ content.push(block);
341
+ }
342
+ }
343
+ return content;
344
+ }
345
+
346
+ /** Reprojects persisted provider-native attachments without changing history. */
347
+ function projectAttachmentsForProvider(
348
+ messages: BaseMessage[],
349
+ provider: t.ProviderName
350
+ ): BaseMessage[] {
351
+ if (
352
+ provider === Providers.BEDROCK ||
353
+ getProviderFamily(provider) === 'bedrock'
354
+ ) {
355
+ return messages;
356
+ }
357
+ let projected: BaseMessage[] | undefined;
358
+ for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
359
+ const message = messages[messageIndex];
360
+ if (!Array.isArray(message.content)) {
361
+ continue;
362
+ }
363
+ const sourceContent = message.content as ProviderContentBlock[];
364
+ let content: ProviderContentBlock[] | undefined;
365
+ for (let blockIndex = 0; blockIndex < sourceContent.length; blockIndex++) {
366
+ const block = sourceContent[blockIndex];
367
+ if (isStandardFileBlock(block)) {
368
+ if (shouldRetainStandardFile(provider, block.mime_type)) {
369
+ content?.push(block);
370
+ continue;
371
+ }
372
+ content ??= copyUsableContentPrefix(sourceContent, blockIndex);
373
+ continue;
374
+ }
375
+ if (!isBedrockDocumentBlock(block)) {
376
+ if (content != null && !isBlankTextBlock(block)) {
377
+ content.push(block);
378
+ }
379
+ continue;
380
+ }
381
+ content ??= copyUsableContentPrefix(sourceContent, blockIndex);
382
+ const mimeType = BEDROCK_DOCUMENT_MIME_TYPES[block.document.format];
383
+ if (
384
+ mimeType == null ||
385
+ !canProjectBedrockDocument(provider, mimeType)
386
+ ) {
387
+ continue;
388
+ }
389
+ const standardFile = toStandardFileBlock(block);
390
+ if (standardFile == null) {
391
+ continue;
392
+ }
393
+ content.push(standardFile);
394
+ }
395
+ if (content == null) {
396
+ continue;
397
+ }
398
+ if (content.length === 0) {
399
+ content.push({ type: 'text', text: OMITTED_ATTACHMENT_TEXT });
400
+ }
401
+ projected ??= [...messages];
402
+ projected[messageIndex] = cloneMessage(
403
+ message,
404
+ content as MessageContentComplex[]
405
+ );
406
+ }
407
+ return projected ?? messages;
408
+ }
409
+
162
410
  function projectMessagesForProviderMode(
163
411
  { messages, provider, maxToolResultChars }: ProjectMessagesForProviderParams,
164
412
  projectionMode: ProviderMessageProjectionMode
165
413
  ): BaseMessage[] {
166
414
  const providerFamily = getProviderFamily(provider);
167
415
  const nativeOpenAIResponses = projectionMode === 'openai-responses';
168
- const providerInputMessages = projectToolStreamContentForProvider(
169
- messages,
170
- nativeOpenAIResponses ? 'native' : 'fallback',
171
- maxToolResultChars
416
+ const providerInputMessages = projectAttachmentsForProvider(
417
+ projectToolStreamContentForProvider(
418
+ messages,
419
+ nativeOpenAIResponses ? 'native' : 'fallback',
420
+ maxToolResultChars
421
+ ),
422
+ provider
172
423
  );
173
424
  if (nativeOpenAIResponses) {
174
425
  return projectOpenAIResponsesToolMessageContent(
@@ -11,6 +11,9 @@ const DEFAULT_MAX_LLM_OUTPUT_CHARS = 50000;
11
11
  * this we drop it whole rather than emit a useless sliver. */
12
12
  const MIN_PARTIAL_HIGHLIGHT_CHARS = 200;
13
13
 
14
+ export const MISSING_SEARCH_RESULT_DATA_ERROR =
15
+ 'Search provider returned no result data';
16
+
14
17
  /** Resolves the per-search highlight budget from config, the
15
18
  * `SEARCH_MAX_LLM_OUTPUT_CHARS` env var, or the default (50,000 chars). */
16
19
  export function resolveMaxLLMOutputChars(maxOutputChars?: number): number {
@@ -219,9 +222,16 @@ function formatSource(
219
222
 
220
223
  export function formatResultsForLLM(
221
224
  turn: number,
222
- results: t.SearchResultData,
225
+ results?: t.SearchResultData | null,
223
226
  maxOutputChars?: number
224
227
  ): { output: string; references: t.ResultReference[] } {
228
+ if (results == null) {
229
+ return {
230
+ output: `Search failed: ${MISSING_SEARCH_RESULT_DATA_ERROR}`,
231
+ references: [],
232
+ };
233
+ }
234
+
225
235
  /** Bound highlight content to the per-search budget before formatting */
226
236
  const trimmedHighlights = trimHighlightsToBudget(
227
237
  results,
@@ -239,6 +249,10 @@ export function formatResultsForLLM(
239
249
 
240
250
  const references: t.ResultReference[] = [];
241
251
 
252
+ if (results.error != null && results.error !== '') {
253
+ outputLines.push(`Search failed: ${results.error}`);
254
+ }
255
+
242
256
  // Organic (web) results
243
257
  if (results.organic?.length != null && results.organic.length > 0) {
244
258
  addSection(`Web Results, Turn ${turn}`);
@@ -21,7 +21,10 @@ import { INTENT_PROPERTY } from '@/tools/intentArg';
21
21
  import { createCrwScraper } from './crw-scraper';
22
22
  import { expandHighlights } from './highlights';
23
23
  import { createSearchMetrics } from './metrics';
24
- import { formatResultsForLLM } from './format';
24
+ import {
25
+ formatResultsForLLM,
26
+ MISSING_SEARCH_RESULT_DATA_ERROR,
27
+ } from './format';
25
28
  import { createDefaultLogger } from './utils';
26
29
  import { createReranker } from './rerankers';
27
30
  import { Constants } from '@/common';
@@ -66,6 +69,12 @@ export function resolveSearchOutcome(
66
69
  return `Found ${count} result${count === 1 ? '' : 's'} for "${query}"`;
67
70
  }
68
71
 
72
+ export function normalizeSearchResultData(
73
+ result: t.SearchResultData | null | undefined
74
+ ): t.SearchResultData {
75
+ return result ?? { error: MISSING_SEARCH_RESULT_DATA_ERROR };
76
+ }
77
+
69
78
  /** Distinct rows across the main search's two collections. SearXNG derives
70
79
  * both from one result array — a row matching its news heuristic lands in
71
80
  * `organic` and `topStories` alike — so summing the lengths would report
@@ -431,12 +440,13 @@ function createTool({
431
440
  }),
432
441
  });
433
442
  const turn = runnableConfig.toolCall?.turn ?? 0;
443
+ const resultData = normalizeSearchResultData(searchResult);
434
444
  const { output, references } = formatResultsForLLM(
435
445
  turn,
436
- searchResult,
446
+ resultData,
437
447
  maxOutputChars
438
448
  );
439
- const data: t.SearchResultData = { turn, ...searchResult, references };
449
+ const data: t.SearchResultData = { turn, ...resultData, references };
440
450
  const outcome = resolveSearchOutcome(data, query);
441
451
  return [
442
452
  output,
@@ -853,6 +853,8 @@ export interface LangfuseConfig {
853
853
 
854
854
  interface AgentInputFields {
855
855
  agentId: string;
856
+ /** Logical endpoint selected by the host before provider resolution. */
857
+ endpoint?: string;
856
858
  /**
857
859
  * Partition key for transient code-session ids and file refs. Agents with
858
860
  * the same key share those refs; different execution profiles/scopes must
package/src/types/llm.ts CHANGED
@@ -81,6 +81,29 @@ export type GoogleThinkingConfig = {
81
81
  export type ManagedRequestOptions = {
82
82
  promptCacheExplicit?: boolean;
83
83
  safety_identifier?: string;
84
+ /**
85
+ * Declares that this client talks to the first-party OpenAI or Azure surface.
86
+ * Gates the model-specific request *shaping* documented only for it —
87
+ * currently GPT-6 Astra's rejected sampling and logprob parameters, its
88
+ * unsupported reasoning efforts, and the encrypted reasoning it supports —
89
+ * and defaults to off.
90
+ *
91
+ * Shaping only. Which API serves the turn is not decided here: GPT-6 Astra
92
+ * serves tool calls only from the Responses API, and a caller wanting them
93
+ * must select it with `useResponsesApi`, alongside the rest of the request
94
+ * shaping that depends on which API is in use.
95
+ *
96
+ * The shaping runs inside this SDK's own request delegates. A caller that
97
+ * supplies its own `completions` or `responses` delegate replaces that
98
+ * construction and owns the request shaping for it — this flag cannot reach
99
+ * inside a delegate it did not build.
100
+ *
101
+ * Declared rather than inferred from a base URL: only the caller knows
102
+ * whether a URL is a faithful first-party route, a gateway, or a proxy with
103
+ * its own semantics, and every gate it controls removes capability, so
104
+ * guessing wrong silently degrades an endpoint the SDK cannot see.
105
+ */
106
+ firstPartyEndpoint?: boolean;
84
107
  };
85
108
  /**
86
109
  * Adaptive stream-smoothing configuration shared by every provider client.