codex-multi-auth 2.16.0 → 2.17.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.
Files changed (256) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +22 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +8 -3
  5. package/dist/lib/account-policy.d.ts +4 -0
  6. package/dist/lib/account-policy.d.ts.map +1 -1
  7. package/dist/lib/account-policy.js +8 -0
  8. package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
  9. package/dist/lib/accounts/rate-limits.js +2 -0
  10. package/dist/lib/accounts.d.ts +21 -3
  11. package/dist/lib/accounts.d.ts.map +1 -1
  12. package/dist/lib/accounts.js +229 -17
  13. package/dist/lib/api-route-store.d.ts +24 -0
  14. package/dist/lib/api-route-store.d.ts.map +1 -0
  15. package/dist/lib/api-route-store.js +145 -0
  16. package/dist/lib/auth/account-access.d.ts +115 -0
  17. package/dist/lib/auth/account-access.d.ts.map +1 -0
  18. package/dist/lib/auth/account-access.js +214 -0
  19. package/dist/lib/auth/token-utils.d.ts +54 -1
  20. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  21. package/dist/lib/auth/token-utils.js +92 -7
  22. package/dist/lib/cli.d.ts +1 -1
  23. package/dist/lib/cli.d.ts.map +1 -1
  24. package/dist/lib/cli.js +4 -0
  25. package/dist/lib/codex-cli/state.d.ts +7 -0
  26. package/dist/lib/codex-cli/state.d.ts.map +1 -1
  27. package/dist/lib/codex-cli/state.js +18 -0
  28. package/dist/lib/codex-cli/writer.d.ts.map +1 -1
  29. package/dist/lib/codex-cli/writer.js +60 -8
  30. package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
  31. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  32. package/dist/lib/codex-manager/account-pool-write.d.ts +3 -1
  33. package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
  34. package/dist/lib/codex-manager/account-pool-write.js +4 -0
  35. package/dist/lib/codex-manager/api-login-menu.d.ts +14 -0
  36. package/dist/lib/codex-manager/api-login-menu.d.ts.map +1 -0
  37. package/dist/lib/codex-manager/api-login-menu.js +224 -0
  38. package/dist/lib/codex-manager/commands/account.d.ts.map +1 -1
  39. package/dist/lib/codex-manager/commands/account.js +27 -0
  40. package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
  41. package/dist/lib/codex-manager/commands/best.js +6 -2
  42. package/dist/lib/codex-manager/commands/check.d.ts +7 -1
  43. package/dist/lib/codex-manager/commands/check.d.ts.map +1 -1
  44. package/dist/lib/codex-manager/commands/check.js +24 -3
  45. package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
  46. package/dist/lib/codex-manager/commands/forecast.js +5 -1
  47. package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
  48. package/dist/lib/codex-manager/commands/report.js +5 -1
  49. package/dist/lib/codex-manager/commands/resets.d.ts +2 -0
  50. package/dist/lib/codex-manager/commands/resets.d.ts.map +1 -0
  51. package/dist/lib/codex-manager/commands/resets.js +67 -0
  52. package/dist/lib/codex-manager/commands/rotation.d.ts +2 -2
  53. package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
  54. package/dist/lib/codex-manager/commands/rotation.js +63 -12
  55. package/dist/lib/codex-manager/commands/status.d.ts +12 -1
  56. package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
  57. package/dist/lib/codex-manager/commands/status.js +166 -22
  58. package/dist/lib/codex-manager/commands/uninstall.js +1 -1
  59. package/dist/lib/codex-manager/formatters/quota-formatters.d.ts.map +1 -1
  60. package/dist/lib/codex-manager/formatters/quota-formatters.js +3 -2
  61. package/dist/lib/codex-manager/health-check.d.ts +3 -0
  62. package/dist/lib/codex-manager/health-check.d.ts.map +1 -1
  63. package/dist/lib/codex-manager/health-check.js +56 -6
  64. package/dist/lib/codex-manager/help.d.ts.map +1 -1
  65. package/dist/lib/codex-manager/help.js +4 -2
  66. package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
  67. package/dist/lib/codex-manager/login-flow.js +69 -7
  68. package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -1
  69. package/dist/lib/codex-manager/login-menu-data.js +14 -5
  70. package/dist/lib/codex-manager/login-oauth.d.ts +3 -0
  71. package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
  72. package/dist/lib/codex-manager/login-oauth.js +6 -1
  73. package/dist/lib/codex-manager/login-workspace-choice.d.ts +11 -0
  74. package/dist/lib/codex-manager/login-workspace-choice.d.ts.map +1 -0
  75. package/dist/lib/codex-manager/login-workspace-choice.js +37 -0
  76. package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
  77. package/dist/lib/codex-manager/persist-selected-account.js +2 -1
  78. package/dist/lib/codex-manager/repair-commands.d.ts +13 -0
  79. package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
  80. package/dist/lib/codex-manager/repair-commands.js +143 -8
  81. package/dist/lib/codex-manager.d.ts.map +1 -1
  82. package/dist/lib/codex-manager.js +25 -6
  83. package/dist/lib/concurrency.d.ts +3 -0
  84. package/dist/lib/concurrency.d.ts.map +1 -0
  85. package/dist/lib/concurrency.js +31 -0
  86. package/dist/lib/model-route-policy.d.ts +52 -0
  87. package/dist/lib/model-route-policy.d.ts.map +1 -0
  88. package/dist/lib/model-route-policy.js +159 -0
  89. package/dist/lib/policy/runtime-policy.d.ts +1 -0
  90. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  91. package/dist/lib/policy/runtime-policy.js +3 -0
  92. package/dist/lib/preemptive-quota-scheduler.d.ts +2 -0
  93. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  94. package/dist/lib/preemptive-quota-scheduler.js +4 -0
  95. package/dist/lib/quota-probe.d.ts +5 -0
  96. package/dist/lib/quota-probe.d.ts.map +1 -1
  97. package/dist/lib/quota-probe.js +26 -2
  98. package/dist/lib/request/client-cancellation.d.ts +5 -0
  99. package/dist/lib/request/client-cancellation.d.ts.map +1 -0
  100. package/dist/lib/request/client-cancellation.js +7 -0
  101. package/dist/lib/request/response-outcome.d.ts +25 -0
  102. package/dist/lib/request/response-outcome.d.ts.map +1 -0
  103. package/dist/lib/request/response-outcome.js +49 -0
  104. package/dist/lib/request/stream-failover-runtime.d.ts +2 -1
  105. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
  106. package/dist/lib/request/stream-failover-runtime.js +29 -10
  107. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  108. package/dist/lib/runtime/account-check.js +2 -8
  109. package/dist/lib/runtime/account-model-catalog.d.ts +64 -0
  110. package/dist/lib/runtime/account-model-catalog.d.ts.map +1 -0
  111. package/dist/lib/runtime/account-model-catalog.js +154 -0
  112. package/dist/lib/runtime/account-pool.d.ts +3 -1
  113. package/dist/lib/runtime/account-pool.d.ts.map +1 -1
  114. package/dist/lib/runtime/account-pool.js +4 -0
  115. package/dist/lib/runtime/account-reset-credits.d.ts +42 -0
  116. package/dist/lib/runtime/account-reset-credits.d.ts.map +1 -0
  117. package/dist/lib/runtime/account-reset-credits.js +82 -0
  118. package/dist/lib/runtime/api-model-capabilities.d.ts +30 -0
  119. package/dist/lib/runtime/api-model-capabilities.d.ts.map +1 -0
  120. package/dist/lib/runtime/api-model-capabilities.js +434 -0
  121. package/dist/lib/runtime/api-model-runtime.d.ts +42 -0
  122. package/dist/lib/runtime/api-model-runtime.d.ts.map +1 -0
  123. package/dist/lib/runtime/api-model-runtime.js +358 -0
  124. package/dist/lib/runtime/app-bind.d.ts +19 -1
  125. package/dist/lib/runtime/app-bind.d.ts.map +1 -1
  126. package/dist/lib/runtime/app-bind.js +79 -9
  127. package/dist/lib/runtime/automatic-account-checks.d.ts +31 -0
  128. package/dist/lib/runtime/automatic-account-checks.d.ts.map +1 -0
  129. package/dist/lib/runtime/automatic-account-checks.js +113 -0
  130. package/dist/lib/runtime/automatic-subscription-checks.d.ts +7 -0
  131. package/dist/lib/runtime/automatic-subscription-checks.d.ts.map +1 -0
  132. package/dist/lib/runtime/automatic-subscription-checks.js +57 -0
  133. package/dist/lib/runtime/catalog-capabilities.d.ts +8 -0
  134. package/dist/lib/runtime/catalog-capabilities.d.ts.map +1 -0
  135. package/dist/lib/runtime/catalog-capabilities.js +61 -0
  136. package/dist/lib/runtime/inference-activity.d.ts +15 -0
  137. package/dist/lib/runtime/inference-activity.d.ts.map +1 -0
  138. package/dist/lib/runtime/inference-activity.js +95 -0
  139. package/dist/lib/runtime/model-discovery-status.d.ts +100 -0
  140. package/dist/lib/runtime/model-discovery-status.d.ts.map +1 -0
  141. package/dist/lib/runtime/model-discovery-status.js +387 -0
  142. package/dist/lib/runtime/native-account-storage.d.ts +11 -0
  143. package/dist/lib/runtime/native-account-storage.d.ts.map +1 -0
  144. package/dist/lib/runtime/native-account-storage.js +50 -0
  145. package/dist/lib/runtime/native-account-sync.d.ts +19 -0
  146. package/dist/lib/runtime/native-account-sync.d.ts.map +1 -0
  147. package/dist/lib/runtime/native-account-sync.js +62 -0
  148. package/dist/lib/runtime/native-binding-lock.d.ts +3 -0
  149. package/dist/lib/runtime/native-binding-lock.d.ts.map +1 -0
  150. package/dist/lib/runtime/native-binding-lock.js +5 -0
  151. package/dist/lib/runtime/native-client-auth.d.ts +3 -0
  152. package/dist/lib/runtime/native-client-auth.d.ts.map +1 -0
  153. package/dist/lib/runtime/native-client-auth.js +42 -0
  154. package/dist/lib/runtime/native-provider-config.d.ts +4 -0
  155. package/dist/lib/runtime/native-provider-config.d.ts.map +1 -0
  156. package/dist/lib/runtime/native-provider-config.js +52 -0
  157. package/dist/lib/runtime/native-rate-limits.d.ts +18 -0
  158. package/dist/lib/runtime/native-rate-limits.d.ts.map +1 -0
  159. package/dist/lib/runtime/native-rate-limits.js +135 -0
  160. package/dist/lib/runtime/reset-credit-routing.d.ts +41 -0
  161. package/dist/lib/runtime/reset-credit-routing.d.ts.map +1 -0
  162. package/dist/lib/runtime/reset-credit-routing.js +66 -0
  163. package/dist/lib/runtime/reset-credits.d.ts +101 -0
  164. package/dist/lib/runtime/reset-credits.d.ts.map +1 -0
  165. package/dist/lib/runtime/reset-credits.js +162 -0
  166. package/dist/lib/runtime/response-output-history.d.ts +17 -0
  167. package/dist/lib/runtime/response-output-history.d.ts.map +1 -0
  168. package/dist/lib/runtime/response-output-history.js +85 -0
  169. package/dist/lib/runtime/responses-websocket.d.ts +31 -0
  170. package/dist/lib/runtime/responses-websocket.d.ts.map +1 -0
  171. package/dist/lib/runtime/responses-websocket.js +636 -0
  172. package/dist/lib/runtime/resume-picker.d.ts +41 -0
  173. package/dist/lib/runtime/resume-picker.d.ts.map +1 -0
  174. package/dist/lib/runtime/resume-picker.js +169 -0
  175. package/dist/lib/runtime/rotation-account-selection.d.ts +6 -0
  176. package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -1
  177. package/dist/lib/runtime/rotation-account-selection.js +63 -0
  178. package/dist/lib/runtime/rotation-proxy-state.d.ts +33 -0
  179. package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -1
  180. package/dist/lib/runtime/rotation-server-types.d.ts +18 -0
  181. package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
  182. package/dist/lib/runtime/runtime-capability-failures.d.ts +14 -0
  183. package/dist/lib/runtime/runtime-capability-failures.d.ts.map +1 -0
  184. package/dist/lib/runtime/runtime-capability-failures.js +76 -0
  185. package/dist/lib/runtime/runtime-observability.d.ts +7 -0
  186. package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
  187. package/dist/lib/runtime/runtime-observability.js +42 -2
  188. package/dist/lib/runtime/subscription-first-use.d.ts +10 -0
  189. package/dist/lib/runtime/subscription-first-use.d.ts.map +1 -0
  190. package/dist/lib/runtime/subscription-first-use.js +64 -0
  191. package/dist/lib/runtime/subscription-quota-event.d.ts +6 -0
  192. package/dist/lib/runtime/subscription-quota-event.d.ts.map +1 -0
  193. package/dist/lib/runtime/subscription-quota-event.js +31 -0
  194. package/dist/lib/runtime/subscription-quota-order.d.ts +17 -0
  195. package/dist/lib/runtime/subscription-quota-order.d.ts.map +1 -0
  196. package/dist/lib/runtime/subscription-quota-order.js +31 -0
  197. package/dist/lib/runtime/workspace-model-scopes.d.ts +15 -0
  198. package/dist/lib/runtime/workspace-model-scopes.d.ts.map +1 -0
  199. package/dist/lib/runtime/workspace-model-scopes.js +34 -0
  200. package/dist/lib/runtime-constants.d.ts +3 -0
  201. package/dist/lib/runtime-constants.d.ts.map +1 -1
  202. package/dist/lib/runtime-constants.js +5 -0
  203. package/dist/lib/runtime-paths.d.ts.map +1 -1
  204. package/dist/lib/runtime-paths.js +4 -1
  205. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  206. package/dist/lib/runtime-rotation-proxy.js +852 -42
  207. package/dist/lib/schemas.d.ts +20 -0
  208. package/dist/lib/schemas.d.ts.map +1 -1
  209. package/dist/lib/schemas.js +3 -0
  210. package/dist/lib/storage/credential-sidecars.d.ts +13 -0
  211. package/dist/lib/storage/credential-sidecars.d.ts.map +1 -0
  212. package/dist/lib/storage/credential-sidecars.js +52 -0
  213. package/dist/lib/storage/file-lock.d.ts +9 -0
  214. package/dist/lib/storage/file-lock.d.ts.map +1 -0
  215. package/dist/lib/storage/file-lock.js +186 -0
  216. package/dist/lib/storage/flagged-storage.d.ts.map +1 -1
  217. package/dist/lib/storage/flagged-storage.js +12 -0
  218. package/dist/lib/storage/path-state.d.ts +2 -0
  219. package/dist/lib/storage/path-state.d.ts.map +1 -1
  220. package/dist/lib/storage/path-state.js +9 -0
  221. package/dist/lib/storage/paths.js +4 -4
  222. package/dist/lib/storage/pending-auth.d.ts +17 -0
  223. package/dist/lib/storage/pending-auth.d.ts.map +1 -0
  224. package/dist/lib/storage/pending-auth.js +153 -0
  225. package/dist/lib/storage/public-types.d.ts +13 -0
  226. package/dist/lib/storage/public-types.d.ts.map +1 -1
  227. package/dist/lib/storage/snapshot-merge.d.ts +16 -0
  228. package/dist/lib/storage/snapshot-merge.d.ts.map +1 -0
  229. package/dist/lib/storage/snapshot-merge.js +206 -0
  230. package/dist/lib/storage/transactions.d.ts.map +1 -1
  231. package/dist/lib/storage/transactions.js +5 -4
  232. package/dist/lib/storage.d.ts +6 -0
  233. package/dist/lib/storage.d.ts.map +1 -1
  234. package/dist/lib/storage.js +77 -7
  235. package/dist/lib/ui/auth-menu-builder.d.ts +2 -0
  236. package/dist/lib/ui/auth-menu-builder.d.ts.map +1 -1
  237. package/dist/lib/ui/auth-menu-builder.js +1 -0
  238. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  239. package/dist/lib/ui/auth-menu.js +1 -0
  240. package/dist/lib/ui/check-progress.d.ts +8 -0
  241. package/dist/lib/ui/check-progress.d.ts.map +1 -0
  242. package/dist/lib/ui/check-progress.js +30 -0
  243. package/dist/lib/ui/format.d.ts +3 -1
  244. package/dist/lib/ui/format.d.ts.map +1 -1
  245. package/dist/lib/ui/format.js +8 -0
  246. package/dist/lib/ui/ui-copy.d.ts +2 -2
  247. package/dist/lib/ui/ui-copy.js +2 -2
  248. package/dist/lib/usage/pricing.js +2 -2
  249. package/dist/lib/usage/usage-extraction.d.ts +3 -1
  250. package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
  251. package/dist/lib/usage/usage-extraction.js +46 -9
  252. package/package.json +7 -4
  253. package/scripts/codex-app-router.js +7 -0
  254. package/scripts/codex-bin-resolver.js +40 -8
  255. package/scripts/codex-routing.js +1 -0
  256. package/scripts/codex.js +100 -22
@@ -0,0 +1,636 @@
1
+ import { ClientCancellationError } from "../request/client-cancellation.js";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import WebSocket, { WebSocketServer } from "ws";
5
+ import { Agent, fetch as undiciFetch } from "undici";
6
+ import { parseModelRoute } from "../model-route-policy.js";
7
+ import { isRecord } from "../utils.js";
8
+ import { ResponseOutputHistory, hasOrphanToolResult } from "./response-output-history.js";
9
+ const CONTEXT_HEADER = "x-local-responses-transport";
10
+ const terminal = new Set([
11
+ "response.completed",
12
+ "response.failed",
13
+ "response.incomplete",
14
+ "response.cancelled",
15
+ "error",
16
+ ]);
17
+ /** Headers that describe the synthetic body or the upgrade itself, never the upstream decision. */
18
+ const LOCAL_ONLY_HEADERS = /^(connection|upgrade|keep-alive|sec-websocket-.*|content-length|content-encoding|content-type|transfer-encoding|set-cookie)$/i;
19
+ function upstreamHeaders(source) {
20
+ const headers = {};
21
+ if (!isRecord(source))
22
+ return headers;
23
+ for (const [key, value] of Object.entries(source)) {
24
+ if (LOCAL_ONLY_HEADERS.test(key))
25
+ continue;
26
+ if (typeof value === "string")
27
+ headers[key] = value;
28
+ else if (typeof value === "number")
29
+ headers[key] = String(value);
30
+ else if (Array.isArray(value) && value.every(item => typeof item === "string"))
31
+ headers[key] = value.join(", ");
32
+ }
33
+ return headers;
34
+ }
35
+ /** Union of comma-separated OpenAI-Beta features; the client's own flags are kept. */
36
+ function mergeBeta(current, client) {
37
+ if (!client)
38
+ return current;
39
+ const features = [...(current ?? "").split(","), ...client.split(",")].map(item => item.trim()).filter(Boolean);
40
+ return [...new Set(features)].join(", ") || null;
41
+ }
42
+ function wireError(code, status = 400) {
43
+ return {
44
+ type: "error",
45
+ status,
46
+ error: {
47
+ type: "invalid_request_error",
48
+ code,
49
+ message: code === "previous_response_not_found"
50
+ ? "Retry with complete input and no previous_response_id."
51
+ : "The local WebSocket request could not be served.",
52
+ },
53
+ };
54
+ }
55
+ function bodyJson(body) {
56
+ const value = JSON.parse(typeof body === "string"
57
+ ? body
58
+ : Buffer.from(body).toString("utf8"));
59
+ if (!isRecord(value))
60
+ throw Error("Invalid response body");
61
+ return value;
62
+ }
63
+ /** Per-client memory only; credentials never leave the existing routing boundary. */
64
+ class SocketSession {
65
+ maxBytes;
66
+ stats;
67
+ httpFetch;
68
+ channels = new Map();
69
+ owners = new Map();
70
+ chains = new Map();
71
+ /** Credentials whose WebSocket handshake was refused; they use HTTP for the session. */
72
+ httpOnly = new Set();
73
+ bytes = 0;
74
+ controller;
75
+ closed = false;
76
+ constructor(maxBytes, stats, httpFetch = fetch) {
77
+ this.maxBytes = maxBytes;
78
+ this.stats = stats;
79
+ this.httpFetch = httpFetch;
80
+ }
81
+ close() {
82
+ this.closed = true;
83
+ this.controller?.abort();
84
+ for (const socket of this.channels.values())
85
+ socket.terminate();
86
+ this.channels.clear();
87
+ this.owners.clear();
88
+ this.chains.clear();
89
+ this.bytes = 0;
90
+ }
91
+ remember(id, body, output, pool) {
92
+ const bytes = Buffer.byteLength(JSON.stringify({ body, output }));
93
+ if (bytes > this.maxBytes)
94
+ return;
95
+ const old = this.chains.get(id);
96
+ if (old)
97
+ this.bytes -= old.bytes;
98
+ this.chains.delete(id);
99
+ this.chains.set(id, { body, output, pool, bytes });
100
+ this.bytes += bytes;
101
+ while (this.bytes > this.maxBytes || this.chains.size > 32) {
102
+ const first = this.chains.keys().next().value;
103
+ if (!first)
104
+ break;
105
+ const entry = this.chains.get(first);
106
+ this.bytes -= entry?.bytes ?? 0;
107
+ this.chains.delete(first);
108
+ this.owners.delete(first);
109
+ }
110
+ }
111
+ async fetch(input, init, context) {
112
+ if (context.signal.aborted || this.closed)
113
+ throw new ClientCancellationError();
114
+ const url = new URL(String(input));
115
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
116
+ const headers = new Headers(init.headers);
117
+ // The routing pipeline stamps its HTTP beta flag; the native client's
118
+ // WebSocket flag (e.g. responses_websockets=<date>) must survive the hop.
119
+ const beta = mergeBeta(headers.get("openai-beta"), context.clientBeta);
120
+ if (beta)
121
+ headers.set("openai-beta", beta);
122
+ const key = createHash("sha256")
123
+ .update(url.toString())
124
+ .update("\0")
125
+ .update(headers.get("authorization") ?? "")
126
+ .update("\0")
127
+ .update(headers.get("chatgpt-account-id") ?? "")
128
+ .digest("hex");
129
+ // API/ZDR credentials are not ChatGPT workspaces: when their WebSocket
130
+ // handshake is refused, the turn (and the session) uses plain HTTP.
131
+ const apiCredential = !headers.has("chatgpt-account-id");
132
+ if (apiCredential && this.httpOnly.has(key))
133
+ return this.httpFetch(input, init);
134
+ const body = bodyJson(init.body);
135
+ delete body.stream;
136
+ delete body.background;
137
+ body.store = false;
138
+ body.type = "response.create";
139
+ let socket = this.channels.get(key);
140
+ if (socket?.readyState !== WebSocket.OPEN) {
141
+ socket?.terminate();
142
+ this.channels.delete(key);
143
+ socket = undefined;
144
+ }
145
+ if (context.previousId &&
146
+ socket &&
147
+ this.owners.get(context.previousId) === socket) {
148
+ body.previous_response_id = context.previousId;
149
+ body.input = context.delta;
150
+ }
151
+ else if (context.previousId && hasOrphanToolResult(body.input)) {
152
+ return Response.json(wireError("previous_response_not_found"), { status: 400 });
153
+ }
154
+ let handshakeHeaders;
155
+ if (!socket) {
156
+ if (context.signal.aborted || this.closed)
157
+ throw new ClientCancellationError();
158
+ if (this.channels.size >= 16) {
159
+ const first = this.channels.keys().next().value;
160
+ if (first) {
161
+ this.channels.get(first)?.terminate();
162
+ this.channels.delete(first);
163
+ }
164
+ }
165
+ for (const name of [
166
+ "connection",
167
+ "upgrade",
168
+ "content-length",
169
+ "content-type",
170
+ "content-encoding",
171
+ "host",
172
+ ])
173
+ headers.delete(name);
174
+ this.stats.upstreamConnections++;
175
+ socket = new WebSocket(url, {
176
+ headers: Object.fromEntries(headers),
177
+ followRedirects: false,
178
+ handshakeTimeout: 15000,
179
+ maxPayload: this.maxBytes,
180
+ perMessageDeflate: false,
181
+ });
182
+ const opening = socket;
183
+ const rejected = await new Promise((resolve, reject) => {
184
+ const abort = () => {
185
+ opening.terminate();
186
+ cleanup();
187
+ reject(context.signal.aborted ? new ClientCancellationError() : Error("WebSocket request cancelled"));
188
+ };
189
+ const cleanup = () => {
190
+ init.signal?.removeEventListener("abort", abort);
191
+ context.signal.removeEventListener("abort", abort);
192
+ };
193
+ context.signal.addEventListener("abort", abort, { once: true });
194
+ init.signal?.addEventListener("abort", abort, { once: true });
195
+ opening.once("upgrade", (response) => {
196
+ handshakeHeaders = upstreamHeaders(response.headers);
197
+ });
198
+ opening.once("open", () => {
199
+ cleanup();
200
+ resolve(undefined);
201
+ });
202
+ opening.once("error", () => {
203
+ cleanup();
204
+ reject(context.signal.aborted ? new ClientCancellationError() : Error("Upstream WebSocket connection failed"));
205
+ });
206
+ opening.once("unexpected-response", (_request, response) => {
207
+ cleanup();
208
+ response.resume();
209
+ const status = response.statusCode ?? 502;
210
+ resolve(Response.json({
211
+ error: {
212
+ code: "websocket_handshake_rejected",
213
+ message: "Upstream rejected the WebSocket handshake.",
214
+ },
215
+ },
216
+ // Retry-After and x-codex-* quota headers drive cooldowns.
217
+ { status: status >= 400 && status <= 599 ? status : 502, headers: upstreamHeaders(response.headers) }));
218
+ opening.terminate();
219
+ });
220
+ });
221
+ if (rejected) {
222
+ if (!apiCredential)
223
+ return rejected;
224
+ this.httpOnly.add(key);
225
+ return this.httpFetch(input, init);
226
+ }
227
+ this.channels.set(key, socket);
228
+ }
229
+ const active = socket;
230
+ return new Promise((resolve, reject) => {
231
+ let accepted = false, finished = false;
232
+ let streamController;
233
+ const stream = new ReadableStream({
234
+ start(controller) {
235
+ streamController = controller;
236
+ },
237
+ cancel() {
238
+ active.terminate();
239
+ },
240
+ }, { highWaterMark: this.maxBytes, size: (chunk) => chunk.byteLength });
241
+ const cleanup = () => {
242
+ active.off("message", onMessage);
243
+ active.off("close", onClose);
244
+ active.off("error", onClose);
245
+ init.signal?.removeEventListener("abort", abort);
246
+ context.signal.removeEventListener("abort", abort);
247
+ };
248
+ const onClose = () => {
249
+ if (finished)
250
+ return;
251
+ finished = true;
252
+ cleanup();
253
+ this.channels.delete(key);
254
+ const cancelled = context.signal.aborted || this.closed;
255
+ // Once create has been sent, a disconnect cannot prove generation
256
+ // did not start. Never replay it, even before the first event.
257
+ const error = cancelled
258
+ ? new ClientCancellationError()
259
+ : Error("Upstream WebSocket disconnected");
260
+ if (accepted)
261
+ streamController.error(error);
262
+ else if (cancelled)
263
+ reject(error);
264
+ else {
265
+ // A sent create has an ambiguous outcome. Return a terminal
266
+ // stream error so the HTTP router cannot retry generation.
267
+ resolve(new Response(`data: ${JSON.stringify(wireError("upstream_websocket_disconnected", 502))}\n\n`, {
268
+ headers: { "content-type": "text/event-stream" },
269
+ }));
270
+ }
271
+ };
272
+ const abort = () => active.terminate();
273
+ const onMessage = (raw, isBinary) => {
274
+ try {
275
+ if (isBinary)
276
+ throw Error("Unexpected binary response");
277
+ const text = raw.toString();
278
+ const event = JSON.parse(text);
279
+ if (!isRecord(event) || typeof event.type !== "string")
280
+ throw Error("Invalid response event");
281
+ if (event.type === "error" && !accepted) {
282
+ finished = true;
283
+ cleanup();
284
+ const status = typeof event.status === "number" &&
285
+ event.status >= 400 &&
286
+ event.status <= 599
287
+ ? event.status
288
+ : 400;
289
+ resolve(Response.json(event, { status, headers: upstreamHeaders(event.headers) }));
290
+ return;
291
+ }
292
+ if (!accepted) {
293
+ accepted = true;
294
+ // Handshake headers describe the account only at connect time,
295
+ // so they ride on the first response of a fresh socket.
296
+ resolve(new Response(stream, {
297
+ headers: { ...handshakeHeaders, "content-type": "text/event-stream" },
298
+ }));
299
+ }
300
+ if ((streamController.desiredSize ?? 0) < -this.maxBytes)
301
+ throw Error("Upstream buffer exceeded");
302
+ streamController.enqueue(Buffer.from(`data: ${text}\n\n`));
303
+ if (isRecord(event.response) &&
304
+ typeof event.response.id === "string") {
305
+ this.owners.set(event.response.id, active);
306
+ if (this.owners.size > 64) {
307
+ const first = this.owners.keys().next().value;
308
+ if (first)
309
+ this.owners.delete(first);
310
+ }
311
+ }
312
+ if (terminal.has(event.type)) {
313
+ finished = true;
314
+ cleanup();
315
+ streamController.close();
316
+ }
317
+ }
318
+ catch {
319
+ active.terminate();
320
+ onClose();
321
+ }
322
+ };
323
+ active.on("message", onMessage);
324
+ active.once("close", onClose);
325
+ active.once("error", onClose);
326
+ init.signal?.addEventListener("abort", abort, { once: true });
327
+ context.signal.addEventListener("abort", abort, { once: true });
328
+ if (init.signal?.aborted || context.signal.aborted) {
329
+ abort();
330
+ return;
331
+ }
332
+ this.stats.upstreamRequests++;
333
+ active.send(JSON.stringify(body), (error) => {
334
+ if (error)
335
+ onClose();
336
+ });
337
+ });
338
+ }
339
+ }
340
+ /** Adapts only the transport; authenticated HTTP routing remains the policy authority. */
341
+ export class ResponsesWebSocketGateway {
342
+ upstreamFetch;
343
+ options;
344
+ stats = {
345
+ connections: 0,
346
+ upstreamConnections: 0,
347
+ upstreamRequests: 0,
348
+ };
349
+ contexts = new Map();
350
+ scope = new AsyncLocalStorage();
351
+ sessions = new Set();
352
+ wss;
353
+ maxBytes;
354
+ /** Loopback hops must never go through an environment HTTP proxy (NODE_USE_ENV_PROXY). */
355
+ loopback = new Agent();
356
+ loopbackFetch = ((input, init) => undiciFetch(input, { ...init, dispatcher: this.loopback }));
357
+ constructor(upstreamFetch, options) {
358
+ this.upstreamFetch = upstreamFetch;
359
+ this.options = options;
360
+ this.maxBytes = options.maxPayloadBytes;
361
+ this.wss = new WebSocketServer({
362
+ noServer: true,
363
+ maxPayload: this.maxBytes,
364
+ perMessageDeflate: false,
365
+ });
366
+ }
367
+ fetch = async (input, init) => {
368
+ const context = this.scope.getStore();
369
+ if (context &&
370
+ init?.method === "POST" &&
371
+ new URL(String(input)).pathname.endsWith("/responses"))
372
+ return context.session.fetch(input, init, context);
373
+ return this.upstreamFetch(input, init);
374
+ };
375
+ handle(req, handler) {
376
+ const id = req.headers[CONTEXT_HEADER];
377
+ delete req.headers[CONTEXT_HEADER];
378
+ const context = typeof id === "string" ? this.contexts.get(id) : undefined;
379
+ if (typeof id === "string")
380
+ this.contexts.delete(id);
381
+ if (context)
382
+ this.scope.run(context, () => {
383
+ void handler();
384
+ });
385
+ else
386
+ void handler();
387
+ }
388
+ attach(server, baseUrl) {
389
+ server.on("upgrade", (req, socket, head) => {
390
+ // Node relinquishes its socket error handler before emitting upgrade.
391
+ socket.on("error", () => socket.destroy());
392
+ const reject = (status) => {
393
+ if (socket.destroyed)
394
+ return;
395
+ socket.end(`HTTP/1.1 ${status} Rejected\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
396
+ };
397
+ void (async () => {
398
+ const credentials = {};
399
+ for (const name of ["authorization", "x-api-key"]) {
400
+ const value = req.headers[name];
401
+ if (typeof value === "string")
402
+ credentials[name] = value;
403
+ }
404
+ // An authenticated unknown HTTP path returns 404; all unknown credentials return 401.
405
+ const auth = await this.loopbackFetch(`${baseUrl}/__websocket_auth_check__`, {
406
+ headers: credentials,
407
+ signal: AbortSignal.timeout(5000),
408
+ });
409
+ await auth.body?.cancel();
410
+ if (auth.status !== 404) {
411
+ // Only a real 401 means bad credentials; a storage or policy outage is
412
+ // transient and must not make the native client distrust its login.
413
+ reject(auth.status === 401 ? 401 : auth.status >= 500 ? 503 : auth.status);
414
+ return;
415
+ }
416
+ if (req.headers.origin) {
417
+ reject(403);
418
+ return;
419
+ }
420
+ if (await this.options.upgrade?.(req, socket, head))
421
+ return;
422
+ const path = new URL(req.url ?? "/", baseUrl).pathname;
423
+ if (![
424
+ "/responses",
425
+ "/v1/responses",
426
+ "/backend-api/codex/responses",
427
+ ].includes(path)) {
428
+ reject(404);
429
+ return;
430
+ }
431
+ if (req.headers.origin || this.sessions.size >= 64) {
432
+ reject(403);
433
+ return;
434
+ }
435
+ if (socket.destroyed)
436
+ return;
437
+ this.wss.handleUpgrade(req, socket, head, (ws) => this.serve(ws, req, baseUrl));
438
+ })().catch(() => reject(503));
439
+ });
440
+ }
441
+ serve(ws, req, baseUrl) {
442
+ this.stats.connections++;
443
+ const session = new SocketSession(this.maxBytes, this.stats, this.upstreamFetch);
444
+ this.sessions.add(session);
445
+ let queue = Promise.resolve(), queued = 0, queuedBytes = 0;
446
+ let generation = 0;
447
+ const send = (event) => {
448
+ if (ws.readyState !== WebSocket.OPEN)
449
+ return;
450
+ if (ws.bufferedAmount > this.maxBytes) {
451
+ ws.terminate();
452
+ return;
453
+ }
454
+ ws.send(JSON.stringify(event));
455
+ };
456
+ ws.on("error", () => ws.terminate());
457
+ ws.once("close", () => {
458
+ session.close();
459
+ this.sessions.delete(session);
460
+ });
461
+ ws.on("message", (raw, isBinary) => {
462
+ let event;
463
+ try {
464
+ const parsed = JSON.parse(raw.toString());
465
+ if (isBinary || !isRecord(parsed))
466
+ throw Error();
467
+ event = parsed;
468
+ }
469
+ catch {
470
+ send(wireError("invalid_websocket_event"));
471
+ return;
472
+ }
473
+ if (event.type === "response.cancel") {
474
+ // Invalidate work accepted before cancellation, including queued creates.
475
+ generation++;
476
+ session.controller?.abort();
477
+ for (const channel of session.channels.values())
478
+ channel.terminate();
479
+ return;
480
+ }
481
+ if (event.type !== "response.create") {
482
+ send(wireError("unsupported_websocket_event"));
483
+ return;
484
+ }
485
+ const eventBytes = Buffer.byteLength(raw.toString());
486
+ if (++queued > 16 || queuedBytes + eventBytes > this.maxBytes) {
487
+ queued--;
488
+ send(wireError("websocket_queue_full", 429));
489
+ return;
490
+ }
491
+ queuedBytes += eventBytes;
492
+ const acceptedGeneration = generation;
493
+ queue = queue
494
+ .then(async () => {
495
+ if (session.closed || acceptedGeneration !== generation)
496
+ return;
497
+ try {
498
+ if (typeof event.model !== "string" || !Array.isArray(event.input))
499
+ throw Error("invalid_request_body");
500
+ const routingHeaders = new Headers();
501
+ for (const [key, value] of Object.entries(req.headers))
502
+ if (typeof value === "string")
503
+ routingHeaders.set(key, value);
504
+ event.model =
505
+ this.options.routeModel?.(event.model, routingHeaders) ??
506
+ event.model;
507
+ const pool = parseModelRoute(event.model).kind;
508
+ const previousId = typeof event.previous_response_id === "string"
509
+ ? event.previous_response_id
510
+ : undefined;
511
+ const prior = previousId
512
+ ? session.chains.get(previousId)
513
+ : undefined;
514
+ if (previousId && !prior) {
515
+ send(wireError("previous_response_not_found"));
516
+ return;
517
+ }
518
+ if (prior && prior.pool !== pool) {
519
+ send(wireError("model_route_pool_mismatch"));
520
+ return;
521
+ }
522
+ const body = {
523
+ ...prior?.body,
524
+ ...event,
525
+ input: prior
526
+ ? [
527
+ ...(Array.isArray(prior.body.input)
528
+ ? prior.body.input
529
+ : []),
530
+ ...prior.output,
531
+ ...event.input,
532
+ ]
533
+ : event.input,
534
+ stream: true,
535
+ store: false,
536
+ };
537
+ delete body.type;
538
+ delete body.previous_response_id;
539
+ // Native prewarm sets generate=false; omission on the next create
540
+ // means generate normally, not inherit the warm-up's control flag.
541
+ if (event.generate === undefined)
542
+ delete body.generate;
543
+ if (Buffer.byteLength(JSON.stringify(body)) > this.maxBytes) {
544
+ send(wireError("websocket_context_too_large", 413));
545
+ return;
546
+ }
547
+ const id = randomUUID();
548
+ const controller = new AbortController();
549
+ session.controller = controller;
550
+ const clientBeta = req.headers["openai-beta"];
551
+ this.contexts.set(id, { session, previousId, delta: event.input, signal: controller.signal, clientBeta: typeof clientBeta === "string" ? clientBeta : undefined });
552
+ const headers = new Headers();
553
+ for (const [key, value] of Object.entries(req.headers)) {
554
+ if (typeof value === "string" &&
555
+ !/^(host|connection|upgrade|sec-websocket-|content-|origin)/i.test(key))
556
+ headers.set(key, value);
557
+ }
558
+ headers.set(CONTEXT_HEADER, id);
559
+ headers.set("content-type", "application/json");
560
+ try {
561
+ const response = await this.loopbackFetch(`${baseUrl}/responses`, {
562
+ method: "POST",
563
+ headers,
564
+ body: JSON.stringify(body),
565
+ signal: controller.signal,
566
+ });
567
+ if (!response.ok) {
568
+ // An empty or non-JSON error body must not turn a real 401 into a 502.
569
+ const data = await response.json().catch(() => undefined);
570
+ send(isRecord(data)
571
+ ? { ...data, type: "error", status: response.status }
572
+ : wireError("websocket_request_failed", response.status));
573
+ return;
574
+ }
575
+ if (!response.body)
576
+ throw Error("missing_response_body");
577
+ let buffer = "";
578
+ const outputHistory = new ResponseOutputHistory(this.maxBytes);
579
+ const decoder = new TextDecoder();
580
+ for await (const chunk of response.body) {
581
+ buffer += decoder.decode(chunk, { stream: true });
582
+ if (Buffer.byteLength(buffer) > this.maxBytes)
583
+ throw Error("websocket_event_too_large");
584
+ let index;
585
+ while ((index = buffer.indexOf("\n\n")) >= 0) {
586
+ const frame = buffer.slice(0, index);
587
+ buffer = buffer.slice(index + 2);
588
+ const data = frame
589
+ .split("\n")
590
+ .filter((line) => line.startsWith("data:"))
591
+ .map((line) => line.slice(5).trimStart())
592
+ .join("\n");
593
+ if (!data || data === "[DONE]")
594
+ continue;
595
+ const parsed = JSON.parse(data);
596
+ if (!isRecord(parsed))
597
+ throw Error("invalid_response_event");
598
+ outputHistory.observe(parsed);
599
+ if (parsed.type === "response.completed" &&
600
+ isRecord(parsed.response) &&
601
+ typeof parsed.response.id === "string") {
602
+ const output = outputHistory.finish(parsed.response.output);
603
+ if (output)
604
+ session.remember(parsed.response.id, body, output, pool);
605
+ }
606
+ send(parsed);
607
+ }
608
+ }
609
+ }
610
+ finally {
611
+ this.contexts.delete(id);
612
+ if (session.controller === controller)
613
+ session.controller = undefined;
614
+ }
615
+ }
616
+ catch {
617
+ if (!session.closed)
618
+ send(wireError("websocket_request_failed", 502));
619
+ }
620
+ })
621
+ .finally(() => {
622
+ queued--;
623
+ queuedBytes -= eventBytes;
624
+ });
625
+ });
626
+ }
627
+ close() {
628
+ for (const ws of this.wss.clients)
629
+ ws.terminate();
630
+ for (const session of this.sessions)
631
+ session.close();
632
+ this.contexts.clear();
633
+ this.wss.close();
634
+ void this.loopback.close().catch(() => undefined);
635
+ }
636
+ }
@@ -0,0 +1,41 @@
1
+ import { type MenuItem } from "../ui/select.js";
2
+ export interface ResumeThread {
3
+ id: string;
4
+ name?: string | null;
5
+ preview?: string;
6
+ cwd: string;
7
+ modelProvider?: string;
8
+ }
9
+ export interface ResumePage {
10
+ data: ResumeThread[];
11
+ nextCursor?: string | null;
12
+ }
13
+ export interface ResumePickerOptions {
14
+ codexBin: {
15
+ path: string;
16
+ launchWithNode?: boolean;
17
+ };
18
+ cwd: string;
19
+ configArgs: string[];
20
+ showAll: boolean;
21
+ includeNonInteractive: boolean;
22
+ env?: NodeJS.ProcessEnv;
23
+ timeoutMs?: number;
24
+ }
25
+ /** Read the canonical index through Codex, rather than depending on SQLite schemas. */
26
+ export declare function createResumeCatalog(options: ResumePickerOptions): {
27
+ page(cursor?: string | null): Promise<ResumePage>;
28
+ close(): void;
29
+ };
30
+ type Choice = {
31
+ kind: "thread";
32
+ id: string;
33
+ } | {
34
+ kind: "next" | "previous" | "cancel";
35
+ };
36
+ export declare function pickResumeThread(options: ResumePickerOptions, deps?: {
37
+ createCatalog: typeof createResumeCatalog;
38
+ select: (items: MenuItem<Choice>[], options: import("../ui/select.js").SelectOptions<Choice>) => Promise<Choice | null>;
39
+ }): Promise<string | null>;
40
+ export {};
41
+ //# sourceMappingURL=resume-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resume-picker.d.ts","sourceRoot":"","sources":["../../../lib/runtime/resume-picker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB;kBAsD3C,MAAM,GAAG,IAAI,GAAU,OAAO,CAAC,UAAU,CAAC;;EAyC9D;AAOD,KAAK,MAAM,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CAAE,CAAC;AAExF,wBAAsB,gBAAgB,CACrC,OAAO,EAAE,mBAAmB,EAC5B,IAAI;;;CAAiE,GACnE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkDxB"}