@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
@@ -0,0 +1,30 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ import type { ProviderPayloadMeasurement } from '@/llm/prepareProviderRequest';
3
+ import type * as t from '@/types';
4
+ interface ContextPressureUsage {
5
+ contextBudget?: number;
6
+ effectiveInstructionTokens?: number;
7
+ remainingContextTokens?: number;
8
+ calibrationRatio?: number;
9
+ }
10
+ interface ContextPressureMeterParams {
11
+ tokenCounter?: t.TokenCounter;
12
+ sourceMessages: BaseMessage[];
13
+ retainedMessages: BaseMessage[];
14
+ indexTokenCountMap: Record<string, number | undefined>;
15
+ contextUsage?: ContextPressureUsage | null;
16
+ instructionTokens: number;
17
+ calibrationRatio: number;
18
+ }
19
+ interface ProviderPayloadMeasureOptions {
20
+ contextBudget?: number;
21
+ forceRawRecount?: boolean;
22
+ }
23
+ export interface ContextPressureMeter {
24
+ trackProjection(before: BaseMessage[], after: BaseMessage[]): BaseMessage[];
25
+ trackClone(source: BaseMessage, clone: BaseMessage): void;
26
+ measure(messages: BaseMessage[], options?: ProviderPayloadMeasureOptions): ProviderPayloadMeasurement;
27
+ }
28
+ /** Measures repeated provider projections while tokenizing each message object once. */
29
+ export declare function createContextPressureMeter({ tokenCounter, sourceMessages, retainedMessages, indexTokenCountMap, contextUsage, instructionTokens, calibrationRatio, }: ContextPressureMeterParams): ContextPressureMeter;
30
+ export {};
@@ -2,18 +2,21 @@ import { AIMessageChunk } from '@langchain/core/messages';
2
2
  import type { RunnableConfig } from '@langchain/core/runnables';
3
3
  import type { BaseMessage } from '@langchain/core/messages';
4
4
  import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
5
+ import type { PreparedProviderRequest } from '@/llm/prepareProviderRequest';
5
6
  import type { StreamLimitState } from '@/llm/streamLimits';
6
7
  import type { ContextOverflowContext } from '@/utils/errors';
7
8
  import type * as t from '@/types';
8
9
  import { ChatModelStreamHandler } from '@/stream';
9
10
  import { Providers } from '@/common';
11
+ export { projectMessagesForProvider, resolveServingModelId, usesNativeOpenAIResponses, } from '@/llm/prepareProviderRequest';
12
+ export type { PreparedProviderRequest, PrepareProviderRequestParams, ProviderMessageProjectionMode, ProviderPayloadMeasurement, ProviderRequestContext, } from '@/llm/prepareProviderRequest';
10
13
  /**
11
14
  * Context passed to `attemptInvoke`. Matches the subset of Graph that
12
15
  * `ChatModelStreamHandler.handle` needs *plus* the explicit
13
- * `getOrCreateToolOutputRegistry()` accessor that `attemptInvoke`
14
- * itself calls to pull the run-scoped tool-output registry off the
15
- * graph and project each relevant ToolMessage into a transient
16
- * annotated copy before the provider call.
16
+ * `getOrCreateToolOutputRegistry()` accessor used while preparing a
17
+ * provider request. Raw callers prepare inside `attemptInvoke`; Graph
18
+ * callers prepare before final payload measurement and pass the exact
19
+ * artifact through.
17
20
  *
18
21
  * The intersection is intentional: `Parameters<...>[3]` resolves
19
22
  * indirectly through the stream handler's signature (which returns
@@ -46,26 +49,6 @@ export type InvokeContext = NonNullable<Parameters<ChatModelStreamHandler['handl
46
49
  * limits key each model attempt separately.
47
50
  */
48
51
  export type OnChunk = (chunk: AIMessageChunk, metadata?: Record<string, unknown>) => void | Promise<void>;
49
- export declare function usesNativeOpenAIResponses(model: t.ChatModel, provider: Providers, callOptions?: unknown): boolean;
50
- /**
51
- * Produces the exact provider-facing message representation before a model
52
- * adapter serializes it. This is shared by invocation and Graph's final budget
53
- * guard so structured tool output cannot grow after the payload was measured.
54
- */
55
- export declare function projectMessagesForProvider({ model, messages, provider, maxToolResultChars, callOptions, }: {
56
- model: t.ChatModel;
57
- messages: BaseMessage[];
58
- provider: Providers;
59
- maxToolResultChars?: number;
60
- callOptions?: unknown;
61
- }): BaseMessage[];
62
- /**
63
- * The serving model's id, read through the same wrapper stack
64
- * `collectModelCallbackSources` walks — `bindTools` returns a
65
- * `RunnableBinding` and a system runnable pipes a `RunnableSequence`, and
66
- * neither exposes the chat model's `model` at the top level.
67
- */
68
- export declare function resolveServingModelId(model: unknown): string | undefined;
69
52
  /**
70
53
  * Invokes a chat model with the given messages, handling both streaming and
71
54
  * non-streaming paths.
@@ -75,10 +58,7 @@ export declare function resolveServingModelId(model: unknown): string | undefine
75
58
  * Pass an `onChunk` callback to override this with custom chunk processing
76
59
  * (e.g. summarization delta events).
77
60
  */
78
- interface AttemptInvokeParams {
79
- model: t.ChatModel;
80
- messages: BaseMessage[];
81
- provider: Providers;
61
+ interface AttemptInvokeCommonParams {
82
62
  context?: InvokeContext;
83
63
  onChunk?: OnChunk;
84
64
  /** Accounting owner for callers that deliberately pass no `context`
@@ -86,6 +66,17 @@ interface AttemptInvokeParams {
86
66
  * for charge claims. */
87
67
  streamLimitState?: StreamLimitState;
88
68
  }
69
+ type AttemptInvokeParams = AttemptInvokeCommonParams & ({
70
+ request: PreparedProviderRequest;
71
+ model?: never;
72
+ messages?: never;
73
+ provider?: never;
74
+ } | {
75
+ request?: never;
76
+ model: t.ChatModel;
77
+ messages: BaseMessage[];
78
+ provider: Providers;
79
+ });
89
80
  /**
90
81
  * One model attempt. Stamps the attempt identity into callback metadata
91
82
  * (see the generation-key notes in `streamLimits.ts`), leases the attempt's
@@ -122,7 +113,7 @@ export declare function getFallbackOverflowCandidates(error: unknown): FallbackO
122
113
  * behind a later unrelated error would surface a dead end instead. Ordinary
123
114
  * failures still throw last-error-wins.
124
115
  */
125
- export declare function tryFallbackProviders({ fallbacks, tools, messages, config, primaryError, context, onChunk, streamLimitState, overflowContext, prepareProviderMessages, }: {
116
+ export declare function tryFallbackProviders({ fallbacks, tools, messages, config, primaryError, context, onChunk, streamLimitState, overflowContext, prepareProviderRequest: prepareFallbackRequest, prepareProviderMessages, }: {
126
117
  fallbacks: t.FallbackConfig[];
127
118
  tools?: t.GraphTools;
128
119
  messages: BaseMessage[];
@@ -141,10 +132,18 @@ export declare function tryFallbackProviders({ fallbacks, tools, messages, confi
141
132
  */
142
133
  overflowContext?: ContextOverflowContext;
143
134
  /**
144
- * Optional final payload guard used by Graph. It receives the initialized,
145
- * tool-bound fallback model so Responses-vs-Chat projection is exact before
146
- * the fallback request is measured and sent.
135
+ * Optional exact-payload preparation used by Graph. The returned request is
136
+ * measured and sent without another provider projection.
147
137
  */
138
+ prepareProviderRequest?: (input: {
139
+ model: t.ChatModel;
140
+ messages: BaseMessage[];
141
+ provider: Providers;
142
+ clientOptions?: t.ClientOptions;
143
+ maxContextTokens?: number;
144
+ config?: RunnableConfig;
145
+ }) => PreparedProviderRequest | Promise<PreparedProviderRequest>;
146
+ /** @deprecated Return a `PreparedProviderRequest` instead. */
148
147
  prepareProviderMessages?: (input: {
149
148
  model: t.ChatModel;
150
149
  messages: BaseMessage[];
@@ -154,4 +153,3 @@ export declare function tryFallbackProviders({ fallbacks, tools, messages, confi
154
153
  config?: RunnableConfig;
155
154
  }) => BaseMessage[] | Promise<BaseMessage[]>;
156
155
  }): Promise<Partial<t.BaseGraphState> | undefined>;
157
- export {};
@@ -0,0 +1,55 @@
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 { Providers } from '@/common';
6
+ declare const preparedProviderRequestBrand: unique symbol;
7
+ export type ProviderMessageProjectionMode = 'chat-messages' | 'openai-responses';
8
+ export interface ProviderPayloadMeasurement {
9
+ readonly fits: boolean;
10
+ readonly projectedMessageTokens?: number;
11
+ readonly availableMessageTokens?: number;
12
+ readonly contextBudget?: number;
13
+ readonly effectiveInstructionTokens?: number;
14
+ }
15
+ export interface PreparedProviderRequest {
16
+ readonly model: t.ChatModel;
17
+ readonly modelId?: string;
18
+ readonly provider: Providers;
19
+ readonly projectionMode: ProviderMessageProjectionMode;
20
+ readonly messages: BaseMessage[];
21
+ readonly measurement?: ProviderPayloadMeasurement;
22
+ readonly [preparedProviderRequestBrand]: true;
23
+ }
24
+ export interface ProviderRequestContext {
25
+ getOrCreateToolOutputRegistry?(): ToolOutputReferenceRegistry | undefined;
26
+ isRunProducedMessage?(message: BaseMessage): boolean;
27
+ }
28
+ export interface PrepareProviderRequestParams {
29
+ model: t.ChatModel;
30
+ messages: BaseMessage[];
31
+ provider: Providers;
32
+ context?: ProviderRequestContext;
33
+ config?: RunnableConfig;
34
+ maxToolResultChars?: number;
35
+ measure?: (messages: BaseMessage[]) => ProviderPayloadMeasurement;
36
+ }
37
+ export declare function usesNativeOpenAIResponses(model: t.ChatModel, provider: Providers, callOptions?: unknown): boolean;
38
+ interface ProjectMessagesForProviderParams {
39
+ model: t.ChatModel;
40
+ messages: BaseMessage[];
41
+ provider: Providers;
42
+ maxToolResultChars?: number;
43
+ callOptions?: unknown;
44
+ }
45
+ /** Produces the provider-facing representation before adapter serialization. */
46
+ export declare function projectMessagesForProvider(params: ProjectMessagesForProviderParams): BaseMessage[];
47
+ /** Reads the serving model id through LangChain binding/sequence wrappers. */
48
+ export declare function resolveServingModelId(model: unknown): string | undefined;
49
+ /**
50
+ * Finalizes one provider request and measures the exact message array that
51
+ * will be passed to LangChain. Source messages remain untouched.
52
+ */
53
+ export declare function prepareProviderRequest({ model, messages, provider, context, config, maxToolResultChars, measure, }: PrepareProviderRequestParams): PreparedProviderRequest;
54
+ export declare function assertPreparedProviderRequestFor(request: PreparedProviderRequest, model: t.ChatModel, provider: Providers, config?: RunnableConfig): void;
55
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.6.11",
3
+ "version": "3.6.12",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -50,8 +50,8 @@ import {
50
50
  addBedrockTailCacheControl,
51
51
  projectArtifactPayload,
52
52
  formatContentStrings,
53
+ cloneMessage,
53
54
  CALIBRATION_RATIO_MAX,
54
- REPLY_PRIMER_TOKENS,
55
55
  createPruneMessages,
56
56
  projectToolCallInputs,
57
57
  calculateMaxToolCallInputChars,
@@ -74,7 +74,6 @@ import {
74
74
  coalesceAdjacentUserTurns,
75
75
  strictAlternationProviders,
76
76
  appendPredecessorHandoffCue,
77
- removePredecessorHandoffCue,
78
77
  stampSyntheticProviderMessage,
79
78
  } from '@/messages';
80
79
  import {
@@ -104,9 +103,9 @@ import {
104
103
  tryFallbackProviders,
105
104
  getFallbackErrorContext,
106
105
  getFallbackOverflowCandidates,
107
- projectMessagesForProvider,
108
- resolveServingModelId,
109
106
  } from '@/llm/invoke';
107
+ import { prepareProviderRequest } from '@/llm/prepareProviderRequest';
108
+ import { createContextPressureMeter } from '@/llm/contextPressureMeter';
110
109
  import {
111
110
  resolveStreamLimits,
112
111
  StreamLimitExceededError,
@@ -3154,105 +3153,18 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3154
3153
  * baseline, then attribute it across retained messages. Provider
3155
3154
  * transforms can shrink one message while expanding or adding another;
3156
3155
  * per-origin accounting prevents that unrelated shrink from canceling
3157
- * the expansion. Raw counts are frozen before in-place formatters run.
3156
+ * the expansion. Exact counts are memoized across repeated projections.
3158
3157
  */
3159
- let providerMessageBaseline:
3160
- | Array<{ rawTokens: number; accountingWeight: number }>
3161
- | undefined;
3162
- const providerMessageOrigins = new WeakMap<BaseMessage, number>();
3163
- if (contextUsage != null && agentContext.tokenCounter != null) {
3164
- const sourceIndices = new WeakMap<BaseMessage, number>();
3165
- for (let i = 0; i < messages.length; i++) {
3166
- sourceIndices.set(messages[i], i);
3167
- }
3168
- providerMessageBaseline = messagesToUse.map((message, index) => {
3169
- const rawTokens = agentContext.tokenCounter!(message);
3170
- const sourceIndex = sourceIndices.get(message);
3171
- const indexedTokens =
3172
- sourceIndex != null
3173
- ? agentContext.indexTokenCountMap[sourceIndex]
3174
- : undefined;
3175
- const accountingWeight =
3176
- indexedTokens != null &&
3177
- Number.isFinite(indexedTokens) &&
3178
- indexedTokens >= 0
3179
- ? indexedTokens
3180
- : rawTokens;
3181
- if (!providerMessageOrigins.has(message)) {
3182
- providerMessageOrigins.set(message, index);
3183
- }
3184
- return { rawTokens, accountingWeight };
3185
- });
3186
- }
3187
-
3188
- const getProviderMessageOriginKey = (
3189
- message: BaseMessage
3190
- ): string | undefined => {
3191
- const type = message.getType();
3192
- if (
3193
- message instanceof ToolMessage &&
3194
- typeof message.tool_call_id === 'string' &&
3195
- message.tool_call_id.length > 0
3196
- ) {
3197
- return `tool:call:${message.tool_call_id}`;
3198
- }
3199
- if (typeof message.id === 'string' && message.id.length > 0) {
3200
- return `${type}:id:${message.id}`;
3201
- }
3202
- return undefined;
3203
- };
3204
-
3205
- /**
3206
- * Provider projections clone messages. Preserve their baseline origin
3207
- * without writing tracking metadata onto the wire. Synthetic fold
3208
- * messages intentionally remain unattributed and are charged in full.
3209
- */
3210
- const trackProviderMessageOrigins = (
3211
- before: BaseMessage[],
3212
- after: BaseMessage[]
3213
- ): BaseMessage[] => {
3214
- if (providerMessageBaseline == null || before === after) {
3215
- return after;
3216
- }
3217
- if (before.length === after.length) {
3218
- for (let i = 0; i < after.length; i++) {
3219
- const origin = providerMessageOrigins.get(before[i]);
3220
- if (
3221
- origin != null &&
3222
- !providerMessageOrigins.has(after[i]) &&
3223
- before[i].getType() === after[i].getType() &&
3224
- !isSyntheticProviderContextMessage(after[i])
3225
- ) {
3226
- providerMessageOrigins.set(after[i], origin);
3227
- }
3228
- }
3229
- return after;
3230
- }
3231
-
3232
- const keyedOrigins = new Map<string, number | null>();
3233
- for (const message of before) {
3234
- const origin = providerMessageOrigins.get(message);
3235
- const key = getProviderMessageOriginKey(message);
3236
- if (origin == null || key == null) {
3237
- continue;
3238
- }
3239
- keyedOrigins.set(key, keyedOrigins.has(key) ? null : origin);
3240
- }
3241
- for (const message of after) {
3242
- if (
3243
- providerMessageOrigins.has(message) ||
3244
- isSyntheticProviderContextMessage(message)
3245
- ) {
3246
- continue;
3247
- }
3248
- const key = getProviderMessageOriginKey(message);
3249
- const origin = key != null ? keyedOrigins.get(key) : undefined;
3250
- if (origin != null) {
3251
- providerMessageOrigins.set(message, origin);
3252
- }
3253
- }
3254
- return after;
3255
- };
3158
+ const contextPressure = createContextPressureMeter({
3159
+ tokenCounter: agentContext.tokenCounter,
3160
+ sourceMessages: messages,
3161
+ retainedMessages: messagesToUse,
3162
+ indexTokenCountMap: agentContext.indexTokenCountMap,
3163
+ contextUsage,
3164
+ instructionTokens: agentContext.instructionTokens,
3165
+ calibrationRatio: agentContext.calibrationRatio,
3166
+ });
3167
+ const trackProviderMessageOrigins = contextPressure.trackProjection;
3256
3168
 
3257
3169
  if (agentContext.useLegacyContent) {
3258
3170
  const before = finalMessages;
@@ -3300,8 +3212,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3300
3212
  typeof lastMessageX.content === 'string'
3301
3213
  ) {
3302
3214
  const trimmed = lastMessageX.content.trim();
3303
- finalMessages[finalMessages.length - 2].content =
3304
- trimmed.length > 0 ? [{ type: 'text' as const, text: trimmed }] : '';
3215
+ const before = finalMessages;
3216
+ finalMessages = [...before];
3217
+ finalMessages[finalMessages.length - 2] = cloneMessage(
3218
+ lastMessageX,
3219
+ trimmed.length > 0 ? [{ type: 'text' as const, text: trimmed }] : ''
3220
+ );
3221
+ finalMessages = trackProviderMessageOrigins(before, finalMessages);
3305
3222
  }
3306
3223
 
3307
3224
  const localProviderOverflowMeasurements = new WeakMap<
@@ -3311,123 +3228,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3311
3228
  estimatedPromptTokens: number;
3312
3229
  }
3313
3230
  >();
3314
- const measureProviderPayload = (
3315
- candidate: BaseMessage[],
3316
- contextBudgetOverride?: number,
3317
- forceRawRecount = false
3318
- ): {
3319
- fits: boolean;
3320
- projectedMessageTokens?: number;
3321
- availableMessageTokens?: number;
3322
- contextBudget?: number;
3323
- effectiveInstructionTokens?: number;
3324
- } => {
3325
- const contextBudget =
3326
- contextBudgetOverride ?? contextUsage?.contextBudget;
3327
- const effectiveInstructionTokens =
3328
- contextUsage?.effectiveInstructionTokens ??
3329
- (forceRawRecount ? agentContext.instructionTokens : undefined);
3330
- if (
3331
- agentContext.tokenCounter == null ||
3332
- contextBudget == null ||
3333
- effectiveInstructionTokens == null
3334
- ) {
3335
- return { fits: true };
3336
- }
3337
- const availableMessageTokens = Math.max(
3338
- 0,
3339
- contextBudget - effectiveInstructionTokens
3340
- );
3341
- let usageRatio =
3342
- agentContext.calibrationRatio > 0 ? agentContext.calibrationRatio : 1;
3343
- if (
3344
- contextUsage?.calibrationRatio != null &&
3345
- contextUsage.calibrationRatio > 0
3346
- ) {
3347
- usageRatio = contextUsage.calibrationRatio;
3348
- }
3349
- if (forceRawRecount) {
3350
- usageRatio = Math.max(1, usageRatio);
3351
- }
3352
- const baselineRemaining = contextUsage?.remainingContextTokens;
3353
- const accountedMessageTokens =
3354
- !forceRawRecount &&
3355
- providerMessageBaseline != null &&
3356
- baselineRemaining != null &&
3357
- Number.isFinite(baselineRemaining)
3358
- ? availableMessageTokens -
3359
- Math.min(availableMessageTokens, Math.max(0, baselineRemaining))
3360
- : undefined;
3361
-
3362
- let projectedMessageTokens: number;
3363
- if (accountedMessageTokens != null && providerMessageBaseline != null) {
3364
- const replyPrimerTokens = Math.round(
3365
- REPLY_PRIMER_TOKENS * usageRatio
3366
- );
3367
- const rawWeights: Record<string, number> = {};
3368
- let totalWeight = 0;
3369
- for (let i = 0; i < providerMessageBaseline.length; i++) {
3370
- const weight = providerMessageBaseline[i].accountingWeight;
3371
- rawWeights[i] = weight;
3372
- totalWeight += weight;
3373
- }
3374
- const attributableTokens =
3375
- totalWeight > 0
3376
- ? Math.min(
3377
- Math.max(0, accountedMessageTokens - replyPrimerTokens),
3378
- Math.round(totalWeight * usageRatio)
3379
- )
3380
- : 0;
3381
- const apportionedTokens =
3382
- totalWeight > 0
3383
- ? apportionTokenCounts(
3384
- rawWeights,
3385
- attributableTokens / totalWeight,
3386
- attributableTokens
3387
- )
3388
- : {};
3389
- const attributedByOrigin = providerMessageBaseline.map(
3390
- (_, origin) => apportionedTokens[origin] || 0
3391
- );
3392
- projectedMessageTokens = Math.max(
3393
- replyPrimerTokens,
3394
- accountedMessageTokens - attributableTokens
3395
- );
3396
- let newRawTokens = 0;
3397
- const usedOrigins = new Set<number>();
3398
- for (const message of candidate) {
3399
- const rawTokens = agentContext.tokenCounter(message);
3400
- const origin = providerMessageOrigins.get(message);
3401
- if (origin == null || usedOrigins.has(origin)) {
3402
- newRawTokens += rawTokens;
3403
- continue;
3404
- }
3405
- usedOrigins.add(origin);
3406
- projectedMessageTokens += Math.max(
3407
- 0,
3408
- attributedByOrigin[origin] +
3409
- Math.round(
3410
- (rawTokens - providerMessageBaseline[origin].rawTokens) *
3411
- usageRatio
3412
- )
3413
- );
3414
- }
3415
- projectedMessageTokens += Math.round(newRawTokens * usageRatio);
3416
- } else {
3417
- let rawTokens = REPLY_PRIMER_TOKENS;
3418
- for (const message of candidate) {
3419
- rawTokens += agentContext.tokenCounter(message);
3420
- }
3421
- projectedMessageTokens = Math.round(rawTokens * usageRatio);
3422
- }
3423
- return {
3424
- fits: projectedMessageTokens <= availableMessageTokens,
3425
- projectedMessageTokens,
3426
- availableMessageTokens,
3427
- contextBudget,
3428
- effectiveInstructionTokens,
3429
- };
3430
- };
3231
+ const measureProviderPayload = contextPressure.measure;
3431
3232
 
3432
3233
  const createProviderPayloadOverflowError = ({
3433
3234
  projection,
@@ -3782,10 +3583,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3782
3583
  finalMessages = trackProviderMessageOrigins(
3783
3584
  beforeSanitizeMessages,
3784
3585
  sanitizeOrphanToolBlocks(beforeSanitizeMessages, (source, clone) => {
3785
- const origin = providerMessageOrigins.get(source);
3786
- if (origin != null) {
3787
- providerMessageOrigins.set(clone, origin);
3788
- }
3586
+ contextPressure.trackClone(source, clone);
3789
3587
  })
3790
3588
  );
3791
3589
  if (finalMessages.length !== beforeSanitize) {
@@ -3858,23 +3656,30 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3858
3656
 
3859
3657
  const fallbackBaseMessages = finalMessages;
3860
3658
  const beforeFinalProviderProjection = fallbackBaseMessages;
3861
- finalMessages = trackProviderMessageOrigins(
3862
- beforeFinalProviderProjection,
3863
- projectMessagesForProvider({
3864
- model: (this.overrideModel ?? model) as t.ChatModel,
3865
- messages: beforeFinalProviderProjection,
3866
- provider: agentContext.provider,
3867
- maxToolResultChars: maxProviderToolResultChars,
3868
- callOptions: config,
3869
- })
3870
- );
3659
+ const preparedRequest = prepareProviderRequest({
3660
+ model: (this.overrideModel ?? model) as t.ChatModel,
3661
+ messages: beforeFinalProviderProjection,
3662
+ provider: agentContext.provider,
3663
+ context: this,
3664
+ config,
3665
+ maxToolResultChars: maxProviderToolResultChars,
3666
+ measure: (preparedMessages) =>
3667
+ measureProviderPayload(
3668
+ trackProviderMessageOrigins(
3669
+ beforeFinalProviderProjection,
3670
+ preparedMessages
3671
+ )
3672
+ ),
3673
+ });
3674
+ finalMessages = preparedRequest.messages;
3871
3675
 
3872
3676
  /**
3873
3677
  * Prompt-cache placement and orphan sanitization are provider-wire
3874
3678
  * transforms too. Re-measure after both so no content added after the
3875
3679
  * earlier artifact/synthetic compaction decision can bypass the guard.
3876
3680
  */
3877
- finalProjection = measureProviderPayload(finalMessages);
3681
+ finalProjection =
3682
+ preparedRequest.measurement ?? measureProviderPayload(finalMessages);
3878
3683
  const preInvokeContextOverflowError = !finalProjection.fits
3879
3684
  ? createProviderPayloadOverflowError({
3880
3685
  projection: finalProjection,
@@ -4083,9 +3888,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4083
3888
  () =>
4084
3889
  attemptInvoke(
4085
3890
  {
4086
- model: (this.overrideModel ?? model) as t.ChatModel,
4087
- messages: finalMessages,
4088
- provider: agentContext.provider,
3891
+ request: preparedRequest,
4089
3892
  context: this,
4090
3893
  },
4091
3894
  invokeConfig
@@ -4288,7 +4091,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4288
4091
  estimatedPromptTokens: getEstimatedPromptTokens(contextUsage),
4289
4092
  maxContextTokens: agentContext.maxContextTokens,
4290
4093
  },
4291
- prepareProviderMessages: ({
4094
+ prepareProviderRequest: ({
4292
4095
  model: fallbackModel,
4293
4096
  messages: fallbackMessages,
4294
4097
  provider: fallbackProvider,
@@ -4300,36 +4103,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4300
4103
  calculateMaxToolResultChars(
4301
4104
  fallbackMaxContextTokens ?? agentContext.maxContextTokens
4302
4105
  );
4303
- /**
4304
- * Serving-provider cue shaping BEFORE the fallback payload
4305
- * is measured: a Claude fallback behind a tolerant primary
4306
- * gains the cue inside the guarded projection (a prompt
4307
- * within the cue's cost of the fallback budget must take
4308
- * the recovery path, not ship oversized), and a tolerant
4309
- * fallback behind an Anthropic primary sheds the baked cue
4310
- * before it is measured against the tighter budget. The
4311
- * attemptInvoke funnel pass then finds nothing to change.
4312
- */
4313
- const cueShapedFallbackMessages = trackProviderMessageOrigins(
4314
- fallbackMessages,
4315
- isAnthropicLike(fallbackProvider, {
4316
- model: resolveServingModelId(fallbackModel),
4317
- })
4318
- ? appendPredecessorHandoffCue(fallbackMessages, (m) =>
4319
- this.isRunProducedMessage(m)
4320
- )
4321
- : removePredecessorHandoffCue(fallbackMessages)
4322
- );
4323
- const projectedFallbackMessages = trackProviderMessageOrigins(
4324
- cueShapedFallbackMessages,
4325
- projectMessagesForProvider({
4326
- model: fallbackModel,
4327
- messages: cueShapedFallbackMessages,
4328
- provider: fallbackProvider,
4329
- maxToolResultChars: fallbackToolResultChars,
4330
- callOptions: fallbackConfig,
4331
- })
4332
- );
4333
4106
  const primaryContextBudget = contextUsage?.contextBudget;
4334
4107
  const fallbackContextBudget =
4335
4108
  fallbackMaxContextTokens == null
@@ -4338,11 +4111,31 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4338
4111
  primaryContextBudget ?? fallbackMaxContextTokens,
4339
4112
  fallbackMaxContextTokens
4340
4113
  );
4341
- const projection = measureProviderPayload(
4342
- projectedFallbackMessages,
4343
- fallbackContextBudget,
4344
- true
4345
- );
4114
+ const preparedFallbackRequest = prepareProviderRequest({
4115
+ model: fallbackModel,
4116
+ messages: fallbackMessages,
4117
+ provider: fallbackProvider,
4118
+ context: this,
4119
+ config: fallbackConfig,
4120
+ maxToolResultChars: fallbackToolResultChars,
4121
+ measure: (preparedMessages) =>
4122
+ measureProviderPayload(
4123
+ trackProviderMessageOrigins(
4124
+ fallbackMessages,
4125
+ preparedMessages
4126
+ ),
4127
+ {
4128
+ contextBudget: fallbackContextBudget,
4129
+ forceRawRecount: true,
4130
+ }
4131
+ ),
4132
+ });
4133
+ const projection =
4134
+ preparedFallbackRequest.measurement ??
4135
+ measureProviderPayload(preparedFallbackRequest.messages, {
4136
+ contextBudget: fallbackContextBudget,
4137
+ forceRawRecount: true,
4138
+ });
4346
4139
  if (!projection.fits) {
4347
4140
  throw createProviderPayloadOverflowError({
4348
4141
  projection,
@@ -4350,7 +4143,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4350
4143
  info: 'Fallback provider message formatting exceeded the context budget before invocation.',
4351
4144
  });
4352
4145
  }
4353
- return projectedFallbackMessages;
4146
+ return preparedFallbackRequest;
4354
4147
  },
4355
4148
  })
4356
4149
  );
package/src/index.ts CHANGED
@@ -88,6 +88,14 @@ export type { SmoothItem, SmoothPiece } from './llm/stream/smoother';
88
88
  export { FakeChatModel, createFakeStreamingLLM } from './llm/fake';
89
89
  export { initializeModel } from './llm/init';
90
90
  export { attemptInvoke, tryFallbackProviders } from './llm/invoke';
91
+ export { prepareProviderRequest } from './llm/prepareProviderRequest';
92
+ export type {
93
+ PreparedProviderRequest,
94
+ PrepareProviderRequestParams,
95
+ ProviderMessageProjectionMode,
96
+ ProviderPayloadMeasurement,
97
+ ProviderRequestContext,
98
+ } from './llm/prepareProviderRequest';
91
99
  export { canSealPreempt } from './llm/preempt';
92
100
  export { isThinkingEnabled, getMaxOutputTokensKey } from './llm/request';
93
101
  export {