@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.
@@ -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.0",
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.3.0",
45
- "@opengeni/network": "3.3.0",
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) {
@@ -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 http(input, init);
381
+ return await viaHttp("not_eligible");
290
382
  }
291
- if (payload.stream !== true) return await http(input, init);
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 http(input, init);
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 http(input, init);
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 http(input, init);
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 http(input, init);
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({ "content-type": "text/event-stream" });
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;