openai-ws-opencode 0.1.6 → 0.1.8
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.md +3 -3
- package/SECURITY.md +1 -1
- package/bin/setup.js +17 -43
- package/dist/constants.d.ts +12 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +12 -1
- package/dist/constants.js.map +1 -1
- package/dist/models/catalog.d.ts +39 -0
- package/dist/models/catalog.d.ts.map +1 -0
- package/dist/models/catalog.js +89 -0
- package/dist/models/catalog.js.map +1 -0
- package/dist/models/defaults.d.ts.map +1 -1
- package/dist/models/defaults.js +15 -43
- package/dist/models/defaults.js.map +1 -1
- package/dist/models/resolve.d.ts +3 -26
- package/dist/models/resolve.d.ts.map +1 -1
- package/dist/models/resolve.js +59 -40
- package/dist/models/resolve.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +15 -7
- package/dist/plugin.js.map +1 -1
- package/dist/testing.d.ts +4 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +4 -2
- package/dist/testing.js.map +1 -1
- package/dist/transport/body.d.ts +3 -2
- package/dist/transport/body.d.ts.map +1 -1
- package/dist/transport/body.js +28 -15
- package/dist/transport/body.js.map +1 -1
- package/dist/transport/bridge.d.ts.map +1 -1
- package/dist/transport/bridge.js +75 -25
- package/dist/transport/bridge.js.map +1 -1
- package/dist/transport/bun-websocket.d.ts +21 -0
- package/dist/transport/bun-websocket.d.ts.map +1 -0
- package/dist/transport/bun-websocket.js +281 -0
- package/dist/transport/bun-websocket.js.map +1 -0
- package/dist/transport/config.d.ts +14 -0
- package/dist/transport/config.d.ts.map +1 -0
- package/dist/transport/config.js +14 -0
- package/dist/transport/config.js.map +1 -0
- package/dist/transport/headers.d.ts.map +1 -1
- package/dist/transport/headers.js +23 -6
- package/dist/transport/headers.js.map +1 -1
- package/dist/transport/pool.d.ts +20 -16
- package/dist/transport/pool.d.ts.map +1 -1
- package/dist/transport/pool.js +394 -93
- package/dist/transport/pool.js.map +1 -1
- package/package.json +1 -1
package/dist/transport/pool.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const IDLE_EVICT_MS = 120_000;
|
|
7
|
-
const STALE_REUSE_MS = 60_000;
|
|
8
|
-
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
9
|
-
const PONG_TIMEOUT_MS = 10_000;
|
|
10
|
-
let WebSocketImpl = WebSocket;
|
|
2
|
+
import { OPENAI_MODEL_HEADER, OPENAI_WS_INSTALLATION_ID_ENV, RESPONSE_PROCESSED_ENV, X_CODEX_INSTALLATION_ID_HEADER, X_CODEX_TURN_STATE_HEADER, X_CODEX_WINDOW_ID_HEADER, X_MODELS_ETAG_HEADER, X_OPENAI_SUBAGENT_HEADER, X_REASONING_INCLUDED_HEADER, } from "../constants.js";
|
|
3
|
+
import { loadDefaultWebSocketConstructor } from "./bun-websocket.js";
|
|
4
|
+
import { transportConfig } from "./config.js";
|
|
5
|
+
let WebSocketImpl = loadDefaultWebSocketConstructor();
|
|
11
6
|
export const connectionPool = [];
|
|
7
|
+
const acquisitionQueues = new Map();
|
|
8
|
+
const turnStateByContext = new Map();
|
|
9
|
+
const lastResponseIDByContext = new Map();
|
|
10
|
+
let nextConnectionID = 1;
|
|
12
11
|
export const readyState = {
|
|
13
12
|
CONNECTING: 0,
|
|
14
13
|
OPEN: 1,
|
|
@@ -27,13 +26,53 @@ function off(ws, event, listener) {
|
|
|
27
26
|
else
|
|
28
27
|
ws.removeEventListener?.(event, listener);
|
|
29
28
|
}
|
|
30
|
-
function
|
|
29
|
+
function authScopeHash(headers) {
|
|
31
30
|
const auth = Object.entries(headers)
|
|
32
31
|
.filter(([key]) => ["authorization", "chatgpt-account-id", "originator", "openai-beta"].includes(key.toLowerCase()))
|
|
33
32
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
34
33
|
.map(([key, value]) => `${key}:${value}`)
|
|
35
34
|
.join("\n");
|
|
36
|
-
return
|
|
35
|
+
return crypto.createHash("sha256").update(auth).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
function scopeKey(wsUrl, headers) {
|
|
38
|
+
return `${wsUrl}::${authScopeHash(headers)}`;
|
|
39
|
+
}
|
|
40
|
+
function contextScopeKey(wsUrl, headers, context) {
|
|
41
|
+
return `${scopeKey(wsUrl, headers)}::session:${context.sessionID ?? ""}::agent:${context.agent ?? ""}`;
|
|
42
|
+
}
|
|
43
|
+
function headersForConnection(wsUrl, baseHeaders, context, key) {
|
|
44
|
+
const headers = { ...baseHeaders };
|
|
45
|
+
if (context.sessionID) {
|
|
46
|
+
headers.session_id = context.sessionID;
|
|
47
|
+
headers["session-id"] = context.sessionID;
|
|
48
|
+
headers.thread_id = context.sessionID;
|
|
49
|
+
headers["thread-id"] = context.sessionID;
|
|
50
|
+
headers["x-client-request-id"] = crypto.createHash("sha256").update(`${wsUrl}:${context.sessionID}`).digest("hex").slice(0, 32);
|
|
51
|
+
headers[X_CODEX_WINDOW_ID_HEADER] = context.sessionID;
|
|
52
|
+
}
|
|
53
|
+
if (context.agent && context.agent !== "primary")
|
|
54
|
+
headers[X_OPENAI_SUBAGENT_HEADER] = context.agent;
|
|
55
|
+
const installationID = process.env[OPENAI_WS_INSTALLATION_ID_ENV];
|
|
56
|
+
if (installationID)
|
|
57
|
+
headers[X_CODEX_INSTALLATION_ID_HEADER] = installationID;
|
|
58
|
+
const turnState = turnStateByContext.get(key);
|
|
59
|
+
if (turnState)
|
|
60
|
+
headers[X_CODEX_TURN_STATE_HEADER] = turnState;
|
|
61
|
+
return headers;
|
|
62
|
+
}
|
|
63
|
+
function headerValue(source, name) {
|
|
64
|
+
const headers = source?.headers;
|
|
65
|
+
if (!headers)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (headers instanceof Headers)
|
|
68
|
+
return headers.get(name) ?? undefined;
|
|
69
|
+
if (typeof headers.get === "function") {
|
|
70
|
+
const value = headers.get(name);
|
|
71
|
+
return Array.isArray(value) ? String(value[0]) : value === undefined || value === null ? undefined : String(value);
|
|
72
|
+
}
|
|
73
|
+
const record = headers;
|
|
74
|
+
const value = record[name] ?? record[name.toLowerCase()];
|
|
75
|
+
return Array.isArray(value) ? String(value[0]) : value === undefined || value === null ? undefined : String(value);
|
|
37
76
|
}
|
|
38
77
|
function clearTimer(timer) {
|
|
39
78
|
if (timer)
|
|
@@ -52,6 +91,19 @@ function clearHeartbeat(conn) {
|
|
|
52
91
|
conn.heartbeatTimer = null;
|
|
53
92
|
conn.pongTimer = null;
|
|
54
93
|
}
|
|
94
|
+
function clearPendingTimer(pending) {
|
|
95
|
+
if (!pending)
|
|
96
|
+
return;
|
|
97
|
+
clearTimer(pending.idleTimer);
|
|
98
|
+
pending.idleTimer = null;
|
|
99
|
+
}
|
|
100
|
+
function finalizePending(pending) {
|
|
101
|
+
if (!pending)
|
|
102
|
+
return;
|
|
103
|
+
clearPendingTimer(pending);
|
|
104
|
+
pending.onFinalize?.();
|
|
105
|
+
pending.onFinalize = undefined;
|
|
106
|
+
}
|
|
55
107
|
function detach(conn) {
|
|
56
108
|
if (conn.detach) {
|
|
57
109
|
conn.detach();
|
|
@@ -71,10 +123,40 @@ function remove(conn) {
|
|
|
71
123
|
connectionPool.splice(index, 1);
|
|
72
124
|
detach(conn);
|
|
73
125
|
}
|
|
126
|
+
function retryDelay(attempt) {
|
|
127
|
+
const exponential = Math.min(transportConfig.reconnectMaxDelayMs, transportConfig.reconnectBaseDelayMs * 2 ** Math.max(0, attempt - 1));
|
|
128
|
+
return exponential;
|
|
129
|
+
}
|
|
130
|
+
function closeSocket(conn, code, reason) {
|
|
131
|
+
try {
|
|
132
|
+
conn.ws?.terminate?.();
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
try {
|
|
136
|
+
conn.ws?.close(code, reason);
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
140
|
+
function terminalRelease(conn) {
|
|
141
|
+
if (conn.turnState) {
|
|
142
|
+
finalizePending(conn.pending);
|
|
143
|
+
conn.pending = null;
|
|
144
|
+
conn.activeSessionID = undefined;
|
|
145
|
+
conn.activeAgent = undefined;
|
|
146
|
+
conn.busy = false;
|
|
147
|
+
removeAndDrain(conn);
|
|
148
|
+
closeSocket(conn, 1000, "turn complete");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
release(conn);
|
|
152
|
+
drainQueue(conn.scopeKey);
|
|
153
|
+
}
|
|
154
|
+
function removeAndDrain(conn) {
|
|
155
|
+
remove(conn);
|
|
156
|
+
drainQueue(conn.scopeKey);
|
|
157
|
+
}
|
|
74
158
|
function scheduleHeartbeat(conn) {
|
|
75
159
|
clearHeartbeat(conn);
|
|
76
|
-
if (conn.busy || conn.pending)
|
|
77
|
-
return;
|
|
78
160
|
const ws = conn.ws;
|
|
79
161
|
if (!ws || ws.readyState !== readyState.OPEN)
|
|
80
162
|
return;
|
|
@@ -83,7 +165,7 @@ function scheduleHeartbeat(conn) {
|
|
|
83
165
|
const generation = conn.generation;
|
|
84
166
|
conn.heartbeatTimer = unrefTimer(setTimeout(() => {
|
|
85
167
|
conn.heartbeatTimer = null;
|
|
86
|
-
if (generation !== conn.generation ||
|
|
168
|
+
if (generation !== conn.generation || !conn.ws)
|
|
87
169
|
return;
|
|
88
170
|
if (conn.ws.readyState !== readyState.OPEN)
|
|
89
171
|
return;
|
|
@@ -91,47 +173,42 @@ function scheduleHeartbeat(conn) {
|
|
|
91
173
|
conn.ws.ping?.();
|
|
92
174
|
}
|
|
93
175
|
catch {
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
conn.ws?.terminate?.();
|
|
97
|
-
}
|
|
98
|
-
catch { }
|
|
176
|
+
handleSocketLoss(conn);
|
|
99
177
|
return;
|
|
100
178
|
}
|
|
101
179
|
conn.pongTimer = unrefTimer(setTimeout(() => {
|
|
102
180
|
conn.pongTimer = null;
|
|
103
|
-
if (generation !== conn.generation
|
|
181
|
+
if (generation !== conn.generation)
|
|
104
182
|
return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
conn.ws?.close(1001, "pong timeout");
|
|
112
|
-
}
|
|
113
|
-
catch { }
|
|
114
|
-
}, PONG_TIMEOUT_MS));
|
|
115
|
-
}, HEARTBEAT_INTERVAL_MS));
|
|
183
|
+
conn.lastCloseCode = 1006;
|
|
184
|
+
conn.lastCloseReason = "pong timeout";
|
|
185
|
+
handleSocketLoss(conn);
|
|
186
|
+
}, transportConfig.pongTimeoutMs));
|
|
187
|
+
}, transportConfig.heartbeatIntervalMs));
|
|
116
188
|
}
|
|
117
189
|
function release(conn) {
|
|
118
190
|
conn.lastSessionID = conn.activeSessionID;
|
|
119
191
|
conn.lastAgent = conn.activeAgent;
|
|
192
|
+
finalizePending(conn.pending);
|
|
120
193
|
conn.pending = null;
|
|
121
194
|
conn.activeSessionID = undefined;
|
|
122
195
|
conn.activeAgent = undefined;
|
|
123
196
|
conn.busy = false;
|
|
124
197
|
conn.reconnectAttempts = 0;
|
|
125
|
-
conn.idleTimer
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
198
|
+
clearTimer(conn.idleTimer);
|
|
199
|
+
conn.idleTimer = null;
|
|
200
|
+
if (!conn.warm) {
|
|
201
|
+
conn.idleTimer = unrefTimer(setTimeout(() => {
|
|
202
|
+
conn.idleTimer = null;
|
|
203
|
+
if (!conn.busy) {
|
|
204
|
+
removeAndDrain(conn);
|
|
205
|
+
try {
|
|
206
|
+
conn.ws?.close(1000, "idle eviction");
|
|
207
|
+
}
|
|
208
|
+
catch { }
|
|
131
209
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}, IDLE_EVICT_MS));
|
|
210
|
+
}, transportConfig.idleEvictMs));
|
|
211
|
+
}
|
|
135
212
|
scheduleHeartbeat(conn);
|
|
136
213
|
}
|
|
137
214
|
function formatFailureMessage(conn, reason) {
|
|
@@ -139,13 +216,14 @@ function formatFailureMessage(conn, reason) {
|
|
|
139
216
|
const closeReason = JSON.stringify(conn.lastCloseReason ?? "");
|
|
140
217
|
const lastError = conn.lastErrorMessage ? JSON.stringify(conn.lastErrorMessage) : "none";
|
|
141
218
|
return (`WebSocket closed before response completed; cannot retry because ${reason}; ` +
|
|
142
|
-
`reconnectAttempts=${conn.reconnectAttempts}/${
|
|
219
|
+
`reconnectAttempts=${conn.reconnectAttempts}/${transportConfig.maxReconnectAttempts}; ` +
|
|
143
220
|
`closeCode=${code}; closeReason=${closeReason}; lastError=${lastError}`);
|
|
144
221
|
}
|
|
145
|
-
function fail(conn, error,
|
|
222
|
+
function fail(conn, error, shouldCloseSocket) {
|
|
146
223
|
const pending = conn.pending;
|
|
147
224
|
if (pending && !pending.done) {
|
|
148
225
|
pending.done = true;
|
|
226
|
+
finalizePending(pending);
|
|
149
227
|
try {
|
|
150
228
|
pending.controller.error(error);
|
|
151
229
|
}
|
|
@@ -155,17 +233,44 @@ function fail(conn, error, closeSocket) {
|
|
|
155
233
|
conn.busy = false;
|
|
156
234
|
conn.activeSessionID = undefined;
|
|
157
235
|
conn.activeAgent = undefined;
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
236
|
+
if (shouldCloseSocket) {
|
|
237
|
+
removeAndDrain(conn);
|
|
238
|
+
closeSocket(conn, 1000, "aborted");
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
drainQueue(conn.scopeKey);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function cacheResponseMetadata(pending, frame) {
|
|
245
|
+
const response = frame.response;
|
|
246
|
+
if (response && typeof response === "object") {
|
|
247
|
+
const value = response;
|
|
248
|
+
if (typeof value.id === "string")
|
|
249
|
+
pending.metadata.responseId = value.id;
|
|
168
250
|
}
|
|
251
|
+
if (typeof frame.response_id === "string")
|
|
252
|
+
pending.metadata.responseId = frame.response_id;
|
|
253
|
+
}
|
|
254
|
+
function isTerminalEvent(eventType) {
|
|
255
|
+
return ["response.completed", "response.failed", "response.incomplete"].includes(eventType);
|
|
256
|
+
}
|
|
257
|
+
function schedulePendingIdleTimeout(conn, reason) {
|
|
258
|
+
const pending = conn.pending;
|
|
259
|
+
if (!pending || pending.done)
|
|
260
|
+
return;
|
|
261
|
+
clearPendingTimer(pending);
|
|
262
|
+
const generation = conn.generation;
|
|
263
|
+
pending.idleTimer = unrefTimer(setTimeout(() => {
|
|
264
|
+
if (generation !== conn.generation)
|
|
265
|
+
return;
|
|
266
|
+
const current = conn.pending;
|
|
267
|
+
if (!current || current !== pending || current.done)
|
|
268
|
+
return;
|
|
269
|
+
current.idleTimer = null;
|
|
270
|
+
conn.lastCloseCode = 1006;
|
|
271
|
+
conn.lastCloseReason = "response idle timeout";
|
|
272
|
+
fail(conn, new Error(formatFailureMessage(conn, `response idle timeout after ${reason}`)), true);
|
|
273
|
+
}, transportConfig.responseIdleTimeoutMs));
|
|
169
274
|
}
|
|
170
275
|
function parseFrames(data) {
|
|
171
276
|
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
@@ -185,11 +290,56 @@ function parseFrames(data) {
|
|
|
185
290
|
}
|
|
186
291
|
return frames;
|
|
187
292
|
}
|
|
293
|
+
function numberFrom(value) {
|
|
294
|
+
if (typeof value === "number")
|
|
295
|
+
return value;
|
|
296
|
+
if (typeof value === "string" && value.trim()) {
|
|
297
|
+
const parsed = Number(value);
|
|
298
|
+
if (Number.isFinite(parsed))
|
|
299
|
+
return parsed;
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
function wrappedWebSocketError(frame) {
|
|
304
|
+
if (frame.type !== "error")
|
|
305
|
+
return undefined;
|
|
306
|
+
const error = frame.error && typeof frame.error === "object" ? frame.error : {};
|
|
307
|
+
const code = typeof error.code === "string" ? error.code : typeof frame.code === "string" ? frame.code : undefined;
|
|
308
|
+
const message = typeof error.message === "string" ? error.message : typeof frame.message === "string" ? frame.message : code;
|
|
309
|
+
if (code === "websocket_connection_limit_reached") {
|
|
310
|
+
return new Error(message ?? "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue.");
|
|
311
|
+
}
|
|
312
|
+
const status = numberFrom(frame.status) ?? numberFrom(frame.status_code) ?? numberFrom(error.status) ?? numberFrom(error.status_code);
|
|
313
|
+
if (status !== undefined && status >= 200 && status < 300)
|
|
314
|
+
return undefined;
|
|
315
|
+
return new Error(`OpenAI WebSocket error${status !== undefined ? ` ${status}` : ""}${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`);
|
|
316
|
+
}
|
|
317
|
+
function sendResponseProcessed(conn, pending, eventType) {
|
|
318
|
+
if (eventType !== "response.completed")
|
|
319
|
+
return;
|
|
320
|
+
if (process.env[RESPONSE_PROCESSED_ENV] !== "1")
|
|
321
|
+
return;
|
|
322
|
+
if (pending.processedAckSent || !pending.metadata.responseId)
|
|
323
|
+
return;
|
|
324
|
+
try {
|
|
325
|
+
conn.ws?.send(JSON.stringify({ type: "response.processed", response_id: pending.metadata.responseId }));
|
|
326
|
+
pending.processedAckSent = true;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
conn.lastErrorMessage = error instanceof Error ? error.message : String(error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
188
332
|
function enqueueSSE(conn, frame) {
|
|
189
333
|
const pending = conn.pending;
|
|
190
334
|
if (!pending || pending.done)
|
|
191
335
|
return;
|
|
192
336
|
const eventType = typeof frame.type === "string" ? frame.type : "message";
|
|
337
|
+
cacheResponseMetadata(pending, frame);
|
|
338
|
+
const mappedError = wrappedWebSocketError(frame);
|
|
339
|
+
if (mappedError) {
|
|
340
|
+
fail(conn, mappedError, true);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
193
343
|
const encoded = new TextEncoder().encode(`event: ${eventType}\ndata: ${JSON.stringify(frame)}\n\n`);
|
|
194
344
|
try {
|
|
195
345
|
pending.controller.enqueue(encoded);
|
|
@@ -197,14 +347,21 @@ function enqueueSSE(conn, frame) {
|
|
|
197
347
|
catch {
|
|
198
348
|
return;
|
|
199
349
|
}
|
|
200
|
-
|
|
201
|
-
|
|
350
|
+
if (!isTerminalEvent(eventType))
|
|
351
|
+
schedulePendingIdleTimeout(conn, eventType);
|
|
352
|
+
if (isTerminalEvent(eventType)) {
|
|
353
|
+
finalizePending(pending);
|
|
202
354
|
pending.done = true;
|
|
355
|
+
sendResponseProcessed(conn, pending, eventType);
|
|
203
356
|
try {
|
|
204
357
|
pending.controller.close();
|
|
205
358
|
}
|
|
206
359
|
catch { }
|
|
207
|
-
|
|
360
|
+
if (pending.metadata.responseId) {
|
|
361
|
+
conn.lastResponseID = pending.metadata.responseId;
|
|
362
|
+
lastResponseIDByContext.set(conn.contextKey, pending.metadata.responseId);
|
|
363
|
+
}
|
|
364
|
+
terminalRelease(conn);
|
|
208
365
|
}
|
|
209
366
|
}
|
|
210
367
|
export function sendPending(conn) {
|
|
@@ -215,7 +372,10 @@ export function sendPending(conn) {
|
|
|
215
372
|
if (!ws || ws.readyState !== readyState.OPEN)
|
|
216
373
|
return false;
|
|
217
374
|
try {
|
|
218
|
-
|
|
375
|
+
const body = { ...pending.body };
|
|
376
|
+
if (body.previous_response_id === undefined && conn.lastResponseID)
|
|
377
|
+
body.previous_response_id = conn.lastResponseID;
|
|
378
|
+
ws.send(JSON.stringify({ ...body, type: "response.create" }));
|
|
219
379
|
}
|
|
220
380
|
catch (error) {
|
|
221
381
|
conn.lastErrorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -223,6 +383,7 @@ export function sendPending(conn) {
|
|
|
223
383
|
return false;
|
|
224
384
|
}
|
|
225
385
|
pending.sent = true;
|
|
386
|
+
schedulePendingIdleTimeout(conn, "response.create");
|
|
226
387
|
return true;
|
|
227
388
|
}
|
|
228
389
|
function normalizeCloseReason(reason) {
|
|
@@ -238,17 +399,17 @@ function normalizeCloseReason(reason) {
|
|
|
238
399
|
}
|
|
239
400
|
function connect(conn) {
|
|
240
401
|
clearHeartbeat(conn);
|
|
241
|
-
const ws = new WebSocketImpl(conn.wsUrl, { headers: conn.headers });
|
|
242
402
|
const generation = ++conn.generation;
|
|
243
|
-
conn.ws = ws;
|
|
244
403
|
clearTimer(conn.retryTimer);
|
|
245
404
|
conn.retryTimer = null;
|
|
246
405
|
clearTimer(conn.connectTimer);
|
|
247
406
|
const connectTimer = setTimeout(() => {
|
|
248
|
-
if (generation !== conn.generation || ws
|
|
407
|
+
if (generation !== conn.generation || conn.ws?.readyState === readyState.OPEN)
|
|
249
408
|
return;
|
|
250
|
-
|
|
251
|
-
|
|
409
|
+
conn.lastCloseCode = 1006;
|
|
410
|
+
conn.lastCloseReason = "connection timed out";
|
|
411
|
+
handleSocketLoss(conn);
|
|
412
|
+
}, transportConfig.connectTimeoutMs);
|
|
252
413
|
conn.connectTimer = unrefTimer(connectTimer);
|
|
253
414
|
const handleOpen = () => {
|
|
254
415
|
if (generation !== conn.generation)
|
|
@@ -256,12 +417,29 @@ function connect(conn) {
|
|
|
256
417
|
clearTimer(conn.connectTimer);
|
|
257
418
|
conn.connectTimer = null;
|
|
258
419
|
conn.lastActivityAt = Date.now();
|
|
420
|
+
scheduleHeartbeat(conn);
|
|
259
421
|
sendPending(conn);
|
|
260
422
|
};
|
|
261
|
-
const
|
|
423
|
+
const handleUpgrade = (response) => {
|
|
424
|
+
if (generation !== conn.generation)
|
|
425
|
+
return;
|
|
426
|
+
const turnState = headerValue(response, X_CODEX_TURN_STATE_HEADER);
|
|
427
|
+
if (turnState) {
|
|
428
|
+
conn.turnState = turnState;
|
|
429
|
+
turnStateByContext.set(conn.contextKey, turnState);
|
|
430
|
+
}
|
|
431
|
+
conn.serverReasoningIncluded = headerValue(response, X_REASONING_INCLUDED_HEADER) !== undefined;
|
|
432
|
+
conn.modelsEtag = headerValue(response, X_MODELS_ETAG_HEADER);
|
|
433
|
+
conn.serverModel = headerValue(response, OPENAI_MODEL_HEADER);
|
|
434
|
+
};
|
|
435
|
+
const handleMessage = (data, isBinary) => {
|
|
262
436
|
if (generation !== conn.generation)
|
|
263
437
|
return;
|
|
264
438
|
conn.lastActivityAt = Date.now();
|
|
439
|
+
if (isBinary) {
|
|
440
|
+
fail(conn, new Error("unexpected binary websocket event"), true);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
265
443
|
for (const frame of parseFrames(data))
|
|
266
444
|
enqueueSSE(conn, frame);
|
|
267
445
|
};
|
|
@@ -271,8 +449,7 @@ function connect(conn) {
|
|
|
271
449
|
conn.lastActivityAt = Date.now();
|
|
272
450
|
clearTimer(conn.pongTimer);
|
|
273
451
|
conn.pongTimer = null;
|
|
274
|
-
|
|
275
|
-
scheduleHeartbeat(conn);
|
|
452
|
+
scheduleHeartbeat(conn);
|
|
276
453
|
};
|
|
277
454
|
const handleError = (error) => {
|
|
278
455
|
if (generation !== conn.generation)
|
|
@@ -291,18 +468,31 @@ function connect(conn) {
|
|
|
291
468
|
conn.lastCloseReason = normalizedReason;
|
|
292
469
|
handleSocketLoss(conn);
|
|
293
470
|
};
|
|
471
|
+
const ws = new WebSocketImpl(conn.wsUrl, {
|
|
472
|
+
headers: conn.headers,
|
|
473
|
+
perMessageDeflate: true,
|
|
474
|
+
finishRequest(request) {
|
|
475
|
+
request.on?.("upgrade", handleUpgrade);
|
|
476
|
+
request.end?.();
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
conn.ws = ws;
|
|
294
480
|
on(ws, "open", handleOpen);
|
|
481
|
+
on(ws, "upgrade", handleUpgrade);
|
|
295
482
|
on(ws, "message", handleMessage);
|
|
296
483
|
on(ws, "pong", handlePong);
|
|
297
484
|
on(ws, "error", handleError);
|
|
298
485
|
on(ws, "close", handleClose);
|
|
299
486
|
conn.detach = () => {
|
|
300
487
|
off(ws, "open", handleOpen);
|
|
488
|
+
off(ws, "upgrade", handleUpgrade);
|
|
301
489
|
off(ws, "message", handleMessage);
|
|
302
490
|
off(ws, "pong", handlePong);
|
|
303
491
|
off(ws, "error", handleError);
|
|
304
492
|
off(ws, "close", handleClose);
|
|
305
493
|
};
|
|
494
|
+
if (ws.readyState === readyState.OPEN)
|
|
495
|
+
queueMicrotask(handleOpen);
|
|
306
496
|
}
|
|
307
497
|
function handleSocketLoss(conn) {
|
|
308
498
|
clearTimer(conn.connectTimer);
|
|
@@ -310,17 +500,18 @@ function handleSocketLoss(conn) {
|
|
|
310
500
|
clearHeartbeat(conn);
|
|
311
501
|
const pending = conn.pending;
|
|
312
502
|
if (!pending || pending.done) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
catch { }
|
|
503
|
+
finalizePending(pending);
|
|
504
|
+
removeAndDrain(conn);
|
|
505
|
+
closeSocket(conn, 1000, "socket lost");
|
|
318
506
|
return;
|
|
319
507
|
}
|
|
320
|
-
|
|
508
|
+
if (pending.sent) {
|
|
509
|
+
fail(conn, new Error(formatFailureMessage(conn, "response.create was already sent")), true);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const canRetry = conn.reconnectAttempts < transportConfig.maxReconnectAttempts;
|
|
321
513
|
if (canRetry) {
|
|
322
514
|
conn.reconnectAttempts++;
|
|
323
|
-
pending.sent = false;
|
|
324
515
|
const priorWs = conn.ws;
|
|
325
516
|
detach(conn);
|
|
326
517
|
try {
|
|
@@ -334,27 +525,32 @@ function handleSocketLoss(conn) {
|
|
|
334
525
|
conn.ws = null;
|
|
335
526
|
conn.retryTimer = unrefTimer(setTimeout(() => {
|
|
336
527
|
conn.retryTimer = null;
|
|
337
|
-
if (!connectionPool.includes(conn) || !conn.pending || conn.pending.done || conn.pending.
|
|
528
|
+
if (!connectionPool.includes(conn) || !conn.pending || conn.pending.done || conn.pending.sent)
|
|
338
529
|
return;
|
|
339
530
|
connect(conn);
|
|
340
|
-
},
|
|
531
|
+
}, retryDelay(conn.reconnectAttempts)));
|
|
341
532
|
return;
|
|
342
533
|
}
|
|
343
|
-
|
|
344
|
-
? "stream already forwarded data"
|
|
345
|
-
: "retry limit reached before any frame was forwarded";
|
|
346
|
-
fail(conn, new Error(formatFailureMessage(conn, reason)), true);
|
|
534
|
+
fail(conn, new Error(formatFailureMessage(conn, "retry limit reached before response.create was sent")), true);
|
|
347
535
|
}
|
|
348
|
-
function create(wsUrl, headers) {
|
|
536
|
+
function create(wsUrl, headers, context = {}, warm = false) {
|
|
537
|
+
const hash = authScopeHash(headers);
|
|
538
|
+
const key = contextScopeKey(wsUrl, headers, context);
|
|
349
539
|
const conn = {
|
|
540
|
+
id: `ws-${nextConnectionID++}`,
|
|
350
541
|
ws: null,
|
|
351
542
|
wsUrl,
|
|
352
|
-
headers,
|
|
353
|
-
scopeKey:
|
|
543
|
+
headers: headersForConnection(wsUrl, headers, context, key),
|
|
544
|
+
scopeKey: key,
|
|
545
|
+
scopeHash: hash,
|
|
546
|
+
contextKey: key,
|
|
354
547
|
busy: false,
|
|
548
|
+
warm,
|
|
355
549
|
pending: null,
|
|
550
|
+
lastResponseID: lastResponseIDByContext.get(key),
|
|
356
551
|
generation: 0,
|
|
357
552
|
reconnectAttempts: 0,
|
|
553
|
+
createdAt: Date.now(),
|
|
358
554
|
lastActivityAt: Date.now(),
|
|
359
555
|
idleTimer: null,
|
|
360
556
|
connectTimer: null,
|
|
@@ -372,9 +568,11 @@ function isReusable(conn, key, context, now) {
|
|
|
372
568
|
return false;
|
|
373
569
|
if (conn.scopeKey !== key)
|
|
374
570
|
return false;
|
|
375
|
-
if (conn.ws?.readyState !== readyState.OPEN)
|
|
571
|
+
if (conn.ws?.readyState !== readyState.OPEN && conn.ws?.readyState !== readyState.CONNECTING)
|
|
376
572
|
return false;
|
|
377
|
-
if (now - conn.lastActivityAt >
|
|
573
|
+
if (now - conn.lastActivityAt > transportConfig.staleReuseMs)
|
|
574
|
+
return false;
|
|
575
|
+
if (now - conn.createdAt > transportConfig.connectionMaxAgeMs)
|
|
378
576
|
return false;
|
|
379
577
|
if (context.sessionID && conn.lastSessionID && conn.lastSessionID !== context.sessionID)
|
|
380
578
|
return false;
|
|
@@ -382,13 +580,16 @@ function isReusable(conn, key, context, now) {
|
|
|
382
580
|
return false;
|
|
383
581
|
return true;
|
|
384
582
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
583
|
+
function activeCount(key) {
|
|
584
|
+
return connectionPool.filter((conn) => conn.scopeKey === key && (conn.busy || conn.ws?.readyState === readyState.CONNECTING)).length;
|
|
585
|
+
}
|
|
586
|
+
function cleanupStaleConnections(key, now) {
|
|
388
587
|
for (const candidate of [...connectionPool]) {
|
|
389
588
|
if (candidate.busy || candidate.scopeKey !== key)
|
|
390
589
|
continue;
|
|
391
|
-
if (candidate.ws?.readyState === readyState.OPEN &&
|
|
590
|
+
if (candidate.ws?.readyState === readyState.OPEN &&
|
|
591
|
+
(now - candidate.lastActivityAt > transportConfig.staleReuseMs ||
|
|
592
|
+
now - candidate.createdAt > transportConfig.connectionMaxAgeMs)) {
|
|
392
593
|
remove(candidate);
|
|
393
594
|
try {
|
|
394
595
|
candidate.ws?.close(1000, "stale idle");
|
|
@@ -396,34 +597,134 @@ export function acquireConnection(wsUrl, headers, context) {
|
|
|
396
597
|
catch { }
|
|
397
598
|
}
|
|
398
599
|
}
|
|
600
|
+
}
|
|
601
|
+
function reserveConnection(wsUrl, headers, context) {
|
|
602
|
+
const key = contextScopeKey(wsUrl, headers, context);
|
|
603
|
+
const now = Date.now();
|
|
604
|
+
cleanupStaleConnections(key, now);
|
|
399
605
|
const reusable = connectionPool.find((conn) => isReusable(conn, key, context, now));
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
606
|
+
if (reusable) {
|
|
607
|
+
clearTimer(reusable.idleTimer);
|
|
608
|
+
reusable.idleTimer = null;
|
|
609
|
+
clearHeartbeat(reusable);
|
|
610
|
+
reusable.busy = true;
|
|
611
|
+
reusable.warm = false;
|
|
612
|
+
return reusable;
|
|
613
|
+
}
|
|
614
|
+
if (activeCount(key) >= transportConfig.maxConnectionsPerScope)
|
|
615
|
+
return null;
|
|
616
|
+
const conn = create(wsUrl, headers, context);
|
|
404
617
|
conn.busy = true;
|
|
405
618
|
return conn;
|
|
406
619
|
}
|
|
407
|
-
|
|
620
|
+
function queueAcquire(wsUrl, headers, context, signal) {
|
|
621
|
+
const key = contextScopeKey(wsUrl, headers, context);
|
|
622
|
+
return new Promise((resolve, reject) => {
|
|
623
|
+
const entry = {
|
|
624
|
+
wsUrl,
|
|
625
|
+
headers,
|
|
626
|
+
context,
|
|
627
|
+
resolve,
|
|
628
|
+
reject,
|
|
629
|
+
signal,
|
|
630
|
+
onAbort: () => {
|
|
631
|
+
const queue = acquisitionQueues.get(key);
|
|
632
|
+
if (queue) {
|
|
633
|
+
const index = queue.indexOf(entry);
|
|
634
|
+
if (index >= 0)
|
|
635
|
+
queue.splice(index, 1);
|
|
636
|
+
if (queue.length === 0)
|
|
637
|
+
acquisitionQueues.delete(key);
|
|
638
|
+
}
|
|
639
|
+
reject(signal?.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
if (signal?.aborted) {
|
|
643
|
+
entry.onAbort();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
signal?.addEventListener("abort", entry.onAbort, { once: true });
|
|
647
|
+
const queue = acquisitionQueues.get(key) ?? [];
|
|
648
|
+
queue.push(entry);
|
|
649
|
+
acquisitionQueues.set(key, queue);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function drainQueue(key) {
|
|
653
|
+
const queue = acquisitionQueues.get(key);
|
|
654
|
+
if (!queue?.length)
|
|
655
|
+
return;
|
|
656
|
+
while (queue.length) {
|
|
657
|
+
const entry = queue[0];
|
|
658
|
+
if (entry.signal?.aborted) {
|
|
659
|
+
queue.shift();
|
|
660
|
+
entry.onAbort();
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const conn = reserveConnection(entry.wsUrl, entry.headers, entry.context);
|
|
664
|
+
if (!conn)
|
|
665
|
+
break;
|
|
666
|
+
queue.shift();
|
|
667
|
+
entry.signal?.removeEventListener("abort", entry.onAbort);
|
|
668
|
+
entry.resolve(conn);
|
|
669
|
+
}
|
|
670
|
+
if (queue.length === 0)
|
|
671
|
+
acquisitionQueues.delete(key);
|
|
672
|
+
}
|
|
673
|
+
export function acquireConnection(wsUrl, headers, context, signal) {
|
|
674
|
+
const conn = reserveConnection(wsUrl, headers, context);
|
|
675
|
+
if (conn)
|
|
676
|
+
return conn;
|
|
677
|
+
return queueAcquire(wsUrl, headers, context, signal);
|
|
678
|
+
}
|
|
679
|
+
export function ensureWarmConnection(wsUrl, headers) {
|
|
680
|
+
const key = contextScopeKey(wsUrl, headers, {});
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
cleanupStaleConnections(key, now);
|
|
683
|
+
const existing = connectionPool.find((conn) => conn.scopeKey === key && !conn.busy && conn.ws?.readyState === readyState.OPEN);
|
|
684
|
+
if (existing) {
|
|
685
|
+
existing.warm = true;
|
|
686
|
+
clearTimer(existing.idleTimer);
|
|
687
|
+
existing.idleTimer = null;
|
|
688
|
+
scheduleHeartbeat(existing);
|
|
689
|
+
return existing;
|
|
690
|
+
}
|
|
691
|
+
if (activeCount(key) >= transportConfig.maxConnectionsPerScope)
|
|
692
|
+
return undefined;
|
|
693
|
+
return create(wsUrl, headers, {}, true);
|
|
694
|
+
}
|
|
695
|
+
export function closeConnections(predicate = () => true, message = "Session disposed") {
|
|
408
696
|
for (const conn of [...connectionPool]) {
|
|
409
697
|
if (!predicate(conn))
|
|
410
698
|
continue;
|
|
411
|
-
fail(conn, new Error(
|
|
699
|
+
fail(conn, new Error(message), true);
|
|
412
700
|
}
|
|
413
701
|
}
|
|
414
702
|
export function resetPoolForTesting() {
|
|
415
703
|
for (const conn of [...connectionPool]) {
|
|
704
|
+
try {
|
|
705
|
+
conn.ws?.terminate?.();
|
|
706
|
+
}
|
|
707
|
+
catch { }
|
|
416
708
|
try {
|
|
417
709
|
conn.ws?.close(1000, "test reset");
|
|
418
710
|
}
|
|
419
711
|
catch { }
|
|
420
712
|
remove(conn);
|
|
421
713
|
}
|
|
714
|
+
for (const queue of acquisitionQueues.values()) {
|
|
715
|
+
for (const entry of queue) {
|
|
716
|
+
entry.signal?.removeEventListener("abort", entry.onAbort);
|
|
717
|
+
entry.reject(new Error("Pool reset"));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
acquisitionQueues.clear();
|
|
721
|
+
turnStateByContext.clear();
|
|
722
|
+
lastResponseIDByContext.clear();
|
|
422
723
|
}
|
|
423
724
|
export function setWebSocketConstructorForTesting(ctor) {
|
|
424
725
|
WebSocketImpl = ctor;
|
|
425
726
|
}
|
|
426
727
|
export function resetWebSocketConstructorForTesting() {
|
|
427
|
-
WebSocketImpl =
|
|
728
|
+
WebSocketImpl = loadDefaultWebSocketConstructor();
|
|
428
729
|
}
|
|
429
730
|
//# sourceMappingURL=pool.js.map
|