@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.
Files changed (130) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +116 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +2 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +229 -48
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +102 -27
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/index.cjs +11 -2
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
  12. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
  13. package/dist/cjs/llm/google/index.cjs +1 -1
  14. package/dist/cjs/llm/invoke.cjs +60 -3
  15. package/dist/cjs/llm/invoke.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +20 -7
  17. package/dist/cjs/messages/prune.cjs +16 -5
  18. package/dist/cjs/messages/prune.cjs.map +1 -1
  19. package/dist/cjs/messages/recency.cjs +2 -0
  20. package/dist/cjs/messages/recency.cjs.map +1 -1
  21. package/dist/cjs/run.cjs +15 -2
  22. package/dist/cjs/run.cjs.map +1 -1
  23. package/dist/cjs/stream.cjs +205 -49
  24. package/dist/cjs/stream.cjs.map +1 -1
  25. package/dist/cjs/summarization/node.cjs +55 -7
  26. package/dist/cjs/summarization/node.cjs.map +1 -1
  27. package/dist/cjs/tools/BashExecutor.cjs +2 -2
  28. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  29. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
  30. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  31. package/dist/cjs/tools/CodeExecutor.cjs +69 -8
  32. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  33. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  35. package/dist/cjs/tools/ToolNode.cjs +32 -3
  36. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  37. package/dist/cjs/utils/errors.cjs +317 -52
  38. package/dist/cjs/utils/errors.cjs.map +1 -1
  39. package/dist/esm/agents/AgentContext.mjs +117 -3
  40. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  41. package/dist/esm/common/enum.mjs +2 -0
  42. package/dist/esm/common/enum.mjs.map +1 -1
  43. package/dist/esm/graphs/Graph.mjs +227 -46
  44. package/dist/esm/graphs/Graph.mjs.map +1 -1
  45. package/dist/esm/graphs/MultiAgentGraph.mjs +102 -27
  46. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  47. package/dist/esm/llm/bedrock/index.mjs +11 -2
  48. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  49. package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
  50. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
  51. package/dist/esm/llm/google/index.mjs +1 -1
  52. package/dist/esm/llm/invoke.mjs +59 -4
  53. package/dist/esm/llm/invoke.mjs.map +1 -1
  54. package/dist/esm/main.mjs +9 -9
  55. package/dist/esm/messages/prune.mjs +14 -6
  56. package/dist/esm/messages/prune.mjs.map +1 -1
  57. package/dist/esm/messages/recency.mjs +2 -1
  58. package/dist/esm/messages/recency.mjs.map +1 -1
  59. package/dist/esm/run.mjs +15 -2
  60. package/dist/esm/run.mjs.map +1 -1
  61. package/dist/esm/stream.mjs +205 -49
  62. package/dist/esm/stream.mjs.map +1 -1
  63. package/dist/esm/summarization/node.mjs +55 -7
  64. package/dist/esm/summarization/node.mjs.map +1 -1
  65. package/dist/esm/tools/BashExecutor.mjs +3 -3
  66. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  67. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  68. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  69. package/dist/esm/tools/CodeExecutor.mjs +62 -9
  70. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  71. package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
  72. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  73. package/dist/esm/tools/ToolNode.mjs +32 -3
  74. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  75. package/dist/esm/utils/errors.mjs +317 -53
  76. package/dist/esm/utils/errors.mjs.map +1 -1
  77. package/dist/types/agents/AgentContext.d.ts +62 -3
  78. package/dist/types/common/enum.d.ts +2 -0
  79. package/dist/types/graphs/Graph.d.ts +16 -2
  80. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  81. package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
  82. package/dist/types/llm/invoke.d.ts +35 -6
  83. package/dist/types/messages/prune.d.ts +10 -2
  84. package/dist/types/messages/recency.d.ts +1 -0
  85. package/dist/types/run.d.ts +2 -0
  86. package/dist/types/tools/CodeExecutor.d.ts +14 -1
  87. package/dist/types/types/llm.d.ts +7 -4
  88. package/dist/types/types/stream.d.ts +5 -4
  89. package/dist/types/types/summarize.d.ts +22 -0
  90. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
  91. package/dist/types/utils/errors.d.ts +65 -16
  92. package/dist/types/utils/redactSecrets.d.ts +3 -0
  93. package/package.json +10 -11
  94. package/src/agents/AgentContext.ts +188 -7
  95. package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
  96. package/src/common/enum.ts +2 -0
  97. package/src/graphs/Graph.ts +389 -58
  98. package/src/graphs/MultiAgentGraph.ts +184 -46
  99. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
  100. package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
  101. package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
  102. package/src/llm/anthropic/llm.spec.ts +8 -0
  103. package/src/llm/bedrock/index.ts +25 -12
  104. package/src/llm/contextOverflowRecovery.ts +292 -0
  105. package/src/llm/invoke.ts +119 -4
  106. package/src/messages/prune.ts +24 -11
  107. package/src/messages/recency.ts +3 -1
  108. package/src/run.ts +24 -1
  109. package/src/scripts/context-overflow-probe.ts +997 -0
  110. package/src/specs/agent-handoffs.test.ts +903 -1
  111. package/src/specs/context-overflow-recovery.live.test.ts +213 -0
  112. package/src/splitStream.test.ts +882 -0
  113. package/src/stream.ts +315 -51
  114. package/src/summarization/__tests__/aggregator.test.ts +83 -0
  115. package/src/summarization/__tests__/node.test.ts +139 -0
  116. package/src/summarization/node.ts +99 -14
  117. package/src/tools/BashExecutor.ts +4 -2
  118. package/src/tools/BashProgrammaticToolCalling.ts +4 -7
  119. package/src/tools/CodeExecutor.ts +119 -8
  120. package/src/tools/ProgrammaticToolCalling.ts +29 -27
  121. package/src/tools/ToolNode.ts +50 -8
  122. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
  123. package/src/types/llm.ts +8 -1
  124. package/src/types/stream.ts +5 -4
  125. package/src/types/summarize.ts +22 -0
  126. package/src/utils/__tests__/errors.test.ts +270 -0
  127. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
  128. package/src/utils/__tests__/redactSecrets.test.ts +56 -0
  129. package/src/utils/errors.ts +484 -66
  130. package/src/utils/redactSecrets.ts +61 -0
@@ -0,0 +1,997 @@
1
+ /**
2
+ * Context-overflow signature probe.
3
+ *
4
+ * Deliberately sends an over-limit prompt to every provider the SDK
5
+ * supports and records the raw error each one throws, so the overflow
6
+ * classifier is grounded in observed provider behavior rather than
7
+ * guessed phrases.
8
+ *
9
+ * Run:
10
+ * DOTENV_CONFIG_PATH=/path/to/.env node --loader ./tsconfig-paths-bootstrap.mjs \
11
+ * --experimental-specifier-resolution=node ./src/scripts/context-overflow-probe.ts
12
+ *
13
+ * Flags:
14
+ * --only <provider[,provider]> restrict to given providers
15
+ * --model <substring> restrict to matching model ids
16
+ * --tier <full|confirm> restrict to a load tier
17
+ * --mode <stream|invoke|both> which invocation path to probe (default: stream)
18
+ * --out <path> output JSON path
19
+ * --list print the resolved matrix and exit
20
+ *
21
+ * Cost note: an over-limit request is rejected at request validation, so
22
+ * providers do not bill the prompt. The `confirm` tier still overshoots by a
23
+ * wide margin so an expensive model can never accidentally accept the prompt.
24
+ */
25
+ import { writeFileSync } from 'fs';
26
+ import { config as loadEnv } from 'dotenv';
27
+ import { HumanMessage } from '@langchain/core/messages';
28
+ import type { BaseMessage } from '@langchain/core/messages';
29
+ import type * as t from '@/types';
30
+ import { isSecretKey, redactSecrets } from '@/utils/redactSecrets';
31
+ import { initializeModel } from '@/llm/init';
32
+ import { Providers } from '@/common';
33
+
34
+ loadEnv({ path: process.env.DOTENV_CONFIG_PATH ?? '.env' });
35
+
36
+ /**
37
+ * `full` targets are cheap enough to probe at a modest overshoot.
38
+ * `confirm` targets are expensive or huge-context; they get a single
39
+ * wide-margin attempt purely to confirm the API surface's signature.
40
+ */
41
+ type ProbeTier = 'full' | 'confirm';
42
+
43
+ type ProbeMode = 'stream' | 'invoke';
44
+
45
+ interface ProbeTarget {
46
+ provider: Providers;
47
+ /** Model id as the provider expects it. */
48
+ model: string;
49
+ /** Documented input context window, in tokens. */
50
+ contextWindow: number;
51
+ tier: ProbeTier;
52
+ /** Upstream API actually serving the request (OpenRouter routes to many). */
53
+ upstream?: string;
54
+ }
55
+
56
+ interface SerializedError {
57
+ constructorName: string;
58
+ name?: string;
59
+ message: string;
60
+ status?: number;
61
+ code?: string;
62
+ type?: string;
63
+ errorType?: string;
64
+ errorCode?: string;
65
+ errorStatus?: string;
66
+ httpStatusCode?: number;
67
+ awsErrorType?: string;
68
+ requestId?: string;
69
+ lcErrorCode?: string;
70
+ /** gaxios-style clients (Vertex AI) hide the API error document here. */
71
+ responseData?: string;
72
+ /** Enumerable own properties, minus noisy/secret-bearing ones. */
73
+ ownProperties: Record<string, string>;
74
+ /** Best-effort raw JSON body if the SDK attached one. */
75
+ rawBody?: string;
76
+ }
77
+
78
+ interface ProbeResult {
79
+ provider: Providers;
80
+ model: string;
81
+ upstream?: string;
82
+ tier: ProbeTier;
83
+ mode: ProbeMode;
84
+ contextWindow: number;
85
+ approxTokensSent: number;
86
+ outcome: 'rejected' | 'accepted' | 'skipped' | 'unavailable';
87
+ skipReason?: string;
88
+ durationMs: number;
89
+ error?: SerializedError;
90
+ }
91
+
92
+ const OVERSHOOT_BY_TIER: Record<ProbeTier, number> = {
93
+ full: 1.3,
94
+ confirm: 2,
95
+ };
96
+
97
+ /** Absolute floor so tiny-context models still clear the window decisively. */
98
+ const MIN_OVERSHOOT_TOKENS = 4_000;
99
+
100
+ /**
101
+ * Payload ceiling (~12MB of text). Beyond this the request stops testing the
102
+ * token limit and starts testing the provider's body-size limit, which is a
103
+ * different failure mode. Targets that would need more are skipped rather
104
+ * than probed with a smaller payload, since an under-limit payload could be
105
+ * *accepted* and billed.
106
+ */
107
+ const MAX_PROBE_TOKENS = 2_400_000;
108
+
109
+ /**
110
+ * Words chosen to be single tokens under BPE vocabularies, so an N-word
111
+ * payload is guaranteed to be at least N tokens for every provider. Under-
112
+ * counting would risk a request being *accepted* and billed; over-counting
113
+ * only costs bandwidth.
114
+ */
115
+ const FILLER_WORDS = [
116
+ 'the',
117
+ 'quick',
118
+ 'brown',
119
+ 'fox',
120
+ 'jumps',
121
+ 'over',
122
+ 'lazy',
123
+ 'dog',
124
+ 'and',
125
+ 'then',
126
+ 'runs',
127
+ 'past',
128
+ 'green',
129
+ 'river',
130
+ 'under',
131
+ 'bright',
132
+ 'morning',
133
+ 'sky',
134
+ ];
135
+
136
+ function buildOverflowText(wordCount: number): string {
137
+ const parts: string[] = new Array(wordCount);
138
+ for (let i = 0; i < wordCount; i++) {
139
+ parts[i] = FILLER_WORDS[i % FILLER_WORDS.length];
140
+ }
141
+ return parts.join(' ');
142
+ }
143
+
144
+ function overflowTokenTarget(
145
+ target: ProbeTarget,
146
+ factorOverride?: number
147
+ ): number {
148
+ const factor = factorOverride ?? OVERSHOOT_BY_TIER[target.tier];
149
+ const scaled = Math.ceil(target.contextWindow * factor);
150
+ if (factorOverride != null) {
151
+ return scaled;
152
+ }
153
+ return Math.max(scaled, target.contextWindow + MIN_OVERSHOOT_TOKENS);
154
+ }
155
+
156
+ function readString(
157
+ source: Record<string, unknown>,
158
+ key: string
159
+ ): string | undefined {
160
+ const value = source[key];
161
+ return typeof value === 'string' && value !== '' ? value : undefined;
162
+ }
163
+
164
+ function readNumber(
165
+ source: Record<string, unknown>,
166
+ key: string
167
+ ): number | undefined {
168
+ const value = source[key];
169
+ return typeof value === 'number' && Number.isFinite(value)
170
+ ? value
171
+ : undefined;
172
+ }
173
+
174
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
175
+ return typeof value === 'object' && value !== null
176
+ ? (value as Record<string, unknown>)
177
+ : undefined;
178
+ }
179
+
180
+ function stringifySafe(value: unknown, limit = 4_000): string {
181
+ if (typeof value === 'string') {
182
+ return value.slice(0, limit);
183
+ }
184
+ try {
185
+ return (
186
+ JSON.stringify(redactSecrets(value))?.slice(0, limit) ?? String(value)
187
+ );
188
+ } catch {
189
+ return String(value).slice(0, limit);
190
+ }
191
+ }
192
+
193
+ function collectOwnProperties(error: object): Record<string, string> {
194
+ const collected: Record<string, string> = {};
195
+ for (const key of Object.keys(error)) {
196
+ if (key === 'stack' || key === 'message') {
197
+ continue;
198
+ }
199
+ collected[key] = isSecretKey(key)
200
+ ? '[REDACTED]'
201
+ : stringifySafe((error as Record<string, unknown>)[key], 800);
202
+ }
203
+ return collected;
204
+ }
205
+
206
+ /**
207
+ * Flattens a provider error into the fields a classifier could realistically
208
+ * key on. Deliberately shallow-but-wide: the point of the probe is to learn
209
+ * which of these fields providers actually populate.
210
+ */
211
+ function serializeError(error: unknown): SerializedError {
212
+ const record = asRecord(error) ?? {};
213
+ const nestedError = asRecord(record.error);
214
+ const metadata = asRecord(record.$metadata);
215
+ const responseBody = asRecord(record.response);
216
+
217
+ const message =
218
+ error instanceof Error
219
+ ? error.message
220
+ : (readString(record, 'message') ?? stringifySafe(error));
221
+
222
+ return {
223
+ constructorName:
224
+ typeof error === 'object' && error !== null
225
+ ? error.constructor.name
226
+ : typeof error,
227
+ name: readString(record, 'name'),
228
+ message: message.slice(0, 4_000),
229
+ status: readNumber(record, 'status') ?? readNumber(record, 'statusCode'),
230
+ code: readString(record, 'code'),
231
+ type: readString(record, 'type'),
232
+ errorType: nestedError ? readString(nestedError, 'type') : undefined,
233
+ errorCode: nestedError ? readString(nestedError, 'code') : undefined,
234
+ errorStatus: nestedError ? readString(nestedError, 'status') : undefined,
235
+ httpStatusCode: metadata
236
+ ? readNumber(metadata, 'httpStatusCode')
237
+ : undefined,
238
+ awsErrorType: readString(record, '__type'),
239
+ requestId:
240
+ readString(record, 'request_id') ?? readString(record, 'requestId'),
241
+ lcErrorCode: readString(record, 'lc_error_code'),
242
+ responseData:
243
+ responseBody?.data != null
244
+ ? stringifySafe(responseBody.data, 2_000)
245
+ : undefined,
246
+ ownProperties:
247
+ typeof error === 'object' && error !== null
248
+ ? collectOwnProperties(error)
249
+ : {},
250
+ rawBody: nestedError
251
+ ? stringifySafe(nestedError)
252
+ : responseBody
253
+ ? stringifySafe(responseBody)
254
+ : undefined,
255
+ };
256
+ }
257
+
258
+ function envValue(name: string): string | undefined {
259
+ const value = process.env[name]?.trim();
260
+ return value != null && value !== ''
261
+ ? value.replace(/^["']|["']$/g, '')
262
+ : undefined;
263
+ }
264
+
265
+ interface CredentialCheck {
266
+ ok: boolean;
267
+ reason?: string;
268
+ }
269
+
270
+ function checkCredentials(provider: Providers): CredentialCheck {
271
+ switch (provider) {
272
+ case Providers.OPENAI:
273
+ return envValue('OPENAI_API_KEY')
274
+ ? { ok: true }
275
+ : { ok: false, reason: 'OPENAI_API_KEY not set' };
276
+ case Providers.AZURE:
277
+ return envValue('AZURE_OPENAI_API_KEY') &&
278
+ envValue('AZURE_OPENAI_API_INSTANCE')
279
+ ? { ok: true }
280
+ : { ok: false, reason: 'AZURE_OPENAI_API_* not set' };
281
+ case Providers.ANTHROPIC:
282
+ return envValue('ANTHROPIC_API_KEY')
283
+ ? { ok: true }
284
+ : { ok: false, reason: 'ANTHROPIC_API_KEY not set' };
285
+ case Providers.BEDROCK:
286
+ return (envValue('BEDROCK_AWS_ACCESS_KEY_ID') ??
287
+ envValue('AWS_ACCESS_KEY_ID'))
288
+ ? { ok: true }
289
+ : { ok: false, reason: 'BEDROCK_AWS_* credentials not set' };
290
+ case Providers.GOOGLE:
291
+ return (envValue('GOOGLE_API_KEY') ?? envValue('GEMINI_API_KEY'))
292
+ ? { ok: true }
293
+ : { ok: false, reason: 'GOOGLE_API_KEY / GEMINI_API_KEY not set' };
294
+ case Providers.VERTEXAI:
295
+ return (envValue('GOOGLE_APPLICATION_CREDENTIALS') ??
296
+ envValue('VERTEXAI_KEY_FILE'))
297
+ ? { ok: true }
298
+ : { ok: false, reason: 'Vertex credentials not set' };
299
+ case Providers.OPENROUTER:
300
+ return envValue('OPENROUTER_API_KEY')
301
+ ? { ok: true }
302
+ : { ok: false, reason: 'OPENROUTER_API_KEY not set' };
303
+ case Providers.DEEPSEEK:
304
+ return envValue('DEEPSEEK_API_KEY')
305
+ ? { ok: true }
306
+ : { ok: false, reason: 'DEEPSEEK_API_KEY not set' };
307
+ case Providers.XAI:
308
+ return envValue('XAI_API_KEY')
309
+ ? { ok: true }
310
+ : { ok: false, reason: 'XAI_API_KEY not set' };
311
+ case Providers.MISTRAL:
312
+ case Providers.MISTRALAI:
313
+ return envValue('MISTRAL_API_KEY')
314
+ ? { ok: true }
315
+ : { ok: false, reason: 'MISTRAL_API_KEY not set' };
316
+ case Providers.MOONSHOT:
317
+ return envValue('MOONSHOT_API_KEY')
318
+ ? { ok: true }
319
+ : { ok: false, reason: 'MOONSHOT_API_KEY not set' };
320
+ default:
321
+ return { ok: false, reason: `no credential rule for ${provider}` };
322
+ }
323
+ }
324
+
325
+ /** Output cap kept minimal — a probe never wants generated tokens. */
326
+ const MAX_OUTPUT_TOKENS = 16;
327
+
328
+ function buildClientOptions(target: ProbeTarget): t.ClientOptions {
329
+ const { provider, model } = target;
330
+
331
+ if (provider === Providers.ANTHROPIC) {
332
+ return {
333
+ model,
334
+ apiKey: envValue('ANTHROPIC_API_KEY'),
335
+ maxTokens: MAX_OUTPUT_TOKENS,
336
+ streaming: true,
337
+ } as t.AnthropicClientOptions;
338
+ }
339
+
340
+ if (provider === Providers.BEDROCK) {
341
+ const accessKeyId =
342
+ envValue('BEDROCK_AWS_ACCESS_KEY_ID') ?? envValue('AWS_ACCESS_KEY_ID');
343
+ const secretAccessKey =
344
+ envValue('BEDROCK_AWS_SECRET_ACCESS_KEY') ??
345
+ envValue('AWS_SECRET_ACCESS_KEY');
346
+ const sessionToken =
347
+ envValue('BEDROCK_AWS_SESSION_TOKEN') ?? envValue('AWS_SESSION_TOKEN');
348
+ return {
349
+ model,
350
+ region:
351
+ envValue('BEDROCK_AWS_REGION') ??
352
+ envValue('AWS_REGION') ??
353
+ envValue('AWS_DEFAULT_REGION') ??
354
+ 'us-east-1',
355
+ maxTokens: MAX_OUTPUT_TOKENS,
356
+ credentials:
357
+ accessKeyId != null && secretAccessKey != null
358
+ ? {
359
+ accessKeyId,
360
+ secretAccessKey,
361
+ ...(sessionToken != null ? { sessionToken } : {}),
362
+ }
363
+ : undefined,
364
+ } as t.BedrockConverseClientOptions;
365
+ }
366
+
367
+ if (provider === Providers.GOOGLE) {
368
+ return {
369
+ model,
370
+ apiKey: envValue('GOOGLE_API_KEY') ?? envValue('GEMINI_API_KEY'),
371
+ maxOutputTokens: MAX_OUTPUT_TOKENS,
372
+ } as t.GoogleClientOptions;
373
+ }
374
+
375
+ if (provider === Providers.VERTEXAI) {
376
+ return {
377
+ model,
378
+ maxOutputTokens: MAX_OUTPUT_TOKENS,
379
+ location:
380
+ envValue('GOOGLE_CLOUD_LOCATION') ??
381
+ envValue('GOOGLE_LOC') ??
382
+ 'us-central1',
383
+ /**
384
+ * Matches `utils/llmConfig`. Without it, an environment that supplies
385
+ * only `VERTEXAI_KEY_FILE` passes the credential check and then records
386
+ * auth failures instead of overflow signatures.
387
+ */
388
+ keyFile: envValue('VERTEXAI_KEY_FILE'),
389
+ } as t.VertexAIClientOptions;
390
+ }
391
+
392
+ if (provider === Providers.OPENROUTER) {
393
+ return {
394
+ model,
395
+ apiKey: envValue('OPENROUTER_API_KEY'),
396
+ maxTokens: MAX_OUTPUT_TOKENS,
397
+ configuration: {
398
+ baseURL:
399
+ envValue('OPENROUTER_BASE_URL') ?? 'https://openrouter.ai/api/v1',
400
+ },
401
+ } as t.OpenAIClientOptions;
402
+ }
403
+
404
+ if (provider === Providers.DEEPSEEK) {
405
+ return {
406
+ model,
407
+ apiKey: envValue('DEEPSEEK_API_KEY'),
408
+ maxTokens: MAX_OUTPUT_TOKENS,
409
+ } as t.DeepSeekClientOptions;
410
+ }
411
+
412
+ if (provider === Providers.XAI) {
413
+ return {
414
+ model,
415
+ apiKey: envValue('XAI_API_KEY'),
416
+ maxTokens: MAX_OUTPUT_TOKENS,
417
+ } as t.XAIClientOptions;
418
+ }
419
+
420
+ if (provider === Providers.MISTRAL || provider === Providers.MISTRALAI) {
421
+ return {
422
+ model,
423
+ apiKey: envValue('MISTRAL_API_KEY'),
424
+ maxTokens: MAX_OUTPUT_TOKENS,
425
+ } as t.MistralAIClientOptions;
426
+ }
427
+
428
+ if (provider === Providers.MOONSHOT) {
429
+ return {
430
+ model,
431
+ apiKey: envValue('MOONSHOT_API_KEY'),
432
+ maxTokens: MAX_OUTPUT_TOKENS,
433
+ } as t.OpenAIClientOptions;
434
+ }
435
+
436
+ if (provider === Providers.AZURE) {
437
+ return {
438
+ model,
439
+ azureOpenAIApiKey: envValue('AZURE_OPENAI_API_KEY'),
440
+ azureOpenAIApiInstanceName: envValue('AZURE_OPENAI_API_INSTANCE'),
441
+ azureOpenAIApiDeploymentName: envValue('AZURE_OPENAI_API_DEPLOYMENT'),
442
+ azureOpenAIApiVersion: envValue('AZURE_OPENAI_API_VERSION'),
443
+ maxTokens: MAX_OUTPUT_TOKENS,
444
+ } as t.AzureClientOptions;
445
+ }
446
+
447
+ return {
448
+ model,
449
+ apiKey: envValue('OPENAI_API_KEY'),
450
+ maxTokens: MAX_OUTPUT_TOKENS,
451
+ } as t.OpenAIClientOptions;
452
+ }
453
+
454
+ /**
455
+ * The probe matrix. Context windows are the documented input limits; the
456
+ * probe only needs them to be right enough to overshoot.
457
+ */
458
+ const PROBE_MATRIX: ProbeTarget[] = [
459
+ /* ---------------- OpenAI ---------------- */
460
+ {
461
+ provider: Providers.OPENAI,
462
+ model: 'gpt-4',
463
+ contextWindow: 8_192,
464
+ tier: 'full',
465
+ },
466
+ {
467
+ provider: Providers.OPENAI,
468
+ model: 'gpt-3.5-turbo',
469
+ contextWindow: 16_385,
470
+ tier: 'full',
471
+ },
472
+ {
473
+ provider: Providers.OPENAI,
474
+ model: 'gpt-4o-mini',
475
+ contextWindow: 128_000,
476
+ tier: 'full',
477
+ },
478
+ {
479
+ provider: Providers.OPENAI,
480
+ model: 'gpt-4o',
481
+ contextWindow: 128_000,
482
+ tier: 'full',
483
+ },
484
+ {
485
+ provider: Providers.OPENAI,
486
+ model: 'gpt-4.1-nano',
487
+ contextWindow: 1_047_576,
488
+ tier: 'confirm',
489
+ },
490
+ {
491
+ provider: Providers.OPENAI,
492
+ model: 'gpt-5-nano',
493
+ contextWindow: 400_000,
494
+ tier: 'full',
495
+ },
496
+ {
497
+ provider: Providers.OPENAI,
498
+ model: 'gpt-5-mini',
499
+ contextWindow: 400_000,
500
+ tier: 'full',
501
+ },
502
+ {
503
+ provider: Providers.OPENAI,
504
+ model: 'gpt-5.4-nano',
505
+ contextWindow: 400_000,
506
+ tier: 'full',
507
+ },
508
+ {
509
+ provider: Providers.OPENAI,
510
+ model: 'gpt-5.4-mini',
511
+ contextWindow: 400_000,
512
+ tier: 'full',
513
+ },
514
+ {
515
+ provider: Providers.OPENAI,
516
+ model: 'gpt-5.4',
517
+ contextWindow: 400_000,
518
+ tier: 'confirm',
519
+ },
520
+ {
521
+ provider: Providers.OPENAI,
522
+ model: 'gpt-5.5',
523
+ contextWindow: 400_000,
524
+ tier: 'confirm',
525
+ },
526
+ {
527
+ provider: Providers.OPENAI,
528
+ model: 'o4-mini',
529
+ contextWindow: 200_000,
530
+ tier: 'full',
531
+ },
532
+ {
533
+ provider: Providers.OPENAI,
534
+ model: 'o3',
535
+ contextWindow: 200_000,
536
+ tier: 'confirm',
537
+ },
538
+
539
+ /* ---------------- Azure OpenAI ---------------- */
540
+ {
541
+ provider: Providers.AZURE,
542
+ model: envValue('AZURE_MODEL_NAME') ?? 'gpt-4o-mini',
543
+ contextWindow: 128_000,
544
+ tier: 'full',
545
+ },
546
+
547
+ /* ---------------- Anthropic ---------------- */
548
+ {
549
+ provider: Providers.ANTHROPIC,
550
+ model: 'claude-haiku-4-5-20251001',
551
+ contextWindow: 200_000,
552
+ tier: 'full',
553
+ },
554
+ {
555
+ provider: Providers.ANTHROPIC,
556
+ model: 'claude-sonnet-4-5-20250929',
557
+ contextWindow: 200_000,
558
+ tier: 'full',
559
+ },
560
+ {
561
+ provider: Providers.ANTHROPIC,
562
+ model: 'claude-sonnet-4-6',
563
+ contextWindow: 200_000,
564
+ tier: 'full',
565
+ },
566
+ {
567
+ provider: Providers.ANTHROPIC,
568
+ model: 'claude-fable-5',
569
+ contextWindow: 200_000,
570
+ tier: 'full',
571
+ },
572
+ {
573
+ provider: Providers.ANTHROPIC,
574
+ model: 'claude-sonnet-5',
575
+ contextWindow: 200_000,
576
+ tier: 'confirm',
577
+ },
578
+ {
579
+ provider: Providers.ANTHROPIC,
580
+ model: 'claude-opus-4-5-20251101',
581
+ contextWindow: 200_000,
582
+ tier: 'confirm',
583
+ },
584
+ {
585
+ provider: Providers.ANTHROPIC,
586
+ model: 'claude-opus-5',
587
+ contextWindow: 200_000,
588
+ tier: 'confirm',
589
+ },
590
+
591
+ /* ---------------- Bedrock ---------------- */
592
+ {
593
+ provider: Providers.BEDROCK,
594
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
595
+ contextWindow: 200_000,
596
+ tier: 'full',
597
+ },
598
+ {
599
+ provider: Providers.BEDROCK,
600
+ model: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
601
+ contextWindow: 200_000,
602
+ tier: 'full',
603
+ },
604
+ {
605
+ provider: Providers.BEDROCK,
606
+ model: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
607
+ contextWindow: 200_000,
608
+ tier: 'full',
609
+ },
610
+ {
611
+ provider: Providers.BEDROCK,
612
+ model: 'us.amazon.nova-lite-v1:0',
613
+ contextWindow: 300_000,
614
+ tier: 'full',
615
+ },
616
+ {
617
+ provider: Providers.BEDROCK,
618
+ model: 'us.meta.llama3-1-70b-instruct-v1:0',
619
+ contextWindow: 128_000,
620
+ tier: 'full',
621
+ },
622
+ {
623
+ provider: Providers.BEDROCK,
624
+ model: 'mistral.mistral-large-2407-v1:0',
625
+ contextWindow: 128_000,
626
+ tier: 'full',
627
+ },
628
+ {
629
+ provider: Providers.BEDROCK,
630
+ model: 'us.anthropic.claude-opus-4-5-20251101-v1:0',
631
+ contextWindow: 200_000,
632
+ tier: 'confirm',
633
+ },
634
+
635
+ /* ---------------- Google (Gemini API) ---------------- */
636
+ {
637
+ provider: Providers.GOOGLE,
638
+ model: 'gemini-3.1-flash-image',
639
+ contextWindow: 65_536,
640
+ tier: 'full',
641
+ },
642
+ {
643
+ provider: Providers.GOOGLE,
644
+ model: 'gemini-omni-flash-preview',
645
+ contextWindow: 131_072,
646
+ tier: 'full',
647
+ },
648
+ {
649
+ provider: Providers.GOOGLE,
650
+ model: 'gemini-2.5-flash-lite',
651
+ contextWindow: 1_048_576,
652
+ tier: 'confirm',
653
+ },
654
+ {
655
+ provider: Providers.GOOGLE,
656
+ model: 'gemini-2.5-flash',
657
+ contextWindow: 1_048_576,
658
+ tier: 'confirm',
659
+ },
660
+ {
661
+ provider: Providers.GOOGLE,
662
+ model: 'gemini-3.5-flash',
663
+ contextWindow: 1_048_576,
664
+ tier: 'confirm',
665
+ },
666
+ {
667
+ provider: Providers.GOOGLE,
668
+ model: 'gemini-2.5-pro',
669
+ contextWindow: 1_048_576,
670
+ tier: 'confirm',
671
+ },
672
+
673
+ /* ---------------- Vertex AI ---------------- */
674
+ {
675
+ provider: Providers.VERTEXAI,
676
+ model: 'gemini-2.5-flash-lite',
677
+ contextWindow: 1_048_576,
678
+ tier: 'confirm',
679
+ },
680
+ {
681
+ provider: Providers.VERTEXAI,
682
+ model: 'gemini-2.5-flash',
683
+ contextWindow: 1_048_576,
684
+ tier: 'confirm',
685
+ },
686
+
687
+ /* ---------------- OpenRouter (many upstreams) ---------------- */
688
+ {
689
+ provider: Providers.OPENROUTER,
690
+ model: 'qwen/qwen-2.5-7b-instruct',
691
+ contextWindow: 32_768,
692
+ tier: 'full',
693
+ upstream: 'qwen',
694
+ },
695
+ {
696
+ provider: Providers.OPENROUTER,
697
+ model: 'mistralai/mistral-nemo',
698
+ contextWindow: 131_072,
699
+ tier: 'full',
700
+ upstream: 'mistral',
701
+ },
702
+ {
703
+ provider: Providers.OPENROUTER,
704
+ model: 'meta-llama/llama-3.1-8b-instruct',
705
+ contextWindow: 131_072,
706
+ tier: 'full',
707
+ upstream: 'meta',
708
+ },
709
+ {
710
+ provider: Providers.OPENROUTER,
711
+ model: 'openai/gpt-4o-mini',
712
+ contextWindow: 128_000,
713
+ tier: 'full',
714
+ upstream: 'openai',
715
+ },
716
+ {
717
+ provider: Providers.OPENROUTER,
718
+ model: 'anthropic/claude-haiku-4.5',
719
+ contextWindow: 200_000,
720
+ tier: 'full',
721
+ upstream: 'anthropic',
722
+ },
723
+ {
724
+ provider: Providers.OPENROUTER,
725
+ model: 'deepseek/deepseek-chat',
726
+ contextWindow: 163_840,
727
+ tier: 'full',
728
+ upstream: 'deepseek',
729
+ },
730
+ {
731
+ provider: Providers.OPENROUTER,
732
+ model: 'moonshotai/kimi-k2',
733
+ contextWindow: 131_072,
734
+ tier: 'full',
735
+ upstream: 'moonshot',
736
+ },
737
+ {
738
+ provider: Providers.OPENROUTER,
739
+ model: 'google/gemini-3.1-flash-lite-image',
740
+ contextWindow: 65_536,
741
+ tier: 'full',
742
+ upstream: 'google',
743
+ },
744
+ {
745
+ provider: Providers.OPENROUTER,
746
+ model: 'google/gemini-3.5-flash-lite',
747
+ contextWindow: 1_048_576,
748
+ tier: 'confirm',
749
+ upstream: 'google',
750
+ },
751
+ {
752
+ provider: Providers.OPENROUTER,
753
+ model: 'x-ai/grok-build-0.1',
754
+ contextWindow: 256_000,
755
+ tier: 'full',
756
+ upstream: 'xai',
757
+ },
758
+
759
+ /* ---------------- DeepSeek ---------------- */
760
+ {
761
+ provider: Providers.DEEPSEEK,
762
+ model: 'deepseek-v4-flash',
763
+ contextWindow: 131_072,
764
+ tier: 'full',
765
+ },
766
+ {
767
+ provider: Providers.DEEPSEEK,
768
+ model: 'deepseek-v4-pro',
769
+ contextWindow: 131_072,
770
+ tier: 'confirm',
771
+ },
772
+
773
+ /* ---------------- xAI ---------------- */
774
+ {
775
+ provider: Providers.XAI,
776
+ model: 'grok-build-0.1',
777
+ contextWindow: 256_000,
778
+ tier: 'full',
779
+ },
780
+ {
781
+ provider: Providers.XAI,
782
+ model: 'grok-4.5',
783
+ contextWindow: 500_000,
784
+ tier: 'confirm',
785
+ },
786
+ {
787
+ provider: Providers.XAI,
788
+ model: 'grok-4.3',
789
+ contextWindow: 1_000_000,
790
+ tier: 'confirm',
791
+ },
792
+
793
+ /* ---------------- Mistral ---------------- */
794
+ {
795
+ provider: Providers.MISTRAL,
796
+ model: 'mistral-tiny-latest',
797
+ contextWindow: 131_072,
798
+ tier: 'full',
799
+ },
800
+ {
801
+ provider: Providers.MISTRAL,
802
+ model: 'mistral-medium-2508',
803
+ contextWindow: 131_072,
804
+ tier: 'confirm',
805
+ },
806
+
807
+ /* ---------------- Moonshot ---------------- */
808
+ {
809
+ provider: Providers.MOONSHOT,
810
+ model: 'moonshot-v1-8k',
811
+ contextWindow: 8_192,
812
+ tier: 'full',
813
+ },
814
+ ];
815
+
816
+ interface CliOptions {
817
+ only?: Set<string>;
818
+ modelFilter?: string;
819
+ tier?: ProbeTier;
820
+ modes: ProbeMode[];
821
+ out: string;
822
+ list: boolean;
823
+ /**
824
+ * Overrides the tier overshoot. Useful for landing a payload *between* the
825
+ * model's context window and the account's per-minute token allowance, which
826
+ * is the only way to observe a provider's true context-window rejection on
827
+ * accounts whose TPM ceiling sits below the window.
828
+ */
829
+ factor?: number;
830
+ }
831
+
832
+ function parseArgs(argv: string[]): CliOptions {
833
+ const options: CliOptions = {
834
+ modes: ['stream'],
835
+ out: 'context-overflow-signatures.json',
836
+ list: false,
837
+ };
838
+ for (let i = 0; i < argv.length; i++) {
839
+ const arg = argv[i];
840
+ if (arg === '--only') {
841
+ options.only = new Set(
842
+ argv[++i]?.split(',').map((value) => value.trim())
843
+ );
844
+ } else if (arg === '--model') {
845
+ options.modelFilter = argv[++i];
846
+ } else if (arg === '--tier') {
847
+ options.tier = argv[++i] as ProbeTier;
848
+ } else if (arg === '--mode') {
849
+ const mode = argv[++i];
850
+ options.modes =
851
+ mode === 'both' ? ['stream', 'invoke'] : [mode as ProbeMode];
852
+ } else if (arg === '--out') {
853
+ options.out = argv[++i];
854
+ } else if (arg === '--factor') {
855
+ options.factor = Number(argv[++i]);
856
+ } else if (arg === '--list') {
857
+ options.list = true;
858
+ }
859
+ }
860
+ return options;
861
+ }
862
+
863
+ function selectTargets(options: CliOptions): ProbeTarget[] {
864
+ return PROBE_MATRIX.filter((target) => {
865
+ if (options.only && !options.only.has(target.provider)) {
866
+ return false;
867
+ }
868
+ if (options.modelFilter && !target.model.includes(options.modelFilter)) {
869
+ return false;
870
+ }
871
+ if (options.tier && target.tier !== options.tier) {
872
+ return false;
873
+ }
874
+ return true;
875
+ });
876
+ }
877
+
878
+ async function runProbe(
879
+ target: ProbeTarget,
880
+ mode: ProbeMode,
881
+ factor?: number
882
+ ): Promise<ProbeResult> {
883
+ const tokenTarget = overflowTokenTarget(target, factor);
884
+ const base: Omit<ProbeResult, 'outcome' | 'durationMs'> = {
885
+ provider: target.provider,
886
+ model: target.model,
887
+ upstream: target.upstream,
888
+ tier: target.tier,
889
+ mode,
890
+ contextWindow: target.contextWindow,
891
+ approxTokensSent: tokenTarget,
892
+ };
893
+
894
+ const credentials = checkCredentials(target.provider);
895
+ if (!credentials.ok) {
896
+ return {
897
+ ...base,
898
+ outcome: 'skipped',
899
+ skipReason: credentials.reason,
900
+ durationMs: 0,
901
+ };
902
+ }
903
+
904
+ if (tokenTarget > MAX_PROBE_TOKENS) {
905
+ return {
906
+ ...base,
907
+ outcome: 'skipped',
908
+ skipReason: `payload would exceed ${MAX_PROBE_TOKENS} tokens`,
909
+ durationMs: 0,
910
+ };
911
+ }
912
+
913
+ const messages: BaseMessage[] = [
914
+ new HumanMessage(buildOverflowText(tokenTarget)),
915
+ ];
916
+ const started = Date.now();
917
+
918
+ try {
919
+ const model = initializeModel({
920
+ provider: target.provider,
921
+ clientOptions: buildClientOptions(target),
922
+ }) as t.ChatModel;
923
+
924
+ if (mode === 'invoke' || model.stream == null) {
925
+ await model.invoke(messages);
926
+ } else {
927
+ const stream = await model.stream(messages);
928
+ for await (const _chunk of stream) {
929
+ void _chunk;
930
+ }
931
+ }
932
+
933
+ return { ...base, outcome: 'accepted', durationMs: Date.now() - started };
934
+ } catch (error) {
935
+ const serialized = serializeError(error);
936
+ const unavailable =
937
+ /not found|does not exist|access|not authorized|no such model|invalid model/i.test(
938
+ serialized.message
939
+ );
940
+ return {
941
+ ...base,
942
+ outcome: unavailable ? 'unavailable' : 'rejected',
943
+ durationMs: Date.now() - started,
944
+ error: serialized,
945
+ };
946
+ }
947
+ }
948
+
949
+ function summarize(result: ProbeResult): string {
950
+ const head = `${result.provider}/${result.model}${result.mode === 'invoke' ? ' [invoke]' : ''}`;
951
+ if (result.outcome === 'skipped') {
952
+ return `SKIP ${head} — ${result.skipReason}`;
953
+ }
954
+ if (result.outcome === 'accepted') {
955
+ return `ACCEPT ${head} — request was NOT rejected (${result.approxTokensSent} tokens)`;
956
+ }
957
+ const error = result.error;
958
+ const status = error?.status ?? error?.httpStatusCode ?? '?';
959
+ const code =
960
+ error?.code ?? error?.errorType ?? error?.awsErrorType ?? error?.name ?? '';
961
+ const label = result.outcome === 'unavailable' ? 'N/A ' : 'HIT ';
962
+ return `${label} ${head} — ${status} ${code}: ${error?.message.slice(0, 160) ?? ''}`;
963
+ }
964
+
965
+ async function main(): Promise<void> {
966
+ const options = parseArgs(process.argv.slice(2));
967
+ const targets = selectTargets(options);
968
+
969
+ if (options.list) {
970
+ for (const target of targets) {
971
+ console.log(
972
+ `${target.provider}\t${target.model}\tctx=${target.contextWindow}\ttier=${target.tier}\tprobe≈${overflowTokenTarget(target, options.factor)} tokens`
973
+ );
974
+ }
975
+ return;
976
+ }
977
+
978
+ const results: ProbeResult[] = [];
979
+ for (const target of targets) {
980
+ for (const mode of options.modes) {
981
+ const result = await runProbe(target, mode, options.factor);
982
+ results.push(result);
983
+ console.log(summarize(result));
984
+ }
985
+ }
986
+
987
+ writeFileSync(
988
+ options.out,
989
+ JSON.stringify({ generatedAt: new Date().toISOString(), results }, null, 2)
990
+ );
991
+ console.log(`\nWrote ${results.length} probe results to ${options.out}`);
992
+ }
993
+
994
+ main().catch((error: unknown) => {
995
+ console.error(error);
996
+ process.exit(1);
997
+ });