@opengeni/codex 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +589 -522
- package/dist/index.js.map +1 -1
- package/dist/request-context.d.ts +3 -0
- package/dist/responses-websocket.d.ts +25 -0
- package/package.json +3 -3
- package/src/fetch.ts +6 -0
- package/src/request-context.ts +3 -0
- package/src/responses-websocket.ts +113 -13
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { CodexRequestTransport } from "./responses-websocket.js";
|
|
2
3
|
export type CodexTokenSnapshot = {
|
|
3
4
|
accessToken: string;
|
|
4
5
|
chatgptAccountId: string | null;
|
|
@@ -50,6 +51,8 @@ export type CodexModelRequestEvent = {
|
|
|
50
51
|
willRetry?: boolean;
|
|
51
52
|
/** Only true for a successful response with substantive assistant/tool output. */
|
|
52
53
|
meaningfulOutput?: boolean;
|
|
54
|
+
/** How the request was carried; reported once its response headers arrive. */
|
|
55
|
+
transport?: CodexRequestTransport;
|
|
53
56
|
};
|
|
54
57
|
export type CodexRequestOpaqueArtifacts = {
|
|
55
58
|
requestId: string;
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import type { FetchLike } from "./fetch.js";
|
|
2
2
|
export declare const CODEX_RESPONSES_WEBSOCKET_BETA = "responses_websockets=2026-02-06";
|
|
3
|
+
/**
|
|
4
|
+
* Response header naming how this transport carried a Responses request, so
|
|
5
|
+
* the request audit can report it per request. Read with
|
|
6
|
+
* {@link parseCodexRequestTransport}.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CODEX_TRANSPORT_HEADER = "x-opengeni-codex-transport";
|
|
9
|
+
/**
|
|
10
|
+
* How one Codex Responses request was carried. A WebSocket request either
|
|
11
|
+
* opened its connection or reused a warm one. An HTTP request names why the
|
|
12
|
+
* socket did not carry it: the request was not a streaming Responses POST, the
|
|
13
|
+
* conversation's connection was busy with another request, a recent handshake
|
|
14
|
+
* failed (backoff), or this handshake failed. `unavailable` means no socket
|
|
15
|
+
* transport was in the path at all (disabled, or a runtime without
|
|
16
|
+
* header-capable WebSockets).
|
|
17
|
+
*/
|
|
18
|
+
export type CodexRequestTransport = {
|
|
19
|
+
kind: "websocket";
|
|
20
|
+
connection: "new" | "reused";
|
|
21
|
+
} | {
|
|
22
|
+
kind: "http";
|
|
23
|
+
reason: "not_eligible" | "connection_busy" | "handshake_backoff" | "handshake_failed" | "unavailable";
|
|
24
|
+
};
|
|
25
|
+
export declare function formatCodexRequestTransport(transport: CodexRequestTransport): string;
|
|
26
|
+
/** The transport a response was carried over; null when it does not say. */
|
|
27
|
+
export declare function parseCodexRequestTransport(value: string | null | undefined): CodexRequestTransport | null;
|
|
3
28
|
/** The subset of the WHATWG/Bun WebSocket this transport uses. */
|
|
4
29
|
export type CodexWebSocketLike = {
|
|
5
30
|
readonly readyState: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/codex",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@opengeni/contracts": "3.
|
|
45
|
-
"@opengeni/network": "3.
|
|
44
|
+
"@opengeni/contracts": "3.4.0",
|
|
45
|
+
"@opengeni/network": "3.4.0",
|
|
46
46
|
"zod": "^4.2.1"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
package/src/fetch.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { hasMeaningfulCodexOutput } from "./meaningful-output";
|
|
|
15
15
|
import { CODEX_ORIGINATOR } from "./constants";
|
|
16
16
|
import { normalizeCodexRequestBody } from "./normalize";
|
|
17
17
|
import { opaqueProviderArtifactFingerprints } from "./opaque-artifact";
|
|
18
|
+
import { CODEX_TRANSPORT_HEADER, parseCodexRequestTransport } from "./responses-websocket";
|
|
18
19
|
import { CODEX_FIVE_HOUR_WINDOW_SECONDS, CODEX_WEEKLY_WINDOW_SECONDS } from "./usage-normalize";
|
|
19
20
|
import {
|
|
20
21
|
codexRequestStorage,
|
|
@@ -882,6 +883,11 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
882
883
|
responseObserved: true,
|
|
883
884
|
status: res.status,
|
|
884
885
|
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
886
|
+
// A response the socket transport did not label never passed through it.
|
|
887
|
+
transport: parseCodexRequestTransport(res.headers.get(CODEX_TRANSPORT_HEADER)) ?? {
|
|
888
|
+
kind: "http",
|
|
889
|
+
reason: "unavailable",
|
|
890
|
+
},
|
|
885
891
|
});
|
|
886
892
|
res = await observedResponse(res, audit, nextInit.signal, semanticTerminal);
|
|
887
893
|
} catch (error) {
|
package/src/request-context.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// time — one cached client, correct per-workspace token, no cross-tenant leak.
|
|
7
7
|
|
|
8
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
|
+
import type { CodexRequestTransport } from "./responses-websocket";
|
|
9
10
|
|
|
10
11
|
export type CodexTokenSnapshot = {
|
|
11
12
|
accessToken: string;
|
|
@@ -62,6 +63,8 @@ export type CodexModelRequestEvent = {
|
|
|
62
63
|
willRetry?: boolean;
|
|
63
64
|
/** Only true for a successful response with substantive assistant/tool output. */
|
|
64
65
|
meaningfulOutput?: boolean;
|
|
66
|
+
/** How the request was carried; reported once its response headers arrive. */
|
|
67
|
+
transport?: CodexRequestTransport;
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
export type CodexRequestOpaqueArtifacts = {
|
|
@@ -15,6 +15,96 @@ import type { FetchLike } from "./fetch";
|
|
|
15
15
|
|
|
16
16
|
export const CODEX_RESPONSES_WEBSOCKET_BETA = "responses_websockets=2026-02-06";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Response header naming how this transport carried a Responses request, so
|
|
20
|
+
* the request audit can report it per request. Read with
|
|
21
|
+
* {@link parseCodexRequestTransport}.
|
|
22
|
+
*/
|
|
23
|
+
export const CODEX_TRANSPORT_HEADER = "x-opengeni-codex-transport";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How one Codex Responses request was carried. A WebSocket request either
|
|
27
|
+
* opened its connection or reused a warm one. An HTTP request names why the
|
|
28
|
+
* socket did not carry it: the request was not a streaming Responses POST, the
|
|
29
|
+
* conversation's connection was busy with another request, a recent handshake
|
|
30
|
+
* failed (backoff), or this handshake failed. `unavailable` means no socket
|
|
31
|
+
* transport was in the path at all (disabled, or a runtime without
|
|
32
|
+
* header-capable WebSockets).
|
|
33
|
+
*/
|
|
34
|
+
export type CodexRequestTransport =
|
|
35
|
+
| { kind: "websocket"; connection: "new" | "reused" }
|
|
36
|
+
| {
|
|
37
|
+
kind: "http";
|
|
38
|
+
reason:
|
|
39
|
+
| "not_eligible"
|
|
40
|
+
| "connection_busy"
|
|
41
|
+
| "handshake_backoff"
|
|
42
|
+
| "handshake_failed"
|
|
43
|
+
| "unavailable";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const HTTP_REASONS = new Set([
|
|
47
|
+
"not_eligible",
|
|
48
|
+
"connection_busy",
|
|
49
|
+
"handshake_backoff",
|
|
50
|
+
"handshake_failed",
|
|
51
|
+
"unavailable",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
export function formatCodexRequestTransport(transport: CodexRequestTransport): string {
|
|
55
|
+
return transport.kind === "websocket"
|
|
56
|
+
? `websocket; connection=${transport.connection}`
|
|
57
|
+
: `http; reason=${transport.reason}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The transport a response was carried over; null when it does not say. */
|
|
61
|
+
export function parseCodexRequestTransport(
|
|
62
|
+
value: string | null | undefined,
|
|
63
|
+
): CodexRequestTransport | null {
|
|
64
|
+
if (!value) return null;
|
|
65
|
+
const [kind, ...parameters] = value.split(";").map((part) => part.trim());
|
|
66
|
+
const fields = new Map(
|
|
67
|
+
parameters.map((parameter) => {
|
|
68
|
+
const index = parameter.indexOf("=");
|
|
69
|
+
return [parameter.slice(0, index), parameter.slice(index + 1)] as const;
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
if (kind === "websocket") {
|
|
73
|
+
const connection = fields.get("connection");
|
|
74
|
+
return connection === "new" || connection === "reused"
|
|
75
|
+
? { kind: "websocket", connection }
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
if (kind === "http") {
|
|
79
|
+
const reason = fields.get("reason");
|
|
80
|
+
return reason && HTTP_REASONS.has(reason)
|
|
81
|
+
? {
|
|
82
|
+
kind: "http",
|
|
83
|
+
reason: reason as Extract<CodexRequestTransport, { kind: "http" }>["reason"],
|
|
84
|
+
}
|
|
85
|
+
: null;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The same response, labelled with how it was carried. */
|
|
91
|
+
function withTransport(response: Response, transport: CodexRequestTransport): Response {
|
|
92
|
+
const value = formatCodexRequestTransport(transport);
|
|
93
|
+
try {
|
|
94
|
+
response.headers.set(CODEX_TRANSPORT_HEADER, value);
|
|
95
|
+
return response;
|
|
96
|
+
} catch {
|
|
97
|
+
// A fetch response's headers can be immutable; carry the same body.
|
|
98
|
+
const headers = new Headers(response.headers);
|
|
99
|
+
headers.set(CODEX_TRANSPORT_HEADER, value);
|
|
100
|
+
return new Response(response.body, {
|
|
101
|
+
status: response.status,
|
|
102
|
+
statusText: response.statusText,
|
|
103
|
+
headers,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
18
108
|
/** The subset of the WHATWG/Bun WebSocket this transport uses. */
|
|
19
109
|
export type CodexWebSocketLike = {
|
|
20
110
|
readonly readyState: number;
|
|
@@ -84,6 +174,8 @@ type Connection = {
|
|
|
84
174
|
openedAt: number;
|
|
85
175
|
busy: boolean;
|
|
86
176
|
closed: boolean;
|
|
177
|
+
/** Requests this connection has started carrying. */
|
|
178
|
+
requests: number;
|
|
87
179
|
idleTimer: ReturnType<typeof setTimeout> | null;
|
|
88
180
|
/** Receives every message while a request is in flight. */
|
|
89
181
|
onEvent: ((event: Record<string, unknown>) => void) | null;
|
|
@@ -213,6 +305,7 @@ export function codexResponsesTransport(
|
|
|
213
305
|
openedAt: now(),
|
|
214
306
|
busy: false,
|
|
215
307
|
closed: false,
|
|
308
|
+
requests: 0,
|
|
216
309
|
idleTimer: null,
|
|
217
310
|
onEvent: null,
|
|
218
311
|
onClose: null,
|
|
@@ -272,23 +365,22 @@ export function codexResponsesTransport(
|
|
|
272
365
|
const url = requestUrl(input);
|
|
273
366
|
const method = (init?.method ?? "GET").toUpperCase();
|
|
274
367
|
const body = init?.body;
|
|
275
|
-
if (
|
|
276
|
-
!url ||
|
|
277
|
-
method !== "POST" ||
|
|
278
|
-
typeof body !== "string" ||
|
|
279
|
-
!/^https:\/\/[^/]+\/.*\/codex\/responses(?:\?|$)/.test(url)
|
|
280
|
-
) {
|
|
368
|
+
if (!url || method !== "POST" || !/^https:\/\/[^/]+\/.*\/codex\/responses(?:\?|$)/.test(url)) {
|
|
281
369
|
return await http(input, init);
|
|
282
370
|
}
|
|
371
|
+
const viaHttp = async (
|
|
372
|
+
reason: Extract<CodexRequestTransport, { kind: "http" }>["reason"],
|
|
373
|
+
): Promise<Response> => withTransport(await http(input, init), { kind: "http", reason });
|
|
374
|
+
if (typeof body !== "string") return await viaHttp("not_eligible");
|
|
283
375
|
let payload: Record<string, unknown>;
|
|
284
376
|
try {
|
|
285
377
|
const parsed = JSON.parse(body) as unknown;
|
|
286
378
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
|
|
287
379
|
payload = parsed as Record<string, unknown>;
|
|
288
380
|
} catch {
|
|
289
|
-
return await
|
|
381
|
+
return await viaHttp("not_eligible");
|
|
290
382
|
}
|
|
291
|
-
if (payload.stream !== true) return await
|
|
383
|
+
if (payload.stream !== true) return await viaHttp("not_eligible");
|
|
292
384
|
|
|
293
385
|
const handshake: Record<string, string> = {};
|
|
294
386
|
for (const [name, value] of new Headers(init?.headers).entries()) {
|
|
@@ -297,7 +389,7 @@ export function codexResponsesTransport(
|
|
|
297
389
|
handshake["openai-beta"] = CODEX_RESPONSES_WEBSOCKET_BETA;
|
|
298
390
|
const wsUrl = url.replace(/^https:/, "wss:");
|
|
299
391
|
const key = JSON.stringify([wsUrl, Object.entries(handshake).sort()]);
|
|
300
|
-
if ((backoffUntil.get(key) ?? 0) > now()) return await
|
|
392
|
+
if ((backoffUntil.get(key) ?? 0) > now()) return await viaHttp("handshake_backoff");
|
|
301
393
|
|
|
302
394
|
const signal = init?.signal ?? null;
|
|
303
395
|
signal?.throwIfAborted();
|
|
@@ -306,13 +398,13 @@ export function codexResponsesTransport(
|
|
|
306
398
|
if (!connection.busy) retire(connection);
|
|
307
399
|
connection = undefined;
|
|
308
400
|
}
|
|
309
|
-
if (connection?.busy) return await
|
|
401
|
+
if (connection?.busy) return await viaHttp("connection_busy");
|
|
310
402
|
if (!connection) {
|
|
311
403
|
try {
|
|
312
404
|
connection = await connect(wsUrl, key, handshake);
|
|
313
405
|
} catch {
|
|
314
406
|
backoffUntil.set(key, now() + handshakeBackoffMs);
|
|
315
|
-
return await
|
|
407
|
+
return await viaHttp("handshake_failed");
|
|
316
408
|
}
|
|
317
409
|
if (pool.has(key)) {
|
|
318
410
|
// A concurrent request of the same conversation opened one first.
|
|
@@ -326,15 +418,23 @@ export function codexResponsesTransport(
|
|
|
326
418
|
} else {
|
|
327
419
|
pool.set(key, connection);
|
|
328
420
|
}
|
|
329
|
-
if (connection.busy) return await
|
|
421
|
+
if (connection.busy) return await viaHttp("connection_busy");
|
|
330
422
|
}
|
|
331
423
|
const active = connection;
|
|
424
|
+
const transport: CodexRequestTransport = {
|
|
425
|
+
kind: "websocket",
|
|
426
|
+
connection: active.requests === 0 ? "new" : "reused",
|
|
427
|
+
};
|
|
428
|
+
active.requests += 1;
|
|
332
429
|
active.busy = true;
|
|
333
430
|
if (active.idleTimer) clearTimeout(active.idleTimer);
|
|
334
431
|
active.idleTimer = null;
|
|
335
432
|
|
|
336
433
|
return await new Promise<Response>((resolve, reject) => {
|
|
337
|
-
const headers = new Headers({
|
|
434
|
+
const headers = new Headers({
|
|
435
|
+
"content-type": "text/event-stream",
|
|
436
|
+
[CODEX_TRANSPORT_HEADER]: formatCodexRequestTransport(transport),
|
|
437
|
+
});
|
|
338
438
|
let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
339
439
|
let started = false;
|
|
340
440
|
let finished = false;
|