@librechat/agents 3.6.11 → 3.6.12

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 (33) hide show
  1. package/dist/cjs/graphs/Graph.cjs +44 -116
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/contextPressureMeter.cjs +132 -0
  4. package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -0
  5. package/dist/cjs/llm/invoke.cjs +46 -75
  6. package/dist/cjs/llm/invoke.cjs.map +1 -1
  7. package/dist/cjs/llm/prepareProviderRequest.cjs +108 -0
  8. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -0
  9. package/dist/cjs/main.cjs +2 -0
  10. package/dist/cjs/messages/content.cjs +5 -4
  11. package/dist/cjs/messages/content.cjs.map +1 -1
  12. package/dist/esm/graphs/Graph.mjs +47 -119
  13. package/dist/esm/graphs/Graph.mjs.map +1 -1
  14. package/dist/esm/llm/contextPressureMeter.mjs +132 -0
  15. package/dist/esm/llm/contextPressureMeter.mjs.map +1 -0
  16. package/dist/esm/llm/invoke.mjs +48 -75
  17. package/dist/esm/llm/invoke.mjs.map +1 -1
  18. package/dist/esm/llm/prepareProviderRequest.mjs +105 -0
  19. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -0
  20. package/dist/esm/main.mjs +2 -1
  21. package/dist/esm/messages/content.mjs +5 -4
  22. package/dist/esm/messages/content.mjs.map +1 -1
  23. package/dist/types/index.d.ts +2 -0
  24. package/dist/types/llm/contextPressureMeter.d.ts +30 -0
  25. package/dist/types/llm/invoke.d.ts +31 -33
  26. package/dist/types/llm/prepareProviderRequest.d.ts +55 -0
  27. package/package.json +1 -1
  28. package/src/graphs/Graph.ts +69 -276
  29. package/src/index.ts +8 -0
  30. package/src/llm/contextPressureMeter.ts +284 -0
  31. package/src/llm/invoke.ts +136 -296
  32. package/src/llm/prepareProviderRequest.ts +357 -0
  33. package/src/messages/content.ts +3 -3
package/src/llm/invoke.ts CHANGED
@@ -12,30 +12,13 @@ import type { ChatGeneration } from '@langchain/core/outputs';
12
12
  import type { ToolCall } from '@langchain/core/messages/tool';
13
13
  import type { BaseMessage } from '@langchain/core/messages';
14
14
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
15
+ import type { PreparedProviderRequest } from '@/llm/prepareProviderRequest';
15
16
  import type { StreamLimitState } from '@/llm/streamLimits';
16
17
  import type { ContextOverflowContext } from '@/utils/errors';
17
18
  import type * as t from '@/types';
18
- import {
19
- projectCacheControlledToolOutputsToText,
20
- projectComputerCallOutputsToText,
21
- projectOpenAIChatToolMessageContent,
22
- projectOpenAIResponsesToolMessageContent,
23
- projectOpenRouterToolMessageContent,
24
- projectSingleTextToolOutputsToText,
25
- projectStructuredToolOutputsToText,
26
- projectToolStreamContentForProvider,
27
- } from '@/messages/core';
28
19
  import {
29
20
  modifyDeltaProperties,
30
- coalesceAdjacentUserTurns,
31
- strictAlternationProviders,
32
- appendPredecessorHandoffCue,
33
- removePredecessorHandoffCue,
34
21
  } from '@/messages';
35
- import {
36
- stripAnthropicCacheControl,
37
- stripBedrockCacheControl,
38
- } from '@/messages/cache';
39
22
  import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
40
23
  import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
41
24
  import {
@@ -48,23 +31,38 @@ import {
48
31
  STREAM_LIMIT_REDISPATCH_KEY,
49
32
  STREAM_LIMIT_ATTEMPT_KEY,
50
33
  } from '@/llm/streamLimits';
51
- import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
34
+ import {
35
+ assertPreparedProviderRequestFor,
36
+ prepareProviderRequest,
37
+ } from '@/llm/prepareProviderRequest';
52
38
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
53
39
  import { manualToolStreamProviders } from '@/llm/providers';
54
- import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
55
40
  import { safeDispatchCustomEvent } from '@/utils/events';
56
41
  import { getContextOverflowInfo } from '@/utils/errors';
57
42
  import { appendCallbacks } from '@/utils/callbacks';
58
43
  import { canSealPreempt } from '@/llm/preempt';
59
44
  import { initializeModel } from '@/llm/init';
60
45
 
46
+ export {
47
+ projectMessagesForProvider,
48
+ resolveServingModelId,
49
+ usesNativeOpenAIResponses,
50
+ } from '@/llm/prepareProviderRequest';
51
+ export type {
52
+ PreparedProviderRequest,
53
+ PrepareProviderRequestParams,
54
+ ProviderMessageProjectionMode,
55
+ ProviderPayloadMeasurement,
56
+ ProviderRequestContext,
57
+ } from '@/llm/prepareProviderRequest';
58
+
61
59
  /**
62
60
  * Context passed to `attemptInvoke`. Matches the subset of Graph that
63
61
  * `ChatModelStreamHandler.handle` needs *plus* the explicit
64
- * `getOrCreateToolOutputRegistry()` accessor that `attemptInvoke`
65
- * itself calls to pull the run-scoped tool-output registry off the
66
- * graph and project each relevant ToolMessage into a transient
67
- * annotated copy before the provider call.
62
+ * `getOrCreateToolOutputRegistry()` accessor used while preparing a
63
+ * provider request. Raw callers prepare inside `attemptInvoke`; Graph
64
+ * callers prepare before final payload measurement and pass the exact
65
+ * artifact through.
68
66
  *
69
67
  * The intersection is intentional: `Parameters<...>[3]` resolves
70
68
  * indirectly through the stream handler's signature (which returns
@@ -107,161 +105,6 @@ export type OnChunk = (
107
105
  /** Unique per-model-attempt sequence; see the stamp in `attemptInvoke`. */
108
106
  let streamLimitAttemptSeq = 0;
109
107
 
110
- export function usesNativeOpenAIResponses(
111
- model: t.ChatModel,
112
- provider: Providers,
113
- callOptions?: unknown
114
- ): boolean {
115
- if (!isOpenAILike(provider)) {
116
- return false;
117
- }
118
- let candidate: unknown = model;
119
- let effectiveCallOptions = callOptions;
120
- const seen = new Set<object>();
121
- for (let depth = 0; depth < 20; depth++) {
122
- if (candidate == null || typeof candidate !== 'object') {
123
- return false;
124
- }
125
- if (seen.has(candidate)) {
126
- return false;
127
- }
128
- seen.add(candidate);
129
- const runnable = candidate as {
130
- _useResponsesApi?: (options?: unknown) => boolean;
131
- bound?: unknown;
132
- defaultOptions?: unknown;
133
- last?: unknown;
134
- constructor?: { name?: unknown };
135
- };
136
- try {
137
- if (
138
- runnable.defaultOptions != null &&
139
- typeof runnable.defaultOptions === 'object' &&
140
- !Array.isArray(runnable.defaultOptions) &&
141
- effectiveCallOptions != null &&
142
- typeof effectiveCallOptions === 'object' &&
143
- !Array.isArray(effectiveCallOptions)
144
- ) {
145
- effectiveCallOptions = {
146
- ...(runnable.defaultOptions as Record<string, unknown>),
147
- ...(effectiveCallOptions as Record<string, unknown>),
148
- };
149
- } else if (effectiveCallOptions == null) {
150
- effectiveCallOptions = runnable.defaultOptions;
151
- }
152
- if (
153
- runnable._useResponsesApi?.(effectiveCallOptions) === true ||
154
- runnable._useResponsesApi?.(undefined) === true
155
- ) {
156
- return true;
157
- }
158
- } catch {
159
- // Continue through RunnableSequence/RunnableBinding wrappers.
160
- }
161
- if (
162
- typeof runnable.constructor?.name === 'string' &&
163
- runnable.constructor.name.includes('Responses')
164
- ) {
165
- return true;
166
- }
167
- if (runnable.last != null && typeof runnable.last === 'object') {
168
- candidate = runnable.last;
169
- continue;
170
- }
171
- if (runnable.bound != null && typeof runnable.bound === 'object') {
172
- candidate = runnable.bound;
173
- continue;
174
- }
175
- return false;
176
- }
177
- return false;
178
- }
179
-
180
- /**
181
- * Produces the exact provider-facing message representation before a model
182
- * adapter serializes it. This is shared by invocation and Graph's final budget
183
- * guard so structured tool output cannot grow after the payload was measured.
184
- */
185
- export function projectMessagesForProvider({
186
- model,
187
- messages,
188
- provider,
189
- maxToolResultChars,
190
- callOptions,
191
- }: {
192
- model: t.ChatModel;
193
- messages: BaseMessage[];
194
- provider: Providers;
195
- maxToolResultChars?: number;
196
- callOptions?: unknown;
197
- }): BaseMessage[] {
198
- const nativeOpenAIResponses = usesNativeOpenAIResponses(
199
- model,
200
- provider,
201
- callOptions
202
- );
203
- const providerInputMessages = projectToolStreamContentForProvider(
204
- messages,
205
- nativeOpenAIResponses ? 'native' : 'fallback',
206
- maxToolResultChars
207
- );
208
- if (nativeOpenAIResponses) {
209
- return projectOpenAIResponsesToolMessageContent(
210
- stripAnthropicCacheControl(
211
- stripBedrockCacheControl(providerInputMessages)
212
- ),
213
- maxToolResultChars
214
- );
215
- }
216
- if (provider === Providers.OPENROUTER) {
217
- return projectComputerCallOutputsToText(
218
- projectOpenRouterToolMessageContent(
219
- stripBedrockCacheControl(providerInputMessages),
220
- maxToolResultChars
221
- )
222
- );
223
- }
224
- if (isOpenAILike(provider)) {
225
- return projectComputerCallOutputsToText(
226
- projectOpenAIChatToolMessageContent(
227
- stripAnthropicCacheControl(
228
- stripBedrockCacheControl(providerInputMessages)
229
- ),
230
- maxToolResultChars
231
- )
232
- );
233
- }
234
- if (provider === Providers.ANTHROPIC) {
235
- return projectComputerCallOutputsToText(
236
- projectSingleTextToolOutputsToText(
237
- stripBedrockCacheControl(providerInputMessages),
238
- maxToolResultChars
239
- )
240
- );
241
- }
242
- if (provider === Providers.BEDROCK) {
243
- return stripAnthropicCacheControl(
244
- projectComputerCallOutputsToText(
245
- projectCacheControlledToolOutputsToText(
246
- providerInputMessages,
247
- maxToolResultChars
248
- )
249
- )
250
- );
251
- }
252
- return projectComputerCallOutputsToText(
253
- projectStructuredToolOutputsToText(
254
- projectSingleTextToolOutputsToText(
255
- stripAnthropicCacheControl(
256
- stripBedrockCacheControl(providerInputMessages)
257
- ),
258
- maxToolResultChars
259
- ),
260
- maxToolResultChars
261
- )
262
- );
263
- }
264
-
265
108
  /**
266
109
  * The registered handler that owns content-part dispatch, if any.
267
110
  *
@@ -554,36 +397,6 @@ function collectModelCallbackSources(model: unknown): Callbacks[] {
554
397
  return sources;
555
398
  }
556
399
 
557
- /**
558
- * The serving model's id, read through the same wrapper stack
559
- * `collectModelCallbackSources` walks — `bindTools` returns a
560
- * `RunnableBinding` and a system runnable pipes a `RunnableSequence`, and
561
- * neither exposes the chat model's `model` at the top level.
562
- */
563
- export function resolveServingModelId(model: unknown): string | undefined {
564
- const seen = new Set<unknown>();
565
- let current: unknown = model;
566
- while (current != null && typeof current === 'object' && !seen.has(current)) {
567
- seen.add(current);
568
- const wrapper = current as {
569
- model?: unknown;
570
- bound?: unknown;
571
- last?: unknown;
572
- steps?: unknown[];
573
- };
574
- if (typeof wrapper.model === 'string' && wrapper.model !== '') {
575
- return wrapper.model;
576
- }
577
- current =
578
- wrapper.bound ??
579
- wrapper.last ??
580
- (Array.isArray(wrapper.steps)
581
- ? wrapper.steps[wrapper.steps.length - 1]
582
- : undefined);
583
- }
584
- return undefined;
585
- }
586
-
587
400
  async function endSealedModelRun(
588
401
  context: InvokeContext | undefined,
589
402
  chunk: AIMessageChunk,
@@ -680,10 +493,7 @@ function appendStreamChunk({
680
493
  * Pass an `onChunk` callback to override this with custom chunk processing
681
494
  * (e.g. summarization delta events).
682
495
  */
683
- interface AttemptInvokeParams {
684
- model: t.ChatModel;
685
- messages: BaseMessage[];
686
- provider: Providers;
496
+ interface AttemptInvokeCommonParams {
687
497
  context?: InvokeContext;
688
498
  onChunk?: OnChunk;
689
499
  /** Accounting owner for callers that deliberately pass no `context`
@@ -692,6 +502,51 @@ interface AttemptInvokeParams {
692
502
  streamLimitState?: StreamLimitState;
693
503
  }
694
504
 
505
+ type AttemptInvokeParams = AttemptInvokeCommonParams &
506
+ (
507
+ | {
508
+ request: PreparedProviderRequest;
509
+ model?: never;
510
+ messages?: never;
511
+ provider?: never;
512
+ }
513
+ | {
514
+ request?: never;
515
+ model: t.ChatModel;
516
+ messages: BaseMessage[];
517
+ provider: Providers;
518
+ }
519
+ );
520
+
521
+ function resolveAttemptProvider(params: AttemptInvokeParams): Providers {
522
+ if (params.request != null) {
523
+ return params.request.provider;
524
+ }
525
+ return params.provider;
526
+ }
527
+
528
+ function resolveAttemptRequest(
529
+ params: AttemptInvokeParams,
530
+ config: RunnableConfig
531
+ ): PreparedProviderRequest {
532
+ if (params.request != null) {
533
+ assertPreparedProviderRequestFor(
534
+ params.request,
535
+ params.request.model,
536
+ params.request.provider,
537
+ config
538
+ );
539
+ return params.request;
540
+ }
541
+ return prepareProviderRequest({
542
+ model: params.model,
543
+ messages: params.messages,
544
+ provider: params.provider,
545
+ context: params.context,
546
+ config,
547
+ });
548
+ }
549
+
695
550
  /**
696
551
  * One model attempt. Stamps the attempt identity into callback metadata
697
552
  * (see the generation-key notes in `streamLimits.ts`), leases the attempt's
@@ -703,11 +558,12 @@ export async function attemptInvoke(
703
558
  params: AttemptInvokeParams,
704
559
  config?: RunnableConfig
705
560
  ): Promise<Partial<t.BaseGraphState>> {
561
+ const provider = resolveAttemptProvider(params);
706
562
  const stampedConfig: RunnableConfig = {
707
563
  ...config,
708
564
  metadata: {
709
565
  ...(config?.metadata ?? {}),
710
- [Constants.INVOKED_PROVIDER]: params.provider,
566
+ [Constants.INVOKED_PROVIDER]: provider,
711
567
  /**
712
568
  * One `attemptInvoke` call is one model attempt; primary, fallback,
713
569
  * and retry attempts within a node otherwise share the same langgraph
@@ -738,7 +594,14 @@ export async function attemptInvoke(
738
594
  registerActiveStreamLimitGeneration(leaseTarget, generationKey);
739
595
  }
740
596
  try {
741
- return await attemptInvokeBody(params, stampedConfig);
597
+ return await attemptInvokeBody(
598
+ {
599
+ request: resolveAttemptRequest(params, stampedConfig),
600
+ context: params.context,
601
+ onChunk: params.onChunk,
602
+ },
603
+ stampedConfig
604
+ );
742
605
  } finally {
743
606
  if (leaseTarget != null && generationKey != null) {
744
607
  releaseStreamLimitGeneration(leaseTarget, generationKey);
@@ -748,77 +611,15 @@ export async function attemptInvoke(
748
611
 
749
612
  async function attemptInvokeBody(
750
613
  {
751
- model,
752
- messages,
753
- provider,
614
+ request,
754
615
  context,
755
616
  onChunk,
756
- }: AttemptInvokeParams,
617
+ }: Pick<AttemptInvokeCommonParams, 'context' | 'onChunk'> & {
618
+ request: PreparedProviderRequest;
619
+ },
757
620
  config: RunnableConfig
758
621
  ): Promise<Partial<t.BaseGraphState>> {
759
- /**
760
- * Pull the run-scoped tool output registry off the graph (when one
761
- * exists) and project ToolMessages carrying ref metadata into a
762
- * transient annotated copy. The original `messages` array stays
763
- * untouched so the graph state never sees `[ref: …]` / `_ref`
764
- * payload.
765
- */
766
- const invocationMessages = projectMessagesForProvider({
767
- model,
768
- messages,
769
- provider,
770
- callOptions: config,
771
- });
772
- const registry = context?.getOrCreateToolOutputRegistry();
773
- const runId = config.configurable?.run_id as string | undefined;
774
- const annotated = annotateMessagesForLLM(invocationMessages, registry, runId);
775
- /**
776
- * Keyed on the provider ACTUALLY serving this call, not the agent's primary.
777
- * `createCallModel` normalizes for the primary, but `tryFallbackProviders`
778
- * re-sends the same array — so an OpenAI primary that fails after a boundary
779
- * injected two human turns would hand a Bedrock or Mistral fallback the
780
- * consecutive user turns those APIs reject, and the recovery request would
781
- * fail for a reason unrelated to the original failure.
782
- *
783
- * `attemptInvoke` is the single funnel for primary, fallback and
784
- * summarization calls, so applying it here covers all three. Idempotent, so
785
- * the primary simply re-runs a no-op over already-coalesced messages.
786
- */
787
- /**
788
- * Serving-provider re-keying for the predecessor handoff cue (#345). The
789
- * PRIMARY's cue is baked in createCallModel's measured transform stage —
790
- * appending after measurement could push a just-fits prompt over budget —
791
- * so this funnel only corrects for fallbacks crossing provider families:
792
- * a tolerant primary falling back to a Claude surface gains the cue here,
793
- * and an Anthropic primary falling back to OpenAI/Mistral/Nova has the
794
- * Claude-only synthetic turn stripped. Both helpers are identity on their
795
- * no-op paths, so the primary's own pass re-runs for free.
796
- *
797
- * The serving model id is read through the wrapper stack (`bindTools`'
798
- * binding, a system runnable's sequence) — a wrapper's top-level `.model`
799
- * is undefined, and `isAnthropicLike` would otherwise default a wrapped
800
- * Bedrock-Nova model to Claude. The context cast is widened deliberately:
801
- * the type says every context is a full Graph, but summarization passes
802
- * none and long-standing tests pass partial stubs.
803
- */
804
- const isRunProduced = (
805
- context as
806
- | { isRunProducedMessage?: (message: BaseMessage) => boolean }
807
- | undefined
808
- )?.isRunProducedMessage;
809
- const cued = isAnthropicLike(provider, {
810
- model: resolveServingModelId(model),
811
- })
812
- ? appendPredecessorHandoffCue(
813
- annotated,
814
- isRunProduced == null
815
- ? undefined
816
- : (message): boolean => isRunProduced.call(context, message)
817
- )
818
- : removePredecessorHandoffCue(annotated);
819
- const messagesForProvider = strictAlternationProviders.has(provider)
820
- ? coalesceAdjacentUserTurns(cued)
821
- : cued;
622
+ const { model, messages: messagesForProvider, provider } = request;
822
623
 
823
624
  /**
824
625
  * Stamp the provider that is ACTUALLY serving this invocation onto the
@@ -1140,6 +941,7 @@ export async function tryFallbackProviders({
1140
941
  onChunk,
1141
942
  streamLimitState,
1142
943
  overflowContext,
944
+ prepareProviderRequest: prepareFallbackRequest,
1143
945
  prepareProviderMessages,
1144
946
  }: {
1145
947
  fallbacks: t.FallbackConfig[];
@@ -1160,10 +962,18 @@ export async function tryFallbackProviders({
1160
962
  */
1161
963
  overflowContext?: ContextOverflowContext;
1162
964
  /**
1163
- * Optional final payload guard used by Graph. It receives the initialized,
1164
- * tool-bound fallback model so Responses-vs-Chat projection is exact before
1165
- * the fallback request is measured and sent.
965
+ * Optional exact-payload preparation used by Graph. The returned request is
966
+ * measured and sent without another provider projection.
1166
967
  */
968
+ prepareProviderRequest?: (input: {
969
+ model: t.ChatModel;
970
+ messages: BaseMessage[];
971
+ provider: Providers;
972
+ clientOptions?: t.ClientOptions;
973
+ maxContextTokens?: number;
974
+ config?: RunnableConfig;
975
+ }) => PreparedProviderRequest | Promise<PreparedProviderRequest>;
976
+ /** @deprecated Return a `PreparedProviderRequest` instead. */
1167
977
  prepareProviderMessages?: (input: {
1168
978
  model: t.ChatModel;
1169
979
  messages: BaseMessage[];
@@ -1213,15 +1023,35 @@ export async function tryFallbackProviders({
1213
1023
  [Constants.INVOKED_MODEL]: fbModelName,
1214
1024
  },
1215
1025
  };
1216
- const fallbackMessages =
1217
- (await prepareProviderMessages?.({
1218
- model: fbModel as t.ChatModel,
1219
- messages,
1220
- provider: fb.provider,
1221
- clientOptions: fb.clientOptions,
1222
- maxContextTokens: fb.maxContextTokens,
1223
- config: fbConfig,
1224
- })) ?? messages;
1026
+ const preparationInput = {
1027
+ model: fbModel as t.ChatModel,
1028
+ messages,
1029
+ provider: fb.provider,
1030
+ clientOptions: fb.clientOptions,
1031
+ maxContextTokens: fb.maxContextTokens,
1032
+ config: fbConfig,
1033
+ };
1034
+ const preparedRequest = await prepareFallbackRequest?.(preparationInput);
1035
+ if (preparedRequest != null) {
1036
+ assertPreparedProviderRequestFor(
1037
+ preparedRequest,
1038
+ fbModel as t.ChatModel,
1039
+ fb.provider,
1040
+ fbConfig
1041
+ );
1042
+ }
1043
+ let fallbackMessages = messages;
1044
+ if (preparedRequest == null) {
1045
+ fallbackMessages =
1046
+ (await prepareProviderMessages?.({
1047
+ model: fbModel as t.ChatModel,
1048
+ messages,
1049
+ provider: fb.provider,
1050
+ clientOptions: fb.clientOptions,
1051
+ maxContextTokens: fb.maxContextTokens,
1052
+ config: fbConfig,
1053
+ })) ?? messages;
1054
+ }
1225
1055
  /** A sibling can trip the breaker while the preparation above is
1226
1056
  * awaited — and the catch below only sees attempts that THROW, so a
1227
1057
  * provider that ignores an aborted signal and succeeds would resolve
@@ -1232,7 +1062,18 @@ export async function tryFallbackProviders({
1232
1062
  ) {
1233
1063
  throw config.signal.reason;
1234
1064
  }
1235
- const result = await attemptInvoke(
1065
+ if (preparedRequest != null) {
1066
+ return await attemptInvoke(
1067
+ {
1068
+ request: preparedRequest,
1069
+ context,
1070
+ onChunk,
1071
+ streamLimitState,
1072
+ },
1073
+ fbConfig
1074
+ );
1075
+ }
1076
+ return await attemptInvoke(
1236
1077
  {
1237
1078
  model: fbModel as t.ChatModel,
1238
1079
  messages: fallbackMessages,
@@ -1243,7 +1084,6 @@ export async function tryFallbackProviders({
1243
1084
  },
1244
1085
  fbConfig
1245
1086
  );
1246
- return result;
1247
1087
  } catch (e) {
1248
1088
  /**
1249
1089
  * A tripped stream circuit breaker is a deliberate abort, not a