@juspay/neurolink 12.12.3 → 12.12.5
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 +2 -2
- package/dist/auth/tokenStore.d.ts +9 -1
- package/dist/auth/tokenStore.js +29 -0
- package/dist/browser/neurolink.min.js +399 -398
- package/dist/cli/commands/proxy.js +65 -80
- package/dist/proxy/claudeFormat.d.ts +2 -2
- package/dist/proxy/claudeFormat.js +2 -2
- package/dist/proxy/codexFallback.d.ts +8 -2
- package/dist/proxy/codexFallback.js +185 -1
- package/dist/proxy/proxyConfig.js +45 -2
- package/dist/proxy/rollingProxyServer.js +10 -4
- package/dist/proxy/rollingWorkerProcess.js +8 -2
- package/dist/proxy/rollingWorkerProtocol.d.ts +2 -0
- package/dist/proxy/rollingWorkerProtocol.js +2 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +5 -0
- package/dist/proxy/rollingWorkerSupervisor.js +75 -14
- package/dist/proxy/routingPolicy.js +3 -0
- package/dist/server/routes/claudeProxyRoutes.js +240 -64
- package/dist/server/routes/codexProxyRoutes.js +33 -6
- package/dist/types/cli.d.ts +2 -1
- package/dist/types/codex.d.ts +10 -0
- package/dist/types/proxy.d.ts +11 -2
- package/dist/types/subscription.d.ts +3 -0
- package/package.json +1 -1
|
@@ -66,7 +66,6 @@ const gatedShareRequests = new WeakSet();
|
|
|
66
66
|
const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
67
67
|
/** How long shutdown waits on the share listener before moving on. */
|
|
68
68
|
const SHARE_LISTENER_CLOSE_TIMEOUT_MS = 10_000;
|
|
69
|
-
const PROXY_STATUS_TOKEN_READ_TIMEOUT_MS = 2_000;
|
|
70
69
|
const PROXY_STATUS_RECONCILE_TIMEOUT_MS = 750;
|
|
71
70
|
const PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS = 750;
|
|
72
71
|
// Allowed drift between a pid's OS-reported start time and the persisted
|
|
@@ -1164,6 +1163,10 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1164
1163
|
});
|
|
1165
1164
|
},
|
|
1166
1165
|
onTerminal: ({ outcome, observedBodyBytes, responseChunks }) => {
|
|
1166
|
+
if (outcome === "completed" &&
|
|
1167
|
+
metadata.terminalErrorType === "stream_error") {
|
|
1168
|
+
outcome = "stream_error";
|
|
1169
|
+
}
|
|
1167
1170
|
logProxyLifecycleEvent({
|
|
1168
1171
|
event: "request_terminal",
|
|
1169
1172
|
requestId: metadata.requestId,
|
|
@@ -1500,6 +1503,7 @@ export async function createProxyStartApp(params) {
|
|
|
1500
1503
|
shareOutcome.release();
|
|
1501
1504
|
}
|
|
1502
1505
|
}
|
|
1506
|
+
const requestAbortController = new AbortController();
|
|
1503
1507
|
const ctx = {
|
|
1504
1508
|
requestId: metadata?.requestId ?? crypto.randomUUID(),
|
|
1505
1509
|
method: c.req.method,
|
|
@@ -1509,6 +1513,10 @@ export async function createProxyStartApp(params) {
|
|
|
1509
1513
|
params: c.req.param(),
|
|
1510
1514
|
body,
|
|
1511
1515
|
rawBody,
|
|
1516
|
+
abortSignal: AbortSignal.any([
|
|
1517
|
+
c.req.raw.signal,
|
|
1518
|
+
requestAbortController.signal,
|
|
1519
|
+
]),
|
|
1512
1520
|
// The proxy runtime exposes only the structural slice of NeuroLink the
|
|
1513
1521
|
// routes use; narrow (overlap-checked) to the full class for ServerContext.
|
|
1514
1522
|
neurolink: params.neurolink,
|
|
@@ -1555,39 +1563,42 @@ export async function createProxyStartApp(params) {
|
|
|
1555
1563
|
Symbol.asyncIterator in Object(result)) {
|
|
1556
1564
|
const iterator = result[Symbol.asyncIterator]();
|
|
1557
1565
|
let cancelled = false;
|
|
1566
|
+
const encoder = new TextEncoder();
|
|
1558
1567
|
const responseStream = new ReadableStream({
|
|
1559
|
-
async
|
|
1568
|
+
async pull(controller) {
|
|
1569
|
+
if (cancelled) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1560
1572
|
try {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
controller.
|
|
1573
|
+
const next = await iterator.next();
|
|
1574
|
+
if (cancelled) {
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (next.done) {
|
|
1578
|
+
controller.close();
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
controller.enqueue(encoder.encode(next.value));
|
|
1567
1582
|
}
|
|
1568
|
-
controller.close();
|
|
1569
1583
|
}
|
|
1570
1584
|
catch (streamErr) {
|
|
1571
1585
|
if (cancelled) {
|
|
1572
|
-
controller.close();
|
|
1573
1586
|
return;
|
|
1574
1587
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
: String(streamErr);
|
|
1578
|
-
const errorEvent = `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `Stream interrupted: ${errMsg}` } })}\n\n`;
|
|
1579
|
-
try {
|
|
1580
|
-
controller.enqueue(new TextEncoder().encode(errorEvent));
|
|
1581
|
-
}
|
|
1582
|
-
catch {
|
|
1583
|
-
// Controller already errored — ignore
|
|
1588
|
+
if (metadata) {
|
|
1589
|
+
metadata.terminalErrorType = "stream_error";
|
|
1584
1590
|
}
|
|
1591
|
+
logger.debug("[proxy] response stream interrupted", {
|
|
1592
|
+
error: streamErr,
|
|
1593
|
+
});
|
|
1594
|
+
controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: "Stream interrupted" } })}\n\n`));
|
|
1585
1595
|
controller.close();
|
|
1586
1596
|
}
|
|
1587
1597
|
},
|
|
1588
|
-
async cancel() {
|
|
1598
|
+
async cancel(reason) {
|
|
1589
1599
|
cancelled = true;
|
|
1590
|
-
|
|
1600
|
+
requestAbortController.abort(reason);
|
|
1601
|
+
await withTimeout(Promise.resolve(iterator.return?.()), 1000, "[proxy] response cancellation timed out").catch(() => undefined);
|
|
1591
1602
|
},
|
|
1592
1603
|
});
|
|
1593
1604
|
return new Response(responseStream, {
|
|
@@ -1697,48 +1708,24 @@ export async function createProxyStartApp(params) {
|
|
|
1697
1708
|
let accountInventoryLoaded = false;
|
|
1698
1709
|
try {
|
|
1699
1710
|
const { tokenStore } = await import("../../auth/tokenStore.js");
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
const inventory = await withTimeout((async () => {
|
|
1716
|
-
const tokenExpirations = await Promise.all(storedKeys.map(async (key) => {
|
|
1717
|
-
try {
|
|
1718
|
-
const tokens = await withTimeout(tokenStore.peekTokens(key), PROXY_STATUS_TOKEN_READ_TIMEOUT_MS, "[proxy] /status token inspection timed out");
|
|
1719
|
-
return tokens ? [key, tokens.expiresAt] : undefined;
|
|
1720
|
-
}
|
|
1721
|
-
catch (error) {
|
|
1722
|
-
logger.debug(`[proxy] /status: failed to inspect token metadata for ${normalizeAnthropicAccountKey(key)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1723
|
-
return undefined;
|
|
1724
|
-
}
|
|
1725
|
-
}));
|
|
1726
|
-
const disabledKeys = await tokenStore.listDisabled();
|
|
1727
|
-
return { tokenExpirations, disabledKeys };
|
|
1728
|
-
})(), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status account metadata timed out");
|
|
1729
|
-
for (const expiration of inventory.tokenExpirations) {
|
|
1730
|
-
if (expiration) {
|
|
1731
|
-
const key = expiration[0].startsWith("anthropic:")
|
|
1732
|
-
? normalizeAnthropicAccountKey(expiration[0])
|
|
1733
|
-
: expiration[0];
|
|
1734
|
-
storedAccountExpirations.set(key, expiration[1]);
|
|
1711
|
+
const inventory = await withTimeout(tokenStore.getProviderSnapshot(), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status account inspection timed out");
|
|
1712
|
+
for (const [storedKey, entry] of Object.entries(inventory)) {
|
|
1713
|
+
if (!storedKey.startsWith("anthropic:") &&
|
|
1714
|
+
!storedKey.startsWith("codex:")) {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const key = storedKey.startsWith("anthropic:")
|
|
1718
|
+
? normalizeAnthropicAccountKey(storedKey)
|
|
1719
|
+
: storedKey;
|
|
1720
|
+
(key.startsWith("anthropic:")
|
|
1721
|
+
? storedAnthropicAccountKeys
|
|
1722
|
+
: storedCodexAccountKeys).add(key);
|
|
1723
|
+
storedAccountExpirations.set(key, entry.tokens.expiresAt);
|
|
1724
|
+
if (entry.disabled) {
|
|
1725
|
+
disabledProviderAccountKeys.add(key);
|
|
1735
1726
|
}
|
|
1736
1727
|
}
|
|
1737
|
-
|
|
1738
|
-
disabledProviderAccountKeys.add(key.startsWith("anthropic:")
|
|
1739
|
-
? normalizeAnthropicAccountKey(key)
|
|
1740
|
-
: key);
|
|
1741
|
-
}
|
|
1728
|
+
accountInventoryLoaded = true;
|
|
1742
1729
|
}
|
|
1743
1730
|
catch (err) {
|
|
1744
1731
|
logger.debug(`[proxy] /status: failed to resolve account cooldown labels: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3090,7 +3077,15 @@ function printStatusStats(stats) {
|
|
|
3090
3077
|
}
|
|
3091
3078
|
}
|
|
3092
3079
|
}
|
|
3093
|
-
|
|
3080
|
+
const historical = (stats.accounts ?? []).filter((account) => account.status === "unattributed");
|
|
3081
|
+
const accounts = (stats.accounts ?? []).filter((account) => account.status !== "unattributed");
|
|
3082
|
+
if (historical.length > 0) {
|
|
3083
|
+
const sum = (field) => historical.reduce((total, account) => total + (account[field] ?? 0), 0);
|
|
3084
|
+
console.info(`\n Historical usage (provider unknown; included in totals):`);
|
|
3085
|
+
console.info(` ${historical.length} legacy records: ${sum("attempts")} attempts, ${sum("success")} success, ${sum("errors")} errors, ${sum("rateLimits")} rate-limited attempts`);
|
|
3086
|
+
console.info(" Per-record history remains available with --format json.");
|
|
3087
|
+
}
|
|
3088
|
+
if (accounts.length > 0) {
|
|
3094
3089
|
console.info(`\n Accounts:`);
|
|
3095
3090
|
const headers = [
|
|
3096
3091
|
"ACCOUNT",
|
|
@@ -3101,8 +3096,8 @@ function printStatusStats(stats) {
|
|
|
3101
3096
|
"RL",
|
|
3102
3097
|
"STATUS",
|
|
3103
3098
|
];
|
|
3104
|
-
const rows =
|
|
3105
|
-
account.label,
|
|
3099
|
+
const rows = accounts.map((account) => [
|
|
3100
|
+
account.key ?? account.label,
|
|
3106
3101
|
account.type,
|
|
3107
3102
|
String(account.attempts ?? account.requests ?? 0),
|
|
3108
3103
|
String(account.success ?? 0),
|
|
@@ -3267,7 +3262,7 @@ export const proxyStatusCommand = {
|
|
|
3267
3262
|
});
|
|
3268
3263
|
if (statusResp.ok) {
|
|
3269
3264
|
const statusData = (await statusResp.json());
|
|
3270
|
-
liveStats = statusData.stats;
|
|
3265
|
+
liveStats = statusData.stats ?? null;
|
|
3271
3266
|
liveConfig = statusData.config;
|
|
3272
3267
|
status.workerVersion =
|
|
3273
3268
|
typeof statusData.version === "string"
|
|
@@ -3342,6 +3337,9 @@ export const proxyStatusCommand = {
|
|
|
3342
3337
|
else if (status.rolling?.draining.length) {
|
|
3343
3338
|
logger.always(` ${chalk.bold("Handoff:")} ${chalk.cyan(`${status.rolling.draining.length} previous worker(s) draining`)}`);
|
|
3344
3339
|
}
|
|
3340
|
+
if (status.rolling) {
|
|
3341
|
+
logger.always(` ${chalk.bold("Socket handoff:")} ${status.rolling.pendingTransfers ?? "unknown"} pending, ${status.rolling.queuedSockets} queued; ${status.rolling.rejectedSockets} rejected, ${status.rolling.failedTransfers} failed transfers since supervisor start`);
|
|
3342
|
+
}
|
|
3345
3343
|
if (status.deferredUpdate) {
|
|
3346
3344
|
const active = status.deferredUpdate.activeRequests === null
|
|
3347
3345
|
? "unknown activity"
|
|
@@ -3398,21 +3396,8 @@ export const proxyStatusCommand = {
|
|
|
3398
3396
|
logger.always("");
|
|
3399
3397
|
logger.always(chalk.gray(" (Could not reach proxy for live status)"));
|
|
3400
3398
|
}
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
const liveUrl = status.url;
|
|
3404
|
-
const statusResp = await fetch(`${liveUrl}/status`, {
|
|
3405
|
-
signal: AbortSignal.timeout(2_000),
|
|
3406
|
-
});
|
|
3407
|
-
if (statusResp.ok) {
|
|
3408
|
-
const statusData = (await statusResp.json());
|
|
3409
|
-
if (statusData.stats) {
|
|
3410
|
-
printStatusStats(statusData.stats);
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
}
|
|
3414
|
-
catch {
|
|
3415
|
-
/* non-fatal */
|
|
3399
|
+
if (liveStats) {
|
|
3400
|
+
printStatusStats(liveStats);
|
|
3416
3401
|
}
|
|
3417
3402
|
}
|
|
3418
3403
|
else {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClaudeErrorResponse, ClaudeRequest, ClaudeResponse, ContentBlockType, InternalResult, ParsedClaudeRequest, StreamLifecycleState } from "../types/index.js";
|
|
1
|
+
import type { ClaudeErrorResponse, ClaudeRequest, ClaudeResponse, ContentBlockType, InternalResult, ParsedClaudeRequest, SSEMessageDelta, StreamLifecycleState } from "../types/index.js";
|
|
2
2
|
/** Generate a unique message id in the Claude format. */
|
|
3
3
|
export declare function generateMessageId(): string;
|
|
4
4
|
/** Generate a Claude-format tool use ID (`toolu_` + 24 random chars). */
|
|
@@ -121,7 +121,7 @@ export declare class ClaudeStreamSerializer {
|
|
|
121
121
|
/**
|
|
122
122
|
* Finalize the stream: content_block_stop, message_delta, message_stop.
|
|
123
123
|
*/
|
|
124
|
-
finish(outputTokens?: number, finishReason?: string): Generator<string>;
|
|
124
|
+
finish(outputTokens?: number, finishReason?: string, finalUsage?: Partial<SSEMessageDelta["usage"]>): Generator<string>;
|
|
125
125
|
/**
|
|
126
126
|
* Emit an error event. Transitions to terminal ERROR state.
|
|
127
127
|
*/
|
|
@@ -582,7 +582,7 @@ export class ClaudeStreamSerializer {
|
|
|
582
582
|
/**
|
|
583
583
|
* Finalize the stream: content_block_stop, message_delta, message_stop.
|
|
584
584
|
*/
|
|
585
|
-
*finish(outputTokens, finishReason) {
|
|
585
|
+
*finish(outputTokens, finishReason, finalUsage) {
|
|
586
586
|
// If we never started (empty response), start first
|
|
587
587
|
if (this.state === "idle") {
|
|
588
588
|
yield* this.ensureMessageStarted();
|
|
@@ -603,7 +603,7 @@ export class ClaudeStreamSerializer {
|
|
|
603
603
|
stop_reason: mapStopReason(resolvedFinishReason),
|
|
604
604
|
stop_sequence: null,
|
|
605
605
|
},
|
|
606
|
-
usage: { output_tokens: this.outputTokens },
|
|
606
|
+
usage: { ...finalUsage, output_tokens: this.outputTokens },
|
|
607
607
|
};
|
|
608
608
|
yield formatSSE("message_delta", messageDelta);
|
|
609
609
|
// message_stop
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* in the native Codex proxy handler so fallback traffic follows the same pool
|
|
7
7
|
* rules as a native Codex request.
|
|
8
8
|
*/
|
|
9
|
-
import type { ClaudeRequest, CodexFallbackResult, CodexResponsesRequest } from "../types/index.js";
|
|
9
|
+
import type { ClaudeRequest, CodexFallbackResult, CodexFallbackStream, CodexReasoningEffort, CodexResponsesRequest } from "../types/index.js";
|
|
10
10
|
export declare class CodexFallbackResponseError extends Error {
|
|
11
11
|
readonly status: number;
|
|
12
12
|
readonly responseBody: string;
|
|
13
13
|
constructor(status: number, responseBody: string);
|
|
14
14
|
}
|
|
15
15
|
/** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
|
|
16
|
-
export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model: string): CodexResponsesRequest;
|
|
16
|
+
export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model: string, reasoningEffort?: CodexReasoningEffort): CodexResponsesRequest;
|
|
17
17
|
/**
|
|
18
18
|
* Parse a complete Codex Responses SSE stream before emitting Claude output.
|
|
19
19
|
*
|
|
@@ -24,3 +24,9 @@ export declare function convertClaudeRequestToCodex(body: ClaudeRequest, model:
|
|
|
24
24
|
export declare function parseCodexFallbackSSE(sse: string): CodexFallbackResult;
|
|
25
25
|
/** Consume and validate a native Codex response before producing Claude output. */
|
|
26
26
|
export declare function consumeCodexFallbackResponse(response: Response): Promise<CodexFallbackResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Translate a Codex stream as events arrive. A completed tool call is emitted
|
|
29
|
+
* once its arguments validate; text does not wait for response.completed.
|
|
30
|
+
* The caller owns error framing and must never retry after emitting output.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createCodexFallbackStream(response: Response, model: string): Promise<CodexFallbackStream>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* in the native Codex proxy handler so fallback traffic follows the same pool
|
|
7
7
|
* rules as a native Codex request.
|
|
8
8
|
*/
|
|
9
|
+
import { ClaudeStreamSerializer, generateToolUseId } from "./claudeFormat.js";
|
|
9
10
|
import { extractCodexUsage } from "./codexUsage.js";
|
|
10
11
|
export class CodexFallbackResponseError extends Error {
|
|
11
12
|
status;
|
|
@@ -131,7 +132,7 @@ function convertClaudeMessage(role, content) {
|
|
|
131
132
|
return input;
|
|
132
133
|
}
|
|
133
134
|
/** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
|
|
134
|
-
export function convertClaudeRequestToCodex(body, model) {
|
|
135
|
+
export function convertClaudeRequestToCodex(body, model, reasoningEffort) {
|
|
135
136
|
const input = body.messages.flatMap((message) => convertClaudeMessage(message.role, message.content));
|
|
136
137
|
const request = {
|
|
137
138
|
model,
|
|
@@ -139,6 +140,9 @@ export function convertClaudeRequestToCodex(body, model) {
|
|
|
139
140
|
stream: true,
|
|
140
141
|
// ChatGPT's backend rejects requests unless this is explicitly false.
|
|
141
142
|
store: false,
|
|
143
|
+
...(reasoningEffort !== undefined
|
|
144
|
+
? { reasoning: { effort: reasoningEffort } }
|
|
145
|
+
: {}),
|
|
142
146
|
};
|
|
143
147
|
const instructions = buildSystemInstructions(body);
|
|
144
148
|
if (instructions) {
|
|
@@ -369,3 +373,183 @@ export async function consumeCodexFallbackResponse(response) {
|
|
|
369
373
|
}
|
|
370
374
|
return parseCodexFallbackSSE(await response.text());
|
|
371
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Translate a Codex stream as events arrive. A completed tool call is emitted
|
|
378
|
+
* once its arguments validate; text does not wait for response.completed.
|
|
379
|
+
* The caller owns error framing and must never retry after emitting output.
|
|
380
|
+
*/
|
|
381
|
+
export async function createCodexFallbackStream(response, model) {
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw new CodexFallbackResponseError(response.status, await response.text().catch(() => ""));
|
|
384
|
+
}
|
|
385
|
+
if (!response.body ||
|
|
386
|
+
!(response.headers.get("content-type") ?? "")
|
|
387
|
+
.toLowerCase()
|
|
388
|
+
.includes("text/event-stream")) {
|
|
389
|
+
await response.body?.cancel().catch(() => undefined);
|
|
390
|
+
throw new Error("Codex fallback returned a non-SSE or empty response");
|
|
391
|
+
}
|
|
392
|
+
const reader = response.body.getReader();
|
|
393
|
+
let cancellation;
|
|
394
|
+
const cancel = (reason) => {
|
|
395
|
+
cancellation ??= reader
|
|
396
|
+
.cancel(reason)
|
|
397
|
+
.catch(() => undefined)
|
|
398
|
+
.finally(() => reader.releaseLock());
|
|
399
|
+
return cancellation;
|
|
400
|
+
};
|
|
401
|
+
async function* frames() {
|
|
402
|
+
const serializer = new ClaudeStreamSerializer(model);
|
|
403
|
+
const decoder = new TextDecoder();
|
|
404
|
+
const toolCalls = new Map();
|
|
405
|
+
const emittedTools = new Set();
|
|
406
|
+
const emittedTextItems = new Set();
|
|
407
|
+
const textParts = [];
|
|
408
|
+
let textLength = 0;
|
|
409
|
+
let toolChars = 0;
|
|
410
|
+
let carry = "";
|
|
411
|
+
let searchFrom = 0;
|
|
412
|
+
let completed = false;
|
|
413
|
+
let usage;
|
|
414
|
+
const maxChars = 16 * 1024 * 1024;
|
|
415
|
+
function* text(value, index) {
|
|
416
|
+
if (!value) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
textLength += value.length;
|
|
420
|
+
if (textLength > maxChars) {
|
|
421
|
+
throw new Error("Codex fallback output exceeded the stream limit");
|
|
422
|
+
}
|
|
423
|
+
textParts.push(value);
|
|
424
|
+
emittedTextItems.add(index);
|
|
425
|
+
yield* serializer.pushDelta(value);
|
|
426
|
+
}
|
|
427
|
+
function* item(value, index) {
|
|
428
|
+
if (!isRecord(value)) {
|
|
429
|
+
throw new Error("Codex fallback output item is malformed");
|
|
430
|
+
}
|
|
431
|
+
if (value.type === "function_call") {
|
|
432
|
+
const id = asNonEmptyString(value.call_id);
|
|
433
|
+
if (!id || !emittedTools.has(id)) {
|
|
434
|
+
toolChars += JSON.stringify(value).length;
|
|
435
|
+
if (toolChars > maxChars || toolCalls.size >= 4096) {
|
|
436
|
+
throw new Error("Codex fallback tools exceeded the stream limit");
|
|
437
|
+
}
|
|
438
|
+
addFunctionCall(value, toolCalls);
|
|
439
|
+
const call = id ? toolCalls.get(id) : undefined;
|
|
440
|
+
if (id && call) {
|
|
441
|
+
emittedTools.add(id);
|
|
442
|
+
yield* serializer.pushToolUse(generateToolUseId(), call.toolName, call.args);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!emittedTextItems.has(index)) {
|
|
447
|
+
yield* text(outputTextFromItem(value), index);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function* event(frame) {
|
|
451
|
+
for (const { event: eventName, payload } of parseSSEPayloads(frame)) {
|
|
452
|
+
const type = asNonEmptyString(payload.type) ?? eventName;
|
|
453
|
+
if (!type) {
|
|
454
|
+
throw new Error("Codex fallback stream event is missing a type");
|
|
455
|
+
}
|
|
456
|
+
if (completed) {
|
|
457
|
+
throw new Error("Codex fallback stream emitted events after completion");
|
|
458
|
+
}
|
|
459
|
+
if (type === "error" ||
|
|
460
|
+
type === "response.failed" ||
|
|
461
|
+
type === "response.incomplete") {
|
|
462
|
+
throw new Error(`Codex fallback stream terminated with ${type}`);
|
|
463
|
+
}
|
|
464
|
+
const index = typeof payload.output_index === "number" ? payload.output_index : 0;
|
|
465
|
+
if (type === "response.output_text.delta") {
|
|
466
|
+
if (typeof payload.delta !== "string") {
|
|
467
|
+
throw new Error("Codex fallback text delta is malformed");
|
|
468
|
+
}
|
|
469
|
+
yield* text(payload.delta, index);
|
|
470
|
+
}
|
|
471
|
+
else if (type === "response.output_item.done") {
|
|
472
|
+
if (!isRecord(payload.item)) {
|
|
473
|
+
throw new Error("Codex fallback output item is malformed");
|
|
474
|
+
}
|
|
475
|
+
yield* item(payload.item, index);
|
|
476
|
+
}
|
|
477
|
+
else if (type === "response.completed") {
|
|
478
|
+
if (responseStatus(payload) !== "completed") {
|
|
479
|
+
throw new Error("Codex fallback response did not complete");
|
|
480
|
+
}
|
|
481
|
+
completed = true;
|
|
482
|
+
const parsedUsage = extractCodexUsage(payload);
|
|
483
|
+
if (parsedUsage) {
|
|
484
|
+
usage = {
|
|
485
|
+
input: parsedUsage.inputTokens,
|
|
486
|
+
output: parsedUsage.outputTokens,
|
|
487
|
+
total: parsedUsage.inputTokens + parsedUsage.outputTokens,
|
|
488
|
+
cacheReadTokens: parsedUsage.cacheReadTokens,
|
|
489
|
+
cacheCreationTokens: parsedUsage.cacheCreationTokens,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const responseBody = payload.response;
|
|
493
|
+
if (isRecord(responseBody) && Array.isArray(responseBody.output)) {
|
|
494
|
+
for (const [i, output] of responseBody.output.entries()) {
|
|
495
|
+
yield* item(output, i);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
yield* serializer.start();
|
|
503
|
+
while (true) {
|
|
504
|
+
const chunk = await reader.read();
|
|
505
|
+
carry += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
506
|
+
// Search only new bytes plus the boundary overlap. Long tool payloads
|
|
507
|
+
// split over many chunks must not rescan their accumulated prefix.
|
|
508
|
+
const boundary = /\r?\n\r?\n/g;
|
|
509
|
+
boundary.lastIndex = searchFrom;
|
|
510
|
+
let match;
|
|
511
|
+
while ((match = boundary.exec(carry)) !== null) {
|
|
512
|
+
if (match.index > maxChars) {
|
|
513
|
+
throw new Error("Codex fallback event exceeded the stream limit");
|
|
514
|
+
}
|
|
515
|
+
yield* event(carry.slice(0, match.index));
|
|
516
|
+
carry = carry.slice(match.index + match[0].length);
|
|
517
|
+
boundary.lastIndex = 0;
|
|
518
|
+
}
|
|
519
|
+
if (carry.length > maxChars) {
|
|
520
|
+
throw new Error("Codex fallback event exceeded the stream limit");
|
|
521
|
+
}
|
|
522
|
+
searchFrom = Math.max(0, carry.length - 3);
|
|
523
|
+
if (chunk.done) {
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (carry.trim()) {
|
|
528
|
+
yield* event(carry);
|
|
529
|
+
}
|
|
530
|
+
if (!completed) {
|
|
531
|
+
throw new Error("Codex fallback stream ended before response.completed");
|
|
532
|
+
}
|
|
533
|
+
if (textLength === 0 && toolCalls.size === 0) {
|
|
534
|
+
throw new Error("Codex fallback returned no content or tool calls");
|
|
535
|
+
}
|
|
536
|
+
const finishReason = toolCalls.size > 0 ? "tool_use" : "end_turn";
|
|
537
|
+
yield* serializer.finish(usage?.output, finishReason, {
|
|
538
|
+
input_tokens: usage?.input,
|
|
539
|
+
cache_read_input_tokens: usage?.cacheReadTokens,
|
|
540
|
+
cache_creation_input_tokens: usage?.cacheCreationTokens,
|
|
541
|
+
});
|
|
542
|
+
return {
|
|
543
|
+
text: textParts.join(""),
|
|
544
|
+
toolCalls: [...toolCalls.values()],
|
|
545
|
+
finishReason,
|
|
546
|
+
...(usage ? { usage } : {}),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
await cancel();
|
|
551
|
+
reader.releaseLock();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return { frames: frames(), cancel };
|
|
555
|
+
}
|
|
@@ -189,6 +189,18 @@ function applyAccountDefaults(account) {
|
|
|
189
189
|
metadata: account.metadata,
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
+
const CODEX_REASONING_EFFORTS = [
|
|
193
|
+
"none",
|
|
194
|
+
"minimal",
|
|
195
|
+
"low",
|
|
196
|
+
"medium",
|
|
197
|
+
"high",
|
|
198
|
+
"xhigh",
|
|
199
|
+
"max",
|
|
200
|
+
];
|
|
201
|
+
function isCodexReasoningEffort(value) {
|
|
202
|
+
return CODEX_REASONING_EFFORTS.some((effort) => effort === value);
|
|
203
|
+
}
|
|
192
204
|
/**
|
|
193
205
|
* Validate the shape of a parsed proxy config.
|
|
194
206
|
* Returns an array of human-readable error strings (empty = valid).
|
|
@@ -215,6 +227,28 @@ export function validateProxyConfig(config) {
|
|
|
215
227
|
}
|
|
216
228
|
if (hasRouting) {
|
|
217
229
|
const routing = cfg.routing;
|
|
230
|
+
const rawFallback = routing["fallback-chain"] ?? routing.fallbackChain;
|
|
231
|
+
if (Array.isArray(rawFallback)) {
|
|
232
|
+
rawFallback.forEach((entry, index) => {
|
|
233
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const fallback = entry;
|
|
237
|
+
const effort = fallback["reasoning-effort"] !== undefined
|
|
238
|
+
? fallback["reasoning-effort"]
|
|
239
|
+
: fallback.reasoningEffort;
|
|
240
|
+
if (effort === undefined) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const field = `routing.fallback-chain[${index}].reasoning-effort`;
|
|
244
|
+
if (!isCodexReasoningEffort(effort)) {
|
|
245
|
+
errors.push(`${field} must be one of: ${CODEX_REASONING_EFFORTS.join(", ")}`);
|
|
246
|
+
}
|
|
247
|
+
if (String(fallback.provider ?? "").trim() !== "codex") {
|
|
248
|
+
errors.push(`${field} is only supported for provider codex`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
218
252
|
const rawAccountAllowlist = routing["account-allowlist"] ?? routing.accountAllowlist;
|
|
219
253
|
if (rawAccountAllowlist !== undefined) {
|
|
220
254
|
if (!Array.isArray(rawAccountAllowlist)) {
|
|
@@ -350,7 +384,7 @@ function warnPlaintextApiKeys(accounts) {
|
|
|
350
384
|
* Extracts:
|
|
351
385
|
* - `strategy` ("round-robin" | "fill-first")
|
|
352
386
|
* - `model-mappings` / `modelMappings` — array of {from, to, provider}
|
|
353
|
-
* - `fallback-chain` / `fallbackChain` — array of {provider, model}
|
|
387
|
+
* - `fallback-chain` / `fallbackChain` — array of {provider, model, reasoningEffort?}
|
|
354
388
|
* - `auto-fallback` / `autoFallback` — opt in to an unspecified provider
|
|
355
389
|
* - `max-inflight-per-account` / `maxInflightPerAccount` — concurrency cap
|
|
356
390
|
* - `passthroughModels` / `passthrough-models` — array of model IDs
|
|
@@ -404,7 +438,16 @@ function parseRoutingConfig(raw) {
|
|
|
404
438
|
logger.warn(`[proxy-config] Skipping fallback entry with empty "provider" or "model": ${JSON.stringify(e)}`);
|
|
405
439
|
return null;
|
|
406
440
|
}
|
|
407
|
-
|
|
441
|
+
const effort = e["reasoning-effort"] !== undefined
|
|
442
|
+
? e["reasoning-effort"]
|
|
443
|
+
: e.reasoningEffort;
|
|
444
|
+
return {
|
|
445
|
+
provider,
|
|
446
|
+
model,
|
|
447
|
+
...(isCodexReasoningEffort(effort)
|
|
448
|
+
? { reasoningEffort: effort }
|
|
449
|
+
: {}),
|
|
450
|
+
};
|
|
408
451
|
})
|
|
409
452
|
.filter((e) => e !== null);
|
|
410
453
|
}
|
|
@@ -13,6 +13,7 @@ export async function startRollingProxyServer(options) {
|
|
|
13
13
|
let requestedReplacementTimer;
|
|
14
14
|
let requestedReplacementSchedule = 0;
|
|
15
15
|
let requestedReplacementPending = false;
|
|
16
|
+
let requestedReplacementReason = "environment";
|
|
16
17
|
let replacementQueueTail = null;
|
|
17
18
|
const recoveryDelayMs = Math.max(1, options.recoveryDelayMs ?? DEFAULT_RECOVERY_DELAY_MS);
|
|
18
19
|
const maxRecoveryDelayMs = Math.max(recoveryDelayMs, options.maxRecoveryDelayMs ?? DEFAULT_MAX_RECOVERY_DELAY_MS);
|
|
@@ -70,11 +71,14 @@ export async function startRollingProxyServer(options) {
|
|
|
70
71
|
scheduleRecovery();
|
|
71
72
|
}
|
|
72
73
|
};
|
|
73
|
-
function scheduleRequestedReplacement() {
|
|
74
|
+
function scheduleRequestedReplacement(request) {
|
|
74
75
|
if (closing) {
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
requestedReplacementPending = true;
|
|
79
|
+
if (request) {
|
|
80
|
+
requestedReplacementReason = request.reason;
|
|
81
|
+
}
|
|
78
82
|
if (requestedReplacementTimer || replacementQueueTail) {
|
|
79
83
|
return;
|
|
80
84
|
}
|
|
@@ -89,15 +93,16 @@ export async function startRollingProxyServer(options) {
|
|
|
89
93
|
}
|
|
90
94
|
requestedReplacementPending = false;
|
|
91
95
|
const replacementVersion = desiredVersion;
|
|
96
|
+
const replacementReason = requestedReplacementReason;
|
|
92
97
|
void queueReplacement(async () => {
|
|
93
98
|
if (closing || !supervisor.snapshot().active) {
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
96
|
-
options.log?.(`[proxy-supervisor] preparing same-version worker replacement version=${replacementVersion} reason
|
|
101
|
+
options.log?.(`[proxy-supervisor] preparing same-version worker replacement version=${replacementVersion} reason=${replacementReason}`);
|
|
97
102
|
await supervisor.replace(replacementVersion);
|
|
98
|
-
options.log?.(`[proxy-supervisor] same-version worker replacement complete version=${replacementVersion} reason
|
|
103
|
+
options.log?.(`[proxy-supervisor] same-version worker replacement complete version=${replacementVersion} reason=${replacementReason}`);
|
|
99
104
|
}).catch((error) => {
|
|
100
|
-
options.log?.(`[proxy-supervisor] same-version worker replacement failed version=${replacementVersion} reason
|
|
105
|
+
options.log?.(`[proxy-supervisor] same-version worker replacement failed version=${replacementVersion} reason=${replacementReason}: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
106
|
});
|
|
102
107
|
}, 50);
|
|
103
108
|
requestedReplacementTimer.unref?.();
|
|
@@ -106,6 +111,7 @@ export async function startRollingProxyServer(options) {
|
|
|
106
111
|
spawnWorker: options.spawnWorker,
|
|
107
112
|
readyTimeoutMs: options.readyTimeoutMs,
|
|
108
113
|
socketQueueLimit: options.socketQueueLimit,
|
|
114
|
+
maxPendingTransfers: options.maxPendingTransfers,
|
|
109
115
|
socketQueueTimeoutMs: options.socketQueueTimeoutMs,
|
|
110
116
|
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
111
117
|
onStateChange: stateChanged,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { ErrorFactory } from "../utils/errorHandling.js";
|
|
3
|
-
import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, } from "./rollingWorkerProtocol.js";
|
|
3
|
+
import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, PROXY_SOCKET_OFFER_TIMEOUT, } from "./rollingWorkerProtocol.js";
|
|
4
4
|
export function spawnProxySocketWorker(options) {
|
|
5
5
|
const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 30_000);
|
|
6
6
|
let nextSocketId = 0;
|
|
@@ -134,7 +134,13 @@ export function spawnProxySocketWorker(options) {
|
|
|
134
134
|
}
|
|
135
135
|
const socketId = `${generation}:${++nextSocketId}`;
|
|
136
136
|
const timeout = setTimeout(() => {
|
|
137
|
-
|
|
137
|
+
const error = new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`);
|
|
138
|
+
if (!pendingSockets.get(socketId)?.accepted) {
|
|
139
|
+
// No commit was sent. The cancel message settles this offer without
|
|
140
|
+
// terminating unrelated requests already owned by the worker.
|
|
141
|
+
error.code = PROXY_SOCKET_OFFER_TIMEOUT;
|
|
142
|
+
}
|
|
143
|
+
settleSocket(socketId, error);
|
|
138
144
|
}, socketAckTimeoutMs);
|
|
139
145
|
timeout.unref?.();
|
|
140
146
|
pendingSockets.set(socketId, {
|