@yansigit/opencodex 2.33.1 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -3
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- 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 +9 -9
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-shell.ts +103 -6
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +74 -39
- package/src/adapters/cursor/request-builder.ts +32 -10
- package/src/adapters/cursor/tool-definitions.ts +13 -10
- package/src/adapters/cursor/tool-result-normalize.ts +35 -3
- package/src/adapters/cursor/types.ts +16 -4
- package/src/adapters/cursor.ts +97 -70
- 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 +156 -65
- 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 +334 -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 +14 -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 +5 -0
- 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 -37
- 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 +229 -153
- 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 +54 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +19 -20
- 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 +28 -0
- 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 +73 -14
- package/src/providers/registry.ts +160 -68
- 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 +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +23 -0
- package/src/server/chat-completions.ts +24 -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 +152 -80
- 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 +1 -1
- package/src/server/management/model-routes.ts +266 -0
- 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/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +46 -20
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +571 -137
- 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/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 +15 -4
- 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 +77 -0
- package/src/types/request.ts +16 -10
- package/src/types/tools.ts +20 -25
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +17 -7
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-CIDo4y4k.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
- package/src/server/aistudio-ws-hub.ts +0 -295
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,
|
|
@@ -40,7 +41,6 @@ import {
|
|
|
40
41
|
enforceAppOwnedMemoryBudget,
|
|
41
42
|
resolveAppOwnedMemoryBudgetBytes,
|
|
42
43
|
} from "../lib/app-owned-memory";
|
|
43
|
-
import { getAiStudioBridgeHtml, getAiStudioUserScript, globalAiStudioRelayHub } from "./aistudio-ws-hub";
|
|
44
44
|
import {
|
|
45
45
|
registerAppOwnedMemorySweepFallback,
|
|
46
46
|
registerDefaultAppOwnedMemoryStores,
|
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
} from "../lib/app-owned-memory-stores";
|
|
49
49
|
import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
|
|
50
50
|
import { activateLab, labActivationRequired } from "../lib/lab-activation";
|
|
51
|
-
import { registerFork } from "../fork/register";
|
|
52
51
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
53
52
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
54
53
|
import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
|
|
@@ -111,6 +110,7 @@ import {
|
|
|
111
110
|
type RequestLogContext,
|
|
112
111
|
type RequestLogEntry,
|
|
113
112
|
} from "./request-log";
|
|
113
|
+
import { sessionLaneIdFromRequest } from "./request-log-conversation";
|
|
114
114
|
export {
|
|
115
115
|
addFinalRequestLog,
|
|
116
116
|
filterRequestLogs,
|
|
@@ -202,6 +202,22 @@ import {
|
|
|
202
202
|
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
203
203
|
import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
|
|
204
204
|
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
205
|
+
import {
|
|
206
|
+
createRuntimePackageTreeIntegrityGuard,
|
|
207
|
+
type PackageTreeIntegrityGuard,
|
|
208
|
+
} from "../lib/package-tree-integrity";
|
|
209
|
+
import { detectInstall } from "../update/index";
|
|
210
|
+
import { runAiStudioNativeLogin } from "../oauth/aistudio-native-daemon";
|
|
211
|
+
|
|
212
|
+
function isAiStudioSessionOrigin(origin: string | null): boolean {
|
|
213
|
+
return !!origin && (origin.startsWith("chrome-extension://") || origin === "https://aistudio.google.com");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function withAiStudioSessionCors(resp: Response, req: Request): Response {
|
|
217
|
+
const origin = req.headers.get("Origin");
|
|
218
|
+
if (isAiStudioSessionOrigin(origin) && origin) resp.headers.set("Access-Control-Allow-Origin", origin);
|
|
219
|
+
return resp;
|
|
220
|
+
}
|
|
205
221
|
|
|
206
222
|
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
207
223
|
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
@@ -454,6 +470,10 @@ export interface StartServerDeps {
|
|
|
454
470
|
localAttestationSecret?: string;
|
|
455
471
|
/** Optional readiness gate; a fresh pending gate is created when omitted. */
|
|
456
472
|
readinessGate?: ReadinessGate;
|
|
473
|
+
/** Test-only package-tree observation; production captures package.json identity at boot. */
|
|
474
|
+
packageTreeIntegrity?: PackageTreeIntegrityGuard;
|
|
475
|
+
/** Test-only seam for the awaited native AI Studio login process. */
|
|
476
|
+
runAiStudioNativeLogin?: typeof runAiStudioNativeLogin;
|
|
457
477
|
}
|
|
458
478
|
|
|
459
479
|
function inspectStartupOwnership(
|
|
@@ -669,12 +689,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
669
689
|
if (path === "/v1/realtime" || path === "/v1/live") {
|
|
670
690
|
return req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
671
691
|
}
|
|
672
|
-
if (path === "/v1/ws/aistudio" || path === "/aistudio/ws") {
|
|
673
|
-
return req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
674
|
-
}
|
|
675
|
-
if (path === "/v1/ws/aistudio/status") return req.method === "GET";
|
|
676
|
-
if (path === "/api/aistudio/session") return req.method === "POST";
|
|
677
|
-
if (path === "/aistudio/bridge" || path === "/aistudio/bridge.user.js") return req.method === "GET";
|
|
678
692
|
return false;
|
|
679
693
|
}
|
|
680
694
|
|
|
@@ -704,12 +718,21 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
704
718
|
}), req, policy);
|
|
705
719
|
}
|
|
706
720
|
|
|
721
|
+
function packageTreeChangedResponse(req: Request, policy: RequestPolicyView, message: string): Response {
|
|
722
|
+
return withCors(new Response(JSON.stringify({
|
|
723
|
+
error: { type: "server_error", code: "package_tree_changed", message },
|
|
724
|
+
}), {
|
|
725
|
+
status: 503,
|
|
726
|
+
headers: { "Content-Type": "application/json" },
|
|
727
|
+
}), req, policy);
|
|
728
|
+
}
|
|
729
|
+
|
|
707
730
|
async function runAdmittedHttpTurn(
|
|
708
731
|
req: Request,
|
|
709
732
|
policy: RequestPolicyView,
|
|
710
733
|
work: (lease: ActiveTurnLease) => Promise<Response>,
|
|
711
734
|
): Promise<Response> {
|
|
712
|
-
const lease = tryAdmitTurn();
|
|
735
|
+
const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
713
736
|
if (!lease) return serverBusyResponse(req, "active turns", policy);
|
|
714
737
|
let response: Response;
|
|
715
738
|
try {
|
|
@@ -730,6 +753,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
730
753
|
// passes it in, and transitions it after the post-startup sync settles. When
|
|
731
754
|
// no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
|
|
732
755
|
const readinessGate = deps.readinessGate ?? createReadinessGate();
|
|
756
|
+
const packageTreeIntegrity = deps.packageTreeIntegrity
|
|
757
|
+
?? createRuntimePackageTreeIntegrityGuard(detectInstall());
|
|
733
758
|
// Actual bound port, filled in after Bun.serve binds so /readyz reports the
|
|
734
759
|
// real ephemeral port for startServer(0). /healthz keeps its existing port
|
|
735
760
|
// field (the requested listenPort) byte-for-byte.
|
|
@@ -856,6 +881,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
856
881
|
if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
|
|
857
882
|
} catch { /* malformed encoding — not a readiness path */ }
|
|
858
883
|
|
|
884
|
+
const packageTreeStatus = packageTreeIntegrity.status();
|
|
885
|
+
if (!packageTreeStatus.ok && (
|
|
886
|
+
url.pathname === "/healthz"
|
|
887
|
+
|| readyzPath !== undefined
|
|
888
|
+
|| url.pathname.startsWith("/v1/")
|
|
889
|
+
)) {
|
|
890
|
+
const message = "OpenCodex package files changed while this proxy was running; restart OpenCodex before retrying.";
|
|
891
|
+
const response = url.pathname === "/healthz" || readyzPath !== undefined
|
|
892
|
+
? jsonResponse({
|
|
893
|
+
status: "restart_required",
|
|
894
|
+
service: "opencodex",
|
|
895
|
+
version: VERSION,
|
|
896
|
+
uptime: process.uptime(),
|
|
897
|
+
pid: process.pid,
|
|
898
|
+
port: boundPort ?? requestServer.port ?? listenPort,
|
|
899
|
+
error: { code: "package_tree_changed", message },
|
|
900
|
+
}, 503, req, policy)
|
|
901
|
+
: packageTreeChangedResponse(req, policy, message);
|
|
902
|
+
const headers = new Headers(response.headers);
|
|
903
|
+
headers.set("Retry-After", "5");
|
|
904
|
+
return new Response(response.body, { status: 503, headers });
|
|
905
|
+
}
|
|
906
|
+
|
|
859
907
|
if (req.method === "OPTIONS") {
|
|
860
908
|
// /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
|
|
861
909
|
// path) must answer the deterministic JSON 404, never the generic 204
|
|
@@ -863,6 +911,20 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
863
911
|
if (readyzPath !== undefined) {
|
|
864
912
|
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
865
913
|
}
|
|
914
|
+
if (url.pathname === "/api/aistudio/session") {
|
|
915
|
+
const origin = req.headers.get("Origin");
|
|
916
|
+
if (isAiStudioSessionOrigin(origin)) {
|
|
917
|
+
return new Response(null, {
|
|
918
|
+
status: 204,
|
|
919
|
+
headers: {
|
|
920
|
+
"Access-Control-Allow-Origin": origin as string,
|
|
921
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
922
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-OpenCodex-API-Key",
|
|
923
|
+
Vary: "Origin, Access-Control-Request-Headers",
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
866
928
|
const managementPreflight = url.pathname.startsWith("/api/");
|
|
867
929
|
const allowed = managementPreflight
|
|
868
930
|
? isAllowedManagementOrigin(req, config)
|
|
@@ -904,51 +966,37 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
904
966
|
// unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
|
|
905
967
|
// request to the public server's upgrade would fail or cross sockets.
|
|
906
968
|
if (requestServer.upgrade(req, {
|
|
907
|
-
data: buildResponsesWsData(
|
|
969
|
+
data: buildResponsesWsData(
|
|
970
|
+
selectForwardHeaders(req.headers),
|
|
971
|
+
admission,
|
|
972
|
+
websocketLease,
|
|
973
|
+
sessionLaneIdFromRequest(req.headers),
|
|
974
|
+
),
|
|
908
975
|
})) return undefined as unknown as Response;
|
|
909
976
|
websocketLease.release();
|
|
910
977
|
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
911
978
|
}
|
|
912
979
|
|
|
913
|
-
if (url.pathname === "/v1/ws/aistudio" || url.pathname === "/aistudio/ws") {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
const origin = req.headers.get("Origin");
|
|
920
|
-
const isExtensionOrigin = origin?.startsWith("chrome-extension://");
|
|
921
|
-
if (!isAllowedRequestOrigin(req, policy) && origin !== "https://aistudio.google.com" && !isExtensionOrigin) {
|
|
922
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "AI Studio relay WebSocket blocked: non-local Origin"), req, policy);
|
|
923
|
-
}
|
|
924
|
-
const sessionId = "aistudio_" + crypto.randomUUID().slice(0, 8);
|
|
925
|
-
if (requestServer.upgrade(req, {
|
|
926
|
-
data: { kind: "aistudio-relay", aistudioSessionId: sessionId },
|
|
927
|
-
})) return undefined as unknown as Response;
|
|
928
|
-
return withCors(formatErrorResponse(426, "upgrade_required", "AI Studio WebSocket upgrade failed"), req, policy);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if (url.pathname === "/v1/ws/aistudio/status" && req.method === "GET") {
|
|
932
|
-
return jsonResponse({
|
|
933
|
-
activeSessions: globalAiStudioRelayHub.getActiveSessionCount(),
|
|
934
|
-
hasActiveSessions: globalAiStudioRelayHub.hasActiveSessions(),
|
|
935
|
-
});
|
|
980
|
+
if (url.pathname === "/v1/ws/aistudio" || url.pathname === "/aistudio/ws" || url.pathname === "/v1/ws/aistudio/status") {
|
|
981
|
+
return withCors(jsonResponse({
|
|
982
|
+
error: "gone",
|
|
983
|
+
message: "AI Studio browser relay endpoints are deprecated and return 410 Gone. Use native macOS login (ocx login) or the session exporter extension.",
|
|
984
|
+
}, 410), req, policy);
|
|
936
985
|
}
|
|
937
986
|
|
|
938
987
|
if (url.pathname === "/api/aistudio/session" && req.method === "POST") {
|
|
939
988
|
const admission = resolveApiAuth(req, policy);
|
|
940
989
|
if (!admission) {
|
|
941
|
-
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
990
|
+
return withAiStudioSessionCors(withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy), req);
|
|
942
991
|
}
|
|
943
992
|
const origin = req.headers.get("Origin");
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy);
|
|
993
|
+
if (!isAllowedRequestOrigin(req, policy) && !isAiStudioSessionOrigin(origin)) {
|
|
994
|
+
return withAiStudioSessionCors(withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy), req);
|
|
947
995
|
}
|
|
948
996
|
try {
|
|
949
|
-
const bodyJson =
|
|
997
|
+
const bodyJson = await req.json() as any;
|
|
950
998
|
const { saveAiStudioSession, saveAiStudioSessionFromToken } = await import("../oauth/aistudio-session-sync");
|
|
951
|
-
if (
|
|
999
|
+
if (typeof bodyJson.token === "string" && bodyJson.token) {
|
|
952
1000
|
saveAiStudioSessionFromToken(bodyJson.token);
|
|
953
1001
|
} else if (Array.isArray(bodyJson.cookies)) {
|
|
954
1002
|
saveAiStudioSession({
|
|
@@ -957,24 +1005,52 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
957
1005
|
cookies: bodyJson.cookies,
|
|
958
1006
|
});
|
|
959
1007
|
} else {
|
|
960
|
-
return withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy);
|
|
1008
|
+
return withAiStudioSessionCors(withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy), req);
|
|
961
1009
|
}
|
|
962
|
-
return withCors(jsonResponse({ ok: true, message: "AI Studio session updated successfully" }), req, policy);
|
|
963
|
-
} catch (
|
|
964
|
-
return withCors(jsonResponse({ error: String(
|
|
1010
|
+
return withAiStudioSessionCors(withCors(jsonResponse({ ok: true, message: "AI Studio session updated successfully" }), req, policy), req);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
return withAiStudioSessionCors(withCors(jsonResponse({ error: String(error) }, 400), req, policy), req);
|
|
965
1013
|
}
|
|
966
1014
|
}
|
|
967
1015
|
|
|
968
1016
|
if (url.pathname === "/aistudio/bridge" && req.method === "GET") {
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
1017
|
+
const bridgeHtml = `<!DOCTYPE html>
|
|
1018
|
+
<html><head><meta charset="utf-8"><title>Google AI Studio Relay Deprecated - OpenCodex</title></head>
|
|
1019
|
+
<body><h1>HTTP 410 Gone: AI Studio Browser Relay Deprecated</h1>
|
|
1020
|
+
<p>The browser relay has been retired. Use native macOS authentication or the session exporter extension.</p>
|
|
1021
|
+
<p>Run <code>ocx login</code> to connect.</p></body></html>`;
|
|
1022
|
+
return new Response(bridgeHtml, { status: 410, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
972
1023
|
}
|
|
973
1024
|
|
|
974
1025
|
if (url.pathname === "/aistudio/bridge.user.js" && req.method === "GET") {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1026
|
+
return new Response("// HTTP 410 Gone: OpenCodex AI Studio browser relay and userscripts are deprecated.\n", {
|
|
1027
|
+
status: 410,
|
|
1028
|
+
headers: { "Content-Type": "application/javascript; charset=utf-8" },
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (url.pathname === "/api/aistudio/login/native" && req.method === "POST") {
|
|
1033
|
+
const admission = resolveApiAuth(req, policy);
|
|
1034
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1035
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1036
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy);
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const login = await (deps.runAiStudioNativeLogin ?? runAiStudioNativeLogin)({ signal: req.signal });
|
|
1040
|
+
if (login.kind === "unsupported") return jsonResponse({ ok: false, error: "Native login is only available on macOS" }, 400, req, policy);
|
|
1041
|
+
if (login.kind === "cancelled") return jsonResponse({ ok: false, error: "Native AI Studio login cancelled" }, 499, req, policy);
|
|
1042
|
+
if (login.kind === "failed") return jsonResponse({ ok: false, error: login.error }, 500, req, policy);
|
|
1043
|
+
const probeRequest = new Request(new URL("/api/providers/test?name=google-aistudio", req.url), {
|
|
1044
|
+
method: "POST",
|
|
1045
|
+
headers: { Host: req.headers.get("Host") ?? "127.0.0.1" },
|
|
1046
|
+
});
|
|
1047
|
+
const probeResponse = await handleManagementAPI(probeRequest, new URL(probeRequest.url), config, deps.managementApi);
|
|
1048
|
+
const probe = await probeResponse?.json().catch(() => null) as { ok?: boolean; error?: string } | null;
|
|
1049
|
+
if (!probe?.ok) return jsonResponse({ ok: false, error: probe?.error ?? "AI Studio connection probe failed" }, 502, req, policy);
|
|
1050
|
+
return jsonResponse({ ok: true, sessionPath: login.sessionPath }, 200, req, policy);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
return jsonResponse({ ok: false, error: String(error) }, 500, req, policy);
|
|
1053
|
+
}
|
|
978
1054
|
}
|
|
979
1055
|
|
|
980
1056
|
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
@@ -1102,6 +1178,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1102
1178
|
: [];
|
|
1103
1179
|
const disabledNatives = disabledNativeSlugs(config);
|
|
1104
1180
|
const disabledModels = new Set(config.disabledModels ?? []);
|
|
1181
|
+
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1105
1182
|
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
1106
1183
|
const suppressedBareNativeSlugs = new Set([
|
|
1107
1184
|
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
@@ -1187,7 +1264,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1187
1264
|
config.subagentModels,
|
|
1188
1265
|
websocketsEnabled(config),
|
|
1189
1266
|
maMode as "v1" | "default" | "v2",
|
|
1190
|
-
|
|
1267
|
+
exactComboSlugs,
|
|
1191
1268
|
accountSelectors,
|
|
1192
1269
|
suppressedBareNativeSlugs,
|
|
1193
1270
|
new Set(),
|
|
@@ -1264,12 +1341,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1264
1341
|
const data = [
|
|
1265
1342
|
...visibleNatives.map(id => nativeModelRow(id)),
|
|
1266
1343
|
...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
|
|
1267
|
-
...uniqueCatalogModelsForRawPublicList(goOrdered).map(m =>
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1344
|
+
...await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
|
|
1345
|
+
const publicId = m.alias ?? `${m.provider}/${m.id}`;
|
|
1346
|
+
const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
|
|
1347
|
+
const provider = config.providers[m.provider];
|
|
1348
|
+
const effective = provider
|
|
1349
|
+
? (await import("../providers/default-aliases")).effectiveModelAliases(
|
|
1350
|
+
config,
|
|
1351
|
+
provider,
|
|
1352
|
+
knownModelIdsForProvider(m.provider, provider, config),
|
|
1353
|
+
).get(m.id)
|
|
1354
|
+
: undefined;
|
|
1355
|
+
return {
|
|
1356
|
+
id: publicId,
|
|
1357
|
+
object: "model",
|
|
1358
|
+
created: 0,
|
|
1359
|
+
// This endpoint is an OpenAI-compatible inbound contract. Some clients use
|
|
1360
|
+
// owned_by as an adapter selector, so a virtual combo must name that wire
|
|
1361
|
+
// adapter rather than the internal catalog authority marker.
|
|
1362
|
+
owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
|
|
1363
|
+
...(isCombo ? { is_combo: true } : {}),
|
|
1364
|
+
...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
|
|
1365
|
+
...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
|
|
1366
|
+
};
|
|
1273
1367
|
})),
|
|
1274
1368
|
];
|
|
1275
1369
|
return jsonResponse({ object: "list", data }, 200, req, policy);
|
|
@@ -1576,7 +1670,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1576
1670
|
provider: "unknown",
|
|
1577
1671
|
...admissionFields(admission),
|
|
1578
1672
|
};
|
|
1579
|
-
const turnAdmissionLease = tryAdmitTurn();
|
|
1673
|
+
const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
1580
1674
|
if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
|
|
1581
1675
|
let resolved;
|
|
1582
1676
|
try {
|
|
@@ -1638,10 +1732,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1638
1732
|
// Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
|
|
1639
1733
|
// Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
|
|
1640
1734
|
open(ws: ServerWebSocket<WsData>) {
|
|
1641
|
-
if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
|
|
1642
|
-
globalAiStudioRelayHub.registerSession(ws.data.aistudioSessionId, ws);
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
1735
|
if (ws.data.kind === "live-sideband") {
|
|
1646
1736
|
if (!ws.data.liveTurnAdmissionLease) {
|
|
1647
1737
|
closeLiveSideband(ws, 1013, "server busy");
|
|
@@ -1658,18 +1748,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1658
1748
|
registerCodexWebSocket(ws);
|
|
1659
1749
|
},
|
|
1660
1750
|
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
1661
|
-
if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
|
|
1662
|
-
const text = typeof raw === "string"
|
|
1663
|
-
? raw
|
|
1664
|
-
: Buffer.isBuffer(raw)
|
|
1665
|
-
? raw.toString("utf-8")
|
|
1666
|
-
: new TextDecoder().decode(raw as any);
|
|
1667
|
-
globalAiStudioRelayHub.handleClientMessage(
|
|
1668
|
-
ws.data.aistudioSessionId,
|
|
1669
|
-
text
|
|
1670
|
-
);
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
1751
|
if (ws.data.kind === "live-sideband") {
|
|
1674
1752
|
if (ws.data.liveClosing) return;
|
|
1675
1753
|
const rawBytes = webSocketFrameBytes(raw);
|
|
@@ -1744,7 +1822,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1744
1822
|
return;
|
|
1745
1823
|
}
|
|
1746
1824
|
|
|
1747
|
-
const turnAdmissionLease = tryAdmitTurn();
|
|
1825
|
+
const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
|
|
1748
1826
|
if (!turnAdmissionLease) {
|
|
1749
1827
|
sendJsonFrame(ws, buildWsErrorFrame(503, {
|
|
1750
1828
|
type: "server_error",
|
|
@@ -1848,10 +1926,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1848
1926
|
})();
|
|
1849
1927
|
},
|
|
1850
1928
|
close(ws: ServerWebSocket<WsData>) {
|
|
1851
|
-
if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
|
|
1852
|
-
globalAiStudioRelayHub.unregisterSession(ws.data.aistudioSessionId);
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
1929
|
if (ws.data.kind === "live-sideband") {
|
|
1856
1930
|
closeLiveSideband(ws);
|
|
1857
1931
|
return;
|
|
@@ -1982,7 +2056,5 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1982
2056
|
activateLab(config, labConfigDir);
|
|
1983
2057
|
}
|
|
1984
2058
|
|
|
1985
|
-
registerFork();
|
|
1986
|
-
|
|
1987
2059
|
return server;
|
|
1988
2060
|
}
|
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 });
|