@librechat/agents 3.6.11 → 3.6.13

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 (50) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +4 -0
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +45 -116
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/llm/contextPressureMeter.cjs +212 -0
  6. package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -0
  7. package/dist/cjs/llm/invoke.cjs +46 -75
  8. package/dist/cjs/llm/invoke.cjs.map +1 -1
  9. package/dist/cjs/llm/prepareProviderRequest.cjs +108 -0
  10. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -0
  11. package/dist/cjs/llm/tokenCounterCacheCompatibility.cjs +14 -0
  12. package/dist/cjs/llm/tokenCounterCacheCompatibility.cjs.map +1 -0
  13. package/dist/cjs/main.cjs +4 -0
  14. package/dist/cjs/messages/content.cjs +5 -4
  15. package/dist/cjs/messages/content.cjs.map +1 -1
  16. package/dist/cjs/utils/tokens.cjs +3 -2
  17. package/dist/cjs/utils/tokens.cjs.map +1 -1
  18. package/dist/esm/agents/AgentContext.mjs +4 -0
  19. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +48 -119
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/llm/contextPressureMeter.mjs +211 -0
  23. package/dist/esm/llm/contextPressureMeter.mjs.map +1 -0
  24. package/dist/esm/llm/invoke.mjs +48 -75
  25. package/dist/esm/llm/invoke.mjs.map +1 -1
  26. package/dist/esm/llm/prepareProviderRequest.mjs +105 -0
  27. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -0
  28. package/dist/esm/llm/tokenCounterCacheCompatibility.mjs +13 -0
  29. package/dist/esm/llm/tokenCounterCacheCompatibility.mjs.map +1 -0
  30. package/dist/esm/main.mjs +3 -1
  31. package/dist/esm/messages/content.mjs +5 -4
  32. package/dist/esm/messages/content.mjs.map +1 -1
  33. package/dist/esm/utils/tokens.mjs +3 -2
  34. package/dist/esm/utils/tokens.mjs.map +1 -1
  35. package/dist/types/agents/AgentContext.d.ts +3 -0
  36. package/dist/types/index.d.ts +3 -0
  37. package/dist/types/llm/contextPressureMeter.d.ts +36 -0
  38. package/dist/types/llm/invoke.d.ts +31 -33
  39. package/dist/types/llm/prepareProviderRequest.d.ts +55 -0
  40. package/dist/types/llm/tokenCounterCacheCompatibility.d.ts +7 -0
  41. package/package.json +2 -1
  42. package/src/agents/AgentContext.ts +9 -0
  43. package/src/graphs/Graph.ts +70 -276
  44. package/src/index.ts +9 -0
  45. package/src/llm/contextPressureMeter.ts +449 -0
  46. package/src/llm/invoke.ts +136 -296
  47. package/src/llm/prepareProviderRequest.ts +357 -0
  48. package/src/llm/tokenCounterCacheCompatibility.ts +22 -0
  49. package/src/messages/content.ts +3 -3
  50. package/src/utils/tokens.ts +3 -2
@@ -0,0 +1,357 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type { BaseMessage } from '@langchain/core/messages';
3
+ import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
4
+ import type * as t from '@/types';
5
+ import {
6
+ projectCacheControlledToolOutputsToText,
7
+ projectComputerCallOutputsToText,
8
+ projectOpenAIChatToolMessageContent,
9
+ projectOpenAIResponsesToolMessageContent,
10
+ projectOpenRouterToolMessageContent,
11
+ projectSingleTextToolOutputsToText,
12
+ projectStructuredToolOutputsToText,
13
+ projectToolStreamContentForProvider,
14
+ } from '@/messages/core';
15
+ import {
16
+ coalesceAdjacentUserTurns,
17
+ strictAlternationProviders,
18
+ appendPredecessorHandoffCue,
19
+ removePredecessorHandoffCue,
20
+ } from '@/messages';
21
+ import {
22
+ stripAnthropicCacheControl,
23
+ stripBedrockCacheControl,
24
+ } from '@/messages/cache';
25
+ import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
26
+ import { Providers } from '@/common';
27
+ import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
28
+
29
+ const preparedProviderRequestBrand = Symbol('PreparedProviderRequest');
30
+
31
+ export type ProviderMessageProjectionMode =
32
+ | 'chat-messages'
33
+ | 'openai-responses';
34
+
35
+ export interface ProviderPayloadMeasurement {
36
+ readonly fits: boolean;
37
+ readonly projectedMessageTokens?: number;
38
+ readonly availableMessageTokens?: number;
39
+ readonly contextBudget?: number;
40
+ readonly effectiveInstructionTokens?: number;
41
+ }
42
+
43
+ export interface PreparedProviderRequest {
44
+ readonly model: t.ChatModel;
45
+ readonly modelId?: string;
46
+ readonly provider: Providers;
47
+ readonly projectionMode: ProviderMessageProjectionMode;
48
+ readonly messages: BaseMessage[];
49
+ readonly measurement?: ProviderPayloadMeasurement;
50
+ readonly [preparedProviderRequestBrand]: true;
51
+ }
52
+
53
+ type PreparedProviderRequestData = Omit<
54
+ PreparedProviderRequest,
55
+ typeof preparedProviderRequestBrand
56
+ >;
57
+
58
+ export interface ProviderRequestContext {
59
+ getOrCreateToolOutputRegistry?(): ToolOutputReferenceRegistry | undefined;
60
+ isRunProducedMessage?(message: BaseMessage): boolean;
61
+ }
62
+
63
+ export interface PrepareProviderRequestParams {
64
+ model: t.ChatModel;
65
+ messages: BaseMessage[];
66
+ provider: Providers;
67
+ context?: ProviderRequestContext;
68
+ config?: RunnableConfig;
69
+ maxToolResultChars?: number;
70
+ measure?: (messages: BaseMessage[]) => ProviderPayloadMeasurement;
71
+ }
72
+
73
+ export function usesNativeOpenAIResponses(
74
+ model: t.ChatModel,
75
+ provider: Providers,
76
+ callOptions?: unknown
77
+ ): boolean {
78
+ if (!isOpenAILike(provider)) {
79
+ return false;
80
+ }
81
+ let candidate: unknown = model;
82
+ let effectiveCallOptions = callOptions;
83
+ const seen = new Set<object>();
84
+ for (let depth = 0; depth < 20; depth++) {
85
+ if (candidate == null || typeof candidate !== 'object') {
86
+ return false;
87
+ }
88
+ if (seen.has(candidate)) {
89
+ return false;
90
+ }
91
+ seen.add(candidate);
92
+ const runnable = candidate as {
93
+ _useResponsesApi?: (options?: unknown) => boolean;
94
+ bound?: unknown;
95
+ defaultOptions?: unknown;
96
+ last?: unknown;
97
+ constructor?: { name?: unknown };
98
+ };
99
+ try {
100
+ if (
101
+ runnable.defaultOptions != null &&
102
+ typeof runnable.defaultOptions === 'object' &&
103
+ !Array.isArray(runnable.defaultOptions) &&
104
+ effectiveCallOptions != null &&
105
+ typeof effectiveCallOptions === 'object' &&
106
+ !Array.isArray(effectiveCallOptions)
107
+ ) {
108
+ effectiveCallOptions = {
109
+ ...(runnable.defaultOptions as Record<string, unknown>),
110
+ ...(effectiveCallOptions as Record<string, unknown>),
111
+ };
112
+ } else if (effectiveCallOptions == null) {
113
+ effectiveCallOptions = runnable.defaultOptions;
114
+ }
115
+ if (
116
+ runnable._useResponsesApi?.(effectiveCallOptions) === true ||
117
+ runnable._useResponsesApi?.(undefined) === true
118
+ ) {
119
+ return true;
120
+ }
121
+ } catch {
122
+ // Continue through RunnableSequence/RunnableBinding wrappers.
123
+ }
124
+ if (
125
+ typeof runnable.constructor?.name === 'string' &&
126
+ runnable.constructor.name.includes('Responses')
127
+ ) {
128
+ return true;
129
+ }
130
+ if (runnable.last != null && typeof runnable.last === 'object') {
131
+ candidate = runnable.last;
132
+ continue;
133
+ }
134
+ if (runnable.bound != null && typeof runnable.bound === 'object') {
135
+ candidate = runnable.bound;
136
+ continue;
137
+ }
138
+ return false;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ function resolveProviderMessageProjectionMode(
144
+ model: t.ChatModel,
145
+ provider: Providers,
146
+ callOptions?: unknown
147
+ ): ProviderMessageProjectionMode {
148
+ return usesNativeOpenAIResponses(model, provider, callOptions)
149
+ ? 'openai-responses'
150
+ : 'chat-messages';
151
+ }
152
+
153
+ interface ProjectMessagesForProviderParams {
154
+ model: t.ChatModel;
155
+ messages: BaseMessage[];
156
+ provider: Providers;
157
+ maxToolResultChars?: number;
158
+ callOptions?: unknown;
159
+ }
160
+
161
+ function projectMessagesForProviderMode({
162
+ messages,
163
+ provider,
164
+ maxToolResultChars,
165
+ }: ProjectMessagesForProviderParams,
166
+ projectionMode: ProviderMessageProjectionMode
167
+ ): BaseMessage[] {
168
+ const nativeOpenAIResponses = projectionMode === 'openai-responses';
169
+ const providerInputMessages = projectToolStreamContentForProvider(
170
+ messages,
171
+ nativeOpenAIResponses ? 'native' : 'fallback',
172
+ maxToolResultChars
173
+ );
174
+ if (nativeOpenAIResponses) {
175
+ return projectOpenAIResponsesToolMessageContent(
176
+ stripAnthropicCacheControl(
177
+ stripBedrockCacheControl(providerInputMessages)
178
+ ),
179
+ maxToolResultChars
180
+ );
181
+ }
182
+ if (provider === Providers.OPENROUTER) {
183
+ return projectComputerCallOutputsToText(
184
+ projectOpenRouterToolMessageContent(
185
+ stripBedrockCacheControl(providerInputMessages),
186
+ maxToolResultChars
187
+ )
188
+ );
189
+ }
190
+ if (isOpenAILike(provider)) {
191
+ return projectComputerCallOutputsToText(
192
+ projectOpenAIChatToolMessageContent(
193
+ stripAnthropicCacheControl(
194
+ stripBedrockCacheControl(providerInputMessages)
195
+ ),
196
+ maxToolResultChars
197
+ )
198
+ );
199
+ }
200
+ if (provider === Providers.ANTHROPIC) {
201
+ return projectComputerCallOutputsToText(
202
+ projectSingleTextToolOutputsToText(
203
+ stripBedrockCacheControl(providerInputMessages),
204
+ maxToolResultChars
205
+ )
206
+ );
207
+ }
208
+ if (provider === Providers.BEDROCK) {
209
+ return stripAnthropicCacheControl(
210
+ projectComputerCallOutputsToText(
211
+ projectCacheControlledToolOutputsToText(
212
+ providerInputMessages,
213
+ maxToolResultChars
214
+ )
215
+ )
216
+ );
217
+ }
218
+ return projectComputerCallOutputsToText(
219
+ projectStructuredToolOutputsToText(
220
+ projectSingleTextToolOutputsToText(
221
+ stripAnthropicCacheControl(
222
+ stripBedrockCacheControl(providerInputMessages)
223
+ ),
224
+ maxToolResultChars
225
+ ),
226
+ maxToolResultChars
227
+ )
228
+ );
229
+ }
230
+
231
+ /** Produces the provider-facing representation before adapter serialization. */
232
+ export function projectMessagesForProvider(
233
+ params: ProjectMessagesForProviderParams
234
+ ): BaseMessage[] {
235
+ return projectMessagesForProviderMode(
236
+ params,
237
+ resolveProviderMessageProjectionMode(
238
+ params.model,
239
+ params.provider,
240
+ params.callOptions
241
+ )
242
+ );
243
+ }
244
+
245
+ /** Reads the serving model id through LangChain binding/sequence wrappers. */
246
+ export function resolveServingModelId(model: unknown): string | undefined {
247
+ const seen = new Set<unknown>();
248
+ let current: unknown = model;
249
+ while (current != null && typeof current === 'object' && !seen.has(current)) {
250
+ seen.add(current);
251
+ const wrapper = current as {
252
+ model?: unknown;
253
+ bound?: unknown;
254
+ last?: unknown;
255
+ steps?: unknown[];
256
+ };
257
+ if (typeof wrapper.model === 'string' && wrapper.model !== '') {
258
+ return wrapper.model;
259
+ }
260
+ current =
261
+ wrapper.bound ??
262
+ wrapper.last ??
263
+ (Array.isArray(wrapper.steps)
264
+ ? wrapper.steps[wrapper.steps.length - 1]
265
+ : undefined);
266
+ }
267
+ return undefined;
268
+ }
269
+
270
+ /**
271
+ * Finalizes one provider request and measures the exact message array that
272
+ * will be passed to LangChain. Source messages remain untouched.
273
+ */
274
+ export function prepareProviderRequest({
275
+ model,
276
+ messages,
277
+ provider,
278
+ context,
279
+ config,
280
+ maxToolResultChars,
281
+ measure,
282
+ }: PrepareProviderRequestParams): PreparedProviderRequest {
283
+ const projectionMode = resolveProviderMessageProjectionMode(
284
+ model,
285
+ provider,
286
+ config
287
+ );
288
+ const projected = projectMessagesForProviderMode(
289
+ {
290
+ model,
291
+ messages,
292
+ provider,
293
+ maxToolResultChars,
294
+ callOptions: config,
295
+ },
296
+ projectionMode
297
+ );
298
+ const registry = context?.getOrCreateToolOutputRegistry?.();
299
+ const runId = config?.configurable?.run_id as string | undefined;
300
+ const annotated = annotateMessagesForLLM(projected, registry, runId);
301
+ const isRunProduced = context?.isRunProducedMessage;
302
+ const modelId = resolveServingModelId(model);
303
+ const cued = isAnthropicLike(provider, {
304
+ model: modelId,
305
+ })
306
+ ? appendPredecessorHandoffCue(
307
+ annotated,
308
+ isRunProduced == null
309
+ ? undefined
310
+ : (message): boolean => isRunProduced.call(context, message)
311
+ )
312
+ : removePredecessorHandoffCue(annotated);
313
+ const preparedMessages = strictAlternationProviders.has(provider)
314
+ ? coalesceAdjacentUserTurns(cued)
315
+ : cued;
316
+
317
+ const request: PreparedProviderRequestData = {
318
+ model,
319
+ modelId,
320
+ provider,
321
+ projectionMode,
322
+ messages: preparedMessages,
323
+ measurement: measure?.(preparedMessages),
324
+ };
325
+ Object.defineProperty(request, preparedProviderRequestBrand, {
326
+ value: true,
327
+ enumerable: false,
328
+ });
329
+ return Object.freeze(request) as PreparedProviderRequest;
330
+ }
331
+
332
+ export function assertPreparedProviderRequestFor(
333
+ request: PreparedProviderRequest,
334
+ model: t.ChatModel,
335
+ provider: Providers,
336
+ config?: RunnableConfig
337
+ ): void {
338
+ if (
339
+ !Object.prototype.hasOwnProperty.call(request, preparedProviderRequestBrand)
340
+ ) {
341
+ throw new Error('Invalid prepared provider request');
342
+ }
343
+ if (request.model !== model) {
344
+ throw new Error('Prepared provider request does not match serving model');
345
+ }
346
+ if (request.provider !== provider) {
347
+ throw new Error('Prepared provider request does not match serving provider');
348
+ }
349
+ if (
350
+ request.projectionMode !==
351
+ resolveProviderMessageProjectionMode(model, provider, config)
352
+ ) {
353
+ throw new Error(
354
+ 'Prepared provider request does not match invocation options'
355
+ );
356
+ }
357
+ }
@@ -0,0 +1,22 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+
3
+ const stableMessageTokenCounters = new WeakSet<
4
+ (message: BaseMessage) => number
5
+ >();
6
+
7
+ /**
8
+ * Opts a deterministic counter into stable-message count reuse. The counter
9
+ * must depend only on the message surface consumed by `getTokenCountForMessage`.
10
+ */
11
+ export function markTokenCounterCacheCompatible<
12
+ TCounter extends (message: BaseMessage) => number,
13
+ >(tokenCounter: TCounter): TCounter {
14
+ stableMessageTokenCounters.add(tokenCounter);
15
+ return tokenCounter;
16
+ }
17
+
18
+ export function isTokenCounterCacheCompatible(
19
+ tokenCounter: (message: BaseMessage) => number
20
+ ): boolean {
21
+ return stableMessageTokenCounters.has(tokenCounter);
22
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  MessageContentComplex,
4
4
  } from '@langchain/core/messages';
5
5
  import { ContentTypes } from '@/common';
6
+ import { cloneMessage } from './cache';
6
7
 
7
8
  /**
8
9
  * Whether {@link formatContentStrings} will flatten this message's content:
@@ -42,13 +43,12 @@ export const formatContentStrings = (
42
43
  const blocks = message.content as MessageContentComplex[];
43
44
  const content = blocks.reduce((acc, curr) => {
44
45
  if (curr.type === ContentTypes.TEXT) {
45
- return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
46
+ return `${acc}${curr[ContentTypes.TEXT] ?? ''}\n`;
46
47
  }
47
48
  return acc;
48
49
  }, '');
49
50
 
50
- message.content = content.trim();
51
- result.push(message);
51
+ result.push(cloneMessage(message, content.trim()));
52
52
  }
53
53
 
54
54
  return result;
@@ -7,6 +7,7 @@ import {
7
7
  serializeStructuredValueBounded,
8
8
  } from './toolContent';
9
9
  import { ContentTypes } from '@/common/enum';
10
+ import { markTokenCounterCacheCompatible } from '@/llm/tokenCounterCacheCompatibility';
10
11
 
11
12
  export type EncodingName = 'o200k_base' | 'claude';
12
13
 
@@ -1266,13 +1267,13 @@ export const createTokenCounter = async (
1266
1267
  const tok = await getTokenizer(encoding);
1267
1268
  const countTokens = (text: string): number => tok.count(text);
1268
1269
  const isClaude = encoding === 'claude';
1269
- return (message: BaseMessage): number => {
1270
+ return markTokenCounterCacheCompatible((message: BaseMessage): number => {
1270
1271
  const count = getTokenCountForMessage(message, countTokens, encoding);
1271
1272
  const correctedCount = isClaude
1272
1273
  ? Math.ceil(count * CLAUDE_TOKEN_CORRECTION)
1273
1274
  : count;
1274
1275
  return ensureSafeTokenMeasurement(correctedCount, 'message');
1275
- };
1276
+ });
1276
1277
  };
1277
1278
 
1278
1279
  /** Utility to manage the token encoder lifecycle explicitly. */