@bitkyc08/opencodex 2.1.12-preview.1 → 2.5.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/README.ko.md +24 -1
- package/README.md +25 -1
- package/gui/dist/assets/{index-BxdKA4PR.js → index-gRjZGtP9.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli.ts +8 -7
- package/src/server.ts +148 -11
- package/src/types.ts +2 -0
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-gRjZGtP9.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-xJdeQjzZ.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { restoreLegacyOpenaiHistory } from "./codex-history-provider";
|
|
|
6
6
|
import { codexAutoStartEnabled, getConfigDir, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
|
|
7
7
|
import { findAvailablePort } from "./ports";
|
|
8
8
|
import { serviceCommand, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
|
|
9
|
-
import { startServer } from "./server";
|
|
9
|
+
import { drainAndShutdown, startServer } from "./server";
|
|
10
10
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
11
11
|
|
|
12
12
|
const args = process.argv.slice(2);
|
|
@@ -200,14 +200,15 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
200
200
|
const server = startServer(port);
|
|
201
201
|
writePid(process.pid);
|
|
202
202
|
|
|
203
|
+
const config = loadConfig();
|
|
203
204
|
const shutdown = () => {
|
|
204
205
|
console.log("\n🛑 Shutting down opencodex proxy...");
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
void (async () => {
|
|
207
|
+
await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
208
|
+
removePid();
|
|
209
|
+
if (!process.env.OCX_SERVICE) { try { restoreNativeCodex(); } catch { /* best-effort restore */ } }
|
|
210
|
+
process.exit(0);
|
|
211
|
+
})();
|
|
211
212
|
};
|
|
212
213
|
|
|
213
214
|
process.on("SIGINT", shutdown);
|
package/src/server.ts
CHANGED
|
@@ -57,6 +57,66 @@ import {
|
|
|
57
57
|
} from "./codex-routing";
|
|
58
58
|
import { registerCodexWebSocket, unregisterCodexWebSocket } from "./codex-websocket-registry";
|
|
59
59
|
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Active turn tracking + graceful shutdown drain
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
const activeTurns = new Set<AbortController>();
|
|
65
|
+
let draining = false;
|
|
66
|
+
|
|
67
|
+
export function registerTurn(ac: AbortController): void { activeTurns.add(ac); }
|
|
68
|
+
export function unregisterTurn(ac: AbortController): void { activeTurns.delete(ac); }
|
|
69
|
+
export function isDraining(): boolean { return draining; }
|
|
70
|
+
export function getActiveTurnCount(): number { return activeTurns.size; }
|
|
71
|
+
|
|
72
|
+
export function trackStreamLifetime(
|
|
73
|
+
body: ReadableStream<Uint8Array>,
|
|
74
|
+
ac: AbortController,
|
|
75
|
+
): ReadableStream<Uint8Array> {
|
|
76
|
+
registerTurn(ac);
|
|
77
|
+
const reader = body.getReader();
|
|
78
|
+
return new ReadableStream<Uint8Array>({
|
|
79
|
+
async pull(controller) {
|
|
80
|
+
try {
|
|
81
|
+
const { done, value } = await reader.read();
|
|
82
|
+
if (done) { unregisterTurn(ac); controller.close(); return; }
|
|
83
|
+
controller.enqueue(value);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
unregisterTurn(ac);
|
|
86
|
+
try { controller.error(err); } catch { /* already closed */ }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
cancel(reason) {
|
|
90
|
+
unregisterTurn(ac);
|
|
91
|
+
ac.abort(reason);
|
|
92
|
+
reader.cancel(reason).catch(() => {});
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let _serverRef: ReturnType<typeof Bun.serve> | undefined;
|
|
98
|
+
|
|
99
|
+
export async function drainAndShutdown(
|
|
100
|
+
server: ReturnType<typeof Bun.serve> | undefined,
|
|
101
|
+
timeoutMs: number,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const s = server ?? _serverRef;
|
|
104
|
+
draining = true;
|
|
105
|
+
const deadline = Date.now() + timeoutMs;
|
|
106
|
+
while (activeTurns.size > 0 && Date.now() < deadline) {
|
|
107
|
+
await Bun.sleep(100);
|
|
108
|
+
}
|
|
109
|
+
if (activeTurns.size > 0) {
|
|
110
|
+
console.log(`⚠️ Aborting ${activeTurns.size} in-flight turn(s) after ${timeoutMs}ms deadline`);
|
|
111
|
+
for (const ac of activeTurns) {
|
|
112
|
+
ac.abort(new Error("server shutdown"));
|
|
113
|
+
}
|
|
114
|
+
activeTurns.clear();
|
|
115
|
+
}
|
|
116
|
+
s?.stop(true);
|
|
117
|
+
draining = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
60
120
|
// Single source of truth = package.json (../ from src/), so /healthz + the GUI badge match the
|
|
61
121
|
// installed npm version instead of a stale hardcode.
|
|
62
122
|
const VERSION = (() => {
|
|
@@ -316,15 +376,28 @@ async function handleResponses(
|
|
|
316
376
|
}
|
|
317
377
|
}
|
|
318
378
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
379
|
+
// Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
|
|
380
|
+
// async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
|
|
381
|
+
// native relay, never enters JS Sink.write); branch[1] is consumed in the
|
|
382
|
+
// background for terminal-outcome/quota inspection only.
|
|
383
|
+
if (isEventStream && upstreamResponse.body) {
|
|
384
|
+
const [nativeBody, inspectBody] = upstreamResponse.body.tee();
|
|
385
|
+
const turnAc = new AbortController();
|
|
386
|
+
const trackedNative = trackStreamLifetime(nativeBody, turnAc);
|
|
387
|
+
if (terminalBodyWillRecord && recordTerminalOutcomes && terminalRecorder) {
|
|
388
|
+
consumeForInspection(inspectBody, terminalRecorder);
|
|
389
|
+
} else {
|
|
390
|
+
inspectBody.cancel().catch(() => {});
|
|
391
|
+
}
|
|
392
|
+
return new Response(trackedNative, {
|
|
393
|
+
status: upstreamResponse.status,
|
|
394
|
+
headers,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
398
|
+
const turnAc = new AbortController();
|
|
399
|
+
const tracked = body ? trackStreamLifetime(body, turnAc) : null;
|
|
400
|
+
return new Response(tracked, {
|
|
328
401
|
status: upstreamResponse.status,
|
|
329
402
|
headers,
|
|
330
403
|
});
|
|
@@ -391,7 +464,9 @@ async function handleResponses(
|
|
|
391
464
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
392
465
|
},
|
|
393
466
|
);
|
|
394
|
-
|
|
467
|
+
const bridgeTurnAc = new AbortController();
|
|
468
|
+
const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc);
|
|
469
|
+
return new Response(trackedSse, {
|
|
395
470
|
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
396
471
|
});
|
|
397
472
|
}
|
|
@@ -608,6 +683,54 @@ export function relaySseWithHeartbeat(
|
|
|
608
683
|
});
|
|
609
684
|
}
|
|
610
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Background-consume an SSE stream purely for terminal-outcome inspection (quota tracking).
|
|
688
|
+
* Does not produce output; safe to ignore errors (the client-facing stream is separate).
|
|
689
|
+
*/
|
|
690
|
+
function consumeForInspection(
|
|
691
|
+
body: ReadableStream<Uint8Array>,
|
|
692
|
+
onTerminal: (status: ResponsesTerminalStatus) => void,
|
|
693
|
+
): void {
|
|
694
|
+
const reader = body.getReader();
|
|
695
|
+
const decoder = new TextDecoder();
|
|
696
|
+
let buffer = "";
|
|
697
|
+
let reported = false;
|
|
698
|
+
const pump = async () => {
|
|
699
|
+
try {
|
|
700
|
+
for (;;) {
|
|
701
|
+
const { done, value } = await reader.read();
|
|
702
|
+
if (done) {
|
|
703
|
+
buffer += decoder.decode();
|
|
704
|
+
if (buffer.trim() && !reported) {
|
|
705
|
+
const payload = sseDataPayload(buffer);
|
|
706
|
+
if (payload) {
|
|
707
|
+
const status = terminalStatusFromSsePayload(payload);
|
|
708
|
+
if (status) { reported = true; onTerminal(status); }
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (!reported) onTerminal("incomplete");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
buffer += decoder.decode(value, { stream: true });
|
|
715
|
+
let next: { block: string; rest: string } | null;
|
|
716
|
+
while ((next = nextSseBlock(buffer))) {
|
|
717
|
+
buffer = next.rest;
|
|
718
|
+
if (!reported) {
|
|
719
|
+
const payload = sseDataPayload(next.block);
|
|
720
|
+
if (payload) {
|
|
721
|
+
const status = terminalStatusFromSsePayload(payload);
|
|
722
|
+
if (status) { reported = true; onTerminal(status); }
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} catch {
|
|
728
|
+
if (!reported) onTerminal("incomplete");
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
pump();
|
|
732
|
+
}
|
|
733
|
+
|
|
611
734
|
/**
|
|
612
735
|
* Bun's fetch auto-decompresses the response body but leaves the upstream `content-encoding`
|
|
613
736
|
* (and a now-stale `content-length`) on `response.headers`. Relaying those with the already-decoded
|
|
@@ -967,7 +1090,10 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
|
|
|
967
1090
|
const { stopServiceIfInstalled } = await import("./service");
|
|
968
1091
|
stopServiceIfInstalled();
|
|
969
1092
|
restoreNativeCodex();
|
|
970
|
-
setTimeout(() =>
|
|
1093
|
+
setTimeout(async () => {
|
|
1094
|
+
await drainAndShutdown(undefined, config.shutdownTimeoutMs ?? 5000);
|
|
1095
|
+
process.exit(0);
|
|
1096
|
+
}, 200);
|
|
971
1097
|
return jsonResponse({ success: true, message: "Proxy stopping, native Codex restored." });
|
|
972
1098
|
}
|
|
973
1099
|
|
|
@@ -1026,6 +1152,9 @@ export function startServer(port?: number) {
|
|
|
1026
1152
|
// Responses WebSocket (phase 120.2). Codex upgrades the same /v1/responses path; auth is
|
|
1027
1153
|
// handshake-time only, so capture inbound headers and thread them into the pipeline.
|
|
1028
1154
|
if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
1155
|
+
if (draining) {
|
|
1156
|
+
return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(), "Retry-After": "5" } });
|
|
1157
|
+
}
|
|
1029
1158
|
const apiAuthError = requireApiAuth(req, config, "data-plane");
|
|
1030
1159
|
if (apiAuthError) return apiAuthError;
|
|
1031
1160
|
if (!isLocalOrigin(req)) {
|
|
@@ -1090,6 +1219,9 @@ export function startServer(port?: number) {
|
|
|
1090
1219
|
}
|
|
1091
1220
|
|
|
1092
1221
|
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
1222
|
+
if (draining) {
|
|
1223
|
+
return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(), "Retry-After": "5" } });
|
|
1224
|
+
}
|
|
1093
1225
|
const apiAuthError = requireApiAuth(req, config, "data-plane");
|
|
1094
1226
|
if (apiAuthError) return apiAuthError;
|
|
1095
1227
|
if (!isLocalOrigin(req)) {
|
|
@@ -1151,6 +1283,8 @@ export function startServer(port?: number) {
|
|
|
1151
1283
|
|
|
1152
1284
|
const payload: Record<string, unknown> = { ...frame };
|
|
1153
1285
|
delete payload.type;
|
|
1286
|
+
const wsTurnAc = new AbortController();
|
|
1287
|
+
registerTurn(wsTurnAc);
|
|
1154
1288
|
void (async () => {
|
|
1155
1289
|
const logCtx = { model: "unknown", provider: "unknown" };
|
|
1156
1290
|
const fwd = new Headers({ "content-type": "application/json" });
|
|
@@ -1192,6 +1326,7 @@ export function startServer(port?: number) {
|
|
|
1192
1326
|
/* socket already gone or send dropped */
|
|
1193
1327
|
}
|
|
1194
1328
|
} finally {
|
|
1329
|
+
unregisterTurn(wsTurnAc);
|
|
1195
1330
|
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
1196
1331
|
}
|
|
1197
1332
|
})();
|
|
@@ -1203,6 +1338,8 @@ export function startServer(port?: number) {
|
|
|
1203
1338
|
},
|
|
1204
1339
|
});
|
|
1205
1340
|
|
|
1341
|
+
_serverRef = server;
|
|
1342
|
+
|
|
1206
1343
|
console.log(`🚀 opencodex proxy running on http://localhost:${listenPort}`);
|
|
1207
1344
|
console.log(` POST /v1/responses → provider translation`);
|
|
1208
1345
|
console.log(` GET /healthz → health check`);
|
package/src/types.ts
CHANGED
|
@@ -183,6 +183,8 @@ export interface OcxConfig {
|
|
|
183
183
|
stallTimeoutSec?: number;
|
|
184
184
|
/** Connect timeout (ms) for upstream fetch — covers DNS, TCP, TLS, and response header. Default 30000. */
|
|
185
185
|
connectTimeoutMs?: number;
|
|
186
|
+
/** Graceful shutdown drain timeout (ms). Active turns are aborted after this deadline. Default 5000. */
|
|
187
|
+
shutdownTimeoutMs?: number;
|
|
186
188
|
/** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
|
|
187
189
|
websockets?: boolean;
|
|
188
190
|
/** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
|