@librechat/agents 3.7.17 → 3.7.19

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 (49) 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/bedrock/utils/message_inputs.cjs +67 -20
  8. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/llm/invoke.cjs +17 -2
  10. package/dist/cjs/llm/invoke.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/tools/search/rerankers.cjs +8 -3
  14. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  15. package/dist/cjs/tools/search/tool.cjs +2 -1
  16. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  17. package/dist/esm/agents/AgentContext.mjs +5 -2
  18. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  19. package/dist/esm/graphs/Graph.mjs +2 -1
  20. package/dist/esm/graphs/Graph.mjs.map +1 -1
  21. package/dist/esm/langfuse.mjs +6 -2
  22. package/dist/esm/langfuse.mjs.map +1 -1
  23. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +67 -20
  24. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  25. package/dist/esm/llm/invoke.mjs +17 -2
  26. package/dist/esm/llm/invoke.mjs.map +1 -1
  27. package/dist/esm/llm/prepareProviderRequest.mjs +104 -3
  28. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
  29. package/dist/esm/tools/search/rerankers.mjs +8 -3
  30. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  31. package/dist/esm/tools/search/tool.mjs +2 -1
  32. package/dist/esm/tools/search/tool.mjs.map +1 -1
  33. package/dist/types/agents/AgentContext.d.ts +4 -1
  34. package/dist/types/langfuse.d.ts +5 -1
  35. package/dist/types/llm/prepareProviderRequest.d.ts +1 -1
  36. package/dist/types/tools/search/rerankers.d.ts +4 -1
  37. package/dist/types/tools/search/types.d.ts +1 -0
  38. package/dist/types/types/graph.d.ts +2 -0
  39. package/package.json +1 -1
  40. package/src/agents/AgentContext.ts +7 -0
  41. package/src/graphs/Graph.ts +2 -1
  42. package/src/langfuse.ts +12 -0
  43. package/src/llm/bedrock/utils/message_inputs.ts +86 -8
  44. package/src/llm/invoke.ts +23 -2
  45. package/src/llm/prepareProviderRequest.ts +257 -6
  46. package/src/tools/search/rerankers.ts +14 -1
  47. package/src/tools/search/tool.ts +3 -1
  48. package/src/tools/search/types.ts +1 -0
  49. package/src/types/graph.ts +2 -0
@@ -151,6 +151,7 @@ import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
151
151
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
152
152
  import { SUBAGENT_REPLAY_CONTROLLER } from '@/tools/subagent/SubagentReplay';
153
153
  import { applyGraphRuntimeConfig } from '@/graphs/applyGraphRuntimeConfig';
154
+ import { isFadingTier, isInformativeFadingTier } from '@/messages/fading';
154
155
  import { createContextPressureMeter } from '@/llm/contextPressureMeter';
155
156
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
156
157
  import { prepareProviderRequest } from '@/llm/prepareProviderRequest';
@@ -163,7 +164,6 @@ import { initializeLangfuseTracing } from '@/instrumentation';
163
164
  import { shouldTriggerSummarization } from '@/summarization';
164
165
  import { isRunStepResumeState } from '@/tools/runStepResume';
165
166
  import { resolveLocalToolsForBinding } from '@/tools/local';
166
- import { isFadingTier, isInformativeFadingTier } from '@/messages/fading';
167
167
  import { createSummarizeNode } from '@/summarization/node';
168
168
  import { getTruncationStopReason } from '@/llm/truncation';
169
169
  import { messagesStateReducer } from '@/messages/reducer';
@@ -3992,6 +3992,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3992
3992
  rootAgentName: this.agentContexts.get(this.defaultAgentId)?.name,
3993
3993
  activeAgentId: agentId,
3994
3994
  activeAgentName: agentContext.name,
3995
+ endpoint: agentContext.endpoint,
3995
3996
  });
3996
3997
  let langfuseHandler: CallbackEntry | undefined;
3997
3998
  let invokeConfig = {
package/src/langfuse.ts CHANGED
@@ -840,6 +840,10 @@ export function createLangfuseTraceMetadata({
840
840
  rootAgentName,
841
841
  activeAgentId,
842
842
  activeAgentName,
843
+ endpoint,
844
+ model,
845
+ provider,
846
+ resolvedProvider,
843
847
  }: {
844
848
  messageId?: unknown;
845
849
  parentMessageId?: unknown;
@@ -849,6 +853,10 @@ export function createLangfuseTraceMetadata({
849
853
  rootAgentName?: unknown;
850
854
  activeAgentId?: unknown;
851
855
  activeAgentName?: unknown;
856
+ endpoint?: unknown;
857
+ model?: unknown;
858
+ provider?: unknown;
859
+ resolvedProvider?: unknown;
852
860
  }): LangfuseTraceMetadata {
853
861
  return createTraceMetadata({
854
862
  messageId,
@@ -859,6 +867,10 @@ export function createLangfuseTraceMetadata({
859
867
  rootAgentName,
860
868
  activeAgentId,
861
869
  activeAgentName,
870
+ endpoint,
871
+ model,
872
+ provider,
873
+ resolvedProvider,
862
874
  });
863
875
  }
864
876
 
@@ -58,6 +58,15 @@ const FOREIGN_SERVER_TOOL_TYPES = ['toolCall', 'toolResponse'];
58
58
  * also has no tool calls, fall back to this placeholder text.
59
59
  */
60
60
  const BEDROCK_EMPTY_TEXT_PLACEHOLDER = '_';
61
+ /**
62
+ * Whether a converted user message carries top-level text and document blocks, recorded
63
+ * during its single conversion pass and OR-ed through merging so finalization never
64
+ * rescans content.
65
+ */
66
+ const userGroupShape = new WeakMap<
67
+ BedrockMessage,
68
+ { hasText: boolean; hasDocument: boolean }
69
+ >();
61
70
 
62
71
  /**
63
72
  * Convert a LangChain reasoning block to a Bedrock reasoning block.
@@ -965,20 +974,72 @@ function convertFromV1ToChatBedrockConverseMessage(
965
974
  function convertHumanMessageToConverseMessage(
966
975
  msg: BaseMessage
967
976
  ): BedrockMessage {
968
- const userMessage: BedrockMessage = {
969
- role: 'user',
970
- content: [],
977
+ const contentBlocks: BedrockContentBlock[] = [];
978
+ let hasText = false;
979
+ let hasDocument = false;
980
+ // Whitespace with nothing before it to fold into is held until the next text fragment,
981
+ // so `[' ', 'hello']` and `[image, ' ', 'hello']` keep their leading space instead of
982
+ // losing it; whitespace left over at the end has nothing to attach to and is dropped.
983
+ let pendingWhitespace = '';
984
+ const appendText = (text: string): void => {
985
+ const candidate = pendingWhitespace + text;
986
+ if (appendSerializableBedrockTextBlock(contentBlocks, candidate)) {
987
+ hasText = true;
988
+ pendingWhitespace = '';
989
+ return;
990
+ }
991
+ pendingWhitespace = candidate;
971
992
  };
972
993
 
973
994
  if (typeof msg.content === 'string') {
974
- userMessage.content = [{ text: msg.content }];
995
+ appendText(msg.content);
975
996
  } else if (Array.isArray(msg.content)) {
976
- userMessage.content = msg.content.map((block) =>
977
- convertLangChainContentBlockToConverseContentBlock({ block })
978
- );
997
+ for (const block of msg.content) {
998
+ if (
999
+ block.type === 'text' &&
1000
+ typeof (block as { text?: unknown }).text === 'string'
1001
+ ) {
1002
+ appendText((block as { text: string }).text);
1003
+ continue;
1004
+ }
1005
+ const converted = convertLangChainContentBlockToConverseContentBlock({
1006
+ block,
1007
+ });
1008
+ if ('document' in converted) {
1009
+ hasDocument = true;
1010
+ }
1011
+ contentBlocks.push(converted);
1012
+ }
979
1013
  }
980
1014
 
981
- return userMessage;
1015
+ const message: BedrockMessage = { role: 'user', content: contentBlocks };
1016
+ userGroupShape.set(message, { hasText, hasDocument });
1017
+ return message;
1018
+ }
1019
+
1020
+ /**
1021
+ * Bedrock rejects an empty user message just as it rejects a blank text block
1022
+ * (`The text field in the ContentBlock object at messages.N.content.0 is blank`), and the
1023
+ * rejection fences the whole conversation on every retry. A promptless send — attachment, no
1024
+ * typed text — serializes to exactly that. Applied only once a user group is complete: an
1025
+ * empty human turn adjacent to a tool result merges into it below, and a per-message
1026
+ * placeholder would inject a synthetic `_` into an otherwise valid message.
1027
+ */
1028
+ function finalizeUserMessage(message: BedrockMessage): void {
1029
+ if (message.role !== 'user') {
1030
+ return;
1031
+ }
1032
+ if (message.content == null || message.content.length === 0) {
1033
+ message.content = [{ text: BEDROCK_EMPTY_TEXT_PLACEHOLDER }];
1034
+ return;
1035
+ }
1036
+ // Converse also requires a text block in any message that carries a document block. A
1037
+ // promptless document upload arrives as blank text plus the document; the blank is
1038
+ // dropped during conversion, so supply the placeholder if no text survived.
1039
+ const shape = userGroupShape.get(message);
1040
+ if (shape != null && shape.hasDocument && !shape.hasText) {
1041
+ message.content.push({ text: BEDROCK_EMPTY_TEXT_PLACEHOLDER });
1042
+ }
982
1043
  }
983
1044
 
984
1045
  /**
@@ -1110,13 +1171,30 @@ export function convertToConverseMessages(messages: BaseMessage[]): {
1110
1171
  const lastMessage = acc[acc.length - 1];
1111
1172
  if (lastMessage.role === 'user' && curr.role === 'user') {
1112
1173
  lastMessage.content = lastMessage.content?.concat(curr.content ?? []);
1174
+ const lastShape = userGroupShape.get(lastMessage);
1175
+ const nextShape = userGroupShape.get(curr);
1176
+ if (lastShape != null || nextShape != null) {
1177
+ userGroupShape.set(lastMessage, {
1178
+ hasText:
1179
+ (lastShape?.hasText ?? false) || (nextShape?.hasText ?? false),
1180
+ hasDocument:
1181
+ (lastShape?.hasDocument ?? false) ||
1182
+ (nextShape?.hasDocument ?? false),
1183
+ });
1184
+ }
1113
1185
  } else {
1186
+ finalizeUserMessage(lastMessage);
1114
1187
  acc.push(curr);
1115
1188
  }
1116
1189
  return acc;
1117
1190
  },
1118
1191
  []
1119
1192
  );
1193
+ if (combinedConverseMessages.length > 0) {
1194
+ finalizeUserMessage(
1195
+ combinedConverseMessages[combinedConverseMessages.length - 1]
1196
+ );
1197
+ }
1120
1198
 
1121
1199
  return { converseMessages: combinedConverseMessages, converseSystem };
1122
1200
  }
package/src/llm/invoke.ts CHANGED
@@ -692,11 +692,32 @@ export async function attemptInvoke(
692
692
  config?: RunnableConfig
693
693
  ): Promise<Partial<t.BaseGraphState>> {
694
694
  const provider = resolveAttemptProvider(params);
695
- const stampedConfig: RunnableConfig = {
695
+ const providerStampedConfig: RunnableConfig = {
696
696
  ...config,
697
697
  metadata: {
698
698
  ...(config?.metadata ?? {}),
699
699
  [Constants.INVOKED_PROVIDER]: provider,
700
+ provider,
701
+ resolvedProvider: provider,
702
+ },
703
+ };
704
+ const request = resolveAttemptRequest(params, providerStampedConfig);
705
+ const configuredModel = providerStampedConfig.metadata?.[
706
+ Constants.INVOKED_MODEL
707
+ ];
708
+ const modelId =
709
+ request.modelId ??
710
+ (typeof configuredModel === 'string' ? configuredModel : undefined);
711
+ const stampedConfig: RunnableConfig = {
712
+ ...providerStampedConfig,
713
+ metadata: {
714
+ ...providerStampedConfig.metadata,
715
+ ...(modelId == null
716
+ ? {}
717
+ : {
718
+ [Constants.INVOKED_MODEL]: modelId,
719
+ model: modelId,
720
+ }),
700
721
  /**
701
722
  * One `attemptInvoke` call is one model attempt; primary, fallback,
702
723
  * and retry attempts within a node otherwise share the same langgraph
@@ -727,7 +748,7 @@ export async function attemptInvoke(
727
748
  try {
728
749
  return await attemptInvokeBody(
729
750
  {
730
- request: resolveAttemptRequest(params, stampedConfig),
751
+ request,
731
752
  context: params.context,
732
753
  onChunk: params.onChunk,
733
754
  preemptAgentId: params.preemptAgentId,
@@ -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(
@@ -4,6 +4,7 @@ import { createDefaultLogger, formatErrorForLog } from './utils';
4
4
  import { createSearchMetrics } from './metrics';
5
5
 
6
6
  const DEFAULT_JINA_API_URL = 'https://api.jina.ai/v1/rerank';
7
+ const DEFAULT_COHERE_API_URL = 'https://api.cohere.com/v2/rerank';
7
8
 
8
9
  /** Every other network call in the search pipeline is bounded (scrapers,
9
10
  * search providers); rerank requests must be too, or a hung rerank API
@@ -15,6 +16,11 @@ const getDefaultJinaApiUrl = (): string =>
15
16
  ? process.env.JINA_API_URL
16
17
  : DEFAULT_JINA_API_URL;
17
18
 
19
+ const getDefaultCohereApiUrl = (): string =>
20
+ process.env.COHERE_API_URL != null && process.env.COHERE_API_URL !== ''
21
+ ? process.env.COHERE_API_URL
22
+ : DEFAULT_COHERE_API_URL;
23
+
18
24
  export abstract class BaseReranker {
19
25
  protected apiKey: string | undefined;
20
26
  protected logger: t.Logger;
@@ -219,23 +225,27 @@ export class JinaReranker extends BaseReranker {
219
225
 
220
226
  export class CohereReranker extends BaseReranker {
221
227
  readonly provider = 'cohere';
228
+ private apiUrl: string;
222
229
  private timeout: number;
223
230
  private httpAgent?: t.HttpAgent;
224
231
  private httpsAgent?: t.HttpsAgent;
225
232
 
226
233
  constructor({
227
234
  apiKey = process.env.COHERE_API_KEY,
235
+ apiUrl = getDefaultCohereApiUrl(),
228
236
  timeout = DEFAULT_RERANKER_TIMEOUT,
229
237
  logger,
230
238
  httpAgent,
231
239
  httpsAgent,
232
240
  }: {
233
241
  apiKey?: string;
242
+ apiUrl?: string;
234
243
  timeout?: number;
235
244
  logger?: t.Logger;
236
245
  } & t.HttpAgentConfig) {
237
246
  super(logger);
238
247
  this.apiKey = apiKey;
248
+ this.apiUrl = apiUrl;
239
249
  this.timeout = timeout;
240
250
  this.httpAgent = httpAgent;
241
251
  this.httpsAgent = httpsAgent;
@@ -263,7 +273,7 @@ export class CohereReranker extends BaseReranker {
263
273
  };
264
274
 
265
275
  const response = await axios.post<t.CohereRerankerResponse | undefined>(
266
- 'https://api.cohere.com/v2/rerank',
276
+ this.apiUrl,
267
277
  requestData,
268
278
  {
269
279
  headers: {
@@ -560,6 +570,7 @@ export const createReranker = (
560
570
  jinaApiKey?: string;
561
571
  jinaApiUrl?: string;
562
572
  cohereApiKey?: string;
573
+ cohereApiUrl?: string;
563
574
  ragApiUrl?: string;
564
575
  ragApiTokenSupplier?: t.RagApiTokenSupplier;
565
576
  ragApiProfile?: string;
@@ -572,6 +583,7 @@ export const createReranker = (
572
583
  jinaApiKey,
573
584
  jinaApiUrl,
574
585
  cohereApiKey,
586
+ cohereApiUrl,
575
587
  ragApiUrl,
576
588
  ragApiTokenSupplier,
577
589
  ragApiProfile,
@@ -597,6 +609,7 @@ export const createReranker = (
597
609
  case 'cohere':
598
610
  return new CohereReranker({
599
611
  apiKey: cohereApiKey,
612
+ apiUrl: cohereApiUrl,
600
613
  timeout: rerankerTimeout,
601
614
  logger: defaultLogger,
602
615
  httpAgent,
@@ -20,8 +20,8 @@ import { createFirecrawlScraper } from './firecrawl';
20
20
  import { INTENT_PROPERTY } from '@/tools/intentArg';
21
21
  import { createCrwScraper } from './crw-scraper';
22
22
  import { expandHighlights } from './highlights';
23
- import { formatResultsForLLM } from './format';
24
23
  import { createSearchMetrics } from './metrics';
24
+ import { formatResultsForLLM } from './format';
25
25
  import { createDefaultLogger } from './utils';
26
26
  import { createReranker } from './rerankers';
27
27
  import { Constants } from '@/common';
@@ -514,6 +514,7 @@ export const createSearchTool = (
514
514
  jinaApiKey,
515
515
  jinaApiUrl,
516
516
  cohereApiKey,
517
+ cohereApiUrl,
517
518
  ragApiUrl,
518
519
  ragApiTokenSupplier,
519
520
  ragApiProfile,
@@ -637,6 +638,7 @@ export const createSearchTool = (
637
638
  jinaApiKey,
638
639
  jinaApiUrl,
639
640
  cohereApiKey,
641
+ cohereApiUrl,
640
642
  ragApiUrl,
641
643
  ragApiTokenSupplier,
642
644
  ragApiProfile,
@@ -519,6 +519,7 @@ export interface SearchToolConfig
519
519
  jinaApiKey?: string;
520
520
  jinaApiUrl?: string;
521
521
  cohereApiKey?: string;
522
+ cohereApiUrl?: string;
522
523
  /** Base URL of the public `rag_api` service (`RAG_API_URL` env fallback).
523
524
  * Requests post to `${ragApiUrl}/v1/rerank`. */
524
525
  ragApiUrl?: string;
@@ -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