@bitkyc08/opencodex 2.55.0 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,1766 @@
1
+ import type { Server, ServerWebSocket } from "bun";
2
+ import type { StartServerDeps } from "./startup-warnings";
3
+ import {
4
+ GUI_PAIRING_EXCHANGE_BODY_LIMIT,
5
+ REMOTE_WORKSPACE_PAIRING_BODY_LIMIT,
6
+ readBoundedRequestText,
7
+ withRemoteCatalogKeyId,
8
+ } from "./bounded-request";
9
+ import {
10
+ LIVE_SIDEBAND_UPSTREAM_OPEN_TIMEOUT_MS,
11
+ MAX_WS_FRAME_BYTES,
12
+ WEBSOCKET_IDLE_TIMEOUT_SECONDS,
13
+ attachLiveSidebandUpstream,
14
+ closeLiveSideband,
15
+ closeLiveSidebandBeforeUpgrade,
16
+ enqueueLiveSidebandPendingFrame,
17
+ exceedsLiveSidebandFrameByteLimit,
18
+ openLiveSidebandUpstream,
19
+ sendUpstreamFrame,
20
+ webSocketFrameBytes,
21
+ } from "./live-sideband";
22
+ import {
23
+ withRequestLogId,
24
+ } from "./startup-warnings";
25
+
26
+ import { remoteWorkspaceEnabled } from "../../remote-control/workspace-activation";
27
+ import { markActivity } from "../../lib/sidecar-tracker";
28
+ import { knownModelIdsForProvider } from "../../router";
29
+ import {
30
+ buildWarmupCompletionFrames,
31
+ buildWsErrorFrame,
32
+ selectForwardHeaders,
33
+ sendJsonFrame,
34
+ buildResponsesWsData,
35
+ sendResponseToWebSocket,
36
+ sendTextFrame,
37
+ type WsData,
38
+ } from "../ws-bridge";
39
+ import { websocketsEnabled } from "../../config";
40
+ import { grokDefaultReasoningEffort } from "../../grok/effort";
41
+ import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
42
+ import { providerCodexAccountMode } from "../../providers/registry";
43
+ import {
44
+ codexAccountNamespaceEntries,
45
+ isMainCodexAccountTarget,
46
+ } from "../../codex/account-namespaces";
47
+ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
48
+ import {
49
+ availableAccountGatedNativeModels,
50
+ codexModelEntitlementStateForAccount,
51
+ resolveCodexModelEntitlements,
52
+ } from "../../codex/model-entitlements";
53
+ import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
54
+ import {
55
+ registerCodexWebSocket,
56
+ tryReserveCodexWebSocket,
57
+ unregisterCodexWebSocket,
58
+ updateCodexWebSocketAuthContext,
59
+ } from "../../codex/websocket-registry";
60
+ import {
61
+ rootFallbackPayload,
62
+ serveGuiFile,
63
+ serveSessionBootstrap,
64
+ } from "../gui-static";
65
+ import {
66
+ formatErrorResponse,
67
+ type ResponsesTerminalStatus,
68
+ } from "../../bridge";
69
+ import {
70
+ isDraining,
71
+ registerTurn,
72
+ tryAdmitTurn,
73
+ unregisterTurn,
74
+ type ActiveTurnLease,
75
+ } from "../lifecycle";
76
+ import {
77
+ addFinalRequestLog,
78
+ httpStatusForRequestLogTerminal,
79
+ inspectResponseLogSsePayload,
80
+ nextRequestLogId,
81
+ recordFirstOutput,
82
+ type RequestLogContext,
83
+ type RequestLogEntry,
84
+ } from "../request-log";
85
+ import { sessionLaneIdFromRequest } from "../request-log-conversation";
86
+ import { responseWithDeferredRequestLog } from "../relay";
87
+ import {
88
+ corsHeaders,
89
+ managementCorsHeaders,
90
+ isAllowedRequestOrigin,
91
+ isAllowedManagementOrigin,
92
+ isApiAuthRequired,
93
+ jsonResponse,
94
+ admissionFields,
95
+ resolveApiAuth,
96
+ resolveResponsesApiAuth,
97
+ type RequestPolicyView,
98
+ withCors,
99
+ withManagementCors,
100
+ } from "../auth-cors";
101
+ import {
102
+ disableResponsesRequestTimeout,
103
+ handleResponses,
104
+ handleResponsesCompact,
105
+ } from "../responses";
106
+ import {
107
+ handleClaudeCountTokens,
108
+ handleClaudeMessages,
109
+ } from "../claude-messages";
110
+ import { handleChatCompletions } from "../chat-completions";
111
+ import { anthropicErrorResponse } from "../../claude/outbound";
112
+ import {
113
+ buildDesktop3pRegistry,
114
+ generateDesktop3pModels,
115
+ } from "../../claude/desktop-3p";
116
+ import { buildDesktopDiscoveryInputs } from "../../claude/desktop-discovery-inputs";
117
+ import { handleImages } from "../images";
118
+ import {
119
+ handleLive,
120
+ logLiveSidebandFrame,
121
+ parseLiveSidebandTarget,
122
+ resolveLiveSidebandUpgrade,
123
+ } from "../live";
124
+ import { handleAudioTranscriptions } from "../audio-transcriptions";
125
+ import {
126
+ resolveAudioAdmission,
127
+ TRANSCRIPTION_MODEL,
128
+ } from "../audio-upstream";
129
+ import { resolveAudioClient } from "../audio-client";
130
+ import { resolveDictationSocket } from "../audio-dictation";
131
+ import {
132
+ handleExternalLive,
133
+ resolveExternalLiveSocket,
134
+ } from "../audio-live";
135
+ import {
136
+ EXTERNAL_CALL_PREFIX,
137
+ type LiveCallBindings,
138
+ } from "../live-call-bindings";
139
+ import { clearableDeadline } from "../../lib/abort";
140
+ import { handleSearch } from "../search";
141
+ import { handleContextHistory } from "../context-history";
142
+ import {
143
+ codexCompatibleUrl,
144
+ contextEndpoint,
145
+ contextRelayActivated,
146
+ } from "../../codex/context-compat";
147
+ import {
148
+ fetchAllModels,
149
+ handleManagementAPI,
150
+ VERSION,
151
+ type ManagementApiDeps,
152
+ } from "../management-api";
153
+ import {
154
+ issueGuiSession,
155
+ managementPrincipal,
156
+ requireManagementAuth,
157
+ type ManagementAuthState,
158
+ type ManagementSessionControl,
159
+ } from "../management-auth";
160
+ import {
161
+ LOCAL_ATTESTATION_CHALLENGE_HEADER,
162
+ LOCAL_ATTESTATION_PROOF_HEADER,
163
+ createLocalAttestationProof,
164
+ } from "../../lib/local-management-attestation";
165
+ import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../../lib/system-restart-contract";
166
+ import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../../lib/local-provider-reload-contract";
167
+ import {
168
+ GUI_PAIR_BROWSER_ORIGIN_HEADER,
169
+ GUI_PAIR_CAPABILITY_VERSION,
170
+ GUI_PAIR_PATH,
171
+ } from "../../lib/gui-pair-capability";
172
+ import {
173
+ GuiPairingGrantRateLimitError,
174
+ consumeGuiPairingGrant,
175
+ createGuiPairingGrant,
176
+ } from "../gui-session";
177
+ import { recordCursorSeen } from "../../integrations/cursor-seen";
178
+ import { detectCursorInstalls } from "../../integrations/cursor-detect";
179
+ import { loadCursorEffortTable } from "../../integrations/cursor-effort-table";
180
+ import {
181
+ expandCursorEffortRow,
182
+ knownEffortRowIds,
183
+ } from "../effort-row";
184
+ import {
185
+ catalogFastRowEligible,
186
+ expandFastRow,
187
+ } from "../fast-row";
188
+ import type { OcxConfig } from "../../types";
189
+ import type { PackageTreeIntegrityGuard } from "../../lib/package-tree-integrity";
190
+ import type { ReadinessGate } from "../readiness";
191
+ import type { WorkflowRefusalLog } from "../workflow-refusal";
192
+
193
+ import { readyProtocolMetadata } from "../../remote/protocol";
194
+ import { modelCapabilityFields } from "../models-capabilities";
195
+ import { createWebsocketHandler } from "./websocket-handler";
196
+
197
+ export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management";
198
+
199
+ export interface ServeOptionsContext {
200
+ readonly server: Server<WsData>;
201
+ readonly boundPort: number | null;
202
+ readonly remoteWorkspaceStopping: boolean;
203
+
204
+ drainingResponse: (req: Request, policy: RequestPolicyView) => Response;
205
+ ingressForServer: (requestServer: Server<WsData>) => ServerIngress;
206
+ loopbackRouteAllowed: (url: URL, req: Request) => boolean;
207
+ managementIngressRouteAllowed: (url: URL, req: Request) => boolean;
208
+ packageTreeChangedResponse: (
209
+ req: Request,
210
+ policy: RequestPolicyView,
211
+ message: string,
212
+ ) => Response;
213
+ serverBusyResponse: (
214
+ req: Request,
215
+ resource: string,
216
+ policy: RequestPolicyView,
217
+ ) => Response;
218
+ runAdmittedHttpTurn: (
219
+ req: Request,
220
+ policy: RequestPolicyView,
221
+ work: (lease: ActiveTurnLease) => Promise<Response>,
222
+ refusalLog?: WorkflowRefusalLog,
223
+ ) => Promise<Response>;
224
+
225
+ config: OcxConfig;
226
+ inboundBodyLimitBytes: number;
227
+ listenPort: number;
228
+ liveCallBindings: LiveCallBindings;
229
+ loadRemoteWorkspaceRuntime: () => Promise<
230
+ typeof import("../../remote-control/workspace-runtime")
231
+ >;
232
+ localAttestationSecret: string;
233
+ loopbackPolicy: () => RequestPolicyView;
234
+ managementApiDeps: ManagementApiDeps;
235
+ managementAuth: ManagementAuthState;
236
+ managementSessionControl: ManagementSessionControl;
237
+ packageTreeIntegrity: PackageTreeIntegrityGuard;
238
+ readinessGate: ReadinessGate;
239
+
240
+ deps: StartServerDeps;
241
+ port: number | undefined;
242
+ }
243
+
244
+ export function createServeOptions(ctx: ServeOptionsContext) {
245
+ const {
246
+ drainingResponse,
247
+ ingressForServer,
248
+ loopbackRouteAllowed,
249
+ managementIngressRouteAllowed,
250
+ packageTreeChangedResponse,
251
+ serverBusyResponse,
252
+ runAdmittedHttpTurn,
253
+ config,
254
+ inboundBodyLimitBytes,
255
+ listenPort,
256
+ liveCallBindings,
257
+ loadRemoteWorkspaceRuntime,
258
+ localAttestationSecret,
259
+ loopbackPolicy,
260
+ managementApiDeps,
261
+ managementAuth,
262
+ managementSessionControl,
263
+ packageTreeIntegrity,
264
+ readinessGate,
265
+ deps,
266
+ port,
267
+ } = ctx;
268
+ void port;
269
+ const serveOptions = {
270
+ idleTimeout: 255,
271
+ // Bun rejects an oversized body before `fetch` runs, so the listener has to be raised
272
+ // with the admission limit or the opt-in would do nothing. Fixed at bind time: a live
273
+ // `maxInboundBodyBytes` edit needs a restart, which the config doc states.
274
+ maxRequestBodySize: inboundBodyLimitBytes,
275
+ async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
276
+ const ingress = ingressForServer(requestServer);
277
+ // The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
278
+ // else. Rejecting here, before any handler runs, is what keeps the surface from growing
279
+ // silently when a route is added below.
280
+ if (ingress === "unauthenticated-loopback" && !loopbackRouteAllowed(codexCompatibleUrl(req.url), req)) {
281
+ return withCors(
282
+ formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
283
+ req,
284
+ loopbackPolicy(),
285
+ );
286
+ }
287
+ // Tailscale Serve terminates only on this separately bound loopback socket. Reject before
288
+ // dispatch so no data, readiness, health, WebSocket, or unknown-static handler can run.
289
+ if (ingress === "hub-management" && !managementIngressRouteAllowed(codexCompatibleUrl(req.url), req)) {
290
+ return withCors(
291
+ formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
292
+ req,
293
+ config,
294
+ );
295
+ }
296
+ // Auth and CORS decisions below read `policy`, not `config`. For the public listener the
297
+ // two are the same object, so its behaviour is unchanged; for the loopback listener the
298
+ // view substitutes 127.0.0.1 as the bind address, which is what routes it through the
299
+ // same code path a plain loopback bind has always taken — Host-header check included.
300
+ // Routing, provider selection and response bodies keep using `config`.
301
+ const policy: RequestPolicyView = ingress === "unauthenticated-loopback" ? loopbackPolicy() : config;
302
+ const url = codexCompatibleUrl(req.url);
303
+ markActivity(`${req.method} ${url.pathname}`);
304
+
305
+ // Readiness is exact-GET on the literal /readyz path. Compare the DECODED
306
+ // pathname so an encoded variant like /readyz%2F (which decodes to
307
+ // /readyz/) cannot bypass the exact-path rejection and reach the GUI
308
+ // fallback (serveGuiFile decodes the pathname and would serve index.html
309
+ // with 200). Malformed percent-sequences fall back to the raw pathname,
310
+ // which still cannot match the exact literal below.
311
+ let readyzPath: string | undefined;
312
+ try {
313
+ const decoded = decodeURIComponent(url.pathname);
314
+ if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
315
+ } catch { /* malformed encoding — not a readiness path */ }
316
+
317
+ const packageTreeStatus = packageTreeIntegrity.status();
318
+ if (!packageTreeStatus.ok && (
319
+ url.pathname === "/healthz"
320
+ || readyzPath !== undefined
321
+ || url.pathname.startsWith("/v1/")
322
+ )) {
323
+ const message = "OpenCodex package files changed while this proxy was running; restart OpenCodex before retrying.";
324
+ const response = url.pathname === "/healthz" || readyzPath !== undefined
325
+ ? jsonResponse({
326
+ status: "restart_required",
327
+ service: "opencodex",
328
+ version: VERSION,
329
+ uptime: process.uptime(),
330
+ pid: process.pid,
331
+ port: ctx.boundPort ?? requestServer.port ?? listenPort,
332
+ error: { code: "package_tree_changed", message },
333
+ }, 503, req, policy)
334
+ : packageTreeChangedResponse(req, policy, message);
335
+ const headers = new Headers(response.headers);
336
+ headers.set("Retry-After", "5");
337
+ return new Response(response.body, { status: 503, headers });
338
+ }
339
+
340
+ if (req.method === "OPTIONS") {
341
+ // /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
342
+ // path) must answer the deterministic JSON 404, never the generic 204
343
+ // preflight response that the SPA fallback would otherwise allow.
344
+ if (readyzPath !== undefined) {
345
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
346
+ }
347
+ const managementPreflight = url.pathname.startsWith("/api/");
348
+ const allowed = managementPreflight
349
+ ? isAllowedManagementOrigin(req, config)
350
+ : isAllowedRequestOrigin(req, policy);
351
+ if (!allowed) {
352
+ return new Response(null, { status: 403, headers: corsHeaders() });
353
+ }
354
+ return new Response(null, {
355
+ status: 204,
356
+ headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, policy),
357
+ });
358
+ }
359
+
360
+ // An OCX-only executor exchanges one short-lived pairing code for a device-scoped
361
+ // token. This is intentionally outside /api: management auth belongs to the browser
362
+ // that created the grant, while the new device owns only that one-time code.
363
+ if (url.pathname === "/remote-workspace/pair" && req.method === "POST") {
364
+ if (!remoteWorkspaceEnabled(config)) {
365
+ return Response.json({ error: "Remote Workspace is not enabled on this OpenCodex instance." }, { status: 404 });
366
+ }
367
+ // Browser JavaScript must use the authenticated dashboard route. Refusing Origin-bearing
368
+ // requests leaves this exchange to an explicit OCX device process and avoids turning a
369
+ // copied pairing code into a cross-site enrollment action.
370
+ if (req.headers.get("origin") !== null) {
371
+ return Response.json({ error: "Remote Workspace device pairing does not accept browser-origin requests." }, {
372
+ status: 403,
373
+ headers: { "cache-control": "no-store" },
374
+ });
375
+ }
376
+ const [{ remoteWorkspaceHubForConfig }, { RemoteWorkspacePairingRateLimitError }] = await Promise.all([
377
+ loadRemoteWorkspaceRuntime(),
378
+ import("../../remote-control/workspace-hub"),
379
+ ]);
380
+ if (ctx.remoteWorkspaceStopping) return Response.json({ error: "Remote Workspace is stopping." }, { status: 503 });
381
+ const hub = deps.managementApi?.remoteWorkspaceHub ?? remoteWorkspaceHubForConfig(config);
382
+ // A loopback socket alone cannot prove that Tailscale Serve supplied its identity header:
383
+ // another local process can connect directly and forge it. Pairing therefore uses only the
384
+ // kernel-observed peer on every listener; proxied management users intentionally share the
385
+ // loopback bucket rather than gaining a header-rotation bypass.
386
+ const peer = requestServer.requestIP(req)?.address ?? "unknown";
387
+ const pairingSource = `${ingress}:${peer}`;
388
+ const rateLimitResponse = (error: unknown): Response | null => {
389
+ if (!(error instanceof RemoteWorkspacePairingRateLimitError)) return null;
390
+ return Response.json({ error: "Remote Workspace pairing is temporarily rate limited." }, {
391
+ status: 429,
392
+ headers: {
393
+ "cache-control": "no-store",
394
+ "retry-after": String(error.retryAfterSeconds),
395
+ },
396
+ });
397
+ };
398
+ try {
399
+ // Check the existing source block before reading or parsing an attacker-controlled body.
400
+ // pairDevice checks again after the await and records only code-shaped authentication
401
+ // failures, so malformed JSON cannot allocate one limiter entry per request.
402
+ hub.assertPairingSourceAllowed(pairingSource);
403
+ } catch (error) {
404
+ const limited = rateLimitResponse(error);
405
+ if (limited) return limited;
406
+ throw error;
407
+ }
408
+ const declaredLength = Number(req.headers.get("content-length") ?? "0");
409
+ if (!Number.isFinite(declaredLength) || declaredLength > REMOTE_WORKSPACE_PAIRING_BODY_LIMIT) {
410
+ return Response.json({ error: "Remote Workspace pairing body is too large." }, { status: 413 });
411
+ }
412
+ const text = await readBoundedRequestText(req, REMOTE_WORKSPACE_PAIRING_BODY_LIMIT);
413
+ if (text === null) return Response.json({ error: "Remote Workspace pairing body is too large." }, { status: 413 });
414
+ if (ctx.remoteWorkspaceStopping) return Response.json({ error: "Remote Workspace is stopping." }, { status: 503 });
415
+ let body: unknown;
416
+ try { body = JSON.parse(text); }
417
+ catch { return Response.json({ error: "Invalid Remote Workspace pairing request." }, { status: 400 }); }
418
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
419
+ return Response.json({ error: "Invalid Remote Workspace pairing request." }, { status: 400 });
420
+ }
421
+ const record = body as Record<string, unknown>;
422
+ const required = ["code", "name", "platform", "publicKey", "roots"];
423
+ const allowed = new Set([...required, "capabilities"]);
424
+ if (required.some(key => !Object.hasOwn(record, key))
425
+ || Object.keys(record).some(key => !allowed.has(key))) {
426
+ return Response.json({ error: "Invalid Remote Workspace pairing request." }, { status: 400 });
427
+ }
428
+ try {
429
+ const paired = hub.pairDevice(record, pairingSource);
430
+ return Response.json(paired, { status: 201, headers: { "cache-control": "no-store" } });
431
+ } catch (error) {
432
+ const limited = rateLimitResponse(error);
433
+ if (limited) return limited;
434
+ const message = error instanceof Error ? error.message : "Remote Workspace pairing failed.";
435
+ const conflict = /already in use|limit reached/i.test(message);
436
+ return Response.json({ error: message }, {
437
+ status: conflict ? 409 : 401,
438
+ headers: { "cache-control": "no-store" },
439
+ });
440
+ }
441
+ }
442
+
443
+ // Each executor holds one device-scoped bearer and opens one outbound WSS. The token is
444
+ // authenticated only at upgrade and never enters ws.data; subsequent frames are bound to
445
+ // the device identity and per-session signed E2EE handshake.
446
+ if (url.pathname === "/remote-workspace/agent" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
447
+ if (!remoteWorkspaceEnabled(config) || req.headers.get("origin") !== null) {
448
+ return Response.json({ error: "Remote Workspace agent upgrade refused." }, { status: 403 });
449
+ }
450
+ const authorization = req.headers.get("authorization") ?? "";
451
+ const match = /^Bearer (ocxrw_[A-Za-z0-9_-]{43})$/.exec(authorization);
452
+ if (!match) return Response.json({ error: "Remote Workspace device authentication required." }, { status: 401 });
453
+ const { remoteWorkspaceHubForConfig } = await loadRemoteWorkspaceRuntime();
454
+ const { RemoteWorkspaceHubAgentConnection } = await import("../../remote-control/workspace-agent-connection");
455
+ if (ctx.remoteWorkspaceStopping) return Response.json({ error: "Remote Workspace is stopping." }, { status: 503 });
456
+ const hub = deps.managementApi?.remoteWorkspaceHub ?? remoteWorkspaceHubForConfig(config);
457
+ const device = hub.authenticateDeviceToken(match[1]!);
458
+ if (!device) return Response.json({ error: "Remote Workspace device authentication failed." }, { status: 401 });
459
+ const upgraded = requestServer.upgrade(req, {
460
+ data: {
461
+ kind: "remote-workspace-agent",
462
+ remoteWorkspaceOpen: socket => {
463
+ const connection = new RemoteWorkspaceHubAgentConnection({
464
+ deviceId: device.id,
465
+ devicePublicKey: device.publicKey,
466
+ hubIdentity: hub.identity(),
467
+ capabilities: device.capabilities,
468
+ onCapabilities: capabilities => hub.updateDeviceCapabilities(device.id, capabilities),
469
+ socket: {
470
+ send: value => {
471
+ if (socket.send(value) === 0) throw new Error("remote workspace socket send dropped");
472
+ },
473
+ close: (code, reason) => socket.close(code, reason),
474
+ },
475
+ });
476
+ hub.attachConnection(device.id, connection);
477
+ socket.data.remoteWorkspaceClose = () => hub.detachConnection(device.id, connection);
478
+ return connection;
479
+ },
480
+ } satisfies WsData,
481
+ });
482
+ return upgraded
483
+ ? undefined as unknown as Response
484
+ : Response.json({ error: "Remote Workspace WebSocket upgrade failed." }, { status: 426 });
485
+ }
486
+
487
+ // Responses WebSocket (phase 120.2). Codex upgrades the same /v1/responses path; auth is
488
+ // handshake-time only, so capture inbound headers and thread them into the pipeline.
489
+ if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
490
+ if (isDraining()) {
491
+ return drainingResponse(req, policy);
492
+ }
493
+ const admission = resolveResponsesApiAuth(req, policy);
494
+ if (!admission) {
495
+ return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
496
+ }
497
+ if (!isAllowedRequestOrigin(req, policy)) {
498
+ return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
499
+ }
500
+ // WS transport gate: Codex's built-in `openai` provider hardcodes supports_websockets=true,
501
+ // so under Design B it always tries the WS transport first. When the feature is off, reject
502
+ // the upgrade with 426 — codex-rs maps a connect-time UPGRADE_REQUIRED to a clean
503
+ // session-scoped HTTP fallback (client.rs WebsocketStreamOutcome::FallbackToHttp) instead of
504
+ // surfacing broken-pipe errors from sockets a "disabled" feature would otherwise accept.
505
+ if (!websocketsEnabled(config)) {
506
+ return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, policy);
507
+ }
508
+ const websocketLease = tryReserveCodexWebSocket();
509
+ if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets", policy);
510
+ // Upgrade on the server that RECEIVED this request, not the captured `server`
511
+ // binding. They are the same object for the public listener, but the
512
+ // unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
513
+ // request to the public server's upgrade would fail or cross sockets.
514
+ if (requestServer.upgrade(req, {
515
+ data: buildResponsesWsData(
516
+ selectForwardHeaders(req.headers),
517
+ admission,
518
+ websocketLease,
519
+ sessionLaneIdFromRequest(req.headers),
520
+ ),
521
+ })) return undefined as unknown as Response;
522
+ websocketLease.release();
523
+ return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
524
+ }
525
+
526
+ if (url.pathname === "/healthz" && req.method === "GET") {
527
+ // service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
528
+ const healthPort = ctx.server.port ?? listenPort;
529
+ const response = jsonResponse({
530
+ status: "ok",
531
+ service: "opencodex",
532
+ version: VERSION,
533
+ uptime: process.uptime(),
534
+ pid: process.pid,
535
+ port: healthPort,
536
+ restartCapability: SYSTEM_RESTART_CAPABILITY_VERSION,
537
+ providerReloadCapability: LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION,
538
+ guiPairCapability: GUI_PAIR_CAPABILITY_VERSION,
539
+ }, 200, req, policy);
540
+ const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
541
+ if (challenge) {
542
+ const proof = createLocalAttestationProof(localAttestationSecret, challenge, process.pid, healthPort);
543
+ if (proof) response.headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
544
+ }
545
+ return response;
546
+ }
547
+
548
+ // Readiness: like /healthz this is exact GET and unauthenticated (so a client can
549
+ // back off BEFORE knowing the admission token), but stricter than liveness. The
550
+ // body carries only sanitized identity + the fixed status enum; the sync message,
551
+ // warning text, catalog path, provider output, and account data are never exposed.
552
+ // POST or "/readyz/" must NOT match (exact pathname + GET method): answer them
553
+ // with a JSON 404 here so they can never be silently accepted by the GUI SPA
554
+ // fallback (which would serve index.html with HTTP 200 once gui/dist exists).
555
+ if (readyzPath !== undefined) {
556
+ if (readyzPath !== "/readyz" || req.method !== "GET") {
557
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
558
+ }
559
+ // A draining proxy must never advertise ready: every data-plane branch
560
+ // answers drainingResponse while isDraining() is set, but the one-shot
561
+ // readiness gate is not mutated on shutdown (it is owned by the startup
562
+ // sync). Report pending so `ocx ready --wait` and external supervisors
563
+ // keep polling instead of promoting a proxy that is draining.
564
+ const status = isDraining() ? "pending" : readinessGate.getStatus();
565
+ const body = {
566
+ service: "opencodex",
567
+ version: VERSION,
568
+ uptime: process.uptime(),
569
+ pid: process.pid,
570
+ port: ctx.boundPort ?? listenPort,
571
+ status,
572
+ ...readyProtocolMetadata(config, req),
573
+ };
574
+ if (status === "ready") {
575
+ return jsonResponse(body, 200, req, policy);
576
+ }
577
+ // Pending/failed: 503 with a conservative Retry-After so well-behaved clients
578
+ // (and `ocx ready --wait`) back off instead of hot-looping.
579
+ const resp = jsonResponse(body, 503, req, policy);
580
+ const headers = new Headers(resp.headers);
581
+ headers.set("Retry-After", "1");
582
+ return new Response(resp.body, { status: 503, headers });
583
+ }
584
+
585
+ if (url.pathname.startsWith("/api/")) {
586
+ const localManagementAuth = {
587
+ attestationSecret: localAttestationSecret,
588
+ pid: process.pid,
589
+ port: ctx.boundPort ?? requestServer.port ?? listenPort,
590
+ };
591
+ const apiAuthError = requireManagementAuth(req, managementAuth, config, localManagementAuth);
592
+ if (apiAuthError) return withManagementCors(apiAuthError, req, config);
593
+ // Which credential passed the gate, resolved from the same session table the
594
+ // gate used. Consent-bearing routes need this: request headers are forgeable
595
+ // by anything holding the admin token, the credential is not.
596
+ const principal = managementPrincipal(req, managementAuth, config, localManagementAuth) ?? undefined;
597
+ if (url.pathname === GUI_PAIR_PATH) {
598
+ if (req.method !== "POST" || principal !== "gui-pair-capability" || !managementAuth.available) {
599
+ return withManagementCors(Response.json({ error: "GUI pairing capability required" }, { status: 403 }), req, config);
600
+ }
601
+ try {
602
+ const grant = createGuiPairingGrant(
603
+ req.headers.get(GUI_PAIR_BROWSER_ORIGIN_HEADER) ?? "",
604
+ config,
605
+ managementAuth,
606
+ );
607
+ return withManagementCors(Response.json(grant, {
608
+ status: 201,
609
+ headers: { "Cache-Control": "no-store" },
610
+ }), req, config);
611
+ } catch (error) {
612
+ const status = error instanceof GuiPairingGrantRateLimitError ? 429 : 403;
613
+ return withManagementCors(Response.json({ error: "GUI pairing grant refused" }, {
614
+ status,
615
+ ...(status === 429 ? { headers: { "Retry-After": "60" } } : {}),
616
+ }), req, config);
617
+ }
618
+ }
619
+ const mgmtResponse = await handleManagementAPI(req, url, config, managementApiDeps, principal, managementSessionControl);
620
+ if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
621
+ return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
622
+ }
623
+
624
+ if (url.pathname === "/v1/catalog" && (req.method === "GET" || req.method === "HEAD")) {
625
+ // #809: remote Codex clients need the model catalog, and the only prior source was
626
+ // GET /api/catalog behind management auth — so operators had to hand out an admin
627
+ // token to read a list of models. This route fixes that on the data plane instead of
628
+ // widening /api/*, which stays exactly as restricted as before.
629
+ //
630
+ // resolveApiAuth (not resolveResponsesApiAuth) for the same reason /v1/models uses
631
+ // it: nothing here forwards a caller credential upstream, so accepting the dedicated
632
+ // header, a recognized bearer, or x-api-key is safe — and rejecting x-api-key would
633
+ // 401 Anthropic-SDK clients holding a perfectly valid data credential.
634
+ const admission = resolveApiAuth(req, policy);
635
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
636
+ if (!isAllowedRequestOrigin(req, policy)) {
637
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
638
+ }
639
+ const { serializePersistedCatalog, persistedCodexVersion, MAX_REMOTE_CATALOG_BYTES } = await import("../catalog-download");
640
+ const serialized = await serializePersistedCatalog();
641
+ if (serialized.body === null) {
642
+ // Built directly rather than through formatErrorResponse: that helper derives
643
+ // `code` from the status and message via classifyError, and these two need stable,
644
+ // specific codes. `catalog_not_found` in particular is what lets a caller — and
645
+ // tests/server/api-key-attribution.test.ts — tell "this route exists and has no catalog"
646
+ // apart from "this route is gone", which is the difference between admission proof
647
+ // and a vacuous pass.
648
+ return withCors(
649
+ new Response(JSON.stringify({
650
+ error: { type: "invalid_request_error", code: "catalog_not_found", message: "no materialized catalog is available" },
651
+ }), {
652
+ status: 404,
653
+ headers: { "content-type": "application/json" },
654
+ }),
655
+ req,
656
+ policy,
657
+ );
658
+ }
659
+ // Size policy belongs to this route, not the shared serializer: the management route
660
+ // must keep its existing behavior for a catalog of any supported size.
661
+ if (serialized.bytes !== undefined && serialized.bytes > MAX_REMOTE_CATALOG_BYTES) {
662
+ return withCors(
663
+ new Response(JSON.stringify({
664
+ error: { type: "server_error", code: "catalog_too_large", message: "catalog exceeds the maximum served size" },
665
+ }), {
666
+ status: 507,
667
+ headers: { "content-type": "application/json" },
668
+ }),
669
+ req,
670
+ policy,
671
+ );
672
+ }
673
+ const headers: Record<string, string> = {
674
+ "content-type": "application/json",
675
+ // Identity-varying content behind a credential: never let a shared cache keep it,
676
+ // and never hand out a validator it could revalidate with. `no-cache` alone does
677
+ // not prevent storage — it forces revalidation, and the revalidation is exactly
678
+ // what would cross identities here, because this body varies by key type and key
679
+ // id while the ETag would be derived from bytes alone. A store keyed on URL plus
680
+ // validator could then serve one credential's representation to another. Proving
681
+ // an identity-partitioned cache key across every intermediary in the path is a
682
+ // much larger commitment than the bandwidth a 304 saves on this payload, so this
683
+ // route declines the trade: no-store, no ETag, no 304.
684
+ //
685
+ // GET /api/catalog keeps its validator. That route is management-authenticated
686
+ // and loopback-scoped, and its representation does not vary by data-key identity.
687
+ "cache-control": "no-store",
688
+ };
689
+ const version = await persistedCodexVersion();
690
+ if (version) headers["x-opencodex-codex-version"] = version;
691
+ // No conditional handling: with no validator emitted, an If-None-Match on this route
692
+ // can only have been guessed or copied from elsewhere, and honoring it would
693
+ // reintroduce the cross-identity path above. Every request gets the full body.
694
+ if (serialized.bytes !== undefined) headers["content-length"] = String(serialized.bytes);
695
+ // HEAD returns identical status and headers with no body.
696
+ return withRemoteCatalogKeyId(
697
+ withCors(
698
+ new Response(req.method === "HEAD" ? null : serialized.body, { status: 200, headers }),
699
+ req,
700
+ policy,
701
+ ),
702
+ admission,
703
+ );
704
+ }
705
+
706
+ if (url.pathname === "/v1/usage" && req.method === "GET") {
707
+ const { handleHubUsage } = await import("../hub-usage");
708
+ return handleHubUsage(req, config, policy);
709
+ }
710
+
711
+ if (url.pathname === "/v1/hub-state" && (req.method === "GET" || req.method === "HEAD")) {
712
+ // #4236: a connected client had no way to learn which providers this hub can actually
713
+ // serve, so `ocx status` on the client reported the CLIENT's empty credential store as
714
+ // if it were the truth — "xai ✗ not logged in" on a machine whose hub has xAI logged
715
+ // in. The fix is one least-privilege data-plane read, in the /v1/catalog (#809)
716
+ // tradition: same admission resolver, same origin check, no parameters, no caller
717
+ // credential forwarded upstream, and a body of booleans plus model ids. Widening
718
+ // `/api/*` or handing the client an admin token to read `GET /api/providers` would
719
+ // have traded a reporting defect for a credential one.
720
+ //
721
+ // What it discloses beyond /v1/catalog and /v1/models, exactly: `hasCredential`,
722
+ // `loggedIn`, `authMode`, the featured roster, and the NAME and adapter of an ENABLED
723
+ // provider those routes omit for want of a usable credential — which is the point of
724
+ // the route. A `disabled` provider is NOT exported (`buildHubState` drops it), because
725
+ // the catalog filters it out too and naming it here would be the only place a data key
726
+ // learns of it.
727
+ //
728
+ // Placed between /v1/catalog and /v1/models so all three least-privilege client reads
729
+ // stay in sight of each other.
730
+ const admission = resolveApiAuth(req, policy);
731
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
732
+ if (!isAllowedRequestOrigin(req, policy)) {
733
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
734
+ }
735
+ // Role gate AFTER admission, deliberately: answering an unauthenticated caller would
736
+ // turn this into a free "is that machine a hub?" probe. A standalone or client install
737
+ // gains no surface at all — the route simply does not exist there.
738
+ //
739
+ // Built, not formatErrorResponse'd, for the same reason /v1/catalog builds its 404: the
740
+ // code has to distinguish "this route exists and this host is not a hub" from "this
741
+ // build has no such route", which is the difference between admission proof and a
742
+ // vacuous pass in tests/server/api-key-attribution.test.ts.
743
+ if (config.runtimeRole !== "hub") {
744
+ return withCors(
745
+ new Response(JSON.stringify({
746
+ error: {
747
+ type: "invalid_request_error",
748
+ code: "hub_state_not_a_hub",
749
+ message: "hub state is served only by a host whose runtimeRole is hub",
750
+ },
751
+ }), { status: 404, headers: { "content-type": "application/json" } }),
752
+ req,
753
+ policy,
754
+ );
755
+ }
756
+ const { buildHubState } = await import("../hub-state");
757
+ const { MAX_HUB_STATE_BYTES } = await import("../../remote/hub-state");
758
+ const { oauthLoginSummary } = await import("../../oauth");
759
+ // `true` masks emails, but the projection drops the field entirely; passing the mask
760
+ // anyway means a future refactor that starts copying fields cannot leak a raw address.
761
+ const body = JSON.stringify(buildHubState(config, oauthLoginSummary(true), VERSION));
762
+ const bytes = Buffer.byteLength(body);
763
+ if (bytes > MAX_HUB_STATE_BYTES) {
764
+ return withCors(
765
+ new Response(JSON.stringify({
766
+ error: { type: "server_error", code: "hub_state_too_large", message: "hub state exceeds the maximum served size" },
767
+ }), { status: 507, headers: { "content-type": "application/json" } }),
768
+ req,
769
+ policy,
770
+ );
771
+ }
772
+ return withCors(
773
+ new Response(req.method === "HEAD" ? null : body, {
774
+ status: 200,
775
+ headers: {
776
+ "content-type": "application/json",
777
+ // Varies by credential-bearing identity and by live login state: never cached,
778
+ // and no validator to revalidate with (same rule as /v1/catalog).
779
+ "cache-control": "no-store",
780
+ "content-length": String(bytes),
781
+ },
782
+ }),
783
+ req,
784
+ policy,
785
+ );
786
+ }
787
+
788
+ if (url.pathname === "/v1/models" && req.method === "GET") {
789
+ // #809: the catalog read sits immediately before model discovery because it shares
790
+ // that route's admission rationale exactly. Keep them adjacent so a future change to
791
+ // one is made in sight of the other.
792
+ // Model discovery never forwards Authorization upstream, so the broader admission
793
+ // set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
794
+ // remote OpenAI-style bearer clients and Claude gateway discovery (anthropic-version).
795
+ const admission = resolveApiAuth(req, policy);
796
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
797
+ if (!isAllowedRequestOrigin(req, policy)) {
798
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
799
+ }
800
+ const wantsDesktopConfig = url.searchParams.get("format") === "desktop-config";
801
+ if (wantsDesktopConfig && (url.searchParams.get("ids") === "cli" || url.searchParams.has("client_version"))) {
802
+ return jsonResponse({ error: "Desktop config format cannot use CLI or client-version selectors" }, 400, req, policy);
803
+ }
804
+ // The Integrations page reports whether a Cursor client has reached this proxy; the
805
+ // recorder keeps only a bounded User-Agent value and a timestamp, in memory.
806
+ recordCursorSeen(req.headers);
807
+ let goModels;
808
+ let modelEntitlements;
809
+ try {
810
+ [goModels, modelEntitlements] = await Promise.all([
811
+ fetchAllModels(config),
812
+ // Codex sends its own client_version on this request, and upstream filters the
813
+ // entitlement roster by it. Passing it through is what stops an entitled account
814
+ // being told it cannot use models a newer client can (#2886).
815
+ resolveCodexModelEntitlements(config, { clientVersion: url.searchParams.get("client_version") }),
816
+ ]);
817
+ } catch (error) {
818
+ if (error instanceof CatalogGatherBusyError) {
819
+ return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
820
+ status: 503,
821
+ headers: { "content-type": "application/json", "Retry-After": "1" },
822
+ }), req, policy);
823
+ }
824
+ throw error;
825
+ }
826
+ const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxOutputTokens, nativeOpenAiContextTier, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
827
+ const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../../codex/catalog/native-models");
828
+ const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
829
+ const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
830
+ const bareEligibleAccountIds = providerCodexAccountMode(
831
+ OPENAI_CODEX_PROVIDER_ID,
832
+ config.providers[OPENAI_CODEX_PROVIDER_ID],
833
+ ) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
834
+ const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
835
+ modelEntitlements,
836
+ bareEligibleAccountIds,
837
+ );
838
+ const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
839
+ const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
840
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
841
+ ));
842
+ const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
843
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
844
+ ));
845
+ const nativeSlugs = includeNativeOpenAi
846
+ ? nativeOpenAiSlugs().filter(slug => (
847
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
848
+ ))
849
+ : [];
850
+ const disabledNatives = disabledNativeSlugs(config);
851
+ const disabledModels = new Set(config.disabledModels ?? []);
852
+ const exactComboSlugs = exactComboCatalogSlugs(config);
853
+ const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
854
+ const suppressedBareNativeSlugs = new Set([
855
+ ...desktopAllowlistSuppressedNativeSlugs(config),
856
+ ...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
857
+ ]);
858
+ const accountSelectors = includeAccountBoundNativeOpenAi
859
+ ? visibleCodexAccountSelectors(config)
860
+ : [];
861
+ const accountTargets = new Map(codexAccountNamespaceEntries(config));
862
+ const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
863
+ ? new Map([...accountBoundNativeOpenAiSlugsBySelector(config)].map(([selector, slugs]) => {
864
+ const target = accountTargets.get(selector);
865
+ const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
866
+ return [selector, slugs.filter(slug => (
867
+ !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)
868
+ || (accountId !== undefined
869
+ && codexModelEntitlementStateForAccount(modelEntitlements, accountId, slug) === "granted")
870
+ ))] as const;
871
+ }))
872
+ : new Map<string, readonly string[]>();
873
+ const accountNativeSlugs = [...new Set(
874
+ [...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
875
+ )];
876
+ const desktopInputs = buildDesktopDiscoveryInputs({
877
+ config, models: goModels, modelEntitlements,
878
+ desktopNativeCandidates: desktopVisibleNativeSlugs(config),
879
+ });
880
+ const desktopNativeSlugs = desktopInputs.nativeSlugs;
881
+ const goOrdered = desktopInputs.routedModels;
882
+ // Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
883
+ // Anthropic-style headers; 003 G1-G8 + devlog 131). Entries use the official
884
+ // ModelInfo shape incl. capabilities (effort ladder / thinking) — Desktop 3P can
885
+ // only learn capabilities through discovery, and Claude Code 2.1.207 strips the
886
+ // extra fields (backward-safe). Ids are the claude-opus-4-8-{code} Desktop
887
+ // aliases; legacy claude-ocx-* ids keep decoding via resolveAlias. Detection:
888
+ // anthropic-version header (Claude Code sends it) or explicit ?flavor=anthropic.
889
+ // Codex catalog (client_version) and the OpenAI list shape below stay byte-identical.
890
+ const wantsAnthropicList = wantsDesktopConfig || req.headers.get("anthropic-version") !== null
891
+ || url.searchParams.get("flavor") === "anthropic";
892
+ /**
893
+ * Whether a NATIVE slug may carry a Fast sibling.
894
+ *
895
+ * Both halves are required. Upstream asserts the tier per model — the same
896
+ * `additional_speed_tiers` the Codex picker's own toggle is built from — but an
897
+ * operator capability override or the final wire resolution can still make the
898
+ * route ineligible, and `decideTier` would then drop the tier the row advertised.
899
+ *
900
+ * Declared here, above the Claude discovery call, because that call reads it while
901
+ * the raw OpenAI mapper further down does too; defining it there would leave this
902
+ * use in its temporal dead zone.
903
+ */
904
+ const nativeFastEligible = (metadataId: string): boolean =>
905
+ catalogFastRowEligible(config, { provider: OPENAI_CODEX_PROVIDER_ID, id: metadataId, native: true });
906
+
907
+ /**
908
+ * Whether a routed catalog row may carry a Fast sibling.
909
+ *
910
+ * A combo is its own namespace with no `config.providers` entry — declaring a
911
+ * provider named `combo` is rejected (combos/types.ts:191) — so provider lookup
912
+ * cannot classify it. Its aggregated `supportsServiceTier` is already true only
913
+ * when EVERY member supports the tier (aggregation.ts:201), which is the right
914
+ * rule for a row that fans out to all of them.
915
+ *
916
+ * Declared beside nativeFastEligible, above the Claude discovery call that reads
917
+ * both; defining it near the raw OpenAI mapper below would leave that use in its
918
+ * temporal dead zone.
919
+ */
920
+ const catalogRowFastEligible = (m: { provider: string; id: string; supportsServiceTier?: boolean }): boolean =>
921
+ catalogFastRowEligible(config, m);
922
+
923
+ if (wantsAnthropicList && !url.searchParams.has("client_version")) {
924
+ if (wantsDesktopConfig) {
925
+ const models = config.claudeCode?.enabled === false ? [] : generateDesktop3pModels(
926
+ desktopInputs.nativeSlugs, desktopInputs.routedModels,
927
+ config.claudeCode?.desktopProfile, desktopInputs.nativeContextCap,
928
+ );
929
+ const response = jsonResponse({ version: 1, models }, 200, req, policy);
930
+ response.headers.set("Cache-Control", "no-store");
931
+ return response;
932
+ }
933
+ if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
934
+ // Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
935
+ buildDesktop3pRegistry(
936
+ desktopNativeSlugs,
937
+ desktopInputs.routedModels,
938
+ config.claudeCode?.desktopProfile,
939
+ desktopInputs.nativeContextCap,
940
+ );
941
+ const { buildAnthropicModelInfos } = await import("../../claude/model-info");
942
+ const { resolveAutoContext } = await import("../../claude/context-windows");
943
+ const { activeDesktop3pAlias } = await import("../../claude/desktop-3p");
944
+ // Per-surface id family (devlog 050): explicit ?ids= wins; otherwise the
945
+ // Claude Code CLI discovery UA (`claude-code/<version>`, binary n_()) gets
946
+ // readable claude-ocx ids and every other client (Desktop 3P) keeps the
947
+ // hashed family its config was written with. Unknown UA -> hashed (safe).
948
+ const idsParam = url.searchParams.get("ids");
949
+ const idStyle = idsParam === "cli"
950
+ ? "readable" as const
951
+ : idsParam === "desktop"
952
+ ? "desktop3p" as const
953
+ : (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
954
+ const data = buildAnthropicModelInfos(
955
+ desktopNativeSlugs,
956
+ goOrdered,
957
+ resolveAutoContext(config.claudeCode),
958
+ idStyle,
959
+ activeDesktop3pAlias,
960
+ desktopInputs.nativeContextCap,
961
+ config.fastMode,
962
+ // Explicit opt-out omits the Fast predicate.
963
+ config.fastRows !== false
964
+ ? (model: { provider: string; id: string; supportsServiceTier?: boolean }) =>
965
+ model.provider === "native"
966
+ ? nativeFastEligible(model.id)
967
+ : catalogRowFastEligible(model)
968
+ : undefined,
969
+ { modelPickerOrder: config.modelPickerOrder, featured: config.subagentModels },
970
+ );
971
+ return jsonResponse({ data }, 200, req, policy);
972
+ }
973
+ if (url.searchParams.has("client_version")) {
974
+ // Codex client → Codex catalog shape: native gpt + namespaced routed models,
975
+ // cloned from a native template so required fields (base_instructions, etc.) are present.
976
+ // Pass the subagent picks so featured models lead by priority (matches the on-disk file).
977
+ // Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
978
+ // on-disk sync; codex-rs keeps them out of the picker itself).
979
+ const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
980
+ // Account rows use the same hidden-inclusive supported set as on-disk sync. This lets a
981
+ // newly re-enabled native reappear under each selector before the next sync, while the
982
+ // no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
983
+ const catalogNativeSlugs = accountSelectors.length > 0
984
+ ? [...new Set([
985
+ ...availableAccountNativeSlugs,
986
+ ...accountNativeSlugs,
987
+ ])]
988
+ : nativeSlugs;
989
+ const entries = buildCatalogEntries(
990
+ loadCatalogTemplate(),
991
+ catalogNativeSlugs,
992
+ goOrdered,
993
+ config.subagentModels,
994
+ websocketsEnabled(config),
995
+ maMode as "v1" | "default" | "v2",
996
+ exactComboSlugs,
997
+ accountSelectors,
998
+ suppressedBareNativeSlugs,
999
+ new Set(),
1000
+ nativeContextLimits(config),
1001
+ accountNativeSlugs,
1002
+ accountNativeSlugsBySelector,
1003
+ config.keepNativeChatGptOnV1 === true,
1004
+ config.modelPickerOrder,
1005
+ );
1006
+ return jsonResponse({
1007
+ models: applyNativeVisibility(
1008
+ entries,
1009
+ disabledModels,
1010
+ accountSelectors.length > 0,
1011
+ new Set(accountNativeSlugs),
1012
+ ),
1013
+ }, 200, req, policy);
1014
+ }
1015
+ // OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
1016
+ // (pure availability list — disabled natives are omitted entirely).
1017
+ // Grok Build discovers models through this endpoint too, and its model picker only
1018
+ // enables /effort for entries that advertise the reasoning ladder in the Grok model
1019
+ // catalog shape (supports_reasoning_effort + reasoning_efforts[]). The Codex catalog
1020
+ // branch above already carries the same ladders, so mirror them here — native rows
1021
+ // from the upstream snapshot, routed rows from the configured provider tiers. The
1022
+ // default uses the same canonical fallback as the Codex catalog resolver
1023
+ // (configured default, then medium, then high, then the first tier). Extra fields
1024
+ // are ignored by plain OpenAI clients.
1025
+ const grokEffortOption = (value: string, isDefault: boolean) => ({
1026
+ value,
1027
+ label: `${value[0].toUpperCase()}${value.slice(1)} Effort`,
1028
+ ...(isDefault ? { default: true } : {}),
1029
+ });
1030
+ const grokEffortFields = (efforts: string[], configuredDefault?: string) => {
1031
+ const defaultEffort = grokDefaultReasoningEffort(efforts, configuredDefault);
1032
+ if (defaultEffort === undefined) return {};
1033
+ return {
1034
+ supports_reasoning_effort: true,
1035
+ reasoning_effort: defaultEffort,
1036
+ reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
1037
+ };
1038
+ };
1039
+ // Cursor's local-agent runtime (Private Inference build) reads api_types + capabilities
1040
+ // to enable its effort control; every other consumer ignores them. See
1041
+ // src/server/models-capabilities.ts.
1042
+ const nativeLimits = nativeContextLimits(config);
1043
+ const nativeContextInput = (metadataId: string) => {
1044
+ const tier = nativeOpenAiContextTier(metadataId, nativeLimits);
1045
+ return tier
1046
+ ? { contextWindow: tier.defaultWindow, longContextWindow: tier.longWindow }
1047
+ : { contextWindow: nativeOpenAiContextWindow(metadataId, nativeLimits) };
1048
+ };
1049
+ const nativeModelRow = (id: string, metadataId = id) => ({
1050
+ id,
1051
+ object: "model",
1052
+ created: 0,
1053
+ owned_by: "openai",
1054
+ ...grokEffortFields(
1055
+ nativeReasoningEfforts(metadataId),
1056
+ nativeDefaultReasoningEffort(metadataId),
1057
+ ),
1058
+ ...modelCapabilityFields({
1059
+ reasoningEfforts: nativeReasoningEfforts(metadataId),
1060
+ // Cursor "Max Mode": advertise the family's default/long pair (272k/922k for
1061
+ // GPT-5.6) so the client can pick per request; without a tier, the effective
1062
+ // window is the only value.
1063
+ ...nativeContextInput(metadataId),
1064
+ maxOutputTokens: nativeOpenAiMaxOutputTokens(metadataId),
1065
+ inputModalities: nativeInputModalities(metadataId),
1066
+ }),
1067
+ });
1068
+ // Resolved once per request, not per model: the global fast switch offers the fast
1069
+ // identity to clients that have no Fast toggle of their own. Null when the switch is
1070
+ // off, so the row mapper does no work and loads no adapter module.
1071
+ const cursorFastIdForListing = config.fastMode === true
1072
+ ? await (async () => {
1073
+ const { cursorFastIdFor } = await import("../../adapters/cursor/catalog");
1074
+ return (modelId: string, provider = "cursor") => provider === "cursor" ? cursorFastIdFor(modelId) : undefined;
1075
+ })()
1076
+ : null;
1077
+ // Selector-active discovery follows the same complete supported set as the Codex catalog
1078
+ // for both bare and qualified rows. Without selectors, the live catalog continues to own
1079
+ // bare availability.
1080
+ const selectorNativeSlugs = accountSelectors.length > 0
1081
+ ? availableBareNativeSlugs.filter(slug => !disabledNatives.has(slug))
1082
+ : [];
1083
+ const bareSelectorNativeSlugs = accountSelectors.length > 0
1084
+ ? selectorNativeSlugs
1085
+ : [];
1086
+ const visibleNatives = includeNativeOpenAi
1087
+ ? accountSelectors.length > 0
1088
+ ? bareSelectorNativeSlugs.filter(slug => !shadowedNativeSlugs.has(slug))
1089
+ : visibleNativeSlugs(config)
1090
+ : [];
1091
+ const visibleAccountNatives = accountSelectors.flatMap(selector =>
1092
+ (accountNativeSlugsBySelector.get(selector) ?? []).filter(metadataId => !disabledNatives.has(metadataId)).flatMap(metadataId => {
1093
+ const id = `${selector}/${metadataId}`;
1094
+ return disabledModels.has(id) ? [] : [{ id, metadataId }];
1095
+ })
1096
+ );
1097
+ // The projection is opt-in. Keep the default path free of Cursor install detection,
1098
+ // and resolve the bundle table once for the whole list rather than once per row.
1099
+ const effortRowsEnabled = config.cursorEffortRows === true;
1100
+ // Explicit opt-out skips policy resolution and additional rows.
1101
+ const fastRowsEnabled = config.fastRows !== false;
1102
+ // One inventory serves both grammars; building it twice would double the work on a
1103
+ // hot path for no benefit.
1104
+ const effortRowKnownIds = effortRowsEnabled || fastRowsEnabled
1105
+ ? knownEffortRowIds(config)
1106
+ : undefined;
1107
+ const privateInference = effortRowsEnabled
1108
+ ? detectCursorInstalls().find(install => install.build === "private-inference")
1109
+ : undefined;
1110
+ const cursorEffortTable = effortRowsEnabled
1111
+ ? (deps.managementApi?.loadCursorEffortTable ?? loadCursorEffortTable)(privateInference)
1112
+ : null;
1113
+ const expandedNativeModelRow = (id: string, metadataId = id) => {
1114
+ const reasoningEfforts = nativeReasoningEfforts(metadataId);
1115
+ return expandCursorEffortRow(nativeModelRow(id, metadataId), reasoningEfforts, config, {
1116
+ knownIds: effortRowKnownIds,
1117
+ table: cursorEffortTable,
1118
+ supportsReasoning: reasoningEfforts.length > 0,
1119
+ }).flatMap(row => expandFastRow(
1120
+ row,
1121
+ // Only the BASE row earns a fast sibling. An effort row already spent the
1122
+ // grammar, and the parser requires the stripped base to be routable, so
1123
+ // `<base>--<effort>--fast` would publish a row no ingress can resolve.
1124
+ row.id === id && nativeFastEligible(metadataId),
1125
+ config,
1126
+ effortRowKnownIds,
1127
+ ));
1128
+ };
1129
+ const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1130
+ // Same rule as the anthropic branch: with the global fast switch on, a client
1131
+ // that has no Fast toggle is offered the fast identity directly. An operator
1132
+ // alias is an explicit decision and still wins.
1133
+ const fastModelId = cursorFastIdForListing?.(m.id, m.provider);
1134
+ const publicId = m.alias ?? `${m.provider}/${fastModelId ?? m.id}`;
1135
+ const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1136
+ const provider = config.providers[m.provider];
1137
+ const effective = provider
1138
+ ? (await import("../../providers/default-aliases")).effectiveModelAliases(
1139
+ config,
1140
+ provider,
1141
+ knownModelIdsForProvider(m.provider, provider, config),
1142
+ ).get(m.id)
1143
+ : undefined;
1144
+ const row = {
1145
+ id: publicId,
1146
+ object: "model",
1147
+ created: 0,
1148
+ // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1149
+ // owned_by as an adapter selector, so a virtual combo must name that wire
1150
+ // adapter rather than the internal catalog authority marker.
1151
+ owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1152
+ ...(isCombo ? { is_combo: true } : {}),
1153
+ ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1154
+ ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1155
+ ...modelCapabilityFields({
1156
+ reasoningEfforts: m.reasoningEfforts,
1157
+ // contextWindow is already the post-cap effective value; contextCap is the raw
1158
+ // operator knob and over-reports models whose real window sits below it.
1159
+ contextWindow: m.contextWindow,
1160
+ maxOutputTokens: m.maxOutputTokens,
1161
+ inputModalities: m.inputModalities,
1162
+ }),
1163
+ };
1164
+ return expandCursorEffortRow(row, m.reasoningEfforts, config, {
1165
+ knownIds: effortRowKnownIds,
1166
+ table: cursorEffortTable,
1167
+ supportsReasoning: (m.reasoningEfforts ?? []).length > 0,
1168
+ }).flatMap(expanded => expandFastRow(
1169
+ expanded,
1170
+ expanded.id === row.id && catalogRowFastEligible(m),
1171
+ config,
1172
+ effortRowKnownIds,
1173
+ ));
1174
+ }));
1175
+ const data = [
1176
+ ...visibleNatives.flatMap(id => expandedNativeModelRow(id)),
1177
+ ...visibleAccountNatives.flatMap(({ id, metadataId }) => expandedNativeModelRow(id, metadataId)),
1178
+ ...routedRows.flat(),
1179
+ ];
1180
+ return jsonResponse({ object: "list", data }, 200, req, policy);
1181
+ }
1182
+
1183
+ // Remote compaction v1 (codex-rs with Feature::RemoteCompactionV2 off — the default).
1184
+ // Must be matched BEFORE the /v1/responses POST branch never sees it (distinct path) and
1185
+ // before the /v1/* 404 guard below.
1186
+ if (url.pathname === "/v1/responses/compact" && req.method === "POST") {
1187
+ if (isDraining()) {
1188
+ return drainingResponse(req, policy);
1189
+ }
1190
+ const admission = resolveResponsesApiAuth(req, policy);
1191
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1192
+ if (!isAllowedRequestOrigin(req, policy)) {
1193
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1194
+ }
1195
+ const start = Date.now();
1196
+ const requestId = nextRequestLogId(start);
1197
+ const logCtx: RequestLogContext = {
1198
+ model: "unknown",
1199
+ provider: "unknown",
1200
+ ...admissionFields(admission),
1201
+ inboundProtocol: "responses",
1202
+ };
1203
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1204
+ let response: Response;
1205
+ try {
1206
+ response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission, {
1207
+ onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
1208
+ });
1209
+ } catch {
1210
+ response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
1211
+ }
1212
+ addFinalRequestLog(requestId, start, logCtx, response.status,
1213
+ response.status === 499 ? { closeReason: "client_cancel" } : undefined);
1214
+ return withCors(response, req, policy);
1215
+ }, { requestId, start, logCtx });
1216
+ }
1217
+
1218
+ if (
1219
+ req.method === "POST"
1220
+ && (url.pathname === "/v1/images/generations" || url.pathname === "/v1/images/edits")
1221
+ ) {
1222
+ disableResponsesRequestTimeout(req, requestServer);
1223
+ if (isDraining()) {
1224
+ return drainingResponse(req, policy);
1225
+ }
1226
+ const admission = resolveApiAuth(req, policy);
1227
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1228
+ if (!isAllowedRequestOrigin(req, policy)) {
1229
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1230
+ }
1231
+ const start = Date.now();
1232
+ const requestId = nextRequestLogId(start);
1233
+ const logCtx: RequestLogContext = {
1234
+ model: "image_gen",
1235
+ provider: "unknown",
1236
+ ...admissionFields(admission),
1237
+ };
1238
+ const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
1239
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1240
+ const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
1241
+ addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
1242
+ return withCors(response, req, policy);
1243
+ }, { requestId, start, logCtx });
1244
+ }
1245
+
1246
+ if (req.method === "GET" && url.pathname.startsWith("/v1/opencodex/artifacts/")) {
1247
+ const admission = resolveApiAuth(req, policy);
1248
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1249
+ if (!isAllowedRequestOrigin(req, policy)) {
1250
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1251
+ }
1252
+ const id = decodeURIComponent(url.pathname.slice("/v1/opencodex/artifacts/".length));
1253
+ const { resolveArtifactPath } = await import("../../images/artifacts");
1254
+ const artifactPath = resolveArtifactPath(id);
1255
+ if (!artifactPath) {
1256
+ return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, policy);
1257
+ }
1258
+ const file = Bun.file(artifactPath);
1259
+ const ext = artifactPath.split(".").pop()?.toLowerCase();
1260
+ const contentType =
1261
+ ext === "png" ? "image/png"
1262
+ : ext === "jpg" || ext === "jpeg" ? "image/jpeg"
1263
+ : ext === "webp" ? "image/webp"
1264
+ : ext === "gif" ? "image/gif"
1265
+ : "application/octet-stream";
1266
+ return withCors(new Response(file, {
1267
+ status: 200,
1268
+ headers: {
1269
+ "content-type": contentType,
1270
+ "cache-control": "private, max-age=3600",
1271
+ "x-content-type-options": "nosniff",
1272
+ },
1273
+ }), req, policy);
1274
+ }
1275
+
1276
+ if (contextEndpoint(url.pathname) !== undefined && req.method === "POST" && contextRelayActivated()) {
1277
+ // No timeout disable here. The relay is a bounded JSON round trip that owns one deadline
1278
+ // from entry; removing the idle timeout first would let an unfinished body hold an
1279
+ // admitted turn slot indefinitely, before that deadline ever starts.
1280
+ if (isDraining()) {
1281
+ return drainingResponse(req, policy);
1282
+ }
1283
+ const admission = resolveApiAuth(req, policy);
1284
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1285
+ if (!isAllowedRequestOrigin(req, policy)) {
1286
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1287
+ }
1288
+ const start = Date.now();
1289
+ const requestId = nextRequestLogId(start);
1290
+ const logCtx: RequestLogContext = {
1291
+ model: "context_history",
1292
+ provider: "unknown",
1293
+ ...admissionFields(admission),
1294
+ };
1295
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1296
+ const response = await handleContextHistory(req, config, logCtx, contextEndpoint(url.pathname)!,
1297
+ turnAdmissionLease, admission, () => resolveApiAuth(req, policy));
1298
+ addFinalRequestLog(requestId, start, logCtx, response.status,
1299
+ response.status === 499 ? { closeReason: "client_cancel" } : undefined);
1300
+ return withCors(response, req, policy);
1301
+ }, { requestId, start, logCtx });
1302
+ }
1303
+
1304
+ if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
1305
+ disableResponsesRequestTimeout(req, requestServer);
1306
+ if (isDraining()) {
1307
+ return drainingResponse(req, policy);
1308
+ }
1309
+ const admission = resolveApiAuth(req, policy);
1310
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1311
+ if (!isAllowedRequestOrigin(req, policy)) {
1312
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1313
+ }
1314
+ const start = Date.now();
1315
+ const requestId = nextRequestLogId(start);
1316
+ const logCtx: RequestLogContext = {
1317
+ model: "web_search",
1318
+ provider: "unknown",
1319
+ ...admissionFields(admission),
1320
+ };
1321
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1322
+ const response = await handleSearch(req, config, logCtx, turnAdmissionLease, admission);
1323
+ addFinalRequestLog(requestId, start, logCtx, response.status,
1324
+ response.status === 499 ? { closeReason: "client_cancel" } : undefined);
1325
+ return withCors(response, req, policy);
1326
+ }, { requestId, start, logCtx });
1327
+ }
1328
+
1329
+ if (url.pathname === "/v1/responses" && req.method === "POST") {
1330
+ if (isDraining()) {
1331
+ return drainingResponse(req, policy);
1332
+ }
1333
+ const admission = resolveResponsesApiAuth(req, policy);
1334
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1335
+ if (!isAllowedRequestOrigin(req, policy)) {
1336
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1337
+ }
1338
+ const start = Date.now();
1339
+ const requestId = nextRequestLogId(start);
1340
+ const logCtx: RequestLogContext = {
1341
+ model: "unknown",
1342
+ provider: "unknown",
1343
+ ...admissionFields(admission),
1344
+ inboundProtocol: "responses",
1345
+ };
1346
+ if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
1347
+ let logged = false;
1348
+ const finalizeNativePassthroughLog = (
1349
+ status: number,
1350
+ meta: { terminalStatus?: ResponsesTerminalStatus; closeReason: "terminal" | "client_cancel" },
1351
+ ) => {
1352
+ if (logged) return;
1353
+ logged = true;
1354
+ addFinalRequestLog(requestId, start, logCtx, status, meta);
1355
+ };
1356
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1357
+ const response = await handleResponses(req, config, logCtx, {
1358
+ turnAdmissionLease,
1359
+ admission,
1360
+ onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
1361
+ abortSignal: req.signal,
1362
+ onFirstOutput: () => recordFirstOutput(logCtx, start),
1363
+ onNativePassthroughTerminal: status => {
1364
+ finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
1365
+ terminalStatus: status,
1366
+ closeReason: "terminal",
1367
+ });
1368
+ },
1369
+ onNativePassthroughCancel: () => {
1370
+ finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
1371
+ },
1372
+ });
1373
+ return withRequestLogId(
1374
+ withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy),
1375
+ requestId,
1376
+ );
1377
+ }, { requestId, start, logCtx });
1378
+ }
1379
+
1380
+ // Anthropic Messages inbound (Claude Code). count_tokens FIRST (longer path).
1381
+ // Claude Code posts `/v1/messages?beta=true` — pathname match ignores the query (003 G9).
1382
+ if (url.pathname === "/v1/messages/count_tokens" && req.method === "POST") {
1383
+ if (isDraining()) {
1384
+ return drainingResponse(req, policy);
1385
+ }
1386
+ const admission = resolveApiAuth(req, policy);
1387
+ if (!admission) {
1388
+ return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
1389
+ }
1390
+ if (!isAllowedRequestOrigin(req, policy)) {
1391
+ return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
1392
+ }
1393
+ return runAdmittedHttpTurn(req, policy, async () => withCors(
1394
+ await handleClaudeCountTokens(req, config, policy),
1395
+ req,
1396
+ policy,
1397
+ ));
1398
+ }
1399
+
1400
+ if (url.pathname === "/v1/messages" && req.method === "POST") {
1401
+ disableResponsesRequestTimeout(req, requestServer);
1402
+ if (isDraining()) {
1403
+ return drainingResponse(req, policy);
1404
+ }
1405
+ const admission = resolveApiAuth(req, policy);
1406
+ if (!admission) {
1407
+ return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
1408
+ }
1409
+ if (!isAllowedRequestOrigin(req, policy)) {
1410
+ return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
1411
+ }
1412
+ const start = Date.now();
1413
+ const requestId = nextRequestLogId(start);
1414
+ const logCtx: RequestLogContext = {
1415
+ model: "unknown",
1416
+ provider: "unknown",
1417
+ ...admissionFields(admission),
1418
+ inboundProtocol: "messages",
1419
+ };
1420
+ // Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
1421
+ // pre-translation stream + native passthrough callbacks) — do not re-wrap the
1422
+ // translated Anthropic stream here.
1423
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1424
+ await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy),
1425
+ req,
1426
+ policy,
1427
+ ), { requestId, start, logCtx });
1428
+ }
1429
+
1430
+ // OpenAI Chat Completions inbound (GitHub Copilot App / OpenAI-compatible clients).
1431
+ if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
1432
+ disableResponsesRequestTimeout(req, requestServer);
1433
+ if (isDraining()) {
1434
+ return drainingResponse(req, policy);
1435
+ }
1436
+ const admission = resolveResponsesApiAuth(req, policy);
1437
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1438
+ if (!isAllowedRequestOrigin(req, policy)) {
1439
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1440
+ }
1441
+ const start = Date.now();
1442
+ const requestId = nextRequestLogId(start);
1443
+ const logCtx: RequestLogContext = {
1444
+ model: "unknown",
1445
+ provider: "unknown",
1446
+ ...admissionFields(admission),
1447
+ inboundProtocol: "chat",
1448
+ };
1449
+ // `policy`, not `config`: this route is now served on the unauthenticated loopback
1450
+ // listener too (#4236), and only the receiving listener's view produces CORS headers
1451
+ // that match the admission decision made above.
1452
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1453
+ await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
1454
+ req,
1455
+ policy,
1456
+ ), { requestId, start, logCtx });
1457
+ }
1458
+
1459
+ if (url.pathname === "/v1/audio/transcriptions" && req.method === "POST") {
1460
+ disableResponsesRequestTimeout(req, requestServer);
1461
+ if (isDraining()) return drainingResponse(req, policy);
1462
+ const admission = resolveAudioAdmission(req.headers, config);
1463
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1464
+ if (!isAllowedRequestOrigin(req, policy)) {
1465
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin audio request blocked"), req, policy);
1466
+ }
1467
+ const start = Date.now();
1468
+ const requestId = nextRequestLogId(start);
1469
+ const logCtx: RequestLogContext = { model: TRANSCRIPTION_MODEL, provider: "unknown", ...admissionFields(admission) };
1470
+ return runAdmittedHttpTurn(req, policy, async lease => {
1471
+ const response = await handleAudioTranscriptions(req, config, logCtx, admission, lease);
1472
+ addFinalRequestLog(requestId, start, logCtx, response.status);
1473
+ return withCors(response, req, policy);
1474
+ }, { requestId, start, logCtx });
1475
+ }
1476
+
1477
+ // ChatGPT / Codex App voice (GPT‑Live / Frameless Bidi) + OpenAI Realtime call-create.
1478
+ // Clients hit either /v1/live (Frameless App) or /v1/realtime/calls (codex RealtimeCallClient /
1479
+ // public Realtime API). Sideband WS joins are handled just below.
1480
+ if (
1481
+ req.method === "POST"
1482
+ && (url.pathname === "/v1/live" || url.pathname === "/v1/realtime/calls")
1483
+ ) {
1484
+ disableResponsesRequestTimeout(req, requestServer);
1485
+ if (isDraining()) {
1486
+ return drainingResponse(req, policy);
1487
+ }
1488
+ const audioClient = resolveAudioClient(req, config);
1489
+ if (audioClient instanceof Response) return withCors(audioClient, req, policy);
1490
+ const admission = audioClient?.admission ?? resolveApiAuth(req, policy);
1491
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1492
+ if (!isAllowedRequestOrigin(req, policy)) {
1493
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1494
+ }
1495
+ const start = Date.now();
1496
+ const requestId = nextRequestLogId(start);
1497
+ const logCtx: RequestLogContext = {
1498
+ model: "gpt-live",
1499
+ provider: "unknown",
1500
+ ...admissionFields(admission),
1501
+ };
1502
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1503
+ const response = audioClient
1504
+ ? await handleExternalLive(req, config, logCtx, { client: audioClient, lease: turnAdmissionLease, bindings: liveCallBindings })
1505
+ : await handleLive(req, config, logCtx, turnAdmissionLease);
1506
+ addFinalRequestLog(
1507
+ requestId,
1508
+ start,
1509
+ logCtx,
1510
+ response.status,
1511
+ response.status === 499 ? { closeReason: "client_cancel" } : undefined,
1512
+ );
1513
+ return withCors(response, req, policy);
1514
+ }, { requestId, start, logCtx });
1515
+ }
1516
+
1517
+ // Voice / Realtime WebSocket relay. Sideband joins: Frameless /v1/live/{callId};
1518
+ // Realtime v1 /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Standalone
1519
+ // sessions (codex-rs thread/realtime/start, WebSocket transport — the desktop voice
1520
+ // path): /v1/realtime?intent=quicksilver&model= and /v1/live?model=.
1521
+ // Transparent bidirectional relay.
1522
+ const liveSidebandTarget = req.headers.get("upgrade")?.toLowerCase() === "websocket"
1523
+ ? parseLiveSidebandTarget(url.pathname, url.searchParams, url.search.replace(/^\?/, ""))
1524
+ : null;
1525
+ const dictationSocket = url.pathname === "/v1/audio/transcriptions/stream"
1526
+ && req.headers.get("upgrade")?.toLowerCase() === "websocket";
1527
+ if (liveSidebandTarget || dictationSocket) {
1528
+ if (isDraining()) {
1529
+ return drainingResponse(req, policy);
1530
+ }
1531
+ const audioClient = resolveAudioClient(req, config, dictationSocket);
1532
+ if (audioClient instanceof Response) return withCors(audioClient, req, policy);
1533
+ if (!audioClient && liveSidebandTarget && "callId" in liveSidebandTarget
1534
+ && liveSidebandTarget.callId.startsWith(EXTERNAL_CALL_PREFIX)) {
1535
+ return withCors(formatErrorResponse(401, "authentication_error", "Live call requires its creator API key"), req, policy);
1536
+ }
1537
+ const admission = audioClient?.admission ?? resolveApiAuth(req, policy);
1538
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1539
+ if (!isAllowedRequestOrigin(req, policy)) {
1540
+ return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
1541
+ }
1542
+ const start = Date.now();
1543
+ const requestId = nextRequestLogId(start);
1544
+ const logCtx: RequestLogContext = {
1545
+ model: "gpt-live",
1546
+ provider: "unknown",
1547
+ ...admissionFields(admission),
1548
+ };
1549
+ const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
1550
+ if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
1551
+ const audioController = audioClient ? new AbortController() : undefined;
1552
+ if (audioController) registerTurn(audioController, turnAdmissionLease);
1553
+ const acquisition = audioController
1554
+ ? clearableDeadline(120_000, AbortSignal.any([req.signal, audioController.signal])) : undefined;
1555
+ const releaseAcquisition = () => {
1556
+ acquisition?.clear();
1557
+ if (audioController) unregisterTurn(audioController);
1558
+ else turnAdmissionLease.release();
1559
+ };
1560
+ let resolved;
1561
+ try {
1562
+ resolved = dictationSocket && audioClient
1563
+ ? await resolveDictationSocket(audioClient, config, logCtx, turnAdmissionLease, acquisition?.signal)
1564
+ : liveSidebandTarget && audioClient
1565
+ ? await resolveExternalLiveSocket(audioClient, config, logCtx, liveSidebandTarget, { lease: turnAdmissionLease, bindings: liveCallBindings, signal: acquisition?.signal })
1566
+ : liveSidebandTarget
1567
+ ? await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease)
1568
+ : formatErrorResponse(401, "authentication_error", "opencodex API key required");
1569
+ } catch (error) {
1570
+ releaseAcquisition();
1571
+ throw error;
1572
+ }
1573
+ if (acquisition?.signal.aborted) {
1574
+ try { if (!(resolved instanceof Response) && "finish" in resolved) resolved.finish(); }
1575
+ finally { releaseAcquisition(); }
1576
+ return withCors(formatErrorResponse(req.signal.aborted ? 499 : acquisition.didExpire() ? 504 : 503,
1577
+ "upstream_error", acquisition.didExpire() ? "Audio connection timed out" : "Audio connection canceled"), req, policy);
1578
+ }
1579
+ if (resolved instanceof Response) {
1580
+ releaseAcquisition();
1581
+ addFinalRequestLog(requestId, start, logCtx, resolved.status);
1582
+ return withCors(resolved, req, policy);
1583
+ }
1584
+ const audio = "finish" in resolved ? resolved : undefined;
1585
+ const finish = audio ? (outcome?: number | "timeout" | "connect_error") => {
1586
+ try { audio.finish(outcome); }
1587
+ finally { releaseAcquisition(); }
1588
+ } : undefined;
1589
+ const discardUpgrade = () => {
1590
+ if (finish) finish();
1591
+ else releaseAcquisition();
1592
+ };
1593
+ if (req.signal.aborted) {
1594
+ discardUpgrade();
1595
+ return withCors(formatErrorResponse(499, "client_closed_request", "Audio connection canceled"), req, policy);
1596
+ }
1597
+ const upstreamHandshake = await openLiveSidebandUpstream(
1598
+ resolved.upstreamWsUrl,
1599
+ resolved.headers,
1600
+ (url, headers) => (deps.liveSidebandWebSocketFactory ?? ((socketUrl, socketHeaders, protocols) => (
1601
+ new WebSocket(socketUrl, { headers: socketHeaders, protocols } as unknown as string[])
1602
+ )))(url, headers, audio?.protocols),
1603
+ LIVE_SIDEBAND_UPSTREAM_OPEN_TIMEOUT_MS,
1604
+ req.signal,
1605
+ );
1606
+ if (!upstreamHandshake.ok) {
1607
+ if (upstreamHandshake.socket) {
1608
+ closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
1609
+ } else {
1610
+ discardUpgrade();
1611
+ }
1612
+ addFinalRequestLog(requestId, start, logCtx, upstreamHandshake.status);
1613
+ console.error("[live] sideband upstream handshake failed: " + upstreamHandshake.message);
1614
+ return withCors(
1615
+ formatErrorResponse(upstreamHandshake.status, upstreamHandshake.code, upstreamHandshake.message),
1616
+ req,
1617
+ policy,
1618
+ );
1619
+ }
1620
+ const handoffFailure = upstreamHandshake.handoff.failure();
1621
+ if (handoffFailure || upstreamHandshake.socket.readyState !== WebSocket.OPEN) {
1622
+ closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
1623
+ const failure = handoffFailure ?? {
1624
+ status: 502,
1625
+ code: "upstream_error",
1626
+ message: "voice upstream closed before client upgrade",
1627
+ };
1628
+ addFinalRequestLog(requestId, start, logCtx, failure.status);
1629
+ return withCors(formatErrorResponse(failure.status, failure.code, failure.message), req, policy);
1630
+ }
1631
+ let upgraded = false;
1632
+ try {
1633
+ upgraded = requestServer.upgrade(req, {
1634
+ ...(audioClient?.protocol ? { headers: { "sec-websocket-protocol": audioClient.protocol } } : {}),
1635
+ data: {
1636
+ kind: "live-sideband",
1637
+ liveUpstream: upstreamHandshake.socket,
1638
+ liveUpstreamUrl: resolved.upstreamWsUrl,
1639
+ liveUpstreamHeaders: resolved.headers,
1640
+ liveUpstreamHandoff: upstreamHandshake.handoff,
1641
+ admission,
1642
+ liveUpstreamProtocols: audio?.protocols,
1643
+ liveValidateFrame: audio?.validateFrame,
1644
+ liveMaxSessionMs: audio?.maxSessionMs,
1645
+ liveFinish: finish,
1646
+ liveAbortSignal: audioController?.signal,
1647
+ livePending: [],
1648
+ livePendingBytes: 0,
1649
+ liveOpened: true,
1650
+ liveTurnAdmissionLease: turnAdmissionLease,
1651
+ } satisfies WsData,
1652
+ });
1653
+ } catch {
1654
+ try {
1655
+ upstreamHandshake.handoff.take();
1656
+ } catch {
1657
+ /* ignore */
1658
+ }
1659
+ closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
1660
+ return withCors(formatErrorResponse(502, "upstream_error", "Audio WebSocket upgrade failed"), req, policy);
1661
+ }
1662
+ if (upgraded) {
1663
+ acquisition?.clear();
1664
+ addFinalRequestLog(requestId, start, logCtx, 101);
1665
+ return undefined as unknown as Response;
1666
+ }
1667
+ try {
1668
+ upstreamHandshake.handoff.take();
1669
+ } catch {
1670
+ /* ignore */
1671
+ }
1672
+ closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
1673
+ return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
1674
+ }
1675
+
1676
+ // Data-plane guard: unknown /v1/* paths must fail with JSON 404, never fall through to the
1677
+ // GUI static handler (extensionless paths would get index.html with HTTP 200 and codex-rs
1678
+ // endpoint clients — memories/*, realtime/* — would surface confusing
1679
+ // serde decode errors instead of a clean not-found).
1680
+ if (url.pathname.startsWith("/v1/")) {
1681
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
1682
+ }
1683
+
1684
+ if (url.pathname === "/opencodex-session") {
1685
+ if (req.method === "GET") {
1686
+ const session = issueGuiSession(req, config, managementAuth, {
1687
+ trustedTailscaleIngress: ingress === "hub-management",
1688
+ });
1689
+ return session
1690
+ ? withManagementCors(serveSessionBootstrap(session), req, config)
1691
+ : withManagementCors(new Response(null, { status: 401, headers: { "Cache-Control": "no-store" } }), req, config);
1692
+ }
1693
+ if (req.method === "POST") {
1694
+ // This endpoint is reachable WITHOUT a credential — that is the point of a pairing
1695
+ // exchange — so the body limit has to hold against a caller who controls the
1696
+ // framing. A declared Content-Length is a claim, not a bound: omit the header and
1697
+ // `Number(null ?? "0")` is 0, send `Transfer-Encoding: chunked` and there is no
1698
+ // header at all. Both used to pass the pre-check and land in `req.text()`, which
1699
+ // buffers whatever arrives. The post-check then measured a string the process had
1700
+ // already been forced to hold.
1701
+ //
1702
+ // So the declared length is only a cheap early reject, and the real bound is
1703
+ // applied while reading: stop at limit+1 bytes and never accumulate more.
1704
+ const declaredLength = Number(req.headers.get("content-length") ?? "0");
1705
+ if (!Number.isFinite(declaredLength) || declaredLength > GUI_PAIRING_EXCHANGE_BODY_LIMIT) {
1706
+ return withManagementCors(Response.json({ error: "pairing exchange body too large" }, { status: 413, headers: { "Cache-Control": "no-store" } }), req, config);
1707
+ }
1708
+ const bounded = await readBoundedRequestText(req, GUI_PAIRING_EXCHANGE_BODY_LIMIT);
1709
+ if (bounded === null) {
1710
+ return withManagementCors(Response.json({ error: "pairing exchange body too large" }, { status: 413, headers: { "Cache-Control": "no-store" } }), req, config);
1711
+ }
1712
+ const text = bounded;
1713
+ let body: unknown;
1714
+ try {
1715
+ body = JSON.parse(text);
1716
+ } catch {
1717
+ return withManagementCors(Response.json({ error: "invalid pairing exchange body" }, { status: 400, headers: { "Cache-Control": "no-store" } }), req, config);
1718
+ }
1719
+ if (!body || typeof body !== "object" || Array.isArray(body)
1720
+ || Object.keys(body as Record<string, unknown>).length !== 1
1721
+ || typeof (body as Record<string, unknown>).grant !== "string") {
1722
+ return withManagementCors(Response.json({ error: "invalid pairing exchange body" }, { status: 400, headers: { "Cache-Control": "no-store" } }), req, config);
1723
+ }
1724
+ const pairing = managementAuth.available
1725
+ ? consumeGuiPairingGrant(req, body, config, managementAuth, Date.now(), {
1726
+ ingress: ingress === "hub-management" ? "hub-management" : "public",
1727
+ peerAddress: requestServer.requestIP(req)?.address ?? null,
1728
+ tailscaleUser: ingress === "hub-management" ? req.headers.get("Tailscale-User-Login") : null,
1729
+ browserOrigin: req.headers.get("Origin") ?? "",
1730
+ })
1731
+ : null;
1732
+ if (pairing && "allowed" in pairing) {
1733
+ return withManagementCors(Response.json({ error: "pairing exchange refused" }, {
1734
+ status: 429,
1735
+ headers: { "Cache-Control": "no-store", "Retry-After": String(pairing.retryAfterSeconds) },
1736
+ }), req, config);
1737
+ }
1738
+ return pairing
1739
+ ? withManagementCors(serveSessionBootstrap(pairing), req, config)
1740
+ : withManagementCors(new Response(null, { status: 401, headers: { "Cache-Control": "no-store" } }), req, config);
1741
+ }
1742
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
1743
+ }
1744
+ const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
1745
+ ? issueGuiSession(req, config, managementAuth, {
1746
+ trustedTailscaleIngress: ingress === "hub-management",
1747
+ })
1748
+ : null;
1749
+ const guiFile = serveGuiFile(
1750
+ url.pathname,
1751
+ undefined,
1752
+ guiSessionCandidate ?? undefined,
1753
+ config.runtimeRole ?? "standalone",
1754
+ isApiAuthRequired(config),
1755
+ );
1756
+ if (guiFile) return guiFile;
1757
+ if (url.pathname === "/" && req.method === "GET") {
1758
+ return jsonResponse(rootFallbackPayload());
1759
+ }
1760
+
1761
+ return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
1762
+ },
1763
+ websocket: createWebsocketHandler(ctx),
1764
+ } as const;
1765
+ return serveOptions;
1766
+ }