@yansigit/opencodex 2.32.0 → 2.33.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 (108) hide show
  1. package/README.md +2 -2
  2. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  3. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +6 -5
  6. package/src/adapters/anthropic.ts +20 -6
  7. package/src/adapters/azure.ts +20 -4
  8. package/src/adapters/base.ts +3 -1
  9. package/src/adapters/command-code.ts +40 -7
  10. package/src/adapters/cursor/live-transport.ts +1 -1
  11. package/src/adapters/cursor/protobuf-events.ts +158 -7
  12. package/src/adapters/cursor/protobuf-request.ts +33 -15
  13. package/src/adapters/cursor/request-builder.ts +4 -3
  14. package/src/adapters/cursor/tool-definitions.ts +27 -1
  15. package/src/adapters/cursor/types.ts +4 -3
  16. package/src/adapters/cursor.ts +9 -0
  17. package/src/adapters/google-antigravity-replay.ts +2 -2
  18. package/src/adapters/google-antigravity-wire.ts +7 -0
  19. package/src/adapters/google-errors.ts +6 -2
  20. package/src/adapters/google-http.ts +30 -7
  21. package/src/adapters/google-truncation.ts +5 -0
  22. package/src/adapters/google-wire-compiler.ts +38 -6
  23. package/src/adapters/google.ts +148 -26
  24. package/src/adapters/kiro-tools.ts +20 -9
  25. package/src/adapters/openai-chat.ts +9 -0
  26. package/src/adapters/openai-responses.ts +1 -1
  27. package/src/bridge.ts +112 -9
  28. package/src/claude/context-windows.ts +16 -9
  29. package/src/cli/doctor.ts +2 -2
  30. package/src/cli/index.ts +9 -2
  31. package/src/cli/provider.ts +6 -0
  32. package/src/cli/status.ts +23 -0
  33. package/src/codex/auth-api.ts +4 -2
  34. package/src/codex/autostart-health.ts +16 -0
  35. package/src/codex/catalog/aggregation.ts +12 -12
  36. package/src/codex/catalog/effort.ts +18 -3
  37. package/src/codex/catalog/metadata.ts +27 -1
  38. package/src/codex/catalog/model-metadata.ts +39 -12
  39. package/src/codex/catalog/parsing.ts +38 -27
  40. package/src/codex/catalog/provider-fetch.ts +198 -133
  41. package/src/codex/catalog/sync.ts +1 -1
  42. package/src/codex/convergence.ts +5 -0
  43. package/src/codex/shim.ts +56 -3
  44. package/src/config/provider-validation.ts +37 -0
  45. package/src/config.ts +78 -2
  46. package/src/generated/compatibility-version.json +120 -96
  47. package/src/images/loop.ts +37 -6
  48. package/src/lib/azure-identity.ts +154 -0
  49. package/src/lib/debug.ts +42 -0
  50. package/src/lib/errors.ts +14 -0
  51. package/src/lib/provider-outbound.ts +45 -33
  52. package/src/lib/provider-tls-profile.ts +309 -0
  53. package/src/lib/proxy-env.ts +49 -0
  54. package/src/lib/redact.ts +10 -1
  55. package/src/oauth/antigravity-routing.ts +282 -236
  56. package/src/oauth/callback-server.ts +22 -2
  57. package/src/oauth/command-code.ts +5 -16
  58. package/src/oauth/google-antigravity.ts +42 -5
  59. package/src/oauth/index.ts +15 -3
  60. package/src/oauth/kimi.ts +9 -1
  61. package/src/oauth/open-browser-choice.ts +26 -0
  62. package/src/oauth/store.ts +6 -0
  63. package/src/providers/antigravity-quota.ts +3 -1
  64. package/src/providers/api-keys.ts +2 -1
  65. package/src/providers/auto-compact-budget.ts +65 -0
  66. package/src/providers/derive.ts +4 -0
  67. package/src/providers/key-failover.ts +5 -1
  68. package/src/providers/openai-tiers.ts +5 -0
  69. package/src/providers/provider-id-rewrite.ts +1 -0
  70. package/src/providers/quota.ts +59 -13
  71. package/src/providers/registry.ts +3 -1
  72. package/src/providers/request-pacing.ts +33 -6
  73. package/src/providers/xai-transport.ts +21 -0
  74. package/src/responses/google-provider-options.ts +36 -0
  75. package/src/responses/namespace-tool-compat.ts +84 -4
  76. package/src/responses/parser.ts +11 -0
  77. package/src/responses/provider-opaque-metadata.ts +3 -3
  78. package/src/responses/schema.ts +37 -0
  79. package/src/responses/state.ts +94 -4
  80. package/src/router.ts +8 -2
  81. package/src/server/auth-cors.ts +28 -0
  82. package/src/server/images.ts +19 -35
  83. package/src/server/management/agent-settings-routes.ts +205 -15
  84. package/src/server/management/combo-routes.ts +6 -0
  85. package/src/server/management/config-routes.ts +31 -5
  86. package/src/server/management/model-rows.ts +4 -0
  87. package/src/server/management/oauth-account-routes.ts +25 -4
  88. package/src/server/management/provider-routes.ts +113 -15
  89. package/src/server/management/routing-profile-routes.ts +3 -0
  90. package/src/server/request-log.ts +21 -0
  91. package/src/server/responses/agent-task-recovery.ts +1 -1
  92. package/src/server/responses/compact.ts +30 -1
  93. package/src/server/responses/core.ts +359 -153
  94. package/src/server/responses/empty-completion-guard.ts +35 -6
  95. package/src/server/responses/fetch-helpers.ts +18 -5
  96. package/src/server/responses/v2-native-parent-override.ts +59 -0
  97. package/src/server/responses/ws-upstream.ts +75 -2
  98. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  99. package/src/service.ts +1 -1
  100. package/src/types/config.ts +16 -1
  101. package/src/types/provider.ts +16 -0
  102. package/src/types/request.ts +28 -0
  103. package/src/types/tools.ts +27 -0
  104. package/src/types.ts +6 -0
  105. package/src/web-search/gemini-executor.ts +6 -4
  106. package/src/web-search/loop.ts +42 -6
  107. package/gui/dist/assets/index-BG43zwVe.js +0 -102
  108. package/gui/dist/assets/index-CiSI-jrP.css +0 -1
@@ -60,6 +60,7 @@ import {
60
60
  webSearchCandidateRows,
61
61
  webSearchModelIsRejected,
62
62
  webSearchModelRejection,
63
+ type WebSearchBackend,
63
64
  } from "./web-search-sidecar-options";
64
65
  import { drainAndShutdown } from "../lifecycle";
65
66
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
@@ -67,6 +68,7 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS
67
68
  import type { PersistedUsageAttempt } from "../../usage/log";
68
69
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
69
70
  import { applySystemEnvToggle } from "../system-env";
71
+ import { routeModel } from "../../router";
70
72
 
71
73
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
72
74
  import {
@@ -86,6 +88,90 @@ let grokApplyFlight: { startedAt: number; promise: Promise<unknown>; bytes: numb
86
88
  let grokApplyHighWaterBytes = 0;
87
89
  let grokApplyTestHooks: { now?: () => number; run?: () => Promise<unknown> } | null = null;
88
90
 
91
+ type V2NativeParentOverrideInput = { enabled: boolean; model: string | null };
92
+ type AgentTaskRecoveryInput = { enabled: boolean; model: string | null };
93
+
94
+ function agentTaskRecoveryDto(
95
+ config: OcxConfig,
96
+ ): { enabled: boolean; model: string | null } {
97
+ const recovery = config.agentTaskRecovery;
98
+ return {
99
+ enabled: recovery?.enabled === true,
100
+ model: recovery?.model ?? null,
101
+ };
102
+ }
103
+
104
+ function persistAgentTaskRecovery(
105
+ config: OcxConfig,
106
+ next: AgentTaskRecoveryInput,
107
+ ): { ok: true } | { ok: false; reason: string } {
108
+ const outcome = mutatePersistedConfig(persisted => {
109
+ const nextPersisted = {
110
+ enabled: next.enabled,
111
+ ...(next.model === null ? {} : { model: next.model }),
112
+ };
113
+ const previous = persisted.agentTaskRecovery;
114
+ const changed = previous?.enabled !== nextPersisted.enabled || previous?.model !== nextPersisted.model;
115
+ if (changed) persisted.agentTaskRecovery = nextPersisted;
116
+ return { changed, value: true };
117
+ });
118
+ if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
119
+ config.agentTaskRecovery = {
120
+ enabled: next.enabled,
121
+ ...(next.model === null ? {} : { model: next.model }),
122
+ };
123
+ return { ok: true };
124
+ }
125
+
126
+ function v2NativeParentOverrideDto(
127
+ config: OcxConfig,
128
+ upstreamEnabled: boolean,
129
+ ): { enabled: boolean; model: string | null; active: boolean } {
130
+ const override = config.v2NativeParentOverride;
131
+ const enabled = override?.enabled === true;
132
+ const model = override?.model ?? null;
133
+ return {
134
+ enabled,
135
+ model,
136
+ active: enabled
137
+ && model !== null
138
+ && v2NativeParentOverrideTargetIsNoncanonical(config, model)
139
+ && config.multiAgentMode === "v2"
140
+ && upstreamEnabled
141
+ && config.keepNativeChatGptOnV1 !== true,
142
+ };
143
+ }
144
+
145
+ function v2NativeParentOverrideTargetIsNoncanonical(config: OcxConfig, model: string): boolean {
146
+ try {
147
+ return !isCanonicalOpenAiForwardProvider(routeModel(config, model).provider);
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ function persistV2NativeParentOverride(
154
+ config: OcxConfig,
155
+ next: V2NativeParentOverrideInput,
156
+ ): { ok: true } | { ok: false; reason: string } {
157
+ const outcome = mutatePersistedConfig(persisted => {
158
+ const nextPersisted = {
159
+ enabled: next.enabled,
160
+ ...(next.model === null ? {} : { model: next.model }),
161
+ };
162
+ const previous = persisted.v2NativeParentOverride;
163
+ const changed = previous?.enabled !== nextPersisted.enabled || previous?.model !== nextPersisted.model;
164
+ if (changed) persisted.v2NativeParentOverride = nextPersisted;
165
+ return { changed, value: true };
166
+ });
167
+ if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
168
+ config.v2NativeParentOverride = {
169
+ enabled: next.enabled,
170
+ ...(next.model === null ? {} : { model: next.model }),
171
+ };
172
+ return { ok: true };
173
+ }
174
+
89
175
  class GrokApplyBusyError extends Error {}
90
176
 
91
177
  /**
@@ -240,6 +326,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
240
326
  getMultiAgentModeHintText,
241
327
  } = await import("../../codex/features");
242
328
  const enabled = isMultiAgentV2Enabled();
329
+ const v2NativeParentOverride = v2NativeParentOverrideDto(config, enabled);
243
330
  return jsonResponse({
244
331
  enabled,
245
332
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
@@ -253,6 +340,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
253
340
  // max_depth is V1-only upstream; this is the global-flag statement, derived
254
341
  // server-side so no client can present it as an effective V2 limit.
255
342
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
343
+ v2NativeParentOverride,
344
+ agentTaskRecovery: agentTaskRecoveryDto(config),
256
345
  });
257
346
  }
258
347
  if (url.pathname === "/api/v2" && req.method === "PUT") {
@@ -265,6 +354,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
265
354
  agentsMaxDepth?: unknown;
266
355
  subagentDeveloperInstructions?: unknown;
267
356
  multiAgentModeHintText?: unknown;
357
+ v2NativeParentOverride?: unknown;
358
+ agentTaskRecovery?: unknown;
268
359
  };
269
360
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
270
361
  const wantsFlag = body.enabled !== undefined;
@@ -275,8 +366,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
275
366
  const wantsMaxDepth = body.agentsMaxDepth !== undefined;
276
367
  const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
277
368
  const wantsModeHintText = body.multiAgentModeHintText !== undefined;
278
- if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
279
- return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
369
+ const wantsV2NativeParentOverride = body.v2NativeParentOverride !== undefined;
370
+ const wantsAgentTaskRecovery = body.agentTaskRecovery !== undefined;
371
+ if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsV2NativeParentOverride && !wantsAgentTaskRecovery) {
372
+ return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, multiAgentModeHintText, v2NativeParentOverride, and/or agentTaskRecovery" }, 400);
280
373
  }
281
374
  if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
282
375
  if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
@@ -316,6 +409,92 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
316
409
  if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
317
410
  return jsonResponse({ error: `body.enabled conflicts with multiAgentMode '${mode}'` }, 400);
318
411
  }
412
+ const { isMultiAgentV2Enabled: readMultiAgentV2Enabled } = await import("../../codex/features");
413
+ const currentUpstreamEnabled = readMultiAgentV2Enabled();
414
+ const prospectiveMode = mode ?? config.multiAgentMode ?? "default";
415
+ const prospectiveKeepNative = wantsKeepNative
416
+ ? body.keepNativeChatGptOnV1 === true
417
+ : config.keepNativeChatGptOnV1 === true;
418
+ const prospectiveUpstreamEnabled = wantsFlag
419
+ ? body.enabled as boolean
420
+ : modeFlag ?? currentUpstreamEnabled;
421
+ let v2NativeParentOverride: V2NativeParentOverrideInput | undefined;
422
+ if (wantsV2NativeParentOverride) {
423
+ const raw = body.v2NativeParentOverride;
424
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
425
+ return jsonResponse({ error: "body.v2NativeParentOverride must be an object" }, 400);
426
+ }
427
+ const keys = Object.keys(raw as object);
428
+ if (keys.length !== 2 || !keys.includes("enabled") || !keys.includes("model")) {
429
+ return jsonResponse({ error: "body.v2NativeParentOverride must contain enabled and model" }, 400);
430
+ }
431
+ const candidate = raw as { enabled?: unknown; model?: unknown };
432
+ if (typeof candidate.enabled !== "boolean") {
433
+ return jsonResponse({ error: "body.v2NativeParentOverride.enabled must be a boolean" }, 400);
434
+ }
435
+ if (candidate.model !== null && (typeof candidate.model !== "string" || candidate.model.trim().length === 0)) {
436
+ return jsonResponse({ error: "body.v2NativeParentOverride.model must be a nonblank string or null" }, 400);
437
+ }
438
+ v2NativeParentOverride = {
439
+ enabled: candidate.enabled,
440
+ model: candidate.model === null ? null : (candidate.model as string).trim(),
441
+ };
442
+ if (v2NativeParentOverride.model !== null) {
443
+ let target;
444
+ try {
445
+ target = routeModel(config, v2NativeParentOverride.model);
446
+ } catch {
447
+ return jsonResponse({ error: "body.v2NativeParentOverride.model must resolve to a configured provider" }, 400);
448
+ }
449
+ if (isCanonicalOpenAiForwardProvider(target.provider)) {
450
+ return jsonResponse({ error: "body.v2NativeParentOverride.model must resolve to a noncanonical provider" }, 400);
451
+ }
452
+ }
453
+ if (v2NativeParentOverride.enabled) {
454
+ if (v2NativeParentOverride.model === null) {
455
+ return jsonResponse({ error: "enabling v2NativeParentOverride requires a model" }, 400);
456
+ }
457
+ if (prospectiveMode !== "v2") {
458
+ return jsonResponse({ error: "enabling v2NativeParentOverride requires multiAgentMode 'v2'" }, 400);
459
+ }
460
+ if (!prospectiveUpstreamEnabled) {
461
+ return jsonResponse({ error: "enabling v2NativeParentOverride requires the upstream V2 feature" }, 400);
462
+ }
463
+ if (prospectiveKeepNative) {
464
+ return jsonResponse({ error: "enabling v2NativeParentOverride conflicts with keepNativeChatGptOnV1" }, 400);
465
+ }
466
+ }
467
+ }
468
+ let agentTaskRecovery: AgentTaskRecoveryInput | undefined;
469
+ if (wantsAgentTaskRecovery) {
470
+ const raw = body.agentTaskRecovery;
471
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
472
+ return jsonResponse({ error: "body.agentTaskRecovery must be an object" }, 400);
473
+ }
474
+ const candidate = raw as { enabled?: unknown; model?: unknown };
475
+ if (typeof candidate.enabled !== "boolean") {
476
+ return jsonResponse({ error: "body.agentTaskRecovery.enabled must be a boolean" }, 400);
477
+ }
478
+ if (candidate.model !== undefined && candidate.model !== null && (typeof candidate.model !== "string" || candidate.model.trim().length === 0)) {
479
+ return jsonResponse({ error: "body.agentTaskRecovery.model must be a nonblank string or null" }, 400);
480
+ }
481
+ agentTaskRecovery = {
482
+ enabled: candidate.enabled,
483
+ model: candidate.model === null || candidate.model === undefined ? null : (candidate.model as string).trim(),
484
+ };
485
+ }
486
+ if (agentTaskRecovery && !wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative
487
+ && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsV2NativeParentOverride) {
488
+ const persisted = persistAgentTaskRecovery(config, agentTaskRecovery);
489
+ if (!persisted.ok) return jsonResponse({ error: `persisting agentTaskRecovery failed: ${persisted.reason}` }, 502);
490
+ return jsonResponse({ ok: true, agentTaskRecovery: agentTaskRecoveryDto(config) });
491
+ }
492
+ if (v2NativeParentOverride && !wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative
493
+ && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
494
+ const persisted = persistV2NativeParentOverride(config, v2NativeParentOverride);
495
+ if (!persisted.ok) return jsonResponse({ error: `persisting v2NativeParentOverride failed: ${persisted.reason}` }, 502);
496
+ return jsonResponse({ ok: true, v2NativeParentOverride: v2NativeParentOverrideDto(config, readMultiAgentV2Enabled()) });
497
+ }
319
498
  const {
320
499
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
321
500
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
@@ -394,6 +573,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
394
573
  if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
395
574
  warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
396
575
  }
576
+ if (v2NativeParentOverride) {
577
+ const persisted = persistV2NativeParentOverride(config, v2NativeParentOverride);
578
+ if (!persisted.ok) return jsonResponse({ error: `persisting v2NativeParentOverride failed: ${persisted.reason}` }, 502);
579
+ }
580
+ if (agentTaskRecovery) {
581
+ const persisted = persistAgentTaskRecovery(config, agentTaskRecovery);
582
+ if (!persisted.ok) return jsonResponse({ error: `persisting agentTaskRecovery failed: ${persisted.reason}` }, 502);
583
+ }
397
584
  const catalogRefresh = await convergeCodexCatalog();
398
585
  if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
399
586
  const enabled = isMultiAgentV2Enabled();
@@ -409,6 +596,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
409
596
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
410
597
  multiAgentModeHintText: getMultiAgentModeHintText(),
411
598
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
599
+ v2NativeParentOverride: v2NativeParentOverrideDto(config, enabled),
600
+ agentTaskRecovery: agentTaskRecoveryDto(config),
412
601
  warnings,
413
602
  catalogRefresh,
414
603
  });
@@ -720,19 +909,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
720
909
  if (!("roles" in body)) return jsonResponse({ error: "body.roles is required" }, 400);
721
910
  const parsed = parseSubagentRoles(body.roles);
722
911
  if (!parsed.ok) return jsonResponse({ error: parsed.error, index: parsed.index }, 400);
723
- if ("syncCodexAgentRoles" in body && body.syncCodexAgentRoles !== null) {
724
- if (typeof body.syncCodexAgentRoles !== "boolean") {
725
- return jsonResponse({ error: "syncCodexAgentRoles must be a boolean" }, 400);
726
- }
912
+ if ("syncCodexAgentRoles" in body && body.syncCodexAgentRoles !== null && typeof body.syncCodexAgentRoles !== "boolean") {
913
+ return jsonResponse({ error: "syncCodexAgentRoles must be a boolean" }, 400);
727
914
  }
728
-
729
915
  const warnings: string[] = [];
730
916
  const union = unionRoleModelsIntoRoster(config.subagentModels, parsed.roles);
731
917
  if (union.droppedRoleIds.length > 0) {
732
918
  warnings.push(`Featured roster truncated to 5 models; dropped role id(s): ${union.droppedRoleIds.join(", ")}`);
733
919
  }
734
920
  warnings.push(...routedOnV2Warnings(parsed.roles, config));
735
-
736
921
  config.subagentRoles = parsed.roles;
737
922
  config.subagentModels = union.models;
738
923
  if ("syncCodexAgentRoles" in body && typeof body.syncCodexAgentRoles === "boolean") {
@@ -1217,13 +1402,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1217
1402
  if (field === "webSearchSidecar"
1218
1403
  && (section.model !== undefined || section.backend !== undefined)) {
1219
1404
  const stored = config.claudeCode?.webSearchSidecar;
1220
- const effectiveBackend = section.backend === "anthropic"
1221
- ? "anthropic"
1222
- : section.backend === "openai"
1223
- ? "openai"
1224
- : section.backend === null
1225
- ? config.webSearchSidecar?.backend ?? "openai"
1226
- : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1405
+ // Validate against the SUBMITTED backend across the whole union, not
1406
+ // just openai/anthropic (#2457). allowedBackends above already refused
1407
+ // unknown literals; Array.includes does not narrow, hence the cast.
1408
+ // null keeps its own meaning here — drop the override and inherit the
1409
+ // global backend which is deliberately NOT the sidecar-settings rule.
1410
+ const submittedBackend = section.backend;
1411
+ const effectiveBackend = typeof submittedBackend === "string"
1412
+ && allowedBackends.includes(submittedBackend)
1413
+ ? submittedBackend as WebSearchBackend
1414
+ : submittedBackend === null
1415
+ ? config.webSearchSidecar?.backend ?? "openai"
1416
+ : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1227
1417
  const effectiveModel = section.model === ""
1228
1418
  ? config.webSearchSidecar?.model
1229
1419
  : typeof section.model === "string"
@@ -200,6 +200,12 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
200
200
  if (config.injectionModel && migratedModels.has(config.injectionModel)) {
201
201
  config.injectionModel = migrateReference(config.injectionModel);
202
202
  }
203
+ if (config.v2NativeParentOverride?.model && migratedModels.has(config.v2NativeParentOverride.model)) {
204
+ config.v2NativeParentOverride = {
205
+ ...config.v2NativeParentOverride,
206
+ model: migrateReference(config.v2NativeParentOverride.model),
207
+ };
208
+ }
203
209
  if (config.shadowCallIntercept?.model && migratedModels.has(config.shadowCallIntercept.model)) {
204
210
  config.shadowCallIntercept = {
205
211
  ...config.shadowCallIntercept,
@@ -298,6 +298,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
298
298
  streamMode: config.streamMode ?? "auto",
299
299
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
300
300
  codexAccountPickerEnabled: codexAccountPickerEnabled(config),
301
+ // Absent means the historical auto-open, so the GUI can render the toggle
302
+ // without having to know that `undefined` and `true` mean the same thing.
303
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
301
304
  startupHealth: await readStartupHealth(config),
302
305
  codexRuntime: {
303
306
  path: displayCodexRuntimePath(resolved.runtime.command),
@@ -382,16 +385,21 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
382
385
  streamMode?: unknown;
383
386
  appOwnedMemoryBudgetMb?: unknown;
384
387
  codexAccountPickerEnabled?: unknown;
388
+ oauthOpenBrowser?: unknown;
385
389
  };
386
390
  if (body.codexAutoStart === undefined
387
391
  && body.streamMode === undefined
388
392
  && body.appOwnedMemoryBudgetMb === undefined
389
- && body.codexAccountPickerEnabled === undefined) {
390
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, or codexAccountPickerEnabled" }, 400);
393
+ && body.codexAccountPickerEnabled === undefined
394
+ && body.oauthOpenBrowser === undefined) {
395
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, or oauthOpenBrowser" }, 400);
391
396
  }
392
397
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
393
398
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
394
399
  }
400
+ if (body.oauthOpenBrowser !== undefined && typeof body.oauthOpenBrowser !== "boolean") {
401
+ return jsonResponse({ error: "oauthOpenBrowser boolean is required" }, 400);
402
+ }
395
403
  if (body.streamMode !== undefined && !isStreamMode(body.streamMode)) {
396
404
  return jsonResponse({ error: "streamMode must be auto, legacy-tee, or eager-relay" }, 400);
397
405
  }
@@ -418,6 +426,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
418
426
  hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"),
419
427
  codexAccountPickerEnabled: config.codexAccountPickerEnabled,
420
428
  hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
429
+ oauthOpenBrowser: config.oauthOpenBrowser,
430
+ hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
421
431
  };
422
432
  const pickerWasEnabled = codexAccountPickerEnabled(config);
423
433
  let pickerIsEnabled = pickerWasEnabled;
@@ -441,6 +451,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
441
451
  } else if (body.codexAccountPickerEnabled === false) {
442
452
  config.codexAccountPickerEnabled = false;
443
453
  }
454
+ if (typeof body.oauthOpenBrowser === "boolean") {
455
+ config.oauthOpenBrowser = body.oauthOpenBrowser;
456
+ }
444
457
  pickerIsEnabled = codexAccountPickerEnabled(config);
445
458
  (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
446
459
  } catch (error) {
@@ -457,6 +470,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
457
470
  if (previousSettings.hasCodexAccountPickerEnabled) {
458
471
  config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
459
472
  } else delete config.codexAccountPickerEnabled;
473
+ if (previousSettings.hasOauthOpenBrowser) {
474
+ config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
475
+ } else delete config.oauthOpenBrowser;
460
476
  throw error;
461
477
  }
462
478
  if (typeof body.appOwnedMemoryBudgetMb === "number") {
@@ -476,6 +492,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
476
492
  streamMode: config.streamMode ?? "auto",
477
493
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
478
494
  codexAccountPickerEnabled: pickerIsEnabled,
495
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
479
496
  catalogRefreshPending,
480
497
  startupHealth: await readStartupHealth(config),
481
498
  });
@@ -665,9 +682,18 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
665
682
 
666
683
  if (body.webSearch) {
667
684
  const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
668
- const effectiveBackend = body.webSearch.backend === "anthropic"
669
- ? "anthropic"
670
- : body.webSearch.backend === "openai" || body.webSearch.backend === null
685
+ // Validate against the backend the caller SUBMITTED, across the whole
686
+ // union — not just openai/anthropic (#2457). The union check above has
687
+ // already refused unknown literals, so a surviving string is a member;
688
+ // Array.includes does not narrow, hence the cast. Falling back to the
689
+ // stored backend for xai/gemini/exa both rejected legal pairs and
690
+ // accepted illegal ones: a submitted gemini was checked against a stored
691
+ // openai. null means "unset the backend", and unset resolves to openai.
692
+ const submittedBackend = body.webSearch.backend;
693
+ const effectiveBackend = typeof submittedBackend === "string"
694
+ && WEB_SEARCH_BACKENDS_UNION.includes(submittedBackend as never)
695
+ ? submittedBackend as typeof WEB_SEARCH_BACKENDS_UNION[number]
696
+ : submittedBackend === null
671
697
  ? "openai"
672
698
  : config.webSearchSidecar?.backend ?? "openai";
673
699
  const effectiveModel = typeof body.webSearch.model === "string"
@@ -63,6 +63,7 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
63
63
  disabled: disabled.has(`${selector}/${slug}`) || disabled.has(slug),
64
64
  contextWindow: undefined,
65
65
  maxInputTokens: undefined,
66
+ autoCompactTokenLimit: undefined,
66
67
  })))
67
68
  : [];
68
69
  const native: ManagementModelRow[] = [...nativeRows, ...accountNativeRows].map(row => {
@@ -82,6 +83,9 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
82
83
  // 1.05M). Dropping it here made /api/models describe a native row as if the whole
83
84
  // window were usable as input, which is the claim the measurement disproved.
84
85
  ...(row.maxInputTokens !== undefined ? { maxInputTokens: row.maxInputTokens } : {}),
86
+ ...(row.autoCompactTokenLimit !== undefined
87
+ ? { autoCompactTokenLimit: row.autoCompactTokenLimit }
88
+ : {}),
85
89
  };
86
90
  });
87
91
  const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
@@ -64,6 +64,7 @@ import type { PersistedUsageAttempt } from "../../usage/log";
64
64
  import { AUTH_MATRIX, isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
65
65
  import { applySystemEnvToggle } from "../system-env";
66
66
  import { buildApiAccessEndpoints } from "./api-access";
67
+ import { isAzureIdentityProvider } from "../../config/provider-validation";
67
68
 
68
69
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
69
70
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
@@ -136,7 +137,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
136
137
  // the provider's loopback callback server (inside this process) captures the redirect in the
137
138
  // background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status.
138
139
  if (url.pathname === "/api/oauth/login" && req.method === "POST") {
139
- const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean };
140
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
140
141
  const provider = (body.provider ?? "").trim().toLowerCase();
141
142
  if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
142
143
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
@@ -167,9 +168,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
167
168
  reconcileLiveStateStores();
168
169
  },
169
170
  });
170
- if (authUrl && !deviceCode) {
171
- // Open the browser server-side (the proxy runs on the user's machine) the GUI's
172
- // window.open is popup-blocked because it runs after an await, not a direct click.
171
+ // Open the browser server-side (the proxy runs on the user's machine) — the GUI's
172
+ // window.open is popup-blocked because it runs after an await, not a direct click.
173
+ //
174
+ // The operator can decline, which is the only way to finish a login in a
175
+ // browser profile other than the OS default, or on a different machine
176
+ // than the proxy. Declining changes nothing else: the URL is still
177
+ // returned below and every login surface renders it with a copy button.
178
+ const { shouldOpenBrowserForLogin } = await import("../../oauth/open-browser-choice");
179
+ if (authUrl && !deviceCode && shouldOpenBrowserForLogin(body.openBrowser, config)) {
173
180
  const { openUrl } = await import("../../lib/open-url");
174
181
  openUrl(authUrl);
175
182
  }
@@ -300,6 +307,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
300
307
  const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
301
308
  resetAnthropicRoutingForManualSelection(body.accountId);
302
309
  }
310
+ if (provider === "google-antigravity") {
311
+ const { resetAntigravityRoutingForManualSelection } = await import("../../oauth/antigravity-routing");
312
+ resetAntigravityRoutingForManualSelection(body.accountId);
313
+ }
303
314
  const { clearModelCache } = await import("../../codex/model-cache");
304
315
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
305
316
  clearModelCache(provider);
@@ -510,6 +521,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
510
521
  clearAnthropicAccountCooldown(id);
511
522
  clearAnthropicSessionAffinityForAccount(id);
512
523
  }
524
+ if (provider === "google-antigravity") {
525
+ const { clearAntigravityAccountCooldown, clearAntigravitySessionAffinityForAccount } = await import("../../oauth/antigravity-routing");
526
+ clearAntigravityAccountCooldown(id);
527
+ clearAntigravitySessionAffinityForAccount(id);
528
+ }
513
529
  if (!getAccountSet(provider)) clearLoginState(provider);
514
530
  const { clearModelCache } = await import("../../codex/model-cache");
515
531
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
@@ -527,6 +543,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
527
543
  if (url.pathname === "/api/providers/keys" && req.method === "GET") {
528
544
  const name = (url.searchParams.get("name") ?? "").trim();
529
545
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
546
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
530
547
  const { listProviderApiKeys } = await import("../../providers/api-keys");
531
548
  return jsonResponse(listProviderApiKeys(config, name));
532
549
  }
@@ -534,6 +551,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
534
551
  const body = await readManagementJsonBodyOr(req, {}) as { name?: string; key?: string; label?: string };
535
552
  const name = (body.name ?? "").trim();
536
553
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
554
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
537
555
  if (typeof body.key !== "string" || !body.key.trim()) return jsonResponse({ error: "key is required" }, 400);
538
556
  const { addProviderApiKey } = await import("../../providers/api-keys");
539
557
  const result = addProviderApiKey(config, name, body.key, body.label);
@@ -550,6 +568,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
550
568
  const body = await readManagementJsonBodyOr(req, {}) as { name?: string; id?: string };
551
569
  const name = (body.name ?? "").trim();
552
570
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
571
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
553
572
  if (!body.id) return jsonResponse({ error: "missing id" }, 400);
554
573
  const { setActiveProviderApiKey } = await import("../../providers/api-keys");
555
574
  if (!setActiveProviderApiKey(config, name, body.id)) return jsonResponse({ error: "key not found" }, 404);
@@ -567,6 +586,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
567
586
  const id = typeof body.id === "string" ? body.id.trim() : "";
568
587
  const alias = typeof body.alias === "string" ? body.alias.trim() : "";
569
588
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
589
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
570
590
  if (!id) return jsonResponse({ error: "missing id" }, 400);
571
591
  if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
572
592
  return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
@@ -579,6 +599,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
579
599
  const name = (url.searchParams.get("name") ?? "").trim();
580
600
  const id = url.searchParams.get("id") ?? "";
581
601
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
602
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
582
603
  if (!id) return jsonResponse({ error: "missing id" }, 400);
583
604
  const { removeProviderApiKey } = await import("../../providers/api-keys");
584
605
  if (!removeProviderApiKey(config, name, id)) return jsonResponse({ error: "key not found" }, 404);