@bitkyc08/opencodex 2.7.23 → 2.7.24

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 (55) hide show
  1. package/README.ko.md +37 -7
  2. package/README.md +52 -11
  3. package/README.zh-CN.md +36 -7
  4. package/bin/ocx.mjs +5 -3
  5. package/gui/dist/assets/index-BzhyTAco.js +40 -0
  6. package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/opencode.svg +1 -1
  9. package/package.json +5 -2
  10. package/src/adapters/anthropic-image-normalize.ts +70 -29
  11. package/src/adapters/cursor/transport-retry.ts +20 -1
  12. package/src/adapters/run-turn-queue.ts +40 -0
  13. package/src/codex/auth-api.ts +10 -1
  14. package/src/codex/auth-context.ts +33 -7
  15. package/src/codex/catalog.ts +357 -23
  16. package/src/codex/routing.ts +10 -4
  17. package/src/combos/failover.ts +102 -0
  18. package/src/combos/index.ts +37 -0
  19. package/src/combos/request.ts +31 -0
  20. package/src/combos/resolve.ts +171 -0
  21. package/src/combos/types.ts +203 -0
  22. package/src/config.ts +280 -11
  23. package/src/lib/errors.ts +86 -24
  24. package/src/lib/upstream-retry.ts +8 -4
  25. package/src/oauth/index.ts +7 -1
  26. package/src/oauth/key-providers.ts +2 -32
  27. package/src/oauth/login-cli.ts +4 -3
  28. package/src/oauth/token-guardian.ts +38 -3
  29. package/src/providers/derive.ts +27 -2
  30. package/src/providers/kiro-models.ts +8 -3
  31. package/src/providers/label.ts +3 -1
  32. package/src/providers/openai-sidecar.ts +94 -0
  33. package/src/providers/openai-tier-startup.ts +27 -0
  34. package/src/providers/openai-tiers.ts +283 -0
  35. package/src/providers/openai-virtual-models.ts +82 -0
  36. package/src/providers/quota.ts +344 -24
  37. package/src/providers/registry.ts +112 -20
  38. package/src/reasoning-effort.ts +12 -11
  39. package/src/router.ts +80 -36
  40. package/src/server/auth-cors.ts +85 -9
  41. package/src/server/images.ts +31 -75
  42. package/src/server/index.ts +45 -86
  43. package/src/server/management-api.ts +273 -21
  44. package/src/server/request-log.ts +221 -20
  45. package/src/server/responses.ts +594 -75
  46. package/src/server/search.ts +22 -37
  47. package/src/types.ts +49 -1
  48. package/src/update/index.ts +50 -6
  49. package/src/update/job.ts +21 -4
  50. package/src/usage/log.ts +124 -1
  51. package/src/usage/summary.ts +147 -56
  52. package/src/vision/index.ts +20 -19
  53. package/src/web-search/index.ts +15 -17
  54. package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
  55. package/gui/dist/assets/index-DQjt6Hly.js +0 -40
@@ -15,20 +15,18 @@ import { formatErrorResponse } from "../bridge";
15
15
  import {
16
16
  CodexAccountCooldownError,
17
17
  CodexAuthContextError,
18
+ CodexPoolAuthenticationError,
18
19
  CodexThreadAffinityExpiredError,
19
- headersForCodexAuthContext,
20
- isCodexAuthContextUsable,
21
- resolveCodexAuthContext,
22
20
  } from "../codex/auth-context";
23
21
  import { formatCodexProviderForLog } from "../codex/routing";
24
- import { resolveEnvValue } from "../config";
25
22
  import { signalWithTimeout } from "../lib/abort";
26
23
  import { sidecarEnter } from "../lib/sidecar-tracker";
27
- import type { OcxConfig, OcxProviderConfig } from "../types";
28
- import { isProxyAdmissionSecret } from "./auth-cors";
24
+ import type { OcxConfig } from "../types";
25
+ import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
29
26
  import { readJsonRequestBody } from "./request-decompress";
27
+ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
30
28
  import type { RequestLogContext } from "./request-log";
31
- import { codexLogAccountId, decodeRequestErrorResponse, sidecarOutcomeRecorder } from "./responses";
29
+ import { codexLogAccountId, decodeRequestErrorResponse } from "./responses";
32
30
 
33
31
  export type ImagesEndpoint = "generations" | "edits";
34
32
 
@@ -42,45 +40,17 @@ const IMAGES_UPSTREAM_TIMEOUT_MS = 300_000;
42
40
  */
43
41
  const IMAGES_RESPONSE_MAX_BYTES = 100 * 1024 * 1024;
44
42
 
45
- interface NamedProvider {
46
- name: string;
47
- provider: OcxProviderConfig;
48
- }
49
-
50
- interface ImagesUpstreamCandidates {
51
- /** ChatGPT passthrough — the backend codex itself would have called absent the base_url override. */
52
- forward?: NamedProvider;
53
- /** Keyed openai-responses provider (e.g. api.openai.com), whose /v1/images/* is the platform Images API. */
54
- keyed?: NamedProvider & { apiKey: string };
55
- }
56
-
57
- /**
58
- * Collect the upstreams that can serve /images/*. The forward provider is preferred (same
59
- * precedence as the vision/web-search sidecars) but only usable when the request actually
60
- * carries relayable ChatGPT auth — startServer auto-upserts a `chatgpt` forward entry into
61
- * every config, so its mere presence proves nothing about credentials.
62
- */
63
- function findImagesUpstreams(config: OcxConfig): ImagesUpstreamCandidates {
64
- const candidates: ImagesUpstreamCandidates = {};
65
- for (const [name, provider] of Object.entries(config.providers)) {
66
- if (provider.disabled === true) continue;
67
- if (provider.authMode === "forward") {
68
- candidates.forward ??= { name, provider };
69
- continue;
70
- }
71
- if (candidates.keyed || provider.adapter !== "openai-responses" || provider.authMode === "oauth") continue;
72
- const apiKey = resolveEnvValue(provider.apiKey);
73
- if (apiKey) candidates.keyed = { name, provider, apiKey };
74
- }
75
- return candidates;
76
- }
77
-
78
43
  export async function handleImages(
79
44
  req: Request,
80
45
  config: OcxConfig,
81
46
  endpoint: ImagesEndpoint,
82
47
  logCtx: RequestLogContext,
83
48
  ): Promise<Response> {
49
+ try { validateForwardAdmissionCredential(req.headers, config); }
50
+ catch (err) {
51
+ if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
52
+ throw err;
53
+ }
84
54
  let body: unknown;
85
55
  try {
86
56
  body = await readJsonRequestBody(req);
@@ -90,8 +60,8 @@ export async function handleImages(
90
60
  const model = (body as { model?: unknown } | null)?.model;
91
61
  if (typeof model === "string" && model) logCtx.model = model;
92
62
 
93
- const candidates = findImagesUpstreams(config);
94
- if (!candidates.forward && !candidates.keyed) {
63
+ const candidates = selectOpenAiImagesProvider(config);
64
+ if (candidates.forwardCandidates.length === 0 && !candidates.keyed) {
95
65
  // 400, not 5xx: codex retries every 5xx up to 5 total attempts, and this is a permanent
96
66
  // configuration state that must surface on the first attempt.
97
67
  return formatErrorResponse(
@@ -106,39 +76,23 @@ export async function handleImages(
106
76
  // Resolve forward auth first; failures are captured, not returned, so a configured keyed
107
77
  // provider can still serve the request (e.g. every pool account cooling down must not
108
78
  // 429 image_gen while api.openai.com sits idle).
109
- let forwardAuthHeaders: Headers | undefined;
79
+ let forward: Awaited<ReturnType<typeof resolveFirstUsableOpenAiSidecar>>;
110
80
  let forwardAuthError: Response | undefined;
111
- let recordOutcome: ReturnType<typeof sidecarOutcomeRecorder>;
112
- if (candidates.forward) {
81
+ if (candidates.forwardCandidates.length > 0) {
113
82
  try {
114
- const authCtx = await resolveCodexAuthContext(req.headers, config);
115
- if (!isCodexAuthContextUsable(authCtx, config)) {
116
- forwardAuthError = formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
117
- } else {
118
- // Forwarded caller auth, overridden by the routed pool account's token when one is selected.
119
- const authHeaders = headersForCodexAuthContext(req.headers, authCtx);
120
- const bearer = authHeaders.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
121
- // A caller may authenticate to the proxy itself with `Authorization: Bearer <admission
122
- // token>` (non-loopback binds); that secret must never be relayed to chatgpt.com.
123
- if (bearer && isProxyAdmissionSecret(bearer, config)) authHeaders.delete("authorization");
124
- // Only relay through the ChatGPT backend when there is a bearer to relay: startServer
125
- // auto-upserts the `chatgpt` provider, so an unauthenticated request must not be bounced
126
- // off chatgpt.com when a keyed OpenAI provider (or an honest error) serves it better.
127
- if (authHeaders.get("authorization")) {
128
- forwardAuthHeaders = authHeaders;
129
- recordOutcome = sidecarOutcomeRecorder(config, authCtx);
130
- logCtx.provider = formatCodexProviderForLog(candidates.forward.name, codexLogAccountId(authCtx), config);
131
- }
132
- }
83
+ forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config);
84
+ if (forward) logCtx.provider = formatCodexProviderForLog(forward.providerName, codexLogAccountId(forward.authContext), config);
133
85
  } catch (err) {
134
86
  if (err instanceof CodexAccountCooldownError) {
135
87
  forwardAuthError = formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
136
88
  } else if (err instanceof CodexThreadAffinityExpiredError) {
137
89
  forwardAuthError = formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
138
90
  } else if (err instanceof CodexAuthContextError) {
139
- const safeAccountLabel = formatCodexProviderForLog(candidates.forward.name, err.accountId, config);
91
+ const safeAccountLabel = formatCodexProviderForLog("openai", err.accountId, config);
140
92
  console.error(`[images] Pool account ${safeAccountLabel} token failed; reauthentication required`);
141
93
  forwardAuthError = formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
94
+ } else if (err instanceof CodexPoolAuthenticationError) {
95
+ forwardAuthError = formatErrorResponse(401, "authentication_error", err.message);
142
96
  } else {
143
97
  throw err;
144
98
  }
@@ -147,21 +101,23 @@ export async function handleImages(
147
101
 
148
102
  const headers: Record<string, string> = { "content-type": "application/json" };
149
103
  let url: string;
150
- if (forwardAuthHeaders && candidates.forward) {
151
- const { provider } = candidates.forward;
104
+ if (forward) {
105
+ const { provider } = forward;
152
106
  if (provider.headers) Object.assign(headers, provider.headers);
153
- for (const [name, value] of forwardAuthHeaders) headers[name] = value;
107
+ for (const [name, value] of forward.headers) headers[name] = value;
154
108
  // The ChatGPT codex backend takes bare paths (matches the adapter's `${baseUrl}/responses`).
155
109
  url = `${provider.baseUrl}/images/${endpoint}`;
110
+ } else if (forwardAuthError) {
111
+ // A configured OpenAI pool mode owns its authentication failure. Do not hide a
112
+ // broken/expired pool behind separately billed API-key image generation.
113
+ return forwardAuthError;
156
114
  } else if (candidates.keyed) {
157
- const { provider, apiKey, name } = candidates.keyed;
115
+ const { provider, apiKey, providerName } = candidates.keyed;
158
116
  if (provider.headers) Object.assign(headers, provider.headers);
159
117
  headers["authorization"] = `Bearer ${apiKey}`;
160
- logCtx.provider = name;
118
+ logCtx.provider = providerName;
161
119
  // Keyed providers tolerate baseUrl with or without /v1 (mirrors openai-responses.ts).
162
120
  url = `${provider.baseUrl.replace(/\/v1\/?$/, "")}/v1/images/${endpoint}`;
163
- } else if (forwardAuthError) {
164
- return forwardAuthError;
165
121
  } else {
166
122
  return formatErrorResponse(
167
123
  401,
@@ -189,7 +145,7 @@ export async function handleImages(
189
145
  if (payload.byteLength > IMAGES_RESPONSE_MAX_BYTES) {
190
146
  return formatErrorResponse(502, "upstream_error", `image ${endpoint} response too large (${payload.byteLength} bytes)`);
191
147
  }
192
- recordOutcome?.(upstreamResponse.status);
148
+ forward?.recordOutcome?.(upstreamResponse.status);
193
149
  const relayHeaders: Record<string, string> = {};
194
150
  const contentType = upstreamResponse.headers.get("content-type");
195
151
  if (contentType) relayHeaders["content-type"] = contentType;
@@ -201,11 +157,11 @@ export async function handleImages(
201
157
  return formatErrorResponse(499, "client_closed_request", `image ${endpoint} request canceled by client`);
202
158
  }
203
159
  if (err instanceof Error && err.name === "TimeoutError") {
204
- recordOutcome?.("timeout");
160
+ forward?.recordOutcome?.("timeout");
205
161
  // codex retries 5xx up to 4 more times; a retried 504 is acceptable for a transient hang.
206
162
  return formatErrorResponse(504, "upstream_error", `image ${endpoint} upstream timed out`);
207
163
  }
208
- recordOutcome?.("connect_error");
164
+ forward?.recordOutcome?.("connect_error");
209
165
  return formatErrorResponse(
210
166
  502,
211
167
  "upstream_error",
@@ -16,18 +16,13 @@ import {
16
16
  saveConfig,
17
17
  websocketsEnabled,
18
18
  } from "../config";
19
- import {
20
- clearLoginState, getLoginStatus, getOAuthCredentialProjectId, getValidAccessToken, isOAuthProvider,
21
- listOAuthProviders, reconcileOAuthProviders, startLoginFlow, UnsupportedOAuthProviderError, upsertOAuthProvider,
22
- } from "../oauth";
19
+ import { reconcileOAuthProviders } from "../oauth";
23
20
  import { invalidateCodexModelsCache } from "../codex/catalog";
21
+ import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
22
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
23
+ import { providerCodexAccountMode } from "../providers/registry";
24
24
  import {
25
25
  CodexAccountCooldownError,
26
- CodexAuthContextError,
27
- CodexThreadAffinityExpiredError,
28
- headersForCodexAuthContext,
29
- resolveCodexAuthContext,
30
- type CodexAuthContext,
31
26
  } from "../codex/auth-context";
32
27
  export {
33
28
  clearThreadAccountMap,
@@ -102,6 +97,7 @@ import {
102
97
  isLoopbackHostname,
103
98
  jsonResponse,
104
99
  requireApiAuth,
100
+ requireResponsesApiAuth,
105
101
  safeConfigDTO,
106
102
  setCorsOrigin,
107
103
  withCors,
@@ -147,17 +143,12 @@ const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
147
143
  // export function relaySseWithHeartbeat
148
144
 
149
145
  export function startServer(port?: number) {
150
- const config = loadConfig();
146
+ const config = runOpenAiTierStartupMigration(loadConfig());
151
147
  applyProxyEnv(config);
152
148
  assertServerAuthConfig(config);
153
149
  // Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
154
150
  // adding/dropping models reaches existing configs on start — not just fresh installs.
155
151
  reconcileOAuthProviders(config);
156
- // Ensure the ChatGPT passthrough provider exists so gpt-* models route correctly.
157
- if (!config.providers["chatgpt"]) {
158
- upsertOAuthProvider(config, "chatgpt");
159
- saveConfig(config);
160
- }
161
152
  // Seed default featured subagent models on first run only (UNSET → defaults). A user-set list,
162
153
  // even [], is left alone so GUI removals persist.
163
154
  if (config.subagentModels === undefined) {
@@ -214,7 +205,7 @@ export function startServer(port?: number) {
214
205
  if (isDraining()) {
215
206
  return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(req, config), "Retry-After": "5" } });
216
207
  }
217
- const apiAuthError = requireApiAuth(req, config, "data-plane");
208
+ const apiAuthError = requireResponsesApiAuth(req, config);
218
209
  if (apiAuthError) return withCors(apiAuthError, req, config);
219
210
  if (!isAllowedRequestOrigin(req, config)) {
220
211
  return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
@@ -227,27 +218,9 @@ export function startServer(port?: number) {
227
218
  if (!websocketsEnabled(config)) {
228
219
  return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, config);
229
220
  }
230
- let authCtx: CodexAuthContext;
231
- try {
232
- authCtx = await resolveCodexAuthContext(req.headers, config);
233
- } catch (err) {
234
- if (err instanceof CodexAccountCooldownError) {
235
- return withCors(formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down"), req, config);
236
- }
237
- if (err instanceof CodexThreadAffinityExpiredError) {
238
- return withCors(formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"), req, config);
239
- }
240
- if (err instanceof CodexAuthContextError) {
241
- const safeAccountLabel = formatCodexProviderForLog("chatgpt", err.accountId, config);
242
- console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed during websocket upgrade; reauthentication required`);
243
- return withCors(formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"), req, config);
244
- }
245
- throw err;
246
- }
247
221
  if (server.upgrade(req, {
248
222
  data: {
249
223
  headers: selectForwardHeaders(req.headers),
250
- authContext: authCtx,
251
224
  },
252
225
  })) return undefined as unknown as Response;
253
226
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
@@ -272,7 +245,7 @@ export function startServer(port?: number) {
272
245
  return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
273
246
  }
274
247
  const goModels = await fetchAllModels(config);
275
- const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, visibleNativeSlugs } = await import("../codex/catalog");
248
+ const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, visibleNativeSlugs } = await import("../codex/catalog");
276
249
  const nativeSlugs = nativeOpenAiSlugs();
277
250
  const goEnabled = filterCatalogVisibleModels(goModels, config);
278
251
  const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
@@ -315,7 +288,7 @@ export function startServer(port?: number) {
315
288
  // Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
316
289
  // on-disk sync; codex-rs keeps them out of the picker itself).
317
290
  const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
318
- const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2");
291
+ const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config));
319
292
  return jsonResponse({ models: applyNativeVisibility(entries, disabledNativeSlugs(config)) }, 200, req, config);
320
293
  }
321
294
  // OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
@@ -334,12 +307,28 @@ export function startServer(port?: number) {
334
307
  if (isDraining()) {
335
308
  return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(req, config), "Retry-After": "5" } });
336
309
  }
337
- const apiAuthError = requireApiAuth(req, config, "data-plane");
310
+ const apiAuthError = requireResponsesApiAuth(req, config);
338
311
  if (apiAuthError) return withCors(apiAuthError, req, config);
339
312
  if (!isAllowedRequestOrigin(req, config)) {
340
313
  return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
341
314
  }
342
- return withCors(await handleResponsesCompact(req, config), req, config);
315
+ const start = Date.now();
316
+ const requestId = nextRequestLogId(start);
317
+ const logCtx: RequestLogContext = { model: "unknown", provider: "unknown" };
318
+ let response: Response;
319
+ try {
320
+ response = await handleResponsesCompact(req, config, logCtx);
321
+ } catch {
322
+ response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
323
+ }
324
+ addFinalRequestLog(
325
+ requestId,
326
+ start,
327
+ logCtx,
328
+ response.status,
329
+ response.status === 499 ? { closeReason: "client_cancel" } : undefined,
330
+ );
331
+ return withCors(response, req, config);
343
332
  }
344
333
 
345
334
  if (
@@ -399,7 +388,7 @@ export function startServer(port?: number) {
399
388
  if (isDraining()) {
400
389
  return new Response("Service shutting down", { status: 503, headers: { ...corsHeaders(req, config), "Retry-After": "5" } });
401
390
  }
402
- const apiAuthError = requireApiAuth(req, config, "data-plane");
391
+ const apiAuthError = requireResponsesApiAuth(req, config);
403
392
  if (apiAuthError) return withCors(apiAuthError, req, config);
404
393
  if (!isAllowedRequestOrigin(req, config)) {
405
394
  return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
@@ -521,6 +510,10 @@ export function startServer(port?: number) {
521
510
  turnAbort.abort("websocket turn superseded or closed");
522
511
  };
523
512
  ws.data.cancel = cancelTurn;
513
+ // A socket may carry several response.create frames. Clear the previous
514
+ // account before resolving this frame so a failed Multi resolution cannot
515
+ // leave stale invalidation ownership behind.
516
+ updateCodexWebSocketAuthContext(ws, undefined);
524
517
 
525
518
  if (frame.generate === false) {
526
519
  for (const payload of buildWarmupCompletionFrames(frame)) {
@@ -548,49 +541,8 @@ export function startServer(port?: number) {
548
541
  addFinalRequestLog(requestId, start, logCtx, status, meta);
549
542
  };
550
543
  const baseHeaders = ws.data.headers ?? new Headers();
551
- let authCtx: CodexAuthContext;
552
- let selectedForwardHeaders: Headers;
553
- try {
554
- authCtx = await resolveCodexAuthContext(baseHeaders, config);
555
- selectedForwardHeaders = headersForCodexAuthContext(baseHeaders, authCtx);
556
- updateCodexWebSocketAuthContext(ws, authCtx);
557
- } catch (err) {
558
- if (!isCurrent()) return;
559
- if (err instanceof CodexAccountCooldownError) {
560
- finalizeLog(429);
561
- sendJsonFrame(ws, buildWsErrorFrame(429, {
562
- type: "rate_limit_error",
563
- message: "Selected Codex account is cooling down",
564
- }));
565
- return;
566
- }
567
- if (err instanceof CodexThreadAffinityExpiredError) {
568
- finalizeLog(409);
569
- sendJsonFrame(ws, buildWsErrorFrame(409, {
570
- type: "invalid_request_error",
571
- message: "Codex thread account affinity expired; start a new session",
572
- }));
573
- return;
574
- }
575
- if (err instanceof CodexAuthContextError) {
576
- const safeAccountLabel = formatCodexProviderForLog("chatgpt", err.accountId, config);
577
- console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed during websocket turn; reauthentication required`);
578
- finalizeLog(401);
579
- sendJsonFrame(ws, buildWsErrorFrame(401, {
580
- type: "authentication_error",
581
- message: "Selected Codex account needs reauthentication",
582
- }));
583
- return;
584
- }
585
- finalizeLog(502);
586
- sendJsonFrame(ws, buildWsErrorFrame(502, {
587
- type: "proxy_error",
588
- message: err instanceof Error ? err.message : String(err),
589
- }));
590
- return;
591
- }
592
544
  const fwd = new Headers({ "content-type": "application/json" });
593
- selectedForwardHeaders.forEach((value, key) => fwd.set(key, value));
545
+ baseHeaders.forEach((value, key) => fwd.set(key, value));
594
546
  const req = new Request("http://localhost/v1/responses", {
595
547
  method: "POST",
596
548
  headers: fwd,
@@ -601,8 +553,7 @@ export function startServer(port?: number) {
601
553
  const response = await handleResponses(req, config, logCtx, {
602
554
  forceEmptyResponseId: true,
603
555
  abortSignal: turnAbort.signal,
604
- authContext: authCtx,
605
- selectedForwardHeaders,
556
+ onCodexAuthContextResolved: context => updateCodexWebSocketAuthContext(ws, context),
606
557
  recordTerminalOutcomes: false,
607
558
  setTerminalOutcomeRecorder: recorder => {
608
559
  terminalRecorder = recorder;
@@ -666,9 +617,17 @@ export function startServer(port?: number) {
666
617
  // usage scores from the first routing decision, even when the dashboard is
667
618
  // never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
668
619
  // listener, and a blocked network silently no-ops (see Phase 30 diagnostics).
669
- import("../codex/auth-api")
670
- .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
671
- .catch(() => {});
620
+ const openAiProvider = config.providers.openai;
621
+ if (
622
+ openAiProvider
623
+ && openAiProvider.disabled !== true
624
+ && isCanonicalOpenAiForwardProvider(openAiProvider)
625
+ && providerCodexAccountMode("openai", openAiProvider) === "pool"
626
+ ) {
627
+ import("../codex/auth-api")
628
+ .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
629
+ .catch(() => {});
630
+ }
672
631
 
673
632
  return server;
674
633
  }