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