@juspay/neurolink 12.20.2 → 12.21.1
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/CHANGELOG.md +4 -2
- package/dist/browser/neurolink.min.js +373 -370
- package/dist/cli/commands/decide.d.ts +22 -0
- package/dist/cli/commands/decide.js +287 -0
- package/dist/cli/parser.js +6 -0
- package/dist/neurolink.js +8 -0
- package/dist/proxy/codexAccountUsage.js +15 -0
- package/dist/proxy/codexFallback.js +15 -1
- package/dist/proxy/vertexAnthropicFallback.d.ts +18 -0
- package/dist/proxy/vertexAnthropicFallback.js +305 -12
- package/dist/routing/classifierStrategies.js +7 -0
- package/dist/server/routes/codexProxyRoutes.js +32 -12
- package/dist/types/classifierRouter.d.ts +4 -0
- package/dist/types/cli.d.ts +12 -0
- package/dist/types/codex.d.ts +3 -0
- package/dist/types/proxy.d.ts +1 -1
- package/dist/types/proxyClient.d.ts +4 -1
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +27 -2
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* here because nothing reshapes them.
|
|
14
14
|
*/
|
|
15
15
|
import { GoogleAuth } from "google-auth-library";
|
|
16
|
+
import { logger } from "../utils/logger.js";
|
|
17
|
+
import { isTransientNetworkError } from "./proxyFetch.js";
|
|
16
18
|
/** Vertex pins the Anthropic API version in the body, not a header. */
|
|
17
19
|
const VERTEX_ANTHROPIC_VERSION = "vertex-2023-10-16";
|
|
18
20
|
const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
|
|
@@ -68,7 +70,114 @@ export function mapVertexOutputConfig(value) {
|
|
|
68
70
|
const { effort: _unsupported, ...rest } = value;
|
|
69
71
|
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
70
72
|
}
|
|
71
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Vertex's message schema is exactly `{ role, content }`. Claude Code 2.1.x
|
|
75
|
+
* also puts `output_config` on its system "directive" turn, which the
|
|
76
|
+
* first-party API accepts and Vertex refuses: "messages.1.output_config:
|
|
77
|
+
* Extra inputs are not permitted".
|
|
78
|
+
*/
|
|
79
|
+
const VERTEX_MESSAGE_KEYS = new Set(["role", "content"]);
|
|
80
|
+
/**
|
|
81
|
+
* Claude Code stamps its version into a system block as
|
|
82
|
+
* `x-anthropic-billing-header: cc_version=…`. That is first-party billing
|
|
83
|
+
* plumbing; on Vertex it only triggers a client-version check against the
|
|
84
|
+
* fallback model, which the client never asked for ("Claude Code 2.1.278 does
|
|
85
|
+
* not support this model"). It also sits first in `system` and varies with the
|
|
86
|
+
* client build, entrypoint and subagent flag, so identical conversations from
|
|
87
|
+
* different builds could never share a Vertex prompt cache while it stayed.
|
|
88
|
+
*
|
|
89
|
+
* Matched as a prefix, not anywhere in the text: the generator
|
|
90
|
+
* (`buildStableClaudeCodeBillingHeader`) always emits it first, and a looser
|
|
91
|
+
* match would delete a caller's own block that merely mentions the header.
|
|
92
|
+
*/
|
|
93
|
+
const BILLING_HEADER_MARKER = "x-anthropic-billing-header";
|
|
94
|
+
/**
|
|
95
|
+
* How each mid-conversation tool change reads once it is plain text: the prefix
|
|
96
|
+
* when the tool is named, and a sentence for a reference with no usable name.
|
|
97
|
+
*/
|
|
98
|
+
const TOOL_CHANGE_TEXT = {
|
|
99
|
+
tool_addition: {
|
|
100
|
+
named: "Tool now available: ",
|
|
101
|
+
unnamed: "A tool became available.",
|
|
102
|
+
},
|
|
103
|
+
tool_removal: {
|
|
104
|
+
named: "Tool no longer available: ",
|
|
105
|
+
unnamed: "A tool was withdrawn.",
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* A change references a tool rather than defining one: `tool_reference` and
|
|
110
|
+
* `mcp_tool_reference` carry a `name`, and an MCP toolset reference carries
|
|
111
|
+
* only its `server_name`.
|
|
112
|
+
*/
|
|
113
|
+
function toolChangeText(type, tool) {
|
|
114
|
+
const wording = TOOL_CHANGE_TEXT[type];
|
|
115
|
+
const label = !record(tool)
|
|
116
|
+
? ""
|
|
117
|
+
: typeof tool.name === "string" && tool.name.trim()
|
|
118
|
+
? tool.name.trim()
|
|
119
|
+
: typeof tool.server_name === "string"
|
|
120
|
+
? tool.server_name.trim()
|
|
121
|
+
: "";
|
|
122
|
+
return label ? `${wording.named}${label}` : wording.unnamed;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Vertex rejects the `tool_addition` and `tool_removal` tags that offer or
|
|
126
|
+
* withdraw a tool mid-conversation. Each one becomes a text block in the same
|
|
127
|
+
* position, so the notice survives and the array never empties. Only the
|
|
128
|
+
* message's own content array is touched: nested `tool_result` content stays
|
|
129
|
+
* exactly as sent.
|
|
130
|
+
*/
|
|
131
|
+
function rewriteToolChanges(content) {
|
|
132
|
+
if (!Array.isArray(content)) {
|
|
133
|
+
return content;
|
|
134
|
+
}
|
|
135
|
+
let changed = false;
|
|
136
|
+
const mapped = content.map((block) => {
|
|
137
|
+
if (!record(block) ||
|
|
138
|
+
typeof block.type !== "string" ||
|
|
139
|
+
!Object.hasOwn(TOOL_CHANGE_TEXT, block.type)) {
|
|
140
|
+
return block;
|
|
141
|
+
}
|
|
142
|
+
changed = true;
|
|
143
|
+
return {
|
|
144
|
+
type: "text",
|
|
145
|
+
text: toolChangeText(block.type, block.tool),
|
|
146
|
+
...(block.cache_control !== undefined
|
|
147
|
+
? { cache_control: block.cache_control }
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
return changed ? mapped : content;
|
|
152
|
+
}
|
|
153
|
+
function isEmptyContent(content) {
|
|
154
|
+
return (content === undefined ||
|
|
155
|
+
content === "" ||
|
|
156
|
+
(Array.isArray(content) && content.length === 0));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Move a `cache_control` set on the message itself onto its last content
|
|
160
|
+
* block, which caches the same prefix, since Vertex accepts only `role` and
|
|
161
|
+
* `content` on a message. String content becomes one text block to carry it.
|
|
162
|
+
* A breakpoint with no block to land on, or whose block already has one, is
|
|
163
|
+
* reported rather than dropped silently.
|
|
164
|
+
*/
|
|
165
|
+
function carryMessageBreakpoint(content, marker) {
|
|
166
|
+
if (marker === undefined) {
|
|
167
|
+
return content;
|
|
168
|
+
}
|
|
169
|
+
if (typeof content === "string" && content !== "") {
|
|
170
|
+
return [{ type: "text", text: content, cache_control: marker }];
|
|
171
|
+
}
|
|
172
|
+
const last = Array.isArray(content) ? content.at(-1) : undefined;
|
|
173
|
+
if (Array.isArray(content) &&
|
|
174
|
+
record(last) &&
|
|
175
|
+
last.cache_control === undefined) {
|
|
176
|
+
return [...content.slice(0, -1), { ...last, cache_control: marker }];
|
|
177
|
+
}
|
|
178
|
+
warnDroppedBreakpoint("a message-level cache_control");
|
|
179
|
+
return content;
|
|
180
|
+
}
|
|
72
181
|
/**
|
|
73
182
|
* Vertex refuses `role: "system"` messages outright — "role 'system' is not
|
|
74
183
|
* supported on this model" — where the first-party Messages API accepts them
|
|
@@ -82,25 +191,156 @@ export function mapVertexOutputConfig(value) {
|
|
|
82
191
|
* instead, which keeps both the text and the position. Consecutive same-role
|
|
83
192
|
* turns are accepted, so this never produces an invalid sequence.
|
|
84
193
|
*/
|
|
85
|
-
function
|
|
194
|
+
function normalizeVertexMessage(message) {
|
|
195
|
+
if (!record(message)) {
|
|
196
|
+
return message;
|
|
197
|
+
}
|
|
198
|
+
const role = message.role === "system" ? "user" : message.role;
|
|
199
|
+
const content = carryMessageBreakpoint(rewriteToolChanges(message.content), message.cache_control);
|
|
200
|
+
const onlyKnownKeys = Object.keys(message).every((key) => VERTEX_MESSAGE_KEYS.has(key));
|
|
201
|
+
if (onlyKnownKeys && role === message.role && content === message.content) {
|
|
202
|
+
return message;
|
|
203
|
+
}
|
|
204
|
+
return { role, ...("content" in message ? { content } : {}) };
|
|
205
|
+
}
|
|
206
|
+
/** Index of the last `role: "user"` message, or -1 when there is none. */
|
|
207
|
+
function lastUserIndex(messages) {
|
|
208
|
+
return messages.findLastIndex((message) => record(message) && message.role === "user");
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* A turn-scoped system message (`clear_at: "next_user_message"`) renders only
|
|
212
|
+
* while no user message comes after it — one carrying only `tool_result`
|
|
213
|
+
* counts — and renders nothing once one does. Sent to Vertex as a user turn it
|
|
214
|
+
* would keep speaking after it should have gone quiet.
|
|
215
|
+
*/
|
|
216
|
+
function isClearedTurnScopedMessage(message, index, lastUser) {
|
|
217
|
+
return (record(message) &&
|
|
218
|
+
message.role === "system" &&
|
|
219
|
+
message.clear_at === "next_user_message" &&
|
|
220
|
+
index < lastUser);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A system turn left with no content carries nothing once its extra keys are
|
|
224
|
+
* gone — the directive-only form is `content: []` plus `output_config`, whose
|
|
225
|
+
* effort is resolved at the top level — so it is dropped rather than sent as
|
|
226
|
+
* an empty user turn. So is a turn-scoped message that has already cleared.
|
|
227
|
+
*/
|
|
228
|
+
function normalizeVertexMessages(messages) {
|
|
86
229
|
if (!Array.isArray(messages)) {
|
|
87
230
|
return messages;
|
|
88
231
|
}
|
|
232
|
+
const lastUser = lastUserIndex(messages);
|
|
89
233
|
let changed = false;
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
|
|
234
|
+
const normalized = messages.flatMap((message, index) => {
|
|
235
|
+
if (isClearedTurnScopedMessage(message, index, lastUser)) {
|
|
236
|
+
changed = true;
|
|
237
|
+
return [];
|
|
93
238
|
}
|
|
94
|
-
|
|
95
|
-
|
|
239
|
+
const next = normalizeVertexMessage(message);
|
|
240
|
+
if (next !== message) {
|
|
241
|
+
changed = true;
|
|
242
|
+
}
|
|
243
|
+
if (record(message) &&
|
|
244
|
+
message.role === "system" &&
|
|
245
|
+
record(next) &&
|
|
246
|
+
isEmptyContent(next.content)) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
return [next];
|
|
96
250
|
});
|
|
97
|
-
return changed ?
|
|
251
|
+
return changed ? normalized : messages;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The effort in force for the turn being answered. A per-message directive
|
|
255
|
+
* "takes effect from the next user turn and holds until a later message changes
|
|
256
|
+
* it", so the governing one is the last directive before the final user
|
|
257
|
+
* message; one after it belongs to a turn not yet taken. It overrides only the
|
|
258
|
+
* settings it names. Vertex has no per-message effort, so this becomes the
|
|
259
|
+
* request's top-level `output_config`.
|
|
260
|
+
*/
|
|
261
|
+
function resolveOutputConfig(topLevel, messages) {
|
|
262
|
+
if (!Array.isArray(messages)) {
|
|
263
|
+
return topLevel;
|
|
264
|
+
}
|
|
265
|
+
const lastUser = lastUserIndex(messages);
|
|
266
|
+
const directive = messages
|
|
267
|
+
.slice(0, Math.max(lastUser, 0))
|
|
268
|
+
.findLast((message) => record(message) &&
|
|
269
|
+
message.role === "system" &&
|
|
270
|
+
record(message.output_config));
|
|
271
|
+
if (!record(directive) || !record(directive.output_config)) {
|
|
272
|
+
return topLevel;
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
...(record(topLevel) ? topLevel : {}),
|
|
276
|
+
...directive.output_config,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function isBillingBlock(block) {
|
|
280
|
+
return (record(block) &&
|
|
281
|
+
typeof block.text === "string" &&
|
|
282
|
+
block.text.trimStart().startsWith(BILLING_HEADER_MARKER));
|
|
283
|
+
}
|
|
284
|
+
function warnDroppedBreakpoint(source) {
|
|
285
|
+
logger.warn(`[proxy] Vertex fallback could not keep the cache_control breakpoint on ${source}`);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Remove the billing block without losing a cache breakpoint it carried: the
|
|
289
|
+
* marker moves onto the block that takes its place. When there is no such
|
|
290
|
+
* block, or that block already has its own marker, the loss is reported rather
|
|
291
|
+
* than passed over, because this path bills in real currency.
|
|
292
|
+
*/
|
|
293
|
+
function stripBillingBlock(system) {
|
|
294
|
+
if (!Array.isArray(system) || !system.some(isBillingBlock)) {
|
|
295
|
+
return system;
|
|
296
|
+
}
|
|
297
|
+
const kept = [];
|
|
298
|
+
let carried;
|
|
299
|
+
for (const block of system) {
|
|
300
|
+
if (isBillingBlock(block)) {
|
|
301
|
+
if (record(block) && block.cache_control !== undefined) {
|
|
302
|
+
if (carried !== undefined) {
|
|
303
|
+
warnDroppedBreakpoint("the Claude Code billing block");
|
|
304
|
+
}
|
|
305
|
+
carried = block.cache_control;
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (carried !== undefined && record(block)) {
|
|
310
|
+
if (block.cache_control === undefined) {
|
|
311
|
+
kept.push({ ...block, cache_control: carried });
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
warnDroppedBreakpoint("the Claude Code billing block");
|
|
315
|
+
kept.push(block);
|
|
316
|
+
}
|
|
317
|
+
carried = undefined;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
kept.push(block);
|
|
321
|
+
}
|
|
322
|
+
if (carried !== undefined) {
|
|
323
|
+
warnDroppedBreakpoint("the Claude Code billing block");
|
|
324
|
+
}
|
|
325
|
+
return kept.length > 0 ? kept : undefined;
|
|
98
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Strip what Vertex rejects and pin the API version it requires.
|
|
329
|
+
*
|
|
330
|
+
* Prompt caching constrains every rewrite here. Anthropic caches only up to an
|
|
331
|
+
* explicit `cache_control` marker, at most four per request, over a prefix
|
|
332
|
+
* rendered `tools → system → messages`; anything removed or inserted before a
|
|
333
|
+
* marker invalidates it and everything after. So: never drop a marker, never
|
|
334
|
+
* reorder `system`, and keep each rewrite deterministic so consecutive turns
|
|
335
|
+
* share a prefix. Vertex holds its own cache, apart from the first-party one,
|
|
336
|
+
* so nothing here can disturb first-party hits.
|
|
337
|
+
*/
|
|
99
338
|
export function buildVertexAnthropicPayload(body) {
|
|
100
339
|
const dropped = new Set(UNSUPPORTED_FIELDS);
|
|
101
340
|
const payload = Object.fromEntries(Object.entries(body).filter(([key]) => !dropped.has(key)));
|
|
102
|
-
|
|
103
|
-
|
|
341
|
+
const outputConfig = resolveOutputConfig(payload.output_config, payload.messages);
|
|
342
|
+
if (outputConfig !== undefined) {
|
|
343
|
+
const mapped = mapVertexOutputConfig(outputConfig);
|
|
104
344
|
if (mapped === undefined) {
|
|
105
345
|
delete payload.output_config;
|
|
106
346
|
}
|
|
@@ -109,7 +349,16 @@ export function buildVertexAnthropicPayload(body) {
|
|
|
109
349
|
}
|
|
110
350
|
}
|
|
111
351
|
if ("messages" in payload) {
|
|
112
|
-
payload.messages =
|
|
352
|
+
payload.messages = normalizeVertexMessages(payload.messages);
|
|
353
|
+
}
|
|
354
|
+
if ("system" in payload) {
|
|
355
|
+
const system = stripBillingBlock(payload.system);
|
|
356
|
+
if (system === undefined) {
|
|
357
|
+
delete payload.system;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
payload.system = system;
|
|
361
|
+
}
|
|
113
362
|
}
|
|
114
363
|
return { ...payload, anthropic_version: VERTEX_ANTHROPIC_VERSION };
|
|
115
364
|
}
|
|
@@ -159,12 +408,56 @@ async function vertexAccessToken() {
|
|
|
159
408
|
export function setVertexAccessTokenProviderForTests(provider) {
|
|
160
409
|
accessTokenProviderForTests = provider;
|
|
161
410
|
}
|
|
411
|
+
const DNS_FAILURE_CODES = new Set([
|
|
412
|
+
"ENOTFOUND",
|
|
413
|
+
"EAI_AGAIN",
|
|
414
|
+
]);
|
|
415
|
+
const DNS_FAILURE_MESSAGE = /\bgetaddrinfo (ENOTFOUND|EAI_AGAIN)\b/;
|
|
416
|
+
const TOKEN_RETRY_DELAY_MS = 250;
|
|
417
|
+
/**
|
|
418
|
+
* A resolver failure while reaching Google's token endpoint. The shared proxy
|
|
419
|
+
* classifier leaves DNS codes out, since elsewhere one can mean a mistyped
|
|
420
|
+
* host, but this endpoint is fixed and its failures on 2026-09-23 were blips
|
|
421
|
+
* reported only in the message text ("getaddrinfo ENOTFOUND
|
|
422
|
+
* oauth2.googleapis.com").
|
|
423
|
+
*/
|
|
424
|
+
function isDnsFailure(error) {
|
|
425
|
+
let current = error;
|
|
426
|
+
for (let depth = 0; depth < 5 && record(current); depth++) {
|
|
427
|
+
if ((typeof current.code === "string" &&
|
|
428
|
+
DNS_FAILURE_CODES.has(current.code)) ||
|
|
429
|
+
(typeof current.message === "string" &&
|
|
430
|
+
DNS_FAILURE_MESSAGE.test(current.message))) {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
current = current.cause;
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Run once more after a short pause when the first attempt hit a transient
|
|
439
|
+
* network failure: anything the shared proxy classifier retries, plus a DNS
|
|
440
|
+
* blip. One retry absorbs a blip without hiding a real outage: a second
|
|
441
|
+
* failure, or any other error, surfaces unchanged.
|
|
442
|
+
*/
|
|
443
|
+
export async function withTransientNetworkRetry(run, delayMs = TOKEN_RETRY_DELAY_MS) {
|
|
444
|
+
try {
|
|
445
|
+
return await run();
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (!isTransientNetworkError(error) && !isDnsFailure(error)) {
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
452
|
+
return run();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
162
455
|
/**
|
|
163
456
|
* Send one Anthropic-shaped request to Vertex and return the upstream response
|
|
164
457
|
* untouched, so the caller can stream its bytes straight to the client.
|
|
165
458
|
*/
|
|
166
459
|
export async function dispatchVertexAnthropicPassthrough(request) {
|
|
167
|
-
const token = await vertexAccessToken();
|
|
460
|
+
const token = await withTransientNetworkRetry(() => vertexAccessToken());
|
|
168
461
|
if (!token) {
|
|
169
462
|
throw new Error("Vertex passthrough could not obtain a Google access token; " +
|
|
170
463
|
"check Application Default Credentials for this project");
|
|
@@ -295,6 +295,9 @@ export async function classifyJev(input, decide, timeoutMs, candidates, threshol
|
|
|
295
295
|
// are already assembled by the caller. Sending them costs almost nothing
|
|
296
296
|
// and they are exactly what distinguishes a long tool-using request from a
|
|
297
297
|
// short conversational one.
|
|
298
|
+
// Continuity is sent as `session_bound` / `prior_messages`, never as the
|
|
299
|
+
// raw sessionId: the id is an opaque identifier that tells a third-party
|
|
300
|
+
// model nothing, so it stays in-process.
|
|
298
301
|
const result = await decide({
|
|
299
302
|
state: {
|
|
300
303
|
request: (input.prompt ?? "").slice(0, 8000),
|
|
@@ -302,6 +305,10 @@ export async function classifyJev(input, decide, timeoutMs, candidates, threshol
|
|
|
302
305
|
has_tools_available: input.hasTools ?? false,
|
|
303
306
|
request_includes_images: input.requiresVision ?? false,
|
|
304
307
|
caller_requested_thinking: input.thinkingLevel ?? "none",
|
|
308
|
+
session_bound: input.sessionBound ?? false,
|
|
309
|
+
...(typeof input.priorMessageCount === "number"
|
|
310
|
+
? { prior_messages: input.priorMessageCount }
|
|
311
|
+
: {}),
|
|
305
312
|
},
|
|
306
313
|
questions,
|
|
307
314
|
timeoutMs,
|
|
@@ -143,6 +143,34 @@ function buildCodexErrorResponse(status, message, code) {
|
|
|
143
143
|
},
|
|
144
144
|
}), { status, headers: { "content-type": "application/json" } });
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Build a terminal Codex Responses SSE stream for a quota-exhaustion failure
|
|
148
|
+
* that never reached an upstream request (every pooled account is cooling).
|
|
149
|
+
* A bare non-2xx status with no body reads to the Codex CLI as a dropped
|
|
150
|
+
* connection, so it shows "Reconnecting... waiting for network" forever
|
|
151
|
+
* instead of a real error. Emitting a well-formed `response.failed` event —
|
|
152
|
+
* the same terminal shape the CLI already parses out of a live stream —
|
|
153
|
+
* lets it render the actual failure instead.
|
|
154
|
+
*/
|
|
155
|
+
function buildCodexQuotaExhaustedResponse(message, retryAfterSec) {
|
|
156
|
+
const payload = {
|
|
157
|
+
type: "response.failed",
|
|
158
|
+
response: {
|
|
159
|
+
error: {
|
|
160
|
+
type: "insufficient_quota",
|
|
161
|
+
code: "insufficient_quota",
|
|
162
|
+
message,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
const headers = {
|
|
167
|
+
"content-type": "text/event-stream",
|
|
168
|
+
};
|
|
169
|
+
if (retryAfterSec !== undefined) {
|
|
170
|
+
headers["retry-after"] = String(retryAfterSec);
|
|
171
|
+
}
|
|
172
|
+
return new Response(`event: response.failed\ndata: ${JSON.stringify(payload)}\n\n`, { status: 200, headers });
|
|
173
|
+
}
|
|
146
174
|
/**
|
|
147
175
|
* Load the Codex account pool from the token store, refreshing expired OAuth
|
|
148
176
|
* tokens and hydrating cooldown + quota state from disk.
|
|
@@ -654,18 +682,10 @@ async function executeCodexResponsesRequest(ctx) {
|
|
|
654
682
|
errorType: "all_accounts_cooling",
|
|
655
683
|
errorMessage: "All Codex accounts are rate-limited",
|
|
656
684
|
});
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
},
|
|
662
|
-
}), {
|
|
663
|
-
status: 429,
|
|
664
|
-
headers: {
|
|
665
|
-
"content-type": "application/json",
|
|
666
|
-
"retry-after": String(retryAfterSec),
|
|
667
|
-
},
|
|
668
|
-
});
|
|
685
|
+
const resetAt = soonest ? new Date(soonest).toISOString() : undefined;
|
|
686
|
+
return buildCodexQuotaExhaustedResponse(resetAt
|
|
687
|
+
? `Codex quota exhausted: all accounts are rate-limited. Resets at ${resetAt}.`
|
|
688
|
+
: "Codex quota exhausted: all accounts are rate-limited.", retryAfterSec);
|
|
669
689
|
}
|
|
670
690
|
let attempt = 0;
|
|
671
691
|
let lastErrorMessage = "All Codex accounts failed";
|
|
@@ -296,6 +296,10 @@ export type ClassifierRouterInput = {
|
|
|
296
296
|
requiresVision?: boolean;
|
|
297
297
|
thinkingLevel?: string;
|
|
298
298
|
sessionId?: string;
|
|
299
|
+
/** Whether this request is tied to a session, independent of the (withheld) session id itself. */
|
|
300
|
+
sessionBound?: boolean;
|
|
301
|
+
/** Number of prior conversation messages the caller supplied, when known. */
|
|
302
|
+
priorMessageCount?: number;
|
|
299
303
|
};
|
|
300
304
|
/** Enriched per-model metadata used while ranking pool members. */
|
|
301
305
|
export type ClassifierModelMeta = {
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -1296,6 +1296,18 @@ export type AutoresearchInitArgs = {
|
|
|
1296
1296
|
provider?: string;
|
|
1297
1297
|
model?: string;
|
|
1298
1298
|
};
|
|
1299
|
+
/** Arguments for `neurolink decide` — the third inference type's CLI surface. */
|
|
1300
|
+
export type CliDecideArgs = {
|
|
1301
|
+
state?: string;
|
|
1302
|
+
stateFile?: string;
|
|
1303
|
+
questions?: string;
|
|
1304
|
+
questionsFile?: string;
|
|
1305
|
+
provider?: string;
|
|
1306
|
+
model?: string;
|
|
1307
|
+
timeout?: number;
|
|
1308
|
+
format?: "text" | "json";
|
|
1309
|
+
debug?: boolean;
|
|
1310
|
+
};
|
|
1299
1311
|
/** Base options shared across all `neurolink evaluate` subcommands. */
|
|
1300
1312
|
export type BaseEvaluateArgs = {
|
|
1301
1313
|
json?: boolean;
|
package/dist/types/codex.d.ts
CHANGED
package/dist/types/proxy.d.ts
CHANGED
|
@@ -3221,7 +3221,7 @@ export type StatusStats = {
|
|
|
3221
3221
|
accounts?: {
|
|
3222
3222
|
/** Provider-qualified key; null for explicitly unattributed legacy rows. */
|
|
3223
3223
|
key?: string | null;
|
|
3224
|
-
provider?: "anthropic" | "codex" | "other" | "unknown";
|
|
3224
|
+
provider?: "anthropic" | "codex" | "vertex" | "other" | "unknown";
|
|
3225
3225
|
label: string;
|
|
3226
3226
|
type: string;
|
|
3227
3227
|
attempts?: number;
|
|
@@ -187,8 +187,11 @@ export type CliAccountsRow = {
|
|
|
187
187
|
/**
|
|
188
188
|
* Which pool engine owns this login. Absent on plumbing rows. Consumers
|
|
189
189
|
* that key a list by row must key by `key`, not `label` — see above.
|
|
190
|
+
*
|
|
191
|
+
* `"vertex"` is not a login: native fallback legs are keyed by served
|
|
192
|
+
* model, with no OAuth account behind them, so they carry no token state.
|
|
190
193
|
*/
|
|
191
|
-
provider?: "anthropic" | "codex";
|
|
194
|
+
provider?: "anthropic" | "codex" | "vertex";
|
|
192
195
|
/**
|
|
193
196
|
* What this row actually is. Only "account" rows are real logins; the proxy
|
|
194
197
|
* also tracks internal and translation pseudo-accounts, which have no quota
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare class NeuroLinkLogger {
|
|
|
19
19
|
private logs;
|
|
20
20
|
private maxLogs;
|
|
21
21
|
private isDebugMode;
|
|
22
|
+
private diagnosticsToStderr;
|
|
22
23
|
private eventEmitter?;
|
|
23
24
|
constructor();
|
|
24
25
|
/**
|
|
@@ -49,6 +50,14 @@ declare class NeuroLinkLogger {
|
|
|
49
50
|
* @param level - The minimum log level to process ("debug", "info", "warn", or "error")
|
|
50
51
|
*/
|
|
51
52
|
setLogLevel(level: LogLevel): void;
|
|
53
|
+
/**
|
|
54
|
+
* Routes debug and info output to stderr instead of stdout. The CLI turns
|
|
55
|
+
* this on for `--format json`, where stdout carries the payload and any
|
|
56
|
+
* diagnostic line in it would make the output unparseable.
|
|
57
|
+
*
|
|
58
|
+
* @param enabled - True to send debug/info to stderr; false restores stdout
|
|
59
|
+
*/
|
|
60
|
+
setDiagnosticsToStderr(enabled: boolean): void;
|
|
52
61
|
/**
|
|
53
62
|
* Determines whether a message with the given log level should be processed.
|
|
54
63
|
* This method considers both the configured log level and the current debug mode.
|
|
@@ -222,6 +231,7 @@ export declare const logger: {
|
|
|
222
231
|
table: (data: unknown) => void;
|
|
223
232
|
shouldLog: (level: LogLevel) => boolean;
|
|
224
233
|
setLogLevel: (level: LogLevel) => void;
|
|
234
|
+
setDiagnosticsToStderr: (enabled: boolean) => void;
|
|
225
235
|
getLogs: (level?: LogLevel) => LogEntry[];
|
|
226
236
|
clearLogs: () => void;
|
|
227
237
|
setEventEmitter: (emitter: {
|
package/dist/utils/logger.js
CHANGED
|
@@ -36,11 +36,24 @@ const UPPERCASE_LOG_LEVELS = {
|
|
|
36
36
|
warn: "WARN",
|
|
37
37
|
error: "ERROR",
|
|
38
38
|
};
|
|
39
|
+
const FORMAT_FLAGS = ["--format", "-f", "--output-format"];
|
|
40
|
+
/**
|
|
41
|
+
* True when the process was started with JSON output requested. Read from
|
|
42
|
+
* argv on every write, like the `--debug` check in `shouldLog`: modules log
|
|
43
|
+
* while they are still being imported, before the CLI middleware that calls
|
|
44
|
+
* `setDiagnosticsToStderr` has run.
|
|
45
|
+
*/
|
|
46
|
+
function argvRequestsJsonOutput() {
|
|
47
|
+
const argv = process.argv;
|
|
48
|
+
return argv.some((arg, index) => (FORMAT_FLAGS.includes(arg) && argv[index + 1] === "json") ||
|
|
49
|
+
FORMAT_FLAGS.some((flag) => arg === `${flag}=json`));
|
|
50
|
+
}
|
|
39
51
|
class NeuroLinkLogger {
|
|
40
52
|
logLevel = "info";
|
|
41
53
|
logs = [];
|
|
42
54
|
maxLogs = 1000;
|
|
43
55
|
isDebugMode;
|
|
56
|
+
diagnosticsToStderr = false;
|
|
44
57
|
eventEmitter;
|
|
45
58
|
constructor() {
|
|
46
59
|
// Cache debug mode check to avoid repeated array searches
|
|
@@ -86,6 +99,16 @@ class NeuroLinkLogger {
|
|
|
86
99
|
setLogLevel(level) {
|
|
87
100
|
this.logLevel = level;
|
|
88
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Routes debug and info output to stderr instead of stdout. The CLI turns
|
|
104
|
+
* this on for `--format json`, where stdout carries the payload and any
|
|
105
|
+
* diagnostic line in it would make the output unparseable.
|
|
106
|
+
*
|
|
107
|
+
* @param enabled - True to send debug/info to stderr; false restores stdout
|
|
108
|
+
*/
|
|
109
|
+
setDiagnosticsToStderr(enabled) {
|
|
110
|
+
this.diagnosticsToStderr = enabled;
|
|
111
|
+
}
|
|
89
112
|
/**
|
|
90
113
|
* Determines whether a message with the given log level should be processed.
|
|
91
114
|
* This method considers both the configured log level and the current debug mode.
|
|
@@ -209,9 +232,10 @@ class NeuroLinkLogger {
|
|
|
209
232
|
* @param data - Optional additional data to log.
|
|
210
233
|
*/
|
|
211
234
|
outputToConsole(level, prefix, message, data) {
|
|
235
|
+
const toStderr = this.diagnosticsToStderr || argvRequestsJsonOutput();
|
|
212
236
|
const logMethod = {
|
|
213
|
-
debug: console.debug,
|
|
214
|
-
info: console.info,
|
|
237
|
+
debug: toStderr ? console.error : console.debug,
|
|
238
|
+
info: toStderr ? console.error : console.info,
|
|
215
239
|
warn: console.warn,
|
|
216
240
|
error: console.error,
|
|
217
241
|
}[level];
|
|
@@ -467,6 +491,7 @@ export const logger = {
|
|
|
467
491
|
shouldLog: (level) => neuroLinkLogger.shouldLog(level),
|
|
468
492
|
// Expose structured logging methods
|
|
469
493
|
setLogLevel: (level) => neuroLinkLogger.setLogLevel(level),
|
|
494
|
+
setDiagnosticsToStderr: (enabled) => neuroLinkLogger.setDiagnosticsToStderr(enabled),
|
|
470
495
|
getLogs: (level) => neuroLinkLogger.getLogs(level),
|
|
471
496
|
clearLogs: () => neuroLinkLogger.clearLogs(),
|
|
472
497
|
setEventEmitter: (emitter) => neuroLinkLogger.setEventEmitter(emitter),
|