@librechat/agents 3.3.0 → 3.3.2
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/agents/AgentContext.cjs +116 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +229 -48
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +102 -27
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +11 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
- package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
- package/dist/cjs/llm/google/index.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +60 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/main.cjs +20 -7
- package/dist/cjs/messages/prune.cjs +16 -5
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +2 -0
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/run.cjs +15 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +205 -49
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -7
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +2 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +69 -8
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +32 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +317 -52
- package/dist/cjs/utils/errors.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +117 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +227 -46
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +102 -27
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +11 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
- package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
- package/dist/esm/llm/google/index.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +59 -4
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/main.mjs +9 -9
- package/dist/esm/messages/prune.mjs +14 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +2 -1
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/run.mjs +15 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +205 -49
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -7
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +62 -9
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +32 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +317 -53
- package/dist/esm/utils/errors.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +62 -3
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +16 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
- package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
- package/dist/types/llm/invoke.d.ts +35 -6
- package/dist/types/messages/prune.d.ts +10 -2
- package/dist/types/messages/recency.d.ts +1 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -1
- package/dist/types/types/llm.d.ts +7 -4
- package/dist/types/types/stream.d.ts +5 -4
- package/dist/types/types/summarize.d.ts +22 -0
- package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
- package/dist/types/utils/errors.d.ts +65 -16
- package/dist/types/utils/redactSecrets.d.ts +3 -0
- package/package.json +10 -11
- package/src/agents/AgentContext.ts +188 -7
- package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +389 -58
- package/src/graphs/MultiAgentGraph.ts +184 -46
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
- package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
- package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
- package/src/llm/anthropic/llm.spec.ts +8 -0
- package/src/llm/bedrock/index.ts +25 -12
- package/src/llm/contextOverflowRecovery.ts +292 -0
- package/src/llm/invoke.ts +119 -4
- package/src/messages/prune.ts +24 -11
- package/src/messages/recency.ts +3 -1
- package/src/run.ts +24 -1
- package/src/scripts/context-overflow-probe.ts +997 -0
- package/src/specs/agent-handoffs.test.ts +903 -1
- package/src/specs/context-overflow-recovery.live.test.ts +213 -0
- package/src/splitStream.test.ts +882 -0
- package/src/stream.ts +315 -51
- package/src/summarization/__tests__/aggregator.test.ts +83 -0
- package/src/summarization/__tests__/node.test.ts +139 -0
- package/src/summarization/node.ts +99 -14
- package/src/tools/BashExecutor.ts +4 -2
- package/src/tools/BashProgrammaticToolCalling.ts +4 -7
- package/src/tools/CodeExecutor.ts +119 -8
- package/src/tools/ProgrammaticToolCalling.ts +29 -27
- package/src/tools/ToolNode.ts +50 -8
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
- package/src/types/llm.ts +8 -1
- package/src/types/stream.ts +5 -4
- package/src/types/summarize.ts +22 -0
- package/src/utils/__tests__/errors.test.ts +270 -0
- package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
- package/src/utils/__tests__/redactSecrets.test.ts +56 -0
- package/src/utils/errors.ts +484 -66
- package/src/utils/redactSecrets.ts +61 -0
package/src/utils/errors.ts
CHANGED
|
@@ -1,45 +1,388 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context overflow error detection
|
|
2
|
+
* Context overflow error detection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Providers disagree on how they report "your input is bigger than I can
|
|
5
|
+
* take" — the class thrown, the HTTP status, whether numbers are reported,
|
|
6
|
+
* and even whether it arrives as an HTTP error at all. Every pattern below
|
|
7
|
+
* was captured from a live over-limit request (see
|
|
8
|
+
* `src/scripts/context-overflow-probe.ts` and
|
|
9
|
+
* `docs/context-overflow-signatures.md`); nothing here is guessed.
|
|
10
|
+
*
|
|
11
|
+
* Consumed by the graph's overflow recovery loop, which converts a detection
|
|
12
|
+
* into a forced summarization pass instead of surfacing the error.
|
|
13
|
+
*/
|
|
14
|
+
import { ContextOverflowError } from '@langchain/core/errors';
|
|
15
|
+
import type { Providers } from '@/common';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Why the request was rejected. Both kinds are fixed by shrinking the
|
|
19
|
+
* prompt, which is what makes them recoverable; they are distinguished
|
|
20
|
+
* because only `context_window` tells us the model's true window.
|
|
8
21
|
*/
|
|
22
|
+
export type ContextOverflowKind =
|
|
23
|
+
/** Input exceeded the model's context window. */
|
|
24
|
+
| 'context_window'
|
|
25
|
+
/**
|
|
26
|
+
* A single request exceeded a per-minute token allowance. Waiting cannot
|
|
27
|
+
* help — the request can never fit the bucket — so this is a payload
|
|
28
|
+
* problem wearing a 429, not throttling.
|
|
29
|
+
*/
|
|
30
|
+
| 'request_too_large';
|
|
31
|
+
|
|
32
|
+
export interface ContextOverflowInfo {
|
|
33
|
+
kind: ContextOverflowKind;
|
|
34
|
+
/** Ceiling the provider reported, when it named one. */
|
|
35
|
+
limitTokens?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Token count the provider attributed to the whole request. Several
|
|
38
|
+
* providers fold the requested completion allowance into this number, so it
|
|
39
|
+
* is not interchangeable with the prompt size.
|
|
40
|
+
*/
|
|
41
|
+
requestedTokens?: number;
|
|
42
|
+
/**
|
|
43
|
+
* The prompt alone, counted by the provider — set only when the provider
|
|
44
|
+
* distinguished input from output, either by reporting an input-only figure
|
|
45
|
+
* or by breaking the total down. Callers comparing provider counts against
|
|
46
|
+
* their own prompt estimate must use this and not `requestedTokens`, whose
|
|
47
|
+
* completion component would inflate the comparison.
|
|
48
|
+
*/
|
|
49
|
+
promptTokens?: number;
|
|
50
|
+
/** Which layer produced the verdict. Surfaced in logs and asserted in tests. */
|
|
51
|
+
source: 'langchain' | 'pattern';
|
|
52
|
+
provider?: Providers;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface OverflowPattern {
|
|
56
|
+
readonly kind: ContextOverflowKind;
|
|
57
|
+
readonly re: RegExp;
|
|
58
|
+
readonly limitGroup?: number;
|
|
59
|
+
readonly requestedGroup?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Whether `requestedGroup` counts the prompt alone. Providers that report a
|
|
62
|
+
* combined input+completion total leave this false, and their number is
|
|
63
|
+
* never used as a prompt measurement.
|
|
64
|
+
*/
|
|
65
|
+
readonly requestedIsPromptOnly?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Marks a signature that is consistent with overflow but not exclusive to
|
|
68
|
+
* it, so it only counts when the caller can corroborate that the prompt was
|
|
69
|
+
* actually near the budget. Without corroboration the error propagates
|
|
70
|
+
* untouched rather than triggering a needless compaction.
|
|
71
|
+
*/
|
|
72
|
+
readonly requiresContextPressure?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fraction of the believed budget the prompt must reach before an ambiguous
|
|
77
|
+
* provider error is read as overflow. Well above normal traffic, low enough to
|
|
78
|
+
* catch the case the budget itself was miscalibrated.
|
|
79
|
+
*/
|
|
80
|
+
const CONTEXT_PRESSURE_RATIO = 0.8;
|
|
81
|
+
|
|
82
|
+
export interface ContextOverflowContext {
|
|
83
|
+
provider?: Providers;
|
|
84
|
+
/** Our own estimate of the prompt size for the call that failed. */
|
|
85
|
+
estimatedPromptTokens?: number;
|
|
86
|
+
/** The budget we believed applied when we built that prompt. */
|
|
87
|
+
maxContextTokens?: number;
|
|
88
|
+
}
|
|
9
89
|
|
|
10
90
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
91
|
+
* Ordered most-specific first; the first match wins. Patterns that capture
|
|
92
|
+
* both numbers come before the bare-phrase fallbacks for the same provider so
|
|
93
|
+
* a recoverable error still yields the numbers when the provider reported
|
|
94
|
+
* them.
|
|
13
95
|
*/
|
|
14
|
-
const
|
|
15
|
-
'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
96
|
+
const OVERFLOW_PATTERNS: readonly OverflowPattern[] = [
|
|
97
|
+
/** Anthropic, and Bedrock's passthrough of the same upstream. */
|
|
98
|
+
{
|
|
99
|
+
kind: 'context_window',
|
|
100
|
+
re: /prompt is too long:\s*(\d+)\s*tokens\s*>\s*(\d+)\s*maximum/i,
|
|
101
|
+
requestedGroup: 1,
|
|
102
|
+
limitGroup: 2,
|
|
103
|
+
requestedIsPromptOnly: true,
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* OpenAI's own wording, which measures the messages and nothing else.
|
|
107
|
+
* Ordered ahead of the shared sentence below so the prompt-only reading is
|
|
108
|
+
* preferred when OpenAI is the one answering.
|
|
109
|
+
*/
|
|
110
|
+
{
|
|
111
|
+
kind: 'context_window',
|
|
112
|
+
re: /maximum context length is\s*(\d+)\s*tokens\.\s*however,\s*your messages resulted in\s*(\d+)/i,
|
|
113
|
+
limitGroup: 1,
|
|
114
|
+
requestedGroup: 2,
|
|
115
|
+
requestedIsPromptOnly: true,
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* OpenRouter (`you requested about`) and DeepSeek (`you requested`). Their
|
|
119
|
+
* total folds in the completion allowance — both then break it down in
|
|
120
|
+
* parentheses, which `PROMPT_ONLY_BREAKDOWN_RE` recovers.
|
|
121
|
+
*/
|
|
122
|
+
{
|
|
123
|
+
kind: 'context_window',
|
|
124
|
+
re: /maximum context length is\s*(\d+)\s*tokens\.\s*however,\s*you requested(?:\s*about)?\s*(\d+)/i,
|
|
125
|
+
limitGroup: 1,
|
|
126
|
+
requestedGroup: 2,
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* xAI. Says "prompt length" rather than "context length", and does not say
|
|
130
|
+
* whether the count it quotes includes the completion allowance — so it is
|
|
131
|
+
* not trusted as a prompt measurement.
|
|
132
|
+
*/
|
|
133
|
+
{
|
|
134
|
+
kind: 'context_window',
|
|
135
|
+
re: /maximum prompt length is\s*(\d+)\s*(?:tokens\s*)?but the request contains\s*(\d+)\s*tokens/i,
|
|
136
|
+
limitGroup: 1,
|
|
137
|
+
requestedGroup: 2,
|
|
138
|
+
},
|
|
139
|
+
/** Mistral. */
|
|
140
|
+
{
|
|
141
|
+
kind: 'context_window',
|
|
142
|
+
re: /prompt contains\s*(\d+)\s*tokens[^.]*?too large for model with\s*(\d+)\s*maximum context length/i,
|
|
143
|
+
requestedGroup: 1,
|
|
144
|
+
limitGroup: 2,
|
|
145
|
+
requestedIsPromptOnly: true,
|
|
146
|
+
},
|
|
147
|
+
/** Google Gemini / Vertex. Reports the ceiling only. */
|
|
148
|
+
{
|
|
149
|
+
kind: 'context_window',
|
|
150
|
+
re: /input token count exceeds the maximum number of tokens allowed\s*\((\d+)\)/i,
|
|
151
|
+
limitGroup: 1,
|
|
152
|
+
},
|
|
153
|
+
/**
|
|
154
|
+
* OpenAI's token-bucket rejection. Numbers are validated by the caller:
|
|
155
|
+
* `Requested >= Limit` means no retry can ever succeed.
|
|
156
|
+
*/
|
|
157
|
+
{
|
|
158
|
+
kind: 'request_too_large',
|
|
159
|
+
re: /request too large[\s\S]*?limit\s*(\d+),\s*requested\s*(\d+)/i,
|
|
160
|
+
limitGroup: 1,
|
|
161
|
+
requestedGroup: 2,
|
|
162
|
+
},
|
|
163
|
+
/** Bedrock (Llama upstream) and any provider naming the ceiling alone. */
|
|
164
|
+
{
|
|
165
|
+
kind: 'context_window',
|
|
166
|
+
re: /(?:maximum context length|maximum prompt length) is\s*(\d+)\s*tokens/i,
|
|
167
|
+
limitGroup: 1,
|
|
168
|
+
},
|
|
169
|
+
/** Bedrock (Nova upstream). */
|
|
170
|
+
{
|
|
171
|
+
kind: 'context_window',
|
|
172
|
+
re: /number of input tokens exceeds maximum length|input tokens exceeded/i,
|
|
173
|
+
},
|
|
174
|
+
/** Bedrock (Claude Sonnet upstream). */
|
|
175
|
+
{
|
|
176
|
+
kind: 'context_window',
|
|
177
|
+
re: /\binput (?:is )?too long(?: for requested model)?\b/i,
|
|
178
|
+
},
|
|
179
|
+
/** OpenAI-compatible error code, and the phrases LangChain itself keys on. */
|
|
180
|
+
{
|
|
181
|
+
kind: 'context_window',
|
|
182
|
+
re: /context[_ ]length[_ ]exceeded|input tokens exceed the configured limit|exceeds the context window|exceeds model context window/i,
|
|
183
|
+
},
|
|
184
|
+
/** Generic long-tail phrasings observed across OpenAI-compatible gateways. */
|
|
185
|
+
{
|
|
186
|
+
kind: 'context_window',
|
|
187
|
+
re: /prompt is too long|too large for model|reduce the length of (?:the |your )?(?:messages|prompt)/i,
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Vertex AI. Its gaxios path discards the API error document, so an
|
|
191
|
+
* over-limit prompt arrives as a bare status line with no reason —
|
|
192
|
+
* identical to every other 400 from the same endpoint. Only a corroborated
|
|
193
|
+
* near-budget prompt makes this readable as overflow.
|
|
194
|
+
*/
|
|
195
|
+
{
|
|
196
|
+
kind: 'context_window',
|
|
197
|
+
re: /google request failed with status code 400(?!\s*:)/i,
|
|
198
|
+
requiresContextPressure: true,
|
|
199
|
+
},
|
|
28
200
|
] as const;
|
|
29
201
|
|
|
30
202
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
203
|
+
* Errors that mention size or limits but are NOT recoverable by compaction.
|
|
204
|
+
* Checked before the positive patterns.
|
|
205
|
+
*
|
|
206
|
+
* Note the deliberate precision: OpenAI's *recoverable* "Request too large"
|
|
207
|
+
* body links to `platform.openai.com/account/rate-limits`, so a loose
|
|
208
|
+
* /rate.?limit/ test would discard the very case this module exists to catch.
|
|
209
|
+
* URLs are stripped from the haystack before matching, and genuine throttling
|
|
210
|
+
* is matched on its own distinct phrasing.
|
|
211
|
+
*/
|
|
212
|
+
const NON_RECOVERABLE_RE =
|
|
213
|
+
/rate limit reached|requests per (?:min|day)|\brpm\b|too many requests|insufficient[_ ]quota|quota exceeded|billing|payment required|invalid[_ ]api[_ ]key|authentication|unauthorized|permission denied|forbidden/i;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Output-cap complaints. `max_tokens` appears in both families, so these are
|
|
217
|
+
* matched on the surrounding grammar rather than the bare parameter name.
|
|
218
|
+
*/
|
|
219
|
+
const OUTPUT_LIMIT_RE =
|
|
220
|
+
/max_?(?:completion_?)?tokens\s*(?:must be|is too|cannot|exceeds|too large|greater than)|maximum number of output tokens|max_tokens.*less than or equal/i;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Recovers the input-only figure from providers that quote a combined total
|
|
224
|
+
* and then break it down — OpenRouter's "(56811 of text input, 16 in the
|
|
225
|
+
* output)" and DeepSeek's "(1179652 in the messages, 16 in the completion)".
|
|
33
226
|
*/
|
|
227
|
+
const PROMPT_ONLY_BREAKDOWN_RE =
|
|
228
|
+
/\(\s*(\d+)\s*(?:of\s+text\s+input|in\s+the\s+messages|of\s+input|input\s+tokens)\b/i;
|
|
229
|
+
|
|
230
|
+
/** Broader hints for the deliberately fuzzy `isLikelyContextOverflowError`. */
|
|
34
231
|
const CONTEXT_OVERFLOW_HINT_RE =
|
|
35
|
-
/413|too large|too
|
|
232
|
+
/413|payload too large|content_too_large|request entity too large|too many tokens|token count.*exceed|exceed.*token count/i;
|
|
233
|
+
|
|
234
|
+
const MAX_CAUSE_DEPTH = 4;
|
|
235
|
+
|
|
236
|
+
interface NestedErrorShape {
|
|
237
|
+
message?: unknown;
|
|
238
|
+
code?: unknown;
|
|
239
|
+
type?: unknown;
|
|
240
|
+
status?: unknown;
|
|
241
|
+
reason?: unknown;
|
|
242
|
+
error?: unknown;
|
|
243
|
+
cause?: unknown;
|
|
244
|
+
body?: unknown;
|
|
245
|
+
response?: unknown;
|
|
246
|
+
/** gaxios-style clients (Vertex AI) put the API error document here. */
|
|
247
|
+
data?: unknown;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function asRecord(value: unknown): NestedErrorShape | undefined {
|
|
251
|
+
return typeof value === 'object' && value !== null
|
|
252
|
+
? (value as NestedErrorShape)
|
|
253
|
+
: undefined;
|
|
254
|
+
}
|
|
36
255
|
|
|
37
256
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
257
|
+
* Flattens an error into a single searchable string.
|
|
258
|
+
*
|
|
259
|
+
* Necessary because providers bury the useful sentence at different depths:
|
|
260
|
+
* Anthropic puts a JSON document in `message`, Mistral puts one in `body`,
|
|
261
|
+
* LangChain's `ContextOverflowError` keeps the original API error under
|
|
262
|
+
* `cause`, and the OpenAI SDK nests the body under `error`.
|
|
40
263
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
264
|
+
function collectErrorText(error: unknown, depth = 0): string {
|
|
265
|
+
if (error == null || depth > MAX_CAUSE_DEPTH) {
|
|
266
|
+
return '';
|
|
267
|
+
}
|
|
268
|
+
if (typeof error === 'string') {
|
|
269
|
+
return error;
|
|
270
|
+
}
|
|
271
|
+
const record = asRecord(error);
|
|
272
|
+
if (record == null) {
|
|
273
|
+
return String(error);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const parts: string[] = [];
|
|
277
|
+
if (typeof record.message === 'string') {
|
|
278
|
+
parts.push(record.message);
|
|
279
|
+
}
|
|
280
|
+
for (const reason of [
|
|
281
|
+
record.code,
|
|
282
|
+
record.type,
|
|
283
|
+
record.status,
|
|
284
|
+
record.reason,
|
|
285
|
+
]) {
|
|
286
|
+
if (typeof reason === 'string') {
|
|
287
|
+
parts.push(reason);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const nested of [
|
|
291
|
+
record.error,
|
|
292
|
+
record.cause,
|
|
293
|
+
record.body,
|
|
294
|
+
record.data,
|
|
295
|
+
record.response,
|
|
296
|
+
]) {
|
|
297
|
+
if (nested == null) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
parts.push(
|
|
301
|
+
typeof nested === 'string' ? nested : collectErrorText(nested, depth + 1)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (parts.length === 0) {
|
|
305
|
+
try {
|
|
306
|
+
/** Non-objects returned above, so this always yields a string. */
|
|
307
|
+
return JSON.stringify(error);
|
|
308
|
+
} catch {
|
|
309
|
+
return String(error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return parts.join(' ');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Strips URLs so their path segments cannot trip the negative matchers. */
|
|
316
|
+
function stripUrls(text: string): string {
|
|
317
|
+
return text.replace(/https?:\/\/\S+/gi, ' ');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readNumber(
|
|
321
|
+
match: RegExpMatchArray,
|
|
322
|
+
group?: number
|
|
323
|
+
): number | undefined {
|
|
324
|
+
if (group == null) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
const parsed = Number(match[group]);
|
|
328
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The provider's count of the prompt alone: its own breakdown when it gave
|
|
333
|
+
* one, otherwise the quoted total but only for providers that quote the
|
|
334
|
+
* prompt rather than the whole request. Returns undefined when the number on
|
|
335
|
+
* offer includes the completion allowance, since treating that as a prompt
|
|
336
|
+
* measurement would overstate how much the prompt has to shrink.
|
|
337
|
+
*/
|
|
338
|
+
function resolvePromptTokens(
|
|
339
|
+
haystack: string,
|
|
340
|
+
pattern: OverflowPattern,
|
|
341
|
+
requestedTokens: number | undefined
|
|
342
|
+
): number | undefined {
|
|
343
|
+
const breakdown = haystack.match(PROMPT_ONLY_BREAKDOWN_RE);
|
|
344
|
+
if (breakdown != null) {
|
|
345
|
+
const parsed = Number(breakdown[1]);
|
|
346
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
347
|
+
return parsed;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return pattern.requestedIsPromptOnly === true ? requestedTokens : undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* True when the caller's own accounting says the failed prompt was close
|
|
355
|
+
* enough to the budget that an otherwise ambiguous provider error is best
|
|
356
|
+
* explained by overflow.
|
|
357
|
+
*/
|
|
358
|
+
function hasContextPressure(context?: ContextOverflowContext): boolean {
|
|
359
|
+
const estimated = context?.estimatedPromptTokens;
|
|
360
|
+
const budget = context?.maxContextTokens;
|
|
361
|
+
if (
|
|
362
|
+
estimated == null ||
|
|
363
|
+
budget == null ||
|
|
364
|
+
!Number.isFinite(estimated) ||
|
|
365
|
+
!Number.isFinite(budget) ||
|
|
366
|
+
budget <= 0
|
|
367
|
+
) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return estimated / budget >= CONTEXT_PRESSURE_RATIO;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function isLangChainOverflowError(error: unknown): boolean {
|
|
374
|
+
if (ContextOverflowError.isInstance(error)) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
/** Duplicate `@langchain/core` copies break branding; the name survives. */
|
|
378
|
+
const record = asRecord(error) as
|
|
379
|
+
| { name?: unknown; lc_error_code?: unknown }
|
|
380
|
+
| undefined;
|
|
381
|
+
return (
|
|
382
|
+
record?.name === 'ContextOverflowError' ||
|
|
383
|
+
record?.lc_error_code === 'CONTEXT_OVERFLOW'
|
|
384
|
+
);
|
|
385
|
+
}
|
|
43
386
|
|
|
44
387
|
/**
|
|
45
388
|
* Extracts a human-readable error message from an unknown error value.
|
|
@@ -54,21 +397,20 @@ export function extractErrorMessage(error: unknown): string {
|
|
|
54
397
|
if (error instanceof Error) {
|
|
55
398
|
return error.message;
|
|
56
399
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
400
|
+
const record = asRecord(error);
|
|
401
|
+
if (record == null) {
|
|
402
|
+
/** Functions and symbols serialize to nothing; describe them instead. */
|
|
403
|
+
return String(error);
|
|
404
|
+
}
|
|
405
|
+
if (typeof record.message === 'string') {
|
|
406
|
+
return record.message;
|
|
407
|
+
}
|
|
408
|
+
if (typeof record.error === 'string') {
|
|
409
|
+
return record.error;
|
|
410
|
+
}
|
|
411
|
+
const nested = asRecord(record.error);
|
|
412
|
+
if (typeof nested?.message === 'string') {
|
|
413
|
+
return nested.message;
|
|
72
414
|
}
|
|
73
415
|
try {
|
|
74
416
|
return JSON.stringify(error);
|
|
@@ -78,40 +420,116 @@ export function extractErrorMessage(error: unknown): string {
|
|
|
78
420
|
}
|
|
79
421
|
|
|
80
422
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
423
|
+
* Classifies a provider error as a recoverable context overflow, returning
|
|
424
|
+
* whatever the provider disclosed about the limit and the request size.
|
|
83
425
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
426
|
+
* Returns `null` for anything that compaction cannot fix — genuine
|
|
427
|
+
* throttling, auth, quota, and output-token-cap errors all mention limits
|
|
428
|
+
* and must not be mistaken for overflow.
|
|
429
|
+
*
|
|
430
|
+
* The reported numbers are the point of the return value: they let the
|
|
431
|
+
* caller retarget the token budget to the provider's real ceiling instead of
|
|
432
|
+
* retrying blindly against a configured value that was evidently wrong.
|
|
86
433
|
*/
|
|
87
|
-
export function
|
|
88
|
-
|
|
89
|
-
|
|
434
|
+
export function getContextOverflowInfo(
|
|
435
|
+
error: unknown,
|
|
436
|
+
context?: ContextOverflowContext
|
|
437
|
+
): ContextOverflowInfo | null {
|
|
438
|
+
const provider = context?.provider;
|
|
439
|
+
const haystack = stripUrls(collectErrorText(error));
|
|
440
|
+
if (haystack === '') {
|
|
441
|
+
return null;
|
|
90
442
|
}
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
return
|
|
443
|
+
|
|
444
|
+
if (OUTPUT_LIMIT_RE.test(haystack)) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const langChainFlagged = isLangChainOverflowError(error);
|
|
449
|
+
if (!langChainFlagged && NON_RECOVERABLE_RE.test(haystack)) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const underContextPressure = hasContextPressure(context);
|
|
454
|
+
|
|
455
|
+
for (const pattern of OVERFLOW_PATTERNS) {
|
|
456
|
+
const match = haystack.match(pattern.re);
|
|
457
|
+
if (match == null) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (pattern.requiresContextPressure === true && !underContextPressure) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const limitTokens = readNumber(match, pattern.limitGroup);
|
|
464
|
+
const requestedTokens = readNumber(match, pattern.requestedGroup);
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* A token-bucket rejection is only unrecoverable-by-waiting when the
|
|
468
|
+
* request alone overruns the bucket. When it merely fills it, the account
|
|
469
|
+
* was busy and the request will fit once the window drains — so equality
|
|
470
|
+
* belongs on the retry side, not the compaction side. Losing conversation
|
|
471
|
+
* history to a temporarily busy account is the worse error.
|
|
472
|
+
*/
|
|
473
|
+
if (
|
|
474
|
+
pattern.kind === 'request_too_large' &&
|
|
475
|
+
limitTokens != null &&
|
|
476
|
+
requestedTokens != null &&
|
|
477
|
+
requestedTokens <= limitTokens
|
|
478
|
+
) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
kind: pattern.kind,
|
|
484
|
+
limitTokens,
|
|
485
|
+
requestedTokens,
|
|
486
|
+
promptTokens: resolvePromptTokens(haystack, pattern, requestedTokens),
|
|
487
|
+
source: 'pattern',
|
|
488
|
+
provider,
|
|
489
|
+
};
|
|
94
490
|
}
|
|
95
|
-
|
|
491
|
+
|
|
492
|
+
if (langChainFlagged) {
|
|
493
|
+
return { kind: 'context_window', source: 'langchain', provider };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return null;
|
|
96
497
|
}
|
|
97
498
|
|
|
98
499
|
/**
|
|
99
|
-
* Returns true if the error
|
|
100
|
-
* Uses broader heuristic matching (regex) in addition to exact phrases.
|
|
500
|
+
* Returns true if the error definitively indicates a context overflow.
|
|
101
501
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
502
|
+
* Accepts either a raw error or a pre-extracted message; passing the error
|
|
503
|
+
* itself is preferred, since several providers report the decisive detail in
|
|
504
|
+
* a nested body rather than in `message`.
|
|
104
505
|
*/
|
|
105
|
-
export function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
506
|
+
export function isContextOverflowError(
|
|
507
|
+
error?: unknown,
|
|
508
|
+
context?: ContextOverflowContext
|
|
509
|
+
): boolean {
|
|
510
|
+
return getContextOverflowInfo(error, context) != null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Returns true if the error likely indicates a context overflow, adding
|
|
515
|
+
* body-size and token-count heuristics on top of the definitive patterns.
|
|
516
|
+
*
|
|
517
|
+
* May produce false positives on unusual messages. Use when the cost of
|
|
518
|
+
* being wrong is one extra compaction pass.
|
|
519
|
+
*/
|
|
520
|
+
export function isLikelyContextOverflowError(
|
|
521
|
+
error?: unknown,
|
|
522
|
+
context?: ContextOverflowContext
|
|
523
|
+
): boolean {
|
|
524
|
+
if (isContextOverflowError(error, context)) {
|
|
110
525
|
return true;
|
|
111
526
|
}
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
527
|
+
const haystack = stripUrls(collectErrorText(error));
|
|
528
|
+
if (haystack === '' || OUTPUT_LIMIT_RE.test(haystack)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
if (NON_RECOVERABLE_RE.test(haystack)) {
|
|
114
532
|
return false;
|
|
115
533
|
}
|
|
116
|
-
return CONTEXT_OVERFLOW_HINT_RE.test(
|
|
534
|
+
return CONTEXT_OVERFLOW_HINT_RE.test(haystack);
|
|
117
535
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const SECRET_KEY_RE =
|
|
2
|
+
/key|token|secret|credential|authorization|password|cookie|signature|(?:^|[_-])sig(?:$|[_-])/i;
|
|
3
|
+
|
|
4
|
+
const REDACTED_VALUE = '[REDACTED]';
|
|
5
|
+
const CIRCULAR_VALUE = '[CIRCULAR]';
|
|
6
|
+
|
|
7
|
+
export function isSecretKey(key: string): boolean {
|
|
8
|
+
return SECRET_KEY_RE.test(key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function redactUrlCredentials(value: string): string {
|
|
12
|
+
if (!/^https?:\/\//i.test(value)) {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
if (url.username !== '') {
|
|
18
|
+
url.username = REDACTED_VALUE;
|
|
19
|
+
}
|
|
20
|
+
if (url.password !== '') {
|
|
21
|
+
url.password = REDACTED_VALUE;
|
|
22
|
+
}
|
|
23
|
+
for (const key of url.searchParams.keys()) {
|
|
24
|
+
if (isSecretKey(key)) {
|
|
25
|
+
url.searchParams.set(key, REDACTED_VALUE);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
29
|
+
} catch {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Recursively removes credentials from structured diagnostic payloads. */
|
|
35
|
+
export function redactSecrets(
|
|
36
|
+
value: unknown,
|
|
37
|
+
seen: WeakSet<object> = new WeakSet()
|
|
38
|
+
): unknown {
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
return redactUrlCredentials(value);
|
|
41
|
+
}
|
|
42
|
+
if (value == null || typeof value !== 'object') {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (seen.has(value)) {
|
|
46
|
+
return CIRCULAR_VALUE;
|
|
47
|
+
}
|
|
48
|
+
seen.add(value);
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return value.map((entry) => redactSecrets(entry, seen));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const redacted: Record<string, unknown> = {};
|
|
55
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
56
|
+
redacted[key] = isSecretKey(key)
|
|
57
|
+
? REDACTED_VALUE
|
|
58
|
+
: redactSecrets(entry, seen);
|
|
59
|
+
}
|
|
60
|
+
return redacted;
|
|
61
|
+
}
|