@librechat/agents 3.3.10 → 3.3.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.
- package/dist/cjs/graphs/Graph.cjs +248 -27
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +13 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +160 -43
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +190 -13
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +723 -0
- package/dist/cjs/llm/streamLimits.cjs.map +1 -0
- package/dist/cjs/main.cjs +12 -3
- package/dist/cjs/messages/core.cjs +592 -27
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +4 -1
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +105 -14
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +157 -37
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +3 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +4 -3
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -3
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +77 -4
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +3 -2
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +7 -1
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-search.cjs +3 -1
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +7 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +7 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +3 -1
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +26 -8
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +30 -10
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +7 -1
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs +7 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-search.cjs +3 -1
- package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +17 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +54 -3
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -1
- package/dist/cjs/utils/misc.cjs +12 -0
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/proxy.cjs +63 -0
- package/dist/cjs/utils/proxy.cjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +247 -26
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +13 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +160 -43
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +192 -15
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +704 -0
- package/dist/esm/llm/streamLimits.mjs.map +1 -0
- package/dist/esm/main.mjs +8 -6
- package/dist/esm/messages/core.mjs +592 -28
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +4 -1
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +105 -14
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +157 -37
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -2
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +4 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -3
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +77 -4
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +3 -2
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +7 -1
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/crw-search.mjs +3 -1
- package/dist/esm/tools/search/crw-search.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +7 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +7 -1
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +3 -1
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +26 -8
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +30 -10
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +7 -1
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-scraper.mjs +7 -1
- package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-search.mjs +3 -1
- package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +17 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +54 -3
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -1
- package/dist/esm/utils/misc.mjs +12 -1
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/proxy.mjs +62 -0
- package/dist/esm/utils/proxy.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +53 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/langfuseToolOutputTracing.d.ts +1 -0
- package/dist/types/llm/invoke.d.ts +26 -5
- package/dist/types/llm/openai/index.d.ts +3 -0
- package/dist/types/llm/streamLimits.d.ts +314 -0
- package/dist/types/messages/core.d.ts +11 -6
- package/dist/types/run.d.ts +1 -0
- package/dist/types/summarization/node.d.ts +27 -2
- package/dist/types/tools/BashExecutor.d.ts +2 -2
- package/dist/types/tools/CodeExecutor.d.ts +3 -3
- package/dist/types/tools/ToolNode.d.ts +11 -1
- package/dist/types/tools/search/crw-scraper.d.ts +2 -0
- package/dist/types/tools/search/firecrawl.d.ts +2 -0
- package/dist/types/tools/search/keenable-scraper.d.ts +2 -0
- package/dist/types/tools/search/rerankers.d.ts +9 -5
- package/dist/types/tools/search/serper-scraper.d.ts +2 -0
- package/dist/types/tools/search/tavily-scraper.d.ts +2 -0
- package/dist/types/tools/search/types.d.ts +28 -33
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +44 -0
- package/dist/types/types/graph.d.ts +7 -1
- package/dist/types/types/run.d.ts +46 -1
- package/dist/types/types/tools.d.ts +21 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/misc.d.ts +7 -0
- package/dist/types/utils/proxy.d.ts +31 -0
- package/package.json +2 -1
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +158 -0
- package/src/__tests__/stream.eagerEventExecution.test.ts +1 -0
- package/src/__tests__/stream.streamLimits.test.ts +1982 -0
- package/src/graphs/Graph.ts +302 -27
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +234 -0
- package/src/index.ts +11 -0
- package/src/langfuseToolOutputTracing.ts +410 -14
- package/src/llm/bedrock/index.ts +22 -2
- package/src/llm/custom-chat-models.smoke.test.ts +747 -0
- package/src/llm/invoke.streamLimits.test.ts +142 -0
- package/src/llm/invoke.test.ts +187 -1
- package/src/llm/invoke.ts +231 -43
- package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
- package/src/llm/openai/index.ts +348 -31
- package/src/llm/openai/llm.spec.ts +107 -6
- package/src/llm/streamLimits.test.ts +450 -0
- package/src/llm/streamLimits.ts +1158 -0
- package/src/messages/core.ts +1290 -42
- package/src/messages/formatAgentMessages.test.ts +2623 -0
- package/src/run.ts +4 -0
- package/src/session/AgentSession.ts +5 -0
- package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
- package/src/specs/preemptSeal.test.ts +374 -5
- package/src/stream.ts +141 -6
- package/src/summarization/__tests__/node.test.ts +269 -0
- package/src/summarization/chunkHandler.test.ts +196 -0
- package/src/summarization/node.ts +203 -6
- package/src/tools/BashExecutor.ts +4 -3
- package/src/tools/CodeExecutor.ts +5 -4
- package/src/tools/ProgrammaticToolCalling.ts +7 -5
- package/src/tools/ToolNode.ts +109 -6
- package/src/tools/ToolSearch.ts +4 -3
- package/src/tools/__tests__/BashExecutor.test.ts +2 -2
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +2 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +126 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +389 -0
- package/src/tools/__tests__/ToolNode.streamLimits.test.ts +69 -0
- package/src/tools/search/crw-scraper.ts +6 -0
- package/src/tools/search/crw-search.ts +6 -1
- package/src/tools/search/firecrawl.ts +6 -0
- package/src/tools/search/http-agent.test.ts +133 -0
- package/src/tools/search/keenable-scraper.ts +6 -0
- package/src/tools/search/keenable-search.ts +6 -1
- package/src/tools/search/rerankers.ts +36 -10
- package/src/tools/search/search.ts +29 -11
- package/src/tools/search/serper-scraper.ts +6 -0
- package/src/tools/search/tavily-scraper.ts +6 -0
- package/src/tools/search/tavily-search.ts +2 -0
- package/src/tools/search/tool.ts +16 -0
- package/src/tools/search/types.ts +31 -33
- package/src/tools/subagent/SubagentExecutor.ts +96 -3
- package/src/types/graph.ts +7 -0
- package/src/types/run.ts +49 -1
- package/src/types/tools.ts +21 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +19 -0
- package/src/utils/proxy.test.ts +176 -0
- package/src/utils/proxy.ts +93 -0
package/src/llm/invoke.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { concat } from '@langchain/core/utils/stream';
|
|
2
2
|
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import { getCallbackManagerForConfig } from '@langchain/core/runnables';
|
|
3
4
|
import {
|
|
4
5
|
CallbackManager,
|
|
5
6
|
CallbackManagerForLLMRun,
|
|
6
7
|
type Callbacks,
|
|
7
8
|
} from '@langchain/core/callbacks/manager';
|
|
8
|
-
import { getCallbackManagerForConfig } from '@langchain/core/runnables';
|
|
9
9
|
import type { Serialized } from '@langchain/core/load/serializable';
|
|
10
|
-
import type { ChatGeneration } from '@langchain/core/outputs';
|
|
11
10
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
11
|
+
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 { StreamLimitState } from '@/llm/streamLimits';
|
|
15
16
|
import type { ContextOverflowContext } from '@/utils/errors';
|
|
16
17
|
import type * as t from '@/types';
|
|
17
18
|
import {
|
|
@@ -24,28 +25,38 @@ import {
|
|
|
24
25
|
projectStructuredToolOutputsToText,
|
|
25
26
|
projectToolStreamContentForProvider,
|
|
26
27
|
} from '@/messages/core';
|
|
28
|
+
import {
|
|
29
|
+
modifyDeltaProperties,
|
|
30
|
+
coalesceAdjacentUserTurns,
|
|
31
|
+
strictAlternationProviders,
|
|
32
|
+
appendPredecessorHandoffCue,
|
|
33
|
+
removePredecessorHandoffCue,
|
|
34
|
+
} from '@/messages';
|
|
27
35
|
import {
|
|
28
36
|
stripAnthropicCacheControl,
|
|
29
37
|
stripBedrockCacheControl,
|
|
30
38
|
} from '@/messages/cache';
|
|
39
|
+
import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
|
|
40
|
+
import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
|
|
41
|
+
import {
|
|
42
|
+
enforceStreamLimitsForWireChunk,
|
|
43
|
+
registerActiveStreamLimitGeneration,
|
|
44
|
+
releaseStreamLimitGeneration,
|
|
45
|
+
resolveGenerationKey,
|
|
46
|
+
streamLimitAccountingEnabled,
|
|
47
|
+
StreamLimitExceededError,
|
|
48
|
+
STREAM_LIMIT_REDISPATCH_KEY,
|
|
49
|
+
STREAM_LIMIT_ATTEMPT_KEY,
|
|
50
|
+
} from '@/llm/streamLimits';
|
|
31
51
|
import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
|
|
32
52
|
import { assertNotTruncatedToolCall } from '@/llm/truncation';
|
|
33
|
-
import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
|
|
34
53
|
import { manualToolStreamProviders } from '@/llm/providers';
|
|
35
|
-
import {
|
|
54
|
+
import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
|
|
36
55
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
37
56
|
import { getContextOverflowInfo } from '@/utils/errors';
|
|
38
|
-
import {
|
|
39
|
-
modifyDeltaProperties,
|
|
40
|
-
coalesceAdjacentUserTurns,
|
|
41
|
-
strictAlternationProviders,
|
|
42
|
-
appendPredecessorHandoffCue,
|
|
43
|
-
removePredecessorHandoffCue,
|
|
44
|
-
} from '@/messages';
|
|
57
|
+
import { appendCallbacks } from '@/utils/callbacks';
|
|
45
58
|
import { canSealPreempt } from '@/llm/preempt';
|
|
46
|
-
import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
|
|
47
59
|
import { initializeModel } from '@/llm/init';
|
|
48
|
-
import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
|
|
49
60
|
|
|
50
61
|
/**
|
|
51
62
|
* Context passed to `attemptInvoke`. Matches the subset of Graph that
|
|
@@ -83,8 +94,18 @@ export type InvokeContext = NonNullable<
|
|
|
83
94
|
/**
|
|
84
95
|
* Per-chunk callback for custom stream processing.
|
|
85
96
|
* When provided, replaces the default `ChatModelStreamHandler`.
|
|
97
|
+
*
|
|
98
|
+
* `metadata` is the attempt's callback metadata (carrying the provider and
|
|
99
|
+
* stream-limit attempt stamps), so consumers that count against the stream
|
|
100
|
+
* limits key each model attempt separately.
|
|
86
101
|
*/
|
|
87
|
-
export type OnChunk = (
|
|
102
|
+
export type OnChunk = (
|
|
103
|
+
chunk: AIMessageChunk,
|
|
104
|
+
metadata?: Record<string, unknown>
|
|
105
|
+
) => void | Promise<void>;
|
|
106
|
+
|
|
107
|
+
/** Unique per-model-attempt sequence; see the stamp in `attemptInvoke`. */
|
|
108
|
+
let streamLimitAttemptSeq = 0;
|
|
88
109
|
|
|
89
110
|
export function usesNativeOpenAIResponses(
|
|
90
111
|
model: t.ChatModel,
|
|
@@ -174,8 +195,17 @@ export function projectMessagesForProvider({
|
|
|
174
195
|
maxToolResultChars?: number;
|
|
175
196
|
callOptions?: unknown;
|
|
176
197
|
}): BaseMessage[] {
|
|
177
|
-
const
|
|
178
|
-
|
|
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) {
|
|
179
209
|
return projectOpenAIResponsesToolMessageContent(
|
|
180
210
|
stripAnthropicCacheControl(
|
|
181
211
|
stripBedrockCacheControl(providerInputMessages)
|
|
@@ -433,15 +463,19 @@ function synthesizeSealedUsage(
|
|
|
433
463
|
const inputTokens =
|
|
434
464
|
(countSealedTokens(context, metadata, prompt) ?? 0) +
|
|
435
465
|
sealedInstructionOverhead(context, metadata);
|
|
436
|
-
|
|
466
|
+
const usageMetadata = {
|
|
437
467
|
input_tokens: inputTokens,
|
|
438
468
|
output_tokens: outputTokens,
|
|
439
469
|
total_tokens: inputTokens + outputTokens,
|
|
440
470
|
};
|
|
441
|
-
chunk.
|
|
471
|
+
chunk.usage_metadata = usageMetadata;
|
|
472
|
+
chunk.lc_kwargs.usage_metadata = usageMetadata;
|
|
473
|
+
const responseMetadata = {
|
|
442
474
|
...chunk.response_metadata,
|
|
443
475
|
estimated_usage: true,
|
|
444
476
|
};
|
|
477
|
+
chunk.response_metadata = responseMetadata;
|
|
478
|
+
chunk.lc_kwargs.response_metadata = responseMetadata;
|
|
445
479
|
}
|
|
446
480
|
|
|
447
481
|
function getMessageText(chunk: AIMessageChunk): string {
|
|
@@ -646,21 +680,81 @@ function appendStreamChunk({
|
|
|
646
680
|
* Pass an `onChunk` callback to override this with custom chunk processing
|
|
647
681
|
* (e.g. summarization delta events).
|
|
648
682
|
*/
|
|
683
|
+
interface AttemptInvokeParams {
|
|
684
|
+
model: t.ChatModel;
|
|
685
|
+
messages: BaseMessage[];
|
|
686
|
+
provider: Providers;
|
|
687
|
+
context?: InvokeContext;
|
|
688
|
+
onChunk?: OnChunk;
|
|
689
|
+
/** Accounting owner for callers that deliberately pass no `context`
|
|
690
|
+
* (summarization) — used ONLY for the attempt's accounting lease, never
|
|
691
|
+
* for charge claims. */
|
|
692
|
+
streamLimitState?: StreamLimitState;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* One model attempt. Stamps the attempt identity into callback metadata
|
|
697
|
+
* (see the generation-key notes in `streamLimits.ts`), leases the attempt's
|
|
698
|
+
* accounting for its LIFETIME, and releases both from `finally`: retention
|
|
699
|
+
* must follow the attempt — a cancellation-ignoring straggler keeps its
|
|
700
|
+
* original budget no matter how many runs start and reset while it drains.
|
|
701
|
+
*/
|
|
649
702
|
export async function attemptInvoke(
|
|
703
|
+
params: AttemptInvokeParams,
|
|
704
|
+
config?: RunnableConfig
|
|
705
|
+
): Promise<Partial<t.BaseGraphState>> {
|
|
706
|
+
const stampedConfig: RunnableConfig = {
|
|
707
|
+
...config,
|
|
708
|
+
metadata: {
|
|
709
|
+
...(config?.metadata ?? {}),
|
|
710
|
+
[Constants.INVOKED_PROVIDER]: params.provider,
|
|
711
|
+
/**
|
|
712
|
+
* One `attemptInvoke` call is one model attempt; primary, fallback,
|
|
713
|
+
* and retry attempts within a node otherwise share the same langgraph
|
|
714
|
+
* metadata, so without a unique attempt stamp a fallback re-streaming
|
|
715
|
+
* a tool call from scratch would be charged the failed primary's
|
|
716
|
+
* partial bytes (or a same-named sibling fallback's) and could
|
|
717
|
+
* falsely trip the stream limits. The stamp rides the same metadata
|
|
718
|
+
* rebuild that already attributes the serving provider.
|
|
719
|
+
*/
|
|
720
|
+
[STREAM_LIMIT_ATTEMPT_KEY]: ++streamLimitAttemptSeq,
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
const rawLeaseTarget = params.context ?? params.streamLimitState;
|
|
724
|
+
/** No lease when no guard can fire: the lease only protects accounting
|
|
725
|
+
* entries, and fully disabled guards must allocate no bookkeeping at
|
|
726
|
+
* all — per-attempt included. */
|
|
727
|
+
const leaseTarget =
|
|
728
|
+
rawLeaseTarget != null && streamLimitAccountingEnabled(rawLeaseTarget)
|
|
729
|
+
? rawLeaseTarget
|
|
730
|
+
: undefined;
|
|
731
|
+
const generationKey =
|
|
732
|
+
leaseTarget != null
|
|
733
|
+
? resolveGenerationKey(
|
|
734
|
+
stampedConfig.metadata as Record<string, unknown>
|
|
735
|
+
)
|
|
736
|
+
: undefined;
|
|
737
|
+
if (leaseTarget != null && generationKey != null) {
|
|
738
|
+
registerActiveStreamLimitGeneration(leaseTarget, generationKey);
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
return await attemptInvokeBody(params, stampedConfig);
|
|
742
|
+
} finally {
|
|
743
|
+
if (leaseTarget != null && generationKey != null) {
|
|
744
|
+
releaseStreamLimitGeneration(leaseTarget, generationKey);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function attemptInvokeBody(
|
|
650
750
|
{
|
|
651
751
|
model,
|
|
652
752
|
messages,
|
|
653
753
|
provider,
|
|
654
754
|
context,
|
|
655
755
|
onChunk,
|
|
656
|
-
}:
|
|
657
|
-
|
|
658
|
-
messages: BaseMessage[];
|
|
659
|
-
provider: Providers;
|
|
660
|
-
context?: InvokeContext;
|
|
661
|
-
onChunk?: OnChunk;
|
|
662
|
-
},
|
|
663
|
-
config?: RunnableConfig
|
|
756
|
+
}: AttemptInvokeParams,
|
|
757
|
+
config: RunnableConfig
|
|
664
758
|
): Promise<Partial<t.BaseGraphState>> {
|
|
665
759
|
/**
|
|
666
760
|
* Pull the run-scoped tool output registry off the graph (when one
|
|
@@ -676,12 +770,8 @@ export async function attemptInvoke(
|
|
|
676
770
|
callOptions: config,
|
|
677
771
|
});
|
|
678
772
|
const registry = context?.getOrCreateToolOutputRegistry();
|
|
679
|
-
const runId = config
|
|
680
|
-
const annotated = annotateMessagesForLLM(
|
|
681
|
-
invocationMessages,
|
|
682
|
-
registry,
|
|
683
|
-
runId
|
|
684
|
-
);
|
|
773
|
+
const runId = config.configurable?.run_id as string | undefined;
|
|
774
|
+
const annotated = annotateMessagesForLLM(invocationMessages, registry, runId);
|
|
685
775
|
/**
|
|
686
776
|
* Keyed on the provider ACTUALLY serving this call, not the agent's primary.
|
|
687
777
|
* `createCallModel` normalizes for the primary, but `tryFallbackProviders`
|
|
@@ -739,14 +829,6 @@ export async function attemptInvoke(
|
|
|
739
829
|
* wrong for fallback-served calls — or `ls_provider` — which derived
|
|
740
830
|
* providers inherit from their base class.
|
|
741
831
|
*/
|
|
742
|
-
config = {
|
|
743
|
-
...config,
|
|
744
|
-
metadata: {
|
|
745
|
-
...(config?.metadata ?? {}),
|
|
746
|
-
[Constants.INVOKED_PROVIDER]: provider,
|
|
747
|
-
},
|
|
748
|
-
};
|
|
749
|
-
|
|
750
832
|
if (model.stream) {
|
|
751
833
|
/**
|
|
752
834
|
* Observed, not dictated. `handleChatModelStart` fires with the chat
|
|
@@ -778,10 +860,40 @@ export async function attemptInvoke(
|
|
|
778
860
|
let preempted = false;
|
|
779
861
|
const registeredStreamHandler =
|
|
780
862
|
getRegisteredDefaultChatStreamHandler(context);
|
|
863
|
+
/** A sibling's trip aborts the composed signal, but an adapter that
|
|
864
|
+
* ignores cancellation keeps yielding — and text-only chunks with the
|
|
865
|
+
* event cap off never throw in enforcement, so nothing else would stop
|
|
866
|
+
* the drain. Checked on every yielded chunk in all three loops;
|
|
867
|
+
* throwing closes the iterator and tears down the provider stream. */
|
|
868
|
+
const throwIfBreakerTripped = (): void => {
|
|
869
|
+
const signal = config.signal;
|
|
870
|
+
if (
|
|
871
|
+
signal?.aborted === true &&
|
|
872
|
+
signal.reason instanceof StreamLimitExceededError
|
|
873
|
+
) {
|
|
874
|
+
throw signal.reason;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
781
877
|
|
|
782
878
|
if (onChunk) {
|
|
879
|
+
const attemptMetadata = config.metadata as
|
|
880
|
+
| Record<string, unknown>
|
|
881
|
+
| undefined;
|
|
783
882
|
for await (const chunk of stream) {
|
|
784
|
-
|
|
883
|
+
throwIfBreakerTripped();
|
|
884
|
+
/** An onChunk consumer replaces the stream handler entirely, so
|
|
885
|
+
* stream limits are enforced here for every such caller — public
|
|
886
|
+
* package consumers get no other accounting. The internal
|
|
887
|
+
* summarization onChunk charges producer-side itself and passes no
|
|
888
|
+
* context, precisely so this claim and its own never stack. */
|
|
889
|
+
if (context != null) {
|
|
890
|
+
enforceStreamLimitsForWireChunk({
|
|
891
|
+
graph: context,
|
|
892
|
+
metadata: attemptMetadata,
|
|
893
|
+
chunk,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
await onChunk(chunk, attemptMetadata);
|
|
785
897
|
finalChunk = appendStreamChunk({
|
|
786
898
|
current: finalChunk,
|
|
787
899
|
next: chunk,
|
|
@@ -792,6 +904,7 @@ export async function attemptInvoke(
|
|
|
792
904
|
const metadata = config.metadata as Record<string, unknown> | undefined;
|
|
793
905
|
const streamHandler = new ChatModelStreamHandler();
|
|
794
906
|
for await (const chunk of stream) {
|
|
907
|
+
throwIfBreakerTripped();
|
|
795
908
|
const handlingChunk = getStreamHandlingChunk({
|
|
796
909
|
current: finalChunk,
|
|
797
910
|
next: chunk,
|
|
@@ -804,6 +917,23 @@ export async function attemptInvoke(
|
|
|
804
917
|
metadata,
|
|
805
918
|
context
|
|
806
919
|
);
|
|
920
|
+
} else if (context != null) {
|
|
921
|
+
/**
|
|
922
|
+
* A replay-skipped chunk yields no handling chunk, and in this
|
|
923
|
+
* local branch no `streamEvents` consumer judges the wire event
|
|
924
|
+
* either — yet a cumulative OpenRouter replay can still carry
|
|
925
|
+
* `tool_call_chunks` or complete `tool_calls` that are appended
|
|
926
|
+
* below. Charge the full limits (event budget and argument bytes)
|
|
927
|
+
* directly so neither cap can be bypassed. Consumer side: the
|
|
928
|
+
* local handler.handle call above claims as consumer, and one
|
|
929
|
+
* reused chunk object can alternate between these two arms.
|
|
930
|
+
*/
|
|
931
|
+
enforceStreamLimitsForWireChunk({
|
|
932
|
+
graph: context,
|
|
933
|
+
metadata,
|
|
934
|
+
chunk,
|
|
935
|
+
side: 'consumer',
|
|
936
|
+
});
|
|
807
937
|
}
|
|
808
938
|
finalChunk = appendStreamChunk({
|
|
809
939
|
current: finalChunk,
|
|
@@ -833,17 +963,40 @@ export async function attemptInvoke(
|
|
|
833
963
|
}
|
|
834
964
|
} else {
|
|
835
965
|
const metadata = config.metadata as Record<string, unknown> | undefined;
|
|
966
|
+
/**
|
|
967
|
+
* The original wire chunk still reaches the registered handler through
|
|
968
|
+
* `streamEvents` (where the late-reasoning skip discards it AFTER the
|
|
969
|
+
* event guard counts it), so this inline re-dispatch of the transformed
|
|
970
|
+
* chunk is marked to not consume a second event-budget slot. Allocated
|
|
971
|
+
* once per attempt, only when a transformation occurs.
|
|
972
|
+
*/
|
|
973
|
+
let redispatchMetadata: Record<string, unknown> | undefined;
|
|
836
974
|
for await (const chunk of stream) {
|
|
975
|
+
throwIfBreakerTripped();
|
|
976
|
+
/**
|
|
977
|
+
* Charged synchronously, ahead of the decoupled `streamEvents`
|
|
978
|
+
* reader that will echo this same chunk to the registered handler:
|
|
979
|
+
* a lagging reader would otherwise let an oversized complete call
|
|
980
|
+
* return to LangGraph and reach ToolNode before the queued handler
|
|
981
|
+
* throws. The chunk is marked so the echo skips accounting.
|
|
982
|
+
*/
|
|
983
|
+
if (context != null) {
|
|
984
|
+
enforceStreamLimitsForWireChunk({ graph: context, metadata, chunk });
|
|
985
|
+
}
|
|
837
986
|
const handlingChunk = getStreamHandlingChunk({
|
|
838
987
|
current: finalChunk,
|
|
839
988
|
next: chunk,
|
|
840
989
|
provider,
|
|
841
990
|
});
|
|
842
991
|
if (handlingChunk != null && handlingChunk !== chunk) {
|
|
992
|
+
redispatchMetadata ??= {
|
|
993
|
+
...(metadata ?? {}),
|
|
994
|
+
[STREAM_LIMIT_REDISPATCH_KEY]: true,
|
|
995
|
+
};
|
|
843
996
|
await registeredStreamHandler.handle(
|
|
844
997
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
845
998
|
{ chunk: handlingChunk },
|
|
846
|
-
|
|
999
|
+
redispatchMetadata,
|
|
847
1000
|
context
|
|
848
1001
|
);
|
|
849
1002
|
}
|
|
@@ -860,10 +1013,12 @@ export async function attemptInvoke(
|
|
|
860
1013
|
}
|
|
861
1014
|
|
|
862
1015
|
if (preempted && finalChunk != null) {
|
|
863
|
-
|
|
1016
|
+
const responseMetadata = {
|
|
864
1017
|
...finalChunk.response_metadata,
|
|
865
1018
|
preempted: true,
|
|
866
1019
|
};
|
|
1020
|
+
finalChunk.response_metadata = responseMetadata;
|
|
1021
|
+
finalChunk.lc_kwargs.response_metadata = responseMetadata;
|
|
867
1022
|
await endSealedModelRun(
|
|
868
1023
|
context,
|
|
869
1024
|
finalChunk,
|
|
@@ -983,6 +1138,7 @@ export async function tryFallbackProviders({
|
|
|
983
1138
|
primaryError,
|
|
984
1139
|
context,
|
|
985
1140
|
onChunk,
|
|
1141
|
+
streamLimitState,
|
|
986
1142
|
overflowContext,
|
|
987
1143
|
prepareProviderMessages,
|
|
988
1144
|
}: {
|
|
@@ -993,6 +1149,9 @@ export async function tryFallbackProviders({
|
|
|
993
1149
|
primaryError: unknown;
|
|
994
1150
|
context?: InvokeContext;
|
|
995
1151
|
onChunk?: OnChunk;
|
|
1152
|
+
/** Accounting-lease owner forwarded to each fallback attempt (see
|
|
1153
|
+
* `AttemptInvokeParams.streamLimitState`). */
|
|
1154
|
+
streamLimitState?: StreamLimitState;
|
|
996
1155
|
/**
|
|
997
1156
|
* Prompt-size corroboration for signatures that are not self-describing.
|
|
998
1157
|
* Vertex AI's overflow is a bare `400` with no reason, so without this a
|
|
@@ -1063,6 +1222,16 @@ export async function tryFallbackProviders({
|
|
|
1063
1222
|
maxContextTokens: fb.maxContextTokens,
|
|
1064
1223
|
config: fbConfig,
|
|
1065
1224
|
})) ?? messages;
|
|
1225
|
+
/** A sibling can trip the breaker while the preparation above is
|
|
1226
|
+
* awaited — and the catch below only sees attempts that THROW, so a
|
|
1227
|
+
* provider that ignores an aborted signal and succeeds would resolve
|
|
1228
|
+
* a run that must reject. Check before every fallback invocation. */
|
|
1229
|
+
if (
|
|
1230
|
+
config?.signal?.aborted === true &&
|
|
1231
|
+
config.signal.reason instanceof StreamLimitExceededError
|
|
1232
|
+
) {
|
|
1233
|
+
throw config.signal.reason;
|
|
1234
|
+
}
|
|
1066
1235
|
const result = await attemptInvoke(
|
|
1067
1236
|
{
|
|
1068
1237
|
model: fbModel as t.ChatModel,
|
|
@@ -1070,11 +1239,30 @@ export async function tryFallbackProviders({
|
|
|
1070
1239
|
provider: fb.provider,
|
|
1071
1240
|
context,
|
|
1072
1241
|
onChunk,
|
|
1242
|
+
streamLimitState,
|
|
1073
1243
|
},
|
|
1074
1244
|
fbConfig
|
|
1075
1245
|
);
|
|
1076
1246
|
return result;
|
|
1077
1247
|
} catch (e) {
|
|
1248
|
+
/**
|
|
1249
|
+
* A tripped stream circuit breaker is a deliberate abort, not a
|
|
1250
|
+
* provider failure. Continuing would try the remaining fallbacks and a
|
|
1251
|
+
* succeeding one would resolve a run that must reject.
|
|
1252
|
+
*/
|
|
1253
|
+
if (e instanceof StreamLimitExceededError) {
|
|
1254
|
+
throw e;
|
|
1255
|
+
}
|
|
1256
|
+
/** A parallel sibling's trip aborts this branch's composed signal, and
|
|
1257
|
+
* a provider can surface that as a generic abort error; advancing to
|
|
1258
|
+
* the next fallback would start new provider work after the safety
|
|
1259
|
+
* abort. Rethrow the breaker's own reason instead. */
|
|
1260
|
+
if (
|
|
1261
|
+
config?.signal?.aborted === true &&
|
|
1262
|
+
config.signal.reason instanceof StreamLimitExceededError
|
|
1263
|
+
) {
|
|
1264
|
+
throw config.signal.reason;
|
|
1265
|
+
}
|
|
1078
1266
|
lastError = e;
|
|
1079
1267
|
const fallbackOverflowContext: ContextOverflowContext = {
|
|
1080
1268
|
provider: fb.provider,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AIMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { OpenAIClient } from '@langchain/openai';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getCacheWriteTokens,
|
|
6
|
+
attachCacheWriteUsage,
|
|
7
|
+
attachCacheWriteMetadata,
|
|
8
|
+
} from './index';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regression coverage for a crash reported against OpenAI-compatible
|
|
12
|
+
* third-party servers (e.g. mlx_vlm.server) whose `/v1/responses` usage
|
|
13
|
+
* payload omits `input_tokens_details` entirely — a shape the OpenAI API
|
|
14
|
+
* itself always populates, but which `ResponsesUsageWithCacheWrite`
|
|
15
|
+
* declares optional. Reading `.cache_write_tokens` off that missing object
|
|
16
|
+
* without a second `?.` threw "Cannot read properties of undefined
|
|
17
|
+
* (reading 'cache_write_tokens')" on every completion from such a server.
|
|
18
|
+
*/
|
|
19
|
+
describe('cache write token extraction (Responses API)', () => {
|
|
20
|
+
describe('getCacheWriteTokens', () => {
|
|
21
|
+
it('returns undefined without throwing when usage has no input_tokens_details', () => {
|
|
22
|
+
const message = new AIMessage({
|
|
23
|
+
content: 'hi',
|
|
24
|
+
response_metadata: {
|
|
25
|
+
usage: {
|
|
26
|
+
input_tokens: 10,
|
|
27
|
+
output_tokens: 2,
|
|
28
|
+
total_tokens: 12,
|
|
29
|
+
// input_tokens_details intentionally omitted, mirroring a
|
|
30
|
+
// minimal OpenAI-compatible server's usage payload.
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(() => getCacheWriteTokens(message)).not.toThrow();
|
|
36
|
+
expect(getCacheWriteTokens(message)).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns undefined without throwing when there is no usage at all', () => {
|
|
40
|
+
const message = new AIMessage({
|
|
41
|
+
content: 'hi',
|
|
42
|
+
response_metadata: {},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(() => getCacheWriteTokens(message)).not.toThrow();
|
|
46
|
+
expect(getCacheWriteTokens(message)).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('still reports cache_write_tokens when the field is present', () => {
|
|
50
|
+
const message = new AIMessage({
|
|
51
|
+
content: 'hi',
|
|
52
|
+
response_metadata: {
|
|
53
|
+
usage: {
|
|
54
|
+
input_tokens: 10,
|
|
55
|
+
output_tokens: 2,
|
|
56
|
+
total_tokens: 12,
|
|
57
|
+
input_tokens_details: { cache_write_tokens: 5 },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(getCacheWriteTokens(message)).toBe(5);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('falls back to the serialized metadata key when usage is absent', () => {
|
|
66
|
+
const message = new AIMessage({
|
|
67
|
+
content: 'hi',
|
|
68
|
+
response_metadata: {
|
|
69
|
+
metadata: { __librechat_cache_write_tokens: '7' },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(getCacheWriteTokens(message)).toBe(7);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('attachCacheWriteUsage', () => {
|
|
78
|
+
it('leaves usage_metadata untouched without throwing when input_tokens_details is missing', () => {
|
|
79
|
+
const message = new AIMessage({
|
|
80
|
+
content: 'hi',
|
|
81
|
+
response_metadata: {
|
|
82
|
+
usage: { input_tokens: 10, output_tokens: 2, total_tokens: 12 },
|
|
83
|
+
},
|
|
84
|
+
usage_metadata: {
|
|
85
|
+
input_tokens: 10,
|
|
86
|
+
output_tokens: 2,
|
|
87
|
+
total_tokens: 12,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(() => attachCacheWriteUsage(message)).not.toThrow();
|
|
92
|
+
expect(message.usage_metadata?.input_token_details).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('attachCacheWriteMetadata', () => {
|
|
97
|
+
it('returns the response unmodified without throwing when input_tokens_details is missing', () => {
|
|
98
|
+
const response = {
|
|
99
|
+
usage: { input_tokens: 10, output_tokens: 2, total_tokens: 12 },
|
|
100
|
+
} as OpenAIClient.Responses.Response;
|
|
101
|
+
|
|
102
|
+
expect(() => attachCacheWriteMetadata(response)).not.toThrow();
|
|
103
|
+
expect(attachCacheWriteMetadata(response)).toBe(response);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns the response unmodified without throwing when usage is missing entirely', () => {
|
|
107
|
+
const response = {} as OpenAIClient.Responses.Response;
|
|
108
|
+
|
|
109
|
+
expect(() => attachCacheWriteMetadata(response)).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|