@yansigit/opencodex 2.33.1 → 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 (177) 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 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  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 +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -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,
@@ -40,7 +41,6 @@ import {
40
41
  enforceAppOwnedMemoryBudget,
41
42
  resolveAppOwnedMemoryBudgetBytes,
42
43
  } from "../lib/app-owned-memory";
43
- import { getAiStudioBridgeHtml, getAiStudioUserScript, globalAiStudioRelayHub } from "./aistudio-ws-hub";
44
44
  import {
45
45
  registerAppOwnedMemorySweepFallback,
46
46
  registerDefaultAppOwnedMemoryStores,
@@ -48,7 +48,6 @@ import {
48
48
  } from "../lib/app-owned-memory-stores";
49
49
  import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
50
50
  import { activateLab, labActivationRequired } from "../lib/lab-activation";
51
- import { registerFork } from "../fork/register";
52
51
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
53
52
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
54
53
  import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
@@ -111,6 +110,7 @@ import {
111
110
  type RequestLogContext,
112
111
  type RequestLogEntry,
113
112
  } from "./request-log";
113
+ import { sessionLaneIdFromRequest } from "./request-log-conversation";
114
114
  export {
115
115
  addFinalRequestLog,
116
116
  filterRequestLogs,
@@ -202,6 +202,22 @@ import {
202
202
  import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
203
203
  import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
204
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
+ }
205
221
 
206
222
  export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
207
223
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
@@ -454,6 +470,10 @@ export interface StartServerDeps {
454
470
  localAttestationSecret?: string;
455
471
  /** Optional readiness gate; a fresh pending gate is created when omitted. */
456
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;
457
477
  }
458
478
 
459
479
  function inspectStartupOwnership(
@@ -669,12 +689,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
669
689
  if (path === "/v1/realtime" || path === "/v1/live") {
670
690
  return req.headers.get("upgrade")?.toLowerCase() === "websocket";
671
691
  }
672
- if (path === "/v1/ws/aistudio" || path === "/aistudio/ws") {
673
- return req.headers.get("upgrade")?.toLowerCase() === "websocket";
674
- }
675
- if (path === "/v1/ws/aistudio/status") return req.method === "GET";
676
- if (path === "/api/aistudio/session") return req.method === "POST";
677
- if (path === "/aistudio/bridge" || path === "/aistudio/bridge.user.js") return req.method === "GET";
678
692
  return false;
679
693
  }
680
694
 
@@ -704,12 +718,21 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
704
718
  }), req, policy);
705
719
  }
706
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
+
707
730
  async function runAdmittedHttpTurn(
708
731
  req: Request,
709
732
  policy: RequestPolicyView,
710
733
  work: (lease: ActiveTurnLease) => Promise<Response>,
711
734
  ): Promise<Response> {
712
- const lease = tryAdmitTurn();
735
+ const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
713
736
  if (!lease) return serverBusyResponse(req, "active turns", policy);
714
737
  let response: Response;
715
738
  try {
@@ -730,6 +753,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
730
753
  // passes it in, and transitions it after the post-startup sync settles. When
731
754
  // no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
732
755
  const readinessGate = deps.readinessGate ?? createReadinessGate();
756
+ const packageTreeIntegrity = deps.packageTreeIntegrity
757
+ ?? createRuntimePackageTreeIntegrityGuard(detectInstall());
733
758
  // Actual bound port, filled in after Bun.serve binds so /readyz reports the
734
759
  // real ephemeral port for startServer(0). /healthz keeps its existing port
735
760
  // field (the requested listenPort) byte-for-byte.
@@ -856,6 +881,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
856
881
  if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
857
882
  } catch { /* malformed encoding — not a readiness path */ }
858
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
+
859
907
  if (req.method === "OPTIONS") {
860
908
  // /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
861
909
  // path) must answer the deterministic JSON 404, never the generic 204
@@ -863,6 +911,20 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
863
911
  if (readyzPath !== undefined) {
864
912
  return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
865
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
+ }
866
928
  const managementPreflight = url.pathname.startsWith("/api/");
867
929
  const allowed = managementPreflight
868
930
  ? isAllowedManagementOrigin(req, config)
@@ -904,51 +966,37 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
904
966
  // unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
905
967
  // request to the public server's upgrade would fail or cross sockets.
906
968
  if (requestServer.upgrade(req, {
907
- data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
969
+ data: buildResponsesWsData(
970
+ selectForwardHeaders(req.headers),
971
+ admission,
972
+ websocketLease,
973
+ sessionLaneIdFromRequest(req.headers),
974
+ ),
908
975
  })) return undefined as unknown as Response;
909
976
  websocketLease.release();
910
977
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
911
978
  }
912
979
 
913
- if (url.pathname === "/v1/ws/aistudio" || url.pathname === "/aistudio/ws") {
914
- if (isDraining()) return drainingResponse(req, policy);
915
- const admission = resolveApiAuth(req, policy);
916
- if (!admission) {
917
- return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
918
- }
919
- const origin = req.headers.get("Origin");
920
- const isExtensionOrigin = origin?.startsWith("chrome-extension://");
921
- if (!isAllowedRequestOrigin(req, policy) && origin !== "https://aistudio.google.com" && !isExtensionOrigin) {
922
- return withCors(formatErrorResponse(403, "origin_rejected", "AI Studio relay WebSocket blocked: non-local Origin"), req, policy);
923
- }
924
- const sessionId = "aistudio_" + crypto.randomUUID().slice(0, 8);
925
- if (requestServer.upgrade(req, {
926
- data: { kind: "aistudio-relay", aistudioSessionId: sessionId },
927
- })) return undefined as unknown as Response;
928
- return withCors(formatErrorResponse(426, "upgrade_required", "AI Studio WebSocket upgrade failed"), req, policy);
929
- }
930
-
931
- if (url.pathname === "/v1/ws/aistudio/status" && req.method === "GET") {
932
- return jsonResponse({
933
- activeSessions: globalAiStudioRelayHub.getActiveSessionCount(),
934
- hasActiveSessions: globalAiStudioRelayHub.hasActiveSessions(),
935
- });
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);
936
985
  }
937
986
 
938
987
  if (url.pathname === "/api/aistudio/session" && req.method === "POST") {
939
988
  const admission = resolveApiAuth(req, policy);
940
989
  if (!admission) {
941
- return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
990
+ return withAiStudioSessionCors(withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy), req);
942
991
  }
943
992
  const origin = req.headers.get("Origin");
944
- const isExtensionOrigin = origin?.startsWith("chrome-extension://");
945
- if (!isAllowedRequestOrigin(req, policy) && origin !== "https://aistudio.google.com" && !isExtensionOrigin) {
946
- return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy);
993
+ if (!isAllowedRequestOrigin(req, policy) && !isAiStudioSessionOrigin(origin)) {
994
+ return withAiStudioSessionCors(withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy), req);
947
995
  }
948
996
  try {
949
- const bodyJson = (await req.json()) as any;
997
+ const bodyJson = await req.json() as any;
950
998
  const { saveAiStudioSession, saveAiStudioSessionFromToken } = await import("../oauth/aistudio-session-sync");
951
- if (bodyJson.token && typeof bodyJson.token === "string") {
999
+ if (typeof bodyJson.token === "string" && bodyJson.token) {
952
1000
  saveAiStudioSessionFromToken(bodyJson.token);
953
1001
  } else if (Array.isArray(bodyJson.cookies)) {
954
1002
  saveAiStudioSession({
@@ -957,24 +1005,52 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
957
1005
  cookies: bodyJson.cookies,
958
1006
  });
959
1007
  } else {
960
- return withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy);
1008
+ return withAiStudioSessionCors(withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy), req);
961
1009
  }
962
- return withCors(jsonResponse({ ok: true, message: "AI Studio session updated successfully" }), req, policy);
963
- } catch (err) {
964
- return withCors(jsonResponse({ error: String(err) }, 400), req, policy);
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);
965
1013
  }
966
1014
  }
967
1015
 
968
1016
  if (url.pathname === "/aistudio/bridge" && req.method === "GET") {
969
- const listenPort = (server.port ?? config.port) || 10100;
970
- const bridgeHtml = getAiStudioBridgeHtml(listenPort);
971
- return new Response(bridgeHtml, { headers: { "Content-Type": "text/html; charset=utf-8" } });
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" } });
972
1023
  }
973
1024
 
974
1025
  if (url.pathname === "/aistudio/bridge.user.js" && req.method === "GET") {
975
- const listenPort = (server.port ?? config.port) || 10100;
976
- const userScript = getAiStudioUserScript(listenPort);
977
- return new Response(userScript, { headers: { "Content-Type": "application/javascript; charset=utf-8" } });
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
+ }
978
1054
  }
979
1055
 
980
1056
  if (url.pathname === "/healthz" && req.method === "GET") {
@@ -1102,6 +1178,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1102
1178
  : [];
1103
1179
  const disabledNatives = disabledNativeSlugs(config);
1104
1180
  const disabledModels = new Set(config.disabledModels ?? []);
1181
+ const exactComboSlugs = exactComboCatalogSlugs(config);
1105
1182
  const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
1106
1183
  const suppressedBareNativeSlugs = new Set([
1107
1184
  ...desktopAllowlistSuppressedNativeSlugs(config),
@@ -1187,7 +1264,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1187
1264
  config.subagentModels,
1188
1265
  websocketsEnabled(config),
1189
1266
  maMode as "v1" | "default" | "v2",
1190
- exactComboCatalogSlugs(config),
1267
+ exactComboSlugs,
1191
1268
  accountSelectors,
1192
1269
  suppressedBareNativeSlugs,
1193
1270
  new Set(),
@@ -1264,12 +1341,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1264
1341
  const data = [
1265
1342
  ...visibleNatives.map(id => nativeModelRow(id)),
1266
1343
  ...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
1267
- ...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
1268
- id: m.alias ?? `${m.provider}/${m.id}`,
1269
- object: "model",
1270
- created: 0,
1271
- owned_by: m.owned_by ?? m.provider,
1272
- ...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
+ };
1273
1367
  })),
1274
1368
  ];
1275
1369
  return jsonResponse({ object: "list", data }, 200, req, policy);
@@ -1576,7 +1670,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1576
1670
  provider: "unknown",
1577
1671
  ...admissionFields(admission),
1578
1672
  };
1579
- const turnAdmissionLease = tryAdmitTurn();
1673
+ const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
1580
1674
  if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
1581
1675
  let resolved;
1582
1676
  try {
@@ -1638,10 +1732,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1638
1732
  // Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
1639
1733
  // Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
1640
1734
  open(ws: ServerWebSocket<WsData>) {
1641
- if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
1642
- globalAiStudioRelayHub.registerSession(ws.data.aistudioSessionId, ws);
1643
- return;
1644
- }
1645
1735
  if (ws.data.kind === "live-sideband") {
1646
1736
  if (!ws.data.liveTurnAdmissionLease) {
1647
1737
  closeLiveSideband(ws, 1013, "server busy");
@@ -1658,18 +1748,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1658
1748
  registerCodexWebSocket(ws);
1659
1749
  },
1660
1750
  message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
1661
- if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
1662
- const text = typeof raw === "string"
1663
- ? raw
1664
- : Buffer.isBuffer(raw)
1665
- ? raw.toString("utf-8")
1666
- : new TextDecoder().decode(raw as any);
1667
- globalAiStudioRelayHub.handleClientMessage(
1668
- ws.data.aistudioSessionId,
1669
- text
1670
- );
1671
- return;
1672
- }
1673
1751
  if (ws.data.kind === "live-sideband") {
1674
1752
  if (ws.data.liveClosing) return;
1675
1753
  const rawBytes = webSocketFrameBytes(raw);
@@ -1744,7 +1822,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1744
1822
  return;
1745
1823
  }
1746
1824
 
1747
- const turnAdmissionLease = tryAdmitTurn();
1825
+ const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
1748
1826
  if (!turnAdmissionLease) {
1749
1827
  sendJsonFrame(ws, buildWsErrorFrame(503, {
1750
1828
  type: "server_error",
@@ -1848,10 +1926,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1848
1926
  })();
1849
1927
  },
1850
1928
  close(ws: ServerWebSocket<WsData>) {
1851
- if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
1852
- globalAiStudioRelayHub.unregisterSession(ws.data.aistudioSessionId);
1853
- return;
1854
- }
1855
1929
  if (ws.data.kind === "live-sideband") {
1856
1930
  closeLiveSideband(ws);
1857
1931
  return;
@@ -1982,7 +2056,5 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1982
2056
  activateLab(config, labConfigDir);
1983
2057
  }
1984
2058
 
1985
- registerFork();
1986
-
1987
2059
  return server;
1988
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 });