@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.
- package/README.md +2 -2
- package/gui/dist/assets/index-DKLr4LTE.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -5
- package/src/adapters/anthropic.ts +20 -6
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +3 -1
- package/src/adapters/command-code.ts +40 -7
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/protobuf-events.ts +158 -7
- package/src/adapters/cursor/protobuf-request.ts +33 -15
- package/src/adapters/cursor/request-builder.ts +4 -3
- package/src/adapters/cursor/tool-definitions.ts +27 -1
- package/src/adapters/cursor/types.ts +4 -3
- package/src/adapters/cursor.ts +9 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +148 -26
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +1 -1
- package/src/bridge.ts +112 -9
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/doctor.ts +2 -2
- package/src/cli/index.ts +9 -2
- package/src/cli/provider.ts +6 -0
- package/src/cli/status.ts +23 -0
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -12
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +39 -12
- package/src/codex/catalog/parsing.ts +38 -27
- package/src/codex/catalog/provider-fetch.ts +198 -133
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/convergence.ts +5 -0
- package/src/codex/shim.ts +56 -3
- package/src/config/provider-validation.ts +37 -0
- package/src/config.ts +78 -2
- package/src/generated/compatibility-version.json +120 -96
- package/src/images/loop.ts +37 -6
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/index.ts +15 -3
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +59 -13
- package/src/providers/registry.ts +3 -1
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +11 -0
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +8 -2
- package/src/server/auth-cors.ts +28 -0
- package/src/server/images.ts +19 -35
- package/src/server/management/agent-settings-routes.ts +205 -15
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +31 -5
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/oauth-account-routes.ts +25 -4
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/request-log.ts +21 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/compact.ts +30 -1
- package/src/server/responses/core.ts +359 -153
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +18 -5
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-undeclared-tool-guard.ts +90 -8
- package/src/service.ts +1 -1
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +28 -0
- package/src/types/tools.ts +27 -0
- package/src/types.ts +6 -0
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-BG43zwVe.js +0 -102
- 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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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);
|