@yansigit/opencodex 2.32.0 → 2.33.1

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 (142) hide show
  1. package/README.md +2 -2
  2. package/gui/dist/assets/index-CIDo4y4k.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 +8 -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 +131 -17
  10. package/src/adapters/cursor/envelope-echo.ts +162 -0
  11. package/src/adapters/cursor/live-transport.ts +4 -2
  12. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  13. package/src/adapters/cursor/native-exec-network.ts +3 -5
  14. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  15. package/src/adapters/cursor/native-exec-shell.ts +13 -25
  16. package/src/adapters/cursor/native-exec.ts +18 -10
  17. package/src/adapters/cursor/protobuf-events.ts +186 -9
  18. package/src/adapters/cursor/protobuf-request.ts +53 -18
  19. package/src/adapters/cursor/request-builder.ts +11 -3
  20. package/src/adapters/cursor/tool-definitions.ts +49 -2
  21. package/src/adapters/cursor/tool-result-normalize.ts +21 -8
  22. package/src/adapters/cursor/types.ts +11 -3
  23. package/src/adapters/cursor.ts +123 -0
  24. package/src/adapters/google-aistudio-parser.ts +49 -0
  25. package/src/adapters/google-antigravity-replay.ts +2 -2
  26. package/src/adapters/google-antigravity-wire.ts +7 -0
  27. package/src/adapters/google-errors.ts +6 -2
  28. package/src/adapters/google-http.ts +30 -7
  29. package/src/adapters/google-truncation.ts +5 -0
  30. package/src/adapters/google-wire-compiler.ts +38 -6
  31. package/src/adapters/google.ts +242 -28
  32. package/src/adapters/kiro-tools.ts +20 -9
  33. package/src/adapters/openai-chat.ts +9 -0
  34. package/src/adapters/openai-responses.ts +2 -1
  35. package/src/bridge.ts +112 -9
  36. package/src/chat/inbound.ts +15 -0
  37. package/src/claude/context-windows.ts +16 -9
  38. package/src/cli/doctor.ts +2 -2
  39. package/src/cli/index.ts +10 -3
  40. package/src/cli/provider.ts +6 -0
  41. package/src/cli/status.ts +23 -0
  42. package/src/codex/auth-api.ts +4 -2
  43. package/src/codex/autostart-health.ts +16 -0
  44. package/src/codex/catalog/aggregation.ts +12 -12
  45. package/src/codex/catalog/effort.ts +18 -3
  46. package/src/codex/catalog/metadata.ts +27 -1
  47. package/src/codex/catalog/model-metadata.ts +39 -12
  48. package/src/codex/catalog/parsing.ts +38 -27
  49. package/src/codex/catalog/provider-fetch.ts +232 -133
  50. package/src/codex/catalog/sync.ts +1 -1
  51. package/src/codex/convergence.ts +5 -0
  52. package/src/codex/shim.ts +56 -3
  53. package/src/config/provider-validation.ts +37 -0
  54. package/src/config.ts +78 -2
  55. package/src/generated/compatibility-version.json +187 -119
  56. package/src/generated/model-metadata.ts +3 -0
  57. package/src/images/loop.ts +37 -6
  58. package/src/lib/azure-identity.ts +154 -0
  59. package/src/lib/debug.ts +42 -0
  60. package/src/lib/errors.ts +14 -0
  61. package/src/lib/provider-outbound.ts +45 -33
  62. package/src/lib/provider-tls-profile.ts +309 -0
  63. package/src/lib/proxy-env.ts +49 -0
  64. package/src/lib/redact.ts +10 -1
  65. package/src/oauth/aistudio-native-daemon.ts +62 -0
  66. package/src/oauth/aistudio-session-sync.ts +95 -0
  67. package/src/oauth/antigravity-routing.ts +282 -236
  68. package/src/oauth/callback-server.ts +22 -2
  69. package/src/oauth/command-code.ts +5 -16
  70. package/src/oauth/google-aistudio-auth.ts +98 -0
  71. package/src/oauth/google-antigravity.ts +42 -5
  72. package/src/oauth/index.ts +15 -3
  73. package/src/oauth/key-providers.ts +8 -0
  74. package/src/oauth/kimi.ts +9 -1
  75. package/src/oauth/login-cli.ts +66 -1
  76. package/src/oauth/open-browser-choice.ts +26 -0
  77. package/src/oauth/store.ts +6 -0
  78. package/src/providers/antigravity-quota.ts +3 -1
  79. package/src/providers/api-keys.ts +2 -1
  80. package/src/providers/auto-compact-budget.ts +65 -0
  81. package/src/providers/derive.ts +5 -1
  82. package/src/providers/key-failover.ts +5 -1
  83. package/src/providers/openai-tiers.ts +5 -0
  84. package/src/providers/provider-id-rewrite.ts +1 -0
  85. package/src/providers/quota.ts +148 -50
  86. package/src/providers/registry.ts +27 -4
  87. package/src/providers/request-pacing.ts +33 -6
  88. package/src/providers/xai-transport.ts +21 -0
  89. package/src/responses/google-provider-options.ts +36 -0
  90. package/src/responses/namespace-tool-compat.ts +84 -4
  91. package/src/responses/parser.ts +11 -0
  92. package/src/responses/provider-opaque-metadata.ts +3 -3
  93. package/src/responses/schema.ts +37 -0
  94. package/src/responses/state.ts +94 -4
  95. package/src/router.ts +11 -2
  96. package/src/routing/account-pool/cooldown.ts +8 -0
  97. package/src/routing/account-pool/index.ts +1 -0
  98. package/src/server/aistudio-ws-hub.ts +295 -0
  99. package/src/server/auth-cors.ts +29 -0
  100. package/src/server/chat-completions.ts +2 -0
  101. package/src/server/images.ts +19 -35
  102. package/src/server/index.ts +94 -0
  103. package/src/server/management/agent-settings-routes.ts +205 -15
  104. package/src/server/management/combo-routes.ts +6 -0
  105. package/src/server/management/config-routes.ts +31 -5
  106. package/src/server/management/logs-usage-routes.ts +11 -5
  107. package/src/server/management/model-rows.ts +4 -0
  108. package/src/server/management/oauth-account-routes.ts +38 -7
  109. package/src/server/management/provider-routes.ts +113 -15
  110. package/src/server/management/routing-profile-routes.ts +3 -0
  111. package/src/server/port-reclaim.ts +19 -1
  112. package/src/server/request-log-conversation.ts +12 -0
  113. package/src/server/request-log.ts +23 -1
  114. package/src/server/responses/agent-task-recovery.ts +1 -1
  115. package/src/server/responses/compact.ts +30 -1
  116. package/src/server/responses/core.ts +363 -156
  117. package/src/server/responses/empty-completion-guard.ts +35 -6
  118. package/src/server/responses/fetch-helpers.ts +18 -5
  119. package/src/server/responses/policy-fallback.ts +1 -1
  120. package/src/server/responses/v2-native-parent-override.ts +59 -0
  121. package/src/server/responses/ws-upstream.ts +75 -2
  122. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  123. package/src/server/ws-bridge.ts +2 -1
  124. package/src/service.ts +1 -1
  125. package/src/smoke/fingerprint-cache.ts +133 -0
  126. package/src/smoke/live-scenarios.ts +33 -0
  127. package/src/smoke/runner.ts +119 -0
  128. package/src/types/config.ts +16 -1
  129. package/src/types/provider.ts +18 -1
  130. package/src/types/request.ts +30 -0
  131. package/src/types/tools.ts +51 -0
  132. package/src/types.ts +6 -0
  133. package/src/usage/command-code-manifest.ts +116 -0
  134. package/src/usage/cost.ts +2 -2
  135. package/src/usage/expected-prices.ts +83 -0
  136. package/src/usage/log.ts +2 -2
  137. package/src/usage/summary.ts +34 -12
  138. package/src/web-search/gemini-executor.ts +6 -4
  139. package/src/web-search/index.ts +16 -8
  140. package/src/web-search/loop.ts +42 -6
  141. package/gui/dist/assets/index-BG43zwVe.js +0 -102
  142. 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"
@@ -21,7 +21,7 @@ import {
21
21
  submitManualLoginCode,
22
22
  upsertOAuthProvider,
23
23
  } from "../../oauth";
24
- import { removeCredential } from "../../oauth/store";
24
+ import { getAccountSet, removeCredential } from "../../oauth/store";
25
25
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
26
26
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
27
27
  import { deriveProviderPresets } from "../../providers/derive";
@@ -204,10 +204,16 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
204
204
  const filter = {
205
205
  provider: url.searchParams.get("provider"),
206
206
  model: url.searchParams.get("model"),
207
+ account: url.searchParams.get("account"),
207
208
  };
209
+ const fallbackAccounts: Record<string, string> = {};
210
+ for (const p of listOAuthProviders()) {
211
+ const set = getAccountSet(p);
212
+ if (set?.activeAccountId) fallbackAccounts[p] = set.activeAccountId;
213
+ }
208
214
  const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
209
- projectUsageSummary(summary, filter, entries);
210
- const filterRequested = Boolean(filter.provider ?? filter.model);
215
+ projectUsageSummary(summary, filter, entries, fallbackAccounts);
216
+ const filterRequested = Boolean(filter.provider ?? filter.model ?? filter.account);
211
217
  const now = Date.now();
212
218
  try {
213
219
  const cacheKey = `${range}:${surface}`;
@@ -242,7 +248,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
242
248
  const revisionReadAt = Date.now();
243
249
  const window = snapshotWindow(snapshot.entries);
244
250
  const summary = {
245
- ...summarizeUsage(snapshot.entries, range, now, surface),
251
+ ...summarizeUsage(snapshot.entries, range, now, surface, fallbackAccounts),
246
252
  historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
247
253
  truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
248
254
  entriesTruncated: snapshot.entriesTruncated,
@@ -270,7 +276,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
270
276
  for (const nextRange of ranges) {
271
277
  for (const nextSurface of surfaces) {
272
278
  const nextSummary = nextRange === range && nextSurface === surface ? summary : {
273
- ...summarizeUsage(snapshot.entries, nextRange, now, nextSurface),
279
+ ...summarizeUsage(snapshot.entries, nextRange, now, nextSurface, fallbackAccounts),
274
280
  historyTruncated: summary.historyTruncated,
275
281
  truncatedPrefixBytes: summary.truncatedPrefixBytes,
276
282
  entriesTruncated: summary.entriesTruncated,
@@ -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);
@@ -427,10 +438,20 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
427
438
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: unknown; accountId?: unknown };
428
439
  const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
429
440
  const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
430
- if (provider !== "anthropic") return jsonResponse({ error: "clear-cooldown is only supported for anthropic" }, 400);
441
+ const supported = provider === "anthropic" || provider === "google-antigravity" || provider === "cursor" || provider === "command-code";
442
+ if (!supported) return jsonResponse({ error: "clear-cooldown is not supported for this provider" }, 400);
431
443
  if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
432
- const { clearAnthropicAccountCooldown } = await import("../../oauth/anthropic-routing");
433
- const cleared = clearAnthropicAccountCooldown(accountId);
444
+ let cleared = false;
445
+ if (provider === "anthropic") {
446
+ const { clearAnthropicAccountCooldown } = await import("../../oauth/anthropic-routing");
447
+ cleared = clearAnthropicAccountCooldown(accountId);
448
+ } else if (provider === "google-antigravity") {
449
+ const { clearAntigravityAccountCooldown } = await import("../../oauth/antigravity-routing");
450
+ cleared = clearAntigravityAccountCooldown(accountId);
451
+ } else if (provider === "cursor" || provider === "command-code") {
452
+ const { clearPoolAccountCooldown } = await import("../../routing/account-pool");
453
+ cleared = clearPoolAccountCooldown(provider, accountId);
454
+ }
434
455
  return jsonResponse({ ok: true, cleared });
435
456
  }
436
457
 
@@ -510,6 +531,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
510
531
  clearAnthropicAccountCooldown(id);
511
532
  clearAnthropicSessionAffinityForAccount(id);
512
533
  }
534
+ if (provider === "google-antigravity") {
535
+ const { clearAntigravityAccountCooldown, clearAntigravitySessionAffinityForAccount } = await import("../../oauth/antigravity-routing");
536
+ clearAntigravityAccountCooldown(id);
537
+ clearAntigravitySessionAffinityForAccount(id);
538
+ }
513
539
  if (!getAccountSet(provider)) clearLoginState(provider);
514
540
  const { clearModelCache } = await import("../../codex/model-cache");
515
541
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
@@ -527,6 +553,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
527
553
  if (url.pathname === "/api/providers/keys" && req.method === "GET") {
528
554
  const name = (url.searchParams.get("name") ?? "").trim();
529
555
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
556
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
530
557
  const { listProviderApiKeys } = await import("../../providers/api-keys");
531
558
  return jsonResponse(listProviderApiKeys(config, name));
532
559
  }
@@ -534,6 +561,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
534
561
  const body = await readManagementJsonBodyOr(req, {}) as { name?: string; key?: string; label?: string };
535
562
  const name = (body.name ?? "").trim();
536
563
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
564
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
537
565
  if (typeof body.key !== "string" || !body.key.trim()) return jsonResponse({ error: "key is required" }, 400);
538
566
  const { addProviderApiKey } = await import("../../providers/api-keys");
539
567
  const result = addProviderApiKey(config, name, body.key, body.label);
@@ -550,6 +578,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
550
578
  const body = await readManagementJsonBodyOr(req, {}) as { name?: string; id?: string };
551
579
  const name = (body.name ?? "").trim();
552
580
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
581
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
553
582
  if (!body.id) return jsonResponse({ error: "missing id" }, 400);
554
583
  const { setActiveProviderApiKey } = await import("../../providers/api-keys");
555
584
  if (!setActiveProviderApiKey(config, name, body.id)) return jsonResponse({ error: "key not found" }, 404);
@@ -567,6 +596,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
567
596
  const id = typeof body.id === "string" ? body.id.trim() : "";
568
597
  const alias = typeof body.alias === "string" ? body.alias.trim() : "";
569
598
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
599
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
570
600
  if (!id) return jsonResponse({ error: "missing id" }, 400);
571
601
  if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
572
602
  return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
@@ -579,6 +609,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
579
609
  const name = (url.searchParams.get("name") ?? "").trim();
580
610
  const id = url.searchParams.get("id") ?? "";
581
611
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
612
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
582
613
  if (!id) return jsonResponse({ error: "missing id" }, 400);
583
614
  const { removeProviderApiKey } = await import("../../providers/api-keys");
584
615
  if (!removeProviderApiKey(config, name, id)) return jsonResponse({ error: "key not found" }, 404);