@yansigit/opencodex 2.33.0 → 2.35.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -6,7 +6,13 @@ import {
6
6
  collectChatCompletion,
7
7
  isChatCompletionsStreamError,
8
8
  } from "../chat/outbound";
9
- import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
9
+ import {
10
+ classifyError,
11
+ cyberPolicyErrorType,
12
+ CYBER_POLICY_ERROR_CODE,
13
+ isCyberPolicyCode,
14
+ isCyberPolicyMessage,
15
+ } from "../lib/errors";
10
16
  import type { AdmissionLease } from "../lib/admission";
11
17
  import { readBoundedResponseBody } from "../lib/bounded-body";
12
18
  import { redactSecretString } from "../lib/redact";
@@ -281,13 +287,24 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
281
287
  const detail = activeAdapter.formatErrorBody?.(response.status, response.headers, bodyText) ?? "";
282
288
  let upstreamType: string | undefined;
283
289
  let upstreamCode: string | null | undefined;
290
+ let upstreamMessage: string | undefined;
284
291
  try {
285
292
  const parsedError = JSON.parse(bodyText) as Rec;
286
293
  const nested = isRec(parsedError.error) ? parsedError.error : undefined;
287
- if (typeof nested?.type === "string") upstreamType = nested.type;
288
- if (nested?.code === null || typeof nested?.code === "string") upstreamCode = nested.code;
294
+ const details = nested ?? parsedError;
295
+ if (typeof details.type === "string") upstreamType = details.type;
296
+ if (details.code === null || typeof details.code === "string") upstreamCode = details.code;
297
+ const rawMessage = typeof details.message === "string"
298
+ ? details.message
299
+ : typeof parsedError.error === "string" ? parsedError.error : undefined;
300
+ if (rawMessage?.trim()) {
301
+ upstreamMessage = redactSecretString(rawMessage.trim());
302
+ }
289
303
  } catch { /* keep generic classification */ }
290
- const message = detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
304
+ const message = upstreamMessage
305
+ && (isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(upstreamMessage))
306
+ ? upstreamMessage
307
+ : detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
291
308
  const classified = classifyError(
292
309
  response.status,
293
310
  upstreamType ?? (response.status === 401 ? "authentication_error"
@@ -295,9 +312,9 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
295
312
  : response.status >= 500 ? "server_error" : "invalid_request_error"),
296
313
  message,
297
314
  );
298
- if (isCyberPolicyCode(upstreamCode)) {
315
+ if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
299
316
  classified.code = CYBER_POLICY_ERROR_CODE;
300
- classified.type = "invalid_request_error";
317
+ classified.type = cyberPolicyErrorType(upstreamType);
301
318
  } else if (upstreamCode === "model_not_found") {
302
319
  classified.code = "model_not_found";
303
320
  classified.type = "invalid_request_error";
@@ -305,11 +322,13 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
305
322
  classified.code = upstreamCode;
306
323
  }
307
324
  const status = isCyberPolicyCode(classified.code) ? 400 : response.status;
308
- const retryAfter = resolveClientRetryAfter({
309
- status: response.status,
310
- message: classified.message,
311
- upstreamRetryAfter: response.headers.get("retry-after"),
312
- });
325
+ const retryAfter = isCyberPolicyCode(classified.code)
326
+ ? undefined
327
+ : resolveClientRetryAfter({
328
+ status: response.status,
329
+ message: classified.message,
330
+ upstreamRetryAfter: response.headers.get("retry-after"),
331
+ });
313
332
  finishLog(status, classified.message);
314
333
  return new Response(JSON.stringify(chatCompletionsErrorBody(status, classified.message, classified.type, classified.code)), {
315
334
  status,
@@ -553,7 +553,7 @@ export async function fetchWithHeaderDeadline(
553
553
  ): Promise<HeaderDeadlineFetchResult> {
554
554
  const deadline = makeDeadline(timeoutMs, parent);
555
555
  try {
556
- const upstream = await fetchImpl(input, { ...init, signal: deadline.signal });
556
+ const upstream = await fetchImpl(input, { ...init, signal: deadline.signal, timeout: 0 });
557
557
  return { kind: "response", upstream };
558
558
  } catch (error) {
559
559
  if (deadline.didExpire()) return { kind: "timeout" };
@@ -116,6 +116,22 @@ export function stripEmptyLadderEffort(
116
116
  return Object.keys(next).length > 0 ? next : undefined;
117
117
  }
118
118
 
119
+ /**
120
+ * Strip reasoning effort from parsed options and _rawBody when the routed model
121
+ * has an empty supported ladder (effortless models like Composer 2.5).
122
+ */
123
+ export function sanitizeEffortForModel(
124
+ parsed: OcxParsedRequest,
125
+ ladder: readonly string[] | undefined,
126
+ ): void {
127
+ if (ladder === undefined || ladder.length > 0) return;
128
+ parsed.options.reasoning = undefined;
129
+ const raw = parsed._rawBody as { reasoning?: unknown } | undefined;
130
+ if (!raw || typeof raw !== "object" || !("reasoning" in raw)) return;
131
+ raw.reasoning = stripEmptyLadderEffort(raw.reasoning, ladder);
132
+ if (raw.reasoning === undefined) delete raw.reasoning;
133
+ }
134
+
119
135
  export function supportedLadderFor(route: { provider: OcxProviderConfig; modelId: string }): string[] | undefined {
120
136
  const { provider, modelId } = route;
121
137
  if (modelInList(provider.noReasoningModels, modelId)) return [];
@@ -1,4 +1,5 @@
1
1
  import { markActivity } from "../lib/sidecar-tracker";
2
+ import { knownModelIdsForProvider } from "../router";
2
3
  import {
3
4
  buildWarmupCompletionFrames,
4
5
  buildWsErrorFrame,
@@ -47,7 +48,6 @@ import {
47
48
  } from "../lib/app-owned-memory-stores";
48
49
  import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
49
50
  import { activateLab, labActivationRequired } from "../lib/lab-activation";
50
- import { registerFork } from "../fork/register";
51
51
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
52
52
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
53
53
  import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
@@ -110,6 +110,7 @@ import {
110
110
  type RequestLogContext,
111
111
  type RequestLogEntry,
112
112
  } from "./request-log";
113
+ import { sessionLaneIdFromRequest } from "./request-log-conversation";
113
114
  export {
114
115
  addFinalRequestLog,
115
116
  filterRequestLogs,
@@ -201,6 +202,22 @@ import {
201
202
  import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
202
203
  import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
203
204
  import { createReadinessGate, type ReadinessGate } from "./readiness";
205
+ import {
206
+ createRuntimePackageTreeIntegrityGuard,
207
+ type PackageTreeIntegrityGuard,
208
+ } from "../lib/package-tree-integrity";
209
+ import { detectInstall } from "../update/index";
210
+ import { runAiStudioNativeLogin } from "../oauth/aistudio-native-daemon";
211
+
212
+ function isAiStudioSessionOrigin(origin: string | null): boolean {
213
+ return !!origin && (origin.startsWith("chrome-extension://") || origin === "https://aistudio.google.com");
214
+ }
215
+
216
+ function withAiStudioSessionCors(resp: Response, req: Request): Response {
217
+ const origin = req.headers.get("Origin");
218
+ if (isAiStudioSessionOrigin(origin) && origin) resp.headers.set("Access-Control-Allow-Origin", origin);
219
+ return resp;
220
+ }
204
221
 
205
222
  export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
206
223
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
@@ -453,6 +470,10 @@ export interface StartServerDeps {
453
470
  localAttestationSecret?: string;
454
471
  /** Optional readiness gate; a fresh pending gate is created when omitted. */
455
472
  readinessGate?: ReadinessGate;
473
+ /** Test-only package-tree observation; production captures package.json identity at boot. */
474
+ packageTreeIntegrity?: PackageTreeIntegrityGuard;
475
+ /** Test-only seam for the awaited native AI Studio login process. */
476
+ runAiStudioNativeLogin?: typeof runAiStudioNativeLogin;
456
477
  }
457
478
 
458
479
  function inspectStartupOwnership(
@@ -697,12 +718,21 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
697
718
  }), req, policy);
698
719
  }
699
720
 
721
+ function packageTreeChangedResponse(req: Request, policy: RequestPolicyView, message: string): Response {
722
+ return withCors(new Response(JSON.stringify({
723
+ error: { type: "server_error", code: "package_tree_changed", message },
724
+ }), {
725
+ status: 503,
726
+ headers: { "Content-Type": "application/json" },
727
+ }), req, policy);
728
+ }
729
+
700
730
  async function runAdmittedHttpTurn(
701
731
  req: Request,
702
732
  policy: RequestPolicyView,
703
733
  work: (lease: ActiveTurnLease) => Promise<Response>,
704
734
  ): Promise<Response> {
705
- const lease = tryAdmitTurn();
735
+ const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
706
736
  if (!lease) return serverBusyResponse(req, "active turns", policy);
707
737
  let response: Response;
708
738
  try {
@@ -723,6 +753,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
723
753
  // passes it in, and transitions it after the post-startup sync settles. When
724
754
  // no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
725
755
  const readinessGate = deps.readinessGate ?? createReadinessGate();
756
+ const packageTreeIntegrity = deps.packageTreeIntegrity
757
+ ?? createRuntimePackageTreeIntegrityGuard(detectInstall());
726
758
  // Actual bound port, filled in after Bun.serve binds so /readyz reports the
727
759
  // real ephemeral port for startServer(0). /healthz keeps its existing port
728
760
  // field (the requested listenPort) byte-for-byte.
@@ -849,6 +881,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
849
881
  if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
850
882
  } catch { /* malformed encoding — not a readiness path */ }
851
883
 
884
+ const packageTreeStatus = packageTreeIntegrity.status();
885
+ if (!packageTreeStatus.ok && (
886
+ url.pathname === "/healthz"
887
+ || readyzPath !== undefined
888
+ || url.pathname.startsWith("/v1/")
889
+ )) {
890
+ const message = "OpenCodex package files changed while this proxy was running; restart OpenCodex before retrying.";
891
+ const response = url.pathname === "/healthz" || readyzPath !== undefined
892
+ ? jsonResponse({
893
+ status: "restart_required",
894
+ service: "opencodex",
895
+ version: VERSION,
896
+ uptime: process.uptime(),
897
+ pid: process.pid,
898
+ port: boundPort ?? requestServer.port ?? listenPort,
899
+ error: { code: "package_tree_changed", message },
900
+ }, 503, req, policy)
901
+ : packageTreeChangedResponse(req, policy, message);
902
+ const headers = new Headers(response.headers);
903
+ headers.set("Retry-After", "5");
904
+ return new Response(response.body, { status: 503, headers });
905
+ }
906
+
852
907
  if (req.method === "OPTIONS") {
853
908
  // /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
854
909
  // path) must answer the deterministic JSON 404, never the generic 204
@@ -856,6 +911,20 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
856
911
  if (readyzPath !== undefined) {
857
912
  return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
858
913
  }
914
+ if (url.pathname === "/api/aistudio/session") {
915
+ const origin = req.headers.get("Origin");
916
+ if (isAiStudioSessionOrigin(origin)) {
917
+ return new Response(null, {
918
+ status: 204,
919
+ headers: {
920
+ "Access-Control-Allow-Origin": origin as string,
921
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
922
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-OpenCodex-API-Key",
923
+ Vary: "Origin, Access-Control-Request-Headers",
924
+ },
925
+ });
926
+ }
927
+ }
859
928
  const managementPreflight = url.pathname.startsWith("/api/");
860
929
  const allowed = managementPreflight
861
930
  ? isAllowedManagementOrigin(req, config)
@@ -897,12 +966,93 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
897
966
  // unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
898
967
  // request to the public server's upgrade would fail or cross sockets.
899
968
  if (requestServer.upgrade(req, {
900
- data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
969
+ data: buildResponsesWsData(
970
+ selectForwardHeaders(req.headers),
971
+ admission,
972
+ websocketLease,
973
+ sessionLaneIdFromRequest(req.headers),
974
+ ),
901
975
  })) return undefined as unknown as Response;
902
976
  websocketLease.release();
903
977
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
904
978
  }
905
979
 
980
+ if (url.pathname === "/v1/ws/aistudio" || url.pathname === "/aistudio/ws" || url.pathname === "/v1/ws/aistudio/status") {
981
+ return withCors(jsonResponse({
982
+ error: "gone",
983
+ message: "AI Studio browser relay endpoints are deprecated and return 410 Gone. Use native macOS login (ocx login) or the session exporter extension.",
984
+ }, 410), req, policy);
985
+ }
986
+
987
+ if (url.pathname === "/api/aistudio/session" && req.method === "POST") {
988
+ const admission = resolveApiAuth(req, policy);
989
+ if (!admission) {
990
+ return withAiStudioSessionCors(withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy), req);
991
+ }
992
+ const origin = req.headers.get("Origin");
993
+ if (!isAllowedRequestOrigin(req, policy) && !isAiStudioSessionOrigin(origin)) {
994
+ return withAiStudioSessionCors(withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy), req);
995
+ }
996
+ try {
997
+ const bodyJson = await req.json() as any;
998
+ const { saveAiStudioSession, saveAiStudioSessionFromToken } = await import("../oauth/aistudio-session-sync");
999
+ if (typeof bodyJson.token === "string" && bodyJson.token) {
1000
+ saveAiStudioSessionFromToken(bodyJson.token);
1001
+ } else if (Array.isArray(bodyJson.cookies)) {
1002
+ saveAiStudioSession({
1003
+ selectedProject: bodyJson.selectedProject || "",
1004
+ windowId: bodyJson.windowId || "",
1005
+ cookies: bodyJson.cookies,
1006
+ });
1007
+ } else {
1008
+ return withAiStudioSessionCors(withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy), req);
1009
+ }
1010
+ return withAiStudioSessionCors(withCors(jsonResponse({ ok: true, message: "AI Studio session updated successfully" }), req, policy), req);
1011
+ } catch (error) {
1012
+ return withAiStudioSessionCors(withCors(jsonResponse({ error: String(error) }, 400), req, policy), req);
1013
+ }
1014
+ }
1015
+
1016
+ if (url.pathname === "/aistudio/bridge" && req.method === "GET") {
1017
+ const bridgeHtml = `<!DOCTYPE html>
1018
+ <html><head><meta charset="utf-8"><title>Google AI Studio Relay Deprecated - OpenCodex</title></head>
1019
+ <body><h1>HTTP 410 Gone: AI Studio Browser Relay Deprecated</h1>
1020
+ <p>The browser relay has been retired. Use native macOS authentication or the session exporter extension.</p>
1021
+ <p>Run <code>ocx login</code> to connect.</p></body></html>`;
1022
+ return new Response(bridgeHtml, { status: 410, headers: { "Content-Type": "text/html; charset=utf-8" } });
1023
+ }
1024
+
1025
+ if (url.pathname === "/aistudio/bridge.user.js" && req.method === "GET") {
1026
+ return new Response("// HTTP 410 Gone: OpenCodex AI Studio browser relay and userscripts are deprecated.\n", {
1027
+ status: 410,
1028
+ headers: { "Content-Type": "application/javascript; charset=utf-8" },
1029
+ });
1030
+ }
1031
+
1032
+ if (url.pathname === "/api/aistudio/login/native" && req.method === "POST") {
1033
+ const admission = resolveApiAuth(req, policy);
1034
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1035
+ if (!isAllowedRequestOrigin(req, policy)) {
1036
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy);
1037
+ }
1038
+ try {
1039
+ const login = await (deps.runAiStudioNativeLogin ?? runAiStudioNativeLogin)({ signal: req.signal });
1040
+ if (login.kind === "unsupported") return jsonResponse({ ok: false, error: "Native login is only available on macOS" }, 400, req, policy);
1041
+ if (login.kind === "cancelled") return jsonResponse({ ok: false, error: "Native AI Studio login cancelled" }, 499, req, policy);
1042
+ if (login.kind === "failed") return jsonResponse({ ok: false, error: login.error }, 500, req, policy);
1043
+ const probeRequest = new Request(new URL("/api/providers/test?name=google-aistudio", req.url), {
1044
+ method: "POST",
1045
+ headers: { Host: req.headers.get("Host") ?? "127.0.0.1" },
1046
+ });
1047
+ const probeResponse = await handleManagementAPI(probeRequest, new URL(probeRequest.url), config, deps.managementApi);
1048
+ const probe = await probeResponse?.json().catch(() => null) as { ok?: boolean; error?: string } | null;
1049
+ if (!probe?.ok) return jsonResponse({ ok: false, error: probe?.error ?? "AI Studio connection probe failed" }, 502, req, policy);
1050
+ return jsonResponse({ ok: true, sessionPath: login.sessionPath }, 200, req, policy);
1051
+ } catch (error) {
1052
+ return jsonResponse({ ok: false, error: String(error) }, 500, req, policy);
1053
+ }
1054
+ }
1055
+
906
1056
  if (url.pathname === "/healthz" && req.method === "GET") {
907
1057
  // service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
908
1058
  const healthPort = server.port ?? listenPort;
@@ -1028,6 +1178,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1028
1178
  : [];
1029
1179
  const disabledNatives = disabledNativeSlugs(config);
1030
1180
  const disabledModels = new Set(config.disabledModels ?? []);
1181
+ const exactComboSlugs = exactComboCatalogSlugs(config);
1031
1182
  const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
1032
1183
  const suppressedBareNativeSlugs = new Set([
1033
1184
  ...desktopAllowlistSuppressedNativeSlugs(config),
@@ -1113,7 +1264,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1113
1264
  config.subagentModels,
1114
1265
  websocketsEnabled(config),
1115
1266
  maMode as "v1" | "default" | "v2",
1116
- exactComboCatalogSlugs(config),
1267
+ exactComboSlugs,
1117
1268
  accountSelectors,
1118
1269
  suppressedBareNativeSlugs,
1119
1270
  new Set(),
@@ -1190,12 +1341,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1190
1341
  const data = [
1191
1342
  ...visibleNatives.map(id => nativeModelRow(id)),
1192
1343
  ...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
1193
- ...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
1194
- id: m.alias ?? `${m.provider}/${m.id}`,
1195
- object: "model",
1196
- created: 0,
1197
- owned_by: m.owned_by ?? m.provider,
1198
- ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1344
+ ...await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1345
+ const publicId = m.alias ?? `${m.provider}/${m.id}`;
1346
+ const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1347
+ const provider = config.providers[m.provider];
1348
+ const effective = provider
1349
+ ? (await import("../providers/default-aliases")).effectiveModelAliases(
1350
+ config,
1351
+ provider,
1352
+ knownModelIdsForProvider(m.provider, provider, config),
1353
+ ).get(m.id)
1354
+ : undefined;
1355
+ return {
1356
+ id: publicId,
1357
+ object: "model",
1358
+ created: 0,
1359
+ // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1360
+ // owned_by as an adapter selector, so a virtual combo must name that wire
1361
+ // adapter rather than the internal catalog authority marker.
1362
+ owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1363
+ ...(isCombo ? { is_combo: true } : {}),
1364
+ ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1365
+ ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1366
+ };
1199
1367
  })),
1200
1368
  ];
1201
1369
  return jsonResponse({ object: "list", data }, 200, req, policy);
@@ -1502,7 +1670,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1502
1670
  provider: "unknown",
1503
1671
  ...admissionFields(admission),
1504
1672
  };
1505
- const turnAdmissionLease = tryAdmitTurn();
1673
+ const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
1506
1674
  if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
1507
1675
  let resolved;
1508
1676
  try {
@@ -1654,7 +1822,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1654
1822
  return;
1655
1823
  }
1656
1824
 
1657
- const turnAdmissionLease = tryAdmitTurn();
1825
+ const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
1658
1826
  if (!turnAdmissionLease) {
1659
1827
  sendJsonFrame(ws, buildWsErrorFrame(503, {
1660
1828
  type: "server_error",
@@ -1888,7 +2056,5 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1888
2056
  activateLab(config, labConfigDir);
1889
2057
  }
1890
2058
 
1891
- registerFork();
1892
-
1893
2059
  return server;
1894
2060
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { flushAntigravityReplay } from "../adapters/google-antigravity-replay";
2
3
  import { flushResponseState } from "../responses/state";
3
4
  import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
@@ -30,6 +31,8 @@ import { releaseNativeMainStartupLifecycle } from "../codex/native-profile-start
30
31
  // ---------------------------------------------------------------------------
31
32
 
32
33
  export const MAX_ACTIVE_TURNS = 256;
34
+ export const MAX_ACTIVE_SESSION_LANES = 64;
35
+ export const SESSION_LANE_ID_BYTES = 32;
33
36
  const turnGate = createAdmissionGate("active_turns", MAX_ACTIVE_TURNS);
34
37
  export interface ActiveTurnLease extends AdmissionLease {
35
38
  bindAbortController(ac: AbortController): void;
@@ -38,6 +41,10 @@ export interface ActiveTurnLease extends AdmissionLease {
38
41
  }
39
42
  const activeTurns = new Map<AbortController, ActiveTurnLease>();
40
43
  const admittedTurns = new Set<ActiveTurnLease>();
44
+ const activeSessionLaneRefCounts = new Map<string, number>();
45
+ let sessionLanePeak = 0;
46
+ let sessionLaneAdmitted = 0;
47
+ let sessionLaneRejected = 0;
41
48
  const knownTurnControllers = new WeakSet<AbortController>();
42
49
  let turnReleaseMisses = 0;
43
50
  let shutdownDraining = false;
@@ -149,6 +156,10 @@ export function resetLifecycleDrainStateForTests(): void {
149
156
  temporaryDrainOwners.clear();
150
157
  nativeMainDrainOwners.clear();
151
158
  nativeMainTurns.clear();
159
+ activeSessionLaneRefCounts.clear();
160
+ sessionLanePeak = 0;
161
+ sessionLaneAdmitted = 0;
162
+ sessionLaneRejected = 0;
152
163
  nativeMainSelections = 0;
153
164
  for (const resolve of temporaryDrainWaiters) resolve();
154
165
  temporaryDrainWaiters.clear();
@@ -157,10 +168,27 @@ export function resetLifecycleDrainStateForTests(): void {
157
168
  serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
158
169
  releaseServerStartupLifecycleImpl = releaseNativeMainStartupLifecycle;
159
170
  }
160
- export function tryAdmitTurn(): ActiveTurnLease | null {
171
+ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
161
172
  if (isDraining()) return null;
173
+ const opaqueSessionLaneId = sessionLaneId
174
+ ? createHash("sha256").update(sessionLaneId).digest("hex").slice(0, SESSION_LANE_ID_BYTES)
175
+ : undefined;
176
+ const sessionLaneRefCount = opaqueSessionLaneId
177
+ ? activeSessionLaneRefCounts.get(opaqueSessionLaneId) ?? 0
178
+ : 0;
179
+ if (opaqueSessionLaneId && sessionLaneRefCount === 0 && activeSessionLaneRefCounts.size >= MAX_ACTIVE_SESSION_LANES) {
180
+ sessionLaneRejected += 1;
181
+ return null;
182
+ }
162
183
  const gateLease = turnGate.tryAcquire();
163
184
  if (!gateLease) return null;
185
+ if (opaqueSessionLaneId) {
186
+ activeSessionLaneRefCounts.set(opaqueSessionLaneId, sessionLaneRefCount + 1);
187
+ if (sessionLaneRefCount === 0) {
188
+ sessionLaneAdmitted += 1;
189
+ sessionLanePeak = Math.max(sessionLanePeak, activeSessionLaneRefCounts.size);
190
+ }
191
+ }
164
192
  const controllers = new Set<AbortController>();
165
193
  let active = true;
166
194
  let transferred = false;
@@ -211,6 +239,13 @@ export function tryAdmitTurn(): ActiveTurnLease | null {
211
239
  }
212
240
  controllers.clear();
213
241
  nativeMainTurns.delete(lease);
242
+ if (opaqueSessionLaneId) {
243
+ const currentRefCount = activeSessionLaneRefCounts.get(opaqueSessionLaneId);
244
+ if (currentRefCount === 1) activeSessionLaneRefCounts.delete(opaqueSessionLaneId);
245
+ else if (currentRefCount && currentRefCount > 1) {
246
+ activeSessionLaneRefCounts.set(opaqueSessionLaneId, currentRefCount - 1);
247
+ }
248
+ }
214
249
  gateLease.release();
215
250
  },
216
251
  };
@@ -261,6 +296,22 @@ export function unregisterTurn(ac: AbortController): void {
261
296
  }
262
297
  export function isDraining(): boolean { return shutdownDraining || temporaryDrainOwners.size > 0; }
263
298
  export function getActiveTurnCount(): number { return turnGate.metrics().active; }
299
+ export interface SessionLaneMetrics {
300
+ active: number;
301
+ peak: number;
302
+ admitted: number;
303
+ rejected: number;
304
+ retainedBytes: number;
305
+ }
306
+ export function sessionLaneMetrics(): SessionLaneMetrics {
307
+ return {
308
+ active: activeSessionLaneRefCounts.size,
309
+ peak: sessionLanePeak,
310
+ admitted: sessionLaneAdmitted,
311
+ rejected: sessionLaneRejected,
312
+ retainedBytes: activeSessionLaneRefCounts.size * SESSION_LANE_ID_BYTES,
313
+ };
314
+ }
264
315
  export function getNativeMainProfileRequestCount(): number {
265
316
  return nativeMainSelections + nativeMainTurns.size;
266
317
  }
@@ -5,6 +5,7 @@ import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nati
5
5
  import {
6
6
  DEFAULT_SUBAGENT_MODELS,
7
7
  codexAutoStartEnabled,
8
+ deleteConfigTopLevelKey,
8
9
  hasOwnProvider,
9
10
  isValidProviderName,
10
11
  loadConfig,
@@ -187,7 +188,7 @@ function mirrorDesiredEnabledOntoSnapshot(config: OcxConfig, client: "claude-des
187
188
  const integrations = { ...(config.clientIntegrations ?? {}) };
188
189
  if (enabled) delete integrations[client];
189
190
  else integrations[client] = false;
190
- if (Object.keys(integrations).length === 0) delete config.clientIntegrations;
191
+ if (Object.keys(integrations).length === 0) deleteConfigTopLevelKey(config, "clientIntegrations");
191
192
  else config.clientIntegrations = integrations;
192
193
  }
193
194
 
@@ -405,9 +406,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
405
406
  return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
406
407
  }
407
408
  const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
408
- const modeFlag = mode === "v2" ? true : mode === "v1" ? false : undefined;
409
+ const effectiveMode = mode ?? config.multiAgentMode ?? "default";
410
+ const effectiveKeepNative = wantsKeepNative
411
+ ? body.keepNativeChatGptOnV1 === true
412
+ : config.keepNativeChatGptOnV1 === true;
413
+ const hybridPinActive = effectiveMode === "v2" && effectiveKeepNative;
414
+ const modeFlag = mode === "v2" ? !hybridPinActive : mode === "v1" ? false : undefined;
409
415
  if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
410
- return jsonResponse({ error: `body.enabled conflicts with multiAgentMode '${mode}'` }, 400);
416
+ return jsonResponse({
417
+ error: hybridPinActive
418
+ ? "body.enabled=true conflicts with keepNativeChatGptOnV1: Codex's global multi_agent_v2 override outranks catalog pins"
419
+ : `body.enabled conflicts with multiAgentMode '${mode}'`,
420
+ }, 400);
411
421
  }
412
422
  const { isMultiAgentV2Enabled: readMultiAgentV2Enabled } = await import("../../codex/features");
413
423
  const currentUpstreamEnabled = readMultiAgentV2Enabled();
@@ -511,7 +521,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
511
521
  }, 502);
512
522
  }
513
523
  const warnings: string[] = [];
514
- const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
524
+ if (wantsFlag && body.enabled === true && hybridPinActive) {
525
+ return jsonResponse({
526
+ error: "body.enabled=true conflicts with keepNativeChatGptOnV1: Codex's global multi_agent_v2 override outranks catalog pins",
527
+ }, 400);
528
+ }
529
+ const requestedFlag = wantsFlag
530
+ ? body.enabled as boolean
531
+ : modeFlag ?? (wantsKeepNative && hybridPinActive ? false : undefined);
515
532
  if (requestedFlag !== undefined || wantsThreads) {
516
533
  const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
517
534
  let toggle = deps.toggleCodexMultiAgentV2;
@@ -526,16 +543,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
526
543
  if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
527
544
  }
528
545
  if (wantsMode) {
529
- if (mode === "default") delete config.multiAgentMode;
546
+ if (mode === "default") deleteConfigTopLevelKey(config, "multiAgentMode");
530
547
  else config.multiAgentMode = mode;
531
548
  saveConfigPreservingClaudeCode(config);
532
549
  warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
533
550
  }
534
551
  if (wantsKeepNative) {
535
552
  if (body.keepNativeChatGptOnV1 === true) config.keepNativeChatGptOnV1 = true;
536
- else delete config.keepNativeChatGptOnV1;
553
+ else deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
537
554
  saveConfigPreservingClaudeCode(config);
538
- const effectiveMode = mode ?? config.multiAgentMode ?? "default";
539
555
  warnings.push(body.keepNativeChatGptOnV1 === true
540
556
  ? (effectiveMode === "v2"
541
557
  ? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
@@ -754,13 +770,13 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
754
770
 
755
771
  config.multiAgentGuidanceEnabled = nextEnabled;
756
772
  if (nextSyncCodexSubagentDefaults) config.syncCodexSubagentDefaults = true;
757
- else delete config.syncCodexSubagentDefaults;
773
+ else deleteConfigTopLevelKey(config, "syncCodexSubagentDefaults");
758
774
  if (nextModel) config.injectionModel = nextModel;
759
- else delete config.injectionModel;
775
+ else deleteConfigTopLevelKey(config, "injectionModel");
760
776
  if (nextEffort) config.injectionEffort = nextEffort;
761
- else delete config.injectionEffort;
777
+ else deleteConfigTopLevelKey(config, "injectionEffort");
762
778
  if (nextPrompt) config.injectionPrompt = nextPrompt;
763
- else delete config.injectionPrompt;
779
+ else deleteConfigTopLevelKey(config, "injectionPrompt");
764
780
 
765
781
  saveConfigPreservingClaudeCode(config);
766
782
  return jsonResponse({
@@ -791,7 +807,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
791
807
  for (const key of ["effortCap", "subagentEffortCap"] as const) {
792
808
  if (!(key in body)) continue;
793
809
  const value = body[key];
794
- if (value === null || value === "") { delete config[key]; continue; }
810
+ if (value === null || value === "") { deleteConfigTopLevelKey(config, key); continue; }
795
811
  if (typeof value !== "string" || !isCodexReasoningEffort(value)) {
796
812
  return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400);
797
813
  }
@@ -998,9 +1014,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
998
1014
  }
999
1015
  }
1000
1016
  if (nextModels !== undefined) config.subagentModelFallback = nextModels;
1001
- else delete config.subagentModelFallback;
1017
+ else deleteConfigTopLevelKey(config, "subagentModelFallback");
1002
1018
  if (nextPollMs !== undefined) config.subagentModelFallbackPollMs = nextPollMs;
1003
- else delete config.subagentModelFallbackPollMs;
1019
+ else deleteConfigTopLevelKey(config, "subagentModelFallbackPollMs");
1004
1020
  saveConfigPreservingClaudeCode(config);
1005
1021
  return jsonResponse({
1006
1022
  ok: true,
@@ -1041,7 +1057,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1041
1057
  // cannot grow config.json without bound.
1042
1058
  const excluded = [...new Set(raw as string[])].sort();
1043
1059
  if (excluded.length > 2000) return jsonResponse({ error: "excluded list is too large" }, 400);
1044
- if (excluded.length === 0) delete config.grokExcludedModels;
1060
+ if (excluded.length === 0) deleteConfigTopLevelKey(config, "grokExcludedModels");
1045
1061
  else config.grokExcludedModels = excluded;
1046
1062
  saveConfigPreservingClaudeCode(config);
1047
1063
  return jsonResponse({ ok: true, excluded });