@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
|
@@ -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
|
+
});
|