@yansigit/opencodex 2.36.1 → 2.36.2-dev.20260829.53
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 +9 -4
- package/gui/dist/assets/{ApiKeys-CsmNyf4I.js → ApiKeys-BhwqQvO8.js} +1 -1
- package/gui/dist/assets/{Claude-B4sHsGTD.js → Claude-DYNY8SHC.js} +1 -1
- package/gui/dist/assets/{CodexSet-BuAQ4YQn.js → CodexSet-tSN1aWmM.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-BC65sEaS.js → FileIntegrationPage-B68tn2JT.js} +1 -1
- package/gui/dist/assets/{Grok-DNJWlISf.js → Grok-Dr6FN8SD.js} +1 -1
- package/gui/dist/assets/{Integrations-vGJshFZK.js → Integrations-jvqYofMm.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-DYMkDFHE.js → IntegrationsOverview-Bjc7JSgw.js} +1 -1
- package/gui/dist/assets/{Logs-Coc7_RPP.js → Logs-C23CdgmP.js} +1 -1
- package/gui/dist/assets/{Models-CKVT8Meb.js → Models-Ci4wK3HO.js} +1 -1
- package/gui/dist/assets/{NumberStepper-Dug4wR8n.js → NumberStepper-nggECFok.js} +1 -1
- package/gui/dist/assets/Providers-DasIL4ng.js +1 -0
- package/gui/dist/assets/{RestoreDialog-BpuvVU2w.js → RestoreDialog-CW7LbS-w.js} +1 -1
- package/gui/dist/assets/{Startup-DAUQqNEN.js → Startup-Cx3dZre0.js} +1 -1
- package/gui/dist/assets/{Storage-DfP95SlJ.js → Storage-D0iUYQED.js} +1 -1
- package/gui/dist/assets/{Subagents-CMNsZLfk.js → Subagents-phLFffyl.js} +1 -1
- package/gui/dist/assets/{Usage-ESFRfBMO.js → Usage-NvYxG04o.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-W4FhikN7.js → codex-stale-banner-B5VQPO3t.js} +1 -1
- package/gui/dist/assets/{data-surface-B8PX8rjm.js → data-surface-B0APtUxm.js} +1 -1
- package/gui/dist/assets/{data-surface-CETFXqiA.js → data-surface-OiQUZok8.js} +1 -1
- package/gui/dist/assets/index-C8flH881.css +1 -0
- package/gui/dist/assets/{index-DHQHBRhb.js → index-CZ1jyQCF.js} +3 -3
- package/gui/dist/assets/{model-display-OBTJhQwT.js → model-display-Dlrzica2.js} +1 -1
- package/gui/dist/assets/{provider-payload-CJRJUn7w.js → provider-payload-Ca3Ff8oP.js} +1 -1
- package/gui/dist/assets/{section-tabs-B8sLMiz6.js → section-tabs-Bpm5lFcZ.js} +1 -1
- package/gui/dist/assets/shared-BHpPx-nZ.js +69 -0
- package/gui/dist/index.html +3 -3
- package/package.json +2 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code.ts +2 -1
- package/src/adapters/google-http.ts +34 -14
- package/src/adapters/google.ts +4 -1
- package/src/cli/dispatch.ts +2 -2
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +9 -1
- package/src/cli/init.ts +52 -3
- package/src/cli/registry.ts +2 -2
- package/src/codex/account-lifecycle.ts +4 -8
- package/src/codex/inject.ts +14 -9
- package/src/config/process-state.ts +15 -0
- package/src/config.ts +17 -0
- package/src/generated/compatibility-version.json +67 -47
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/pinned-http.ts +7 -1
- package/src/lib/process-control.ts +3 -2
- package/src/lib/provider-outbound.ts +77 -27
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +97 -11
- package/src/lib/server-tls.ts +47 -0
- package/src/lib/test-provider-fetch.ts +6 -0
- package/src/lib/test-server-start.ts +5 -0
- package/src/lib/upstream-retry.ts +39 -25
- package/src/oauth/anthropic.ts +4 -3
- package/src/oauth/chatgpt.ts +7 -3
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/cursor.ts +3 -2
- package/src/oauth/github-copilot.ts +7 -5
- package/src/oauth/google-antigravity.ts +5 -4
- package/src/oauth/key-providers.ts +11 -13
- package/src/oauth/kimi.ts +7 -6
- package/src/oauth/kiro.ts +3 -2
- package/src/oauth/nous.ts +5 -5
- package/src/oauth/transport.ts +77 -0
- package/src/oauth/xai.ts +5 -4
- package/src/providers/replit/probe.ts +8 -3
- package/src/providers/xai-transport.ts +16 -2
- package/src/server/auth-cors.ts +15 -1
- package/src/server/chat-native.ts +9 -3
- package/src/server/index.ts +63 -23
- package/src/server/management/config-routes.ts +117 -15
- package/src/server/management/context.ts +4 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/responses/compact.ts +5 -2
- package/src/server/responses/core.ts +16 -19
- package/src/server/responses/fetch-helpers.ts +15 -7
- package/src/server/ws-bridge.ts +33 -4
- package/src/service.ts +10 -1
- package/src/types/config.ts +9 -0
- package/src/types/provider.ts +2 -0
- package/src/types.ts +1 -0
- package/gui/dist/assets/Providers-DyaCHRWL.js +0 -1
- package/gui/dist/assets/index-pLho_LBF.css +0 -1
- package/gui/dist/assets/shared-DI18-uF7.js +0 -69
package/src/server/index.ts
CHANGED
|
@@ -60,7 +60,7 @@ import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-st
|
|
|
60
60
|
import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
|
|
61
61
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
62
62
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
63
|
-
import type { StorageCleanupPolicy } from "../types";
|
|
63
|
+
import type { OcxConfig, StorageCleanupPolicy } from "../types";
|
|
64
64
|
import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
|
|
65
65
|
import {
|
|
66
66
|
CodexAccountCooldownError,
|
|
@@ -158,6 +158,7 @@ import {
|
|
|
158
158
|
jsonResponse,
|
|
159
159
|
admissionFields,
|
|
160
160
|
resolveApiAuth,
|
|
161
|
+
resolveDataPlaneAdmissionSecret,
|
|
161
162
|
resolveResponsesApiAuth,
|
|
162
163
|
requestPolicyView,
|
|
163
164
|
type RequestPolicyView,
|
|
@@ -166,6 +167,10 @@ import {
|
|
|
166
167
|
withCors,
|
|
167
168
|
withManagementCors,
|
|
168
169
|
} from "./auth-cors";
|
|
170
|
+
import {
|
|
171
|
+
managementBodyTooLargeResponse,
|
|
172
|
+
readManagementJsonBody,
|
|
173
|
+
} from "./management/body";
|
|
169
174
|
export {
|
|
170
175
|
assertServerAuthConfig,
|
|
171
176
|
corsHeaders,
|
|
@@ -210,6 +215,8 @@ import {
|
|
|
210
215
|
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
211
216
|
import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
|
|
212
217
|
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
218
|
+
import { allowPlaintextRemoteForTests } from "../lib/test-server-start";
|
|
219
|
+
import { canonicalServerOrigin } from "../lib/server-tls";
|
|
213
220
|
import {
|
|
214
221
|
createRuntimePackageTreeIntegrityGuard,
|
|
215
222
|
type PackageTreeIntegrityGuard,
|
|
@@ -217,18 +224,22 @@ import {
|
|
|
217
224
|
import { detectInstall } from "../update/index";
|
|
218
225
|
import { runAiStudioNativeLogin } from "../oauth/aistudio-native-daemon";
|
|
219
226
|
|
|
220
|
-
function isAiStudioSessionOrigin(origin: string | null): boolean {
|
|
221
|
-
return !!origin && (
|
|
227
|
+
function isAiStudioSessionOrigin(origin: string | null, config: Pick<OcxConfig, "corsAllowOrigins">): boolean {
|
|
228
|
+
return !!origin && (
|
|
229
|
+
origin === "https://aistudio.google.com"
|
|
230
|
+
|| (origin.startsWith("chrome-extension://") && config.corsAllowOrigins?.includes(origin) === true)
|
|
231
|
+
);
|
|
222
232
|
}
|
|
223
233
|
|
|
224
|
-
function withAiStudioSessionCors(resp: Response, req: Request): Response {
|
|
234
|
+
function withAiStudioSessionCors(resp: Response, req: Request, config: RequestPolicyView): Response {
|
|
225
235
|
const origin = req.headers.get("Origin");
|
|
226
|
-
if (isAiStudioSessionOrigin(origin) && origin) resp.headers.set("Access-Control-Allow-Origin", origin);
|
|
236
|
+
if (isAiStudioSessionOrigin(origin, config) && origin) resp.headers.set("Access-Control-Allow-Origin", origin);
|
|
227
237
|
return resp;
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
231
|
-
const WEBSOCKET_IDLE_TIMEOUT_SECONDS =
|
|
241
|
+
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 255;
|
|
242
|
+
const WEBSOCKET_BACKPRESSURE_LIMIT = 1024 * 1024;
|
|
232
243
|
const LIVE_SIDEBAND_PENDING_MAX = 32;
|
|
233
244
|
const LIVE_SIDEBAND_PENDING_BYTES_MAX = 1024 * 1024;
|
|
234
245
|
const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
|
|
@@ -407,11 +418,16 @@ function attachLiveSidebandUpstream(
|
|
|
407
418
|
return;
|
|
408
419
|
}
|
|
409
420
|
logLiveSidebandFrame("u2c", event.data);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
const result = typeof event.data === "string"
|
|
422
|
+
? ws.send(event.data)
|
|
423
|
+
: event.data instanceof ArrayBuffer
|
|
424
|
+
? ws.send(event.data)
|
|
425
|
+
: ArrayBuffer.isView(event.data)
|
|
426
|
+
? ws.send(event.data.buffer.slice(event.data.byteOffset, event.data.byteOffset + event.data.byteLength))
|
|
427
|
+
: ws.send(event.data as Buffer);
|
|
428
|
+
// The upstream cannot be paused; shed the socket as soon as Bun reports
|
|
429
|
+
// backpressure or a dropped send rather than building an application queue.
|
|
430
|
+
if (result <= 0) closeLiveSideband(ws, 1009, "backpressure limit");
|
|
415
431
|
} catch {
|
|
416
432
|
closeLiveSideband(ws, 1011, "client send failed");
|
|
417
433
|
}
|
|
@@ -554,7 +570,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
554
570
|
warnAgentTaskRecoveryStartup(config);
|
|
555
571
|
setLiveStateStoreConfig(config);
|
|
556
572
|
applyProxyEnv(config);
|
|
557
|
-
assertServerAuthConfig(config);
|
|
573
|
+
assertServerAuthConfig(config, { allowPlaintextRemoteForTests: allowPlaintextRemoteForTests() });
|
|
558
574
|
const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
|
|
559
575
|
let userCostOverlayReconciler: { stop(): void } | null = null;
|
|
560
576
|
// Arm synchronously before listen. A pending journal therefore makes __main__ unusable
|
|
@@ -865,6 +881,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
865
881
|
// release the owner-scoped lease on any listener failure.
|
|
866
882
|
userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
|
|
867
883
|
const serveOptions = {
|
|
884
|
+
...(config.tls ? { tls: { cert: Bun.file(config.tls.certFile), key: Bun.file(config.tls.keyFile) } } : {}),
|
|
868
885
|
idleTimeout: 255,
|
|
869
886
|
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
|
|
870
887
|
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
|
|
@@ -931,13 +948,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
931
948
|
}
|
|
932
949
|
if (url.pathname === "/api/aistudio/session") {
|
|
933
950
|
const origin = req.headers.get("Origin");
|
|
934
|
-
if (isAiStudioSessionOrigin(origin)) {
|
|
951
|
+
if (isAiStudioSessionOrigin(origin, policy)) {
|
|
935
952
|
return new Response(null, {
|
|
936
953
|
status: 204,
|
|
937
954
|
headers: {
|
|
938
955
|
"Access-Control-Allow-Origin": origin as string,
|
|
939
956
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
940
|
-
"Access-Control-Allow-Headers": "Content-Type,
|
|
957
|
+
"Access-Control-Allow-Headers": "Content-Type, X-OpenCodex-API-Key",
|
|
941
958
|
Vary: "Origin, Access-Control-Request-Headers",
|
|
942
959
|
},
|
|
943
960
|
});
|
|
@@ -1005,16 +1022,19 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1005
1022
|
}
|
|
1006
1023
|
|
|
1007
1024
|
if (url.pathname === "/api/aistudio/session" && req.method === "POST") {
|
|
1008
|
-
const
|
|
1025
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim() ?? "";
|
|
1026
|
+
const admission = dedicated
|
|
1027
|
+
? resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated")
|
|
1028
|
+
: null;
|
|
1009
1029
|
if (!admission) {
|
|
1010
|
-
return withAiStudioSessionCors(withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy), req);
|
|
1030
|
+
return withAiStudioSessionCors(withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy), req, policy);
|
|
1011
1031
|
}
|
|
1012
1032
|
const origin = req.headers.get("Origin");
|
|
1013
|
-
if (!
|
|
1014
|
-
return withAiStudioSessionCors(withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy), req);
|
|
1033
|
+
if (!isAiStudioSessionOrigin(origin, policy)) {
|
|
1034
|
+
return withAiStudioSessionCors(withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy), req, policy);
|
|
1015
1035
|
}
|
|
1016
1036
|
try {
|
|
1017
|
-
const bodyJson = await req
|
|
1037
|
+
const bodyJson = await readManagementJsonBody(req) as any;
|
|
1018
1038
|
const { saveAiStudioSession, saveAiStudioSessionFromToken } = await import("../oauth/aistudio-session-sync");
|
|
1019
1039
|
if (typeof bodyJson.token === "string" && bodyJson.token) {
|
|
1020
1040
|
saveAiStudioSessionFromToken(bodyJson.token);
|
|
@@ -1025,11 +1045,16 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1025
1045
|
cookies: bodyJson.cookies,
|
|
1026
1046
|
});
|
|
1027
1047
|
} else {
|
|
1028
|
-
return withAiStudioSessionCors(withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy), req);
|
|
1048
|
+
return withAiStudioSessionCors(withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy), req, policy);
|
|
1029
1049
|
}
|
|
1030
|
-
return withAiStudioSessionCors(withCors(jsonResponse({ ok: true, message: "AI Studio session updated successfully" }), req, policy), req);
|
|
1050
|
+
return withAiStudioSessionCors(withCors(jsonResponse({ ok: true, message: "AI Studio session updated successfully" }), req, policy), req, policy);
|
|
1031
1051
|
} catch (error) {
|
|
1032
|
-
|
|
1052
|
+
const tooLarge = managementBodyTooLargeResponse(error, req, config);
|
|
1053
|
+
return withAiStudioSessionCors(
|
|
1054
|
+
withCors(tooLarge ?? jsonResponse({ error: "invalid session payload" }, 400), req, policy),
|
|
1055
|
+
req,
|
|
1056
|
+
policy,
|
|
1057
|
+
);
|
|
1033
1058
|
}
|
|
1034
1059
|
}
|
|
1035
1060
|
|
|
@@ -1749,6 +1774,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1749
1774
|
websocket: {
|
|
1750
1775
|
maxPayloadLength: MAX_WS_FRAME_BYTES,
|
|
1751
1776
|
idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
1777
|
+
backpressureLimit: WEBSOCKET_BACKPRESSURE_LIMIT,
|
|
1778
|
+
closeOnBackpressureLimit: true,
|
|
1752
1779
|
// Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
|
|
1753
1780
|
// socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
|
|
1754
1781
|
// Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
|
|
@@ -1769,6 +1796,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1769
1796
|
ws.data.admissionLease.bind(ws);
|
|
1770
1797
|
registerCodexWebSocket(ws);
|
|
1771
1798
|
},
|
|
1799
|
+
drain(ws: ServerWebSocket<WsData>) {
|
|
1800
|
+
ws.data.wsDrain?.();
|
|
1801
|
+
},
|
|
1772
1802
|
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
1773
1803
|
if (ws.data.kind === "live-sideband") {
|
|
1774
1804
|
if (ws.data.liveClosing) return;
|
|
@@ -1907,6 +1937,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1907
1937
|
},
|
|
1908
1938
|
});
|
|
1909
1939
|
await sendResponseToWebSocket(ws, response, isCurrent, {
|
|
1940
|
+
abortSignal: turnAbort.signal,
|
|
1910
1941
|
onSsePayload: payload => inspectResponseLogSsePayload(logCtx, payload),
|
|
1911
1942
|
onTerminal: status => {
|
|
1912
1943
|
terminalRecorder?.(status, logCtx.terminalHttpStatus);
|
|
@@ -1957,6 +1988,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1957
1988
|
ws.data.admissionLease?.release();
|
|
1958
1989
|
ws.data.admissionLease = undefined;
|
|
1959
1990
|
ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
|
|
1991
|
+
ws.data.wsDrainWaiter?.resolve();
|
|
1992
|
+
ws.data.wsDrainWaiter = undefined;
|
|
1960
1993
|
},
|
|
1961
1994
|
},
|
|
1962
1995
|
} as const;
|
|
@@ -1971,6 +2004,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1971
2004
|
try {
|
|
1972
2005
|
loopbackServer = Bun.serve<WsData>({
|
|
1973
2006
|
...serveOptions,
|
|
2007
|
+
tls: undefined,
|
|
1974
2008
|
port: loopbackListenerPort,
|
|
1975
2009
|
hostname: "127.0.0.1",
|
|
1976
2010
|
});
|
|
@@ -2022,9 +2056,15 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
2022
2056
|
setServerRef(server);
|
|
2023
2057
|
const actualPort = server.port ?? listenPort;
|
|
2024
2058
|
boundPort = actualPort;
|
|
2059
|
+
managementApi.activeServerOrigin = canonicalServerOrigin(config, actualPort);
|
|
2060
|
+
managementApi.activeServerConfig = {
|
|
2061
|
+
hostname: config.hostname,
|
|
2062
|
+
port: actualPort,
|
|
2063
|
+
tls: config.tls ? { ...config.tls } : undefined,
|
|
2064
|
+
};
|
|
2025
2065
|
setCorsOrigin(actualPort);
|
|
2026
2066
|
|
|
2027
|
-
console.log(`🚀 opencodex proxy running on
|
|
2067
|
+
console.log(`🚀 opencodex proxy running on ${managementApi.activeServerOrigin}`);
|
|
2028
2068
|
console.log(` POST /v1/responses → provider translation`);
|
|
2029
2069
|
console.log(` POST /v1/chat/completions → OpenAI-compatible clients`);
|
|
2030
2070
|
console.log(` GET /healthz → health check`);
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
multiAgentGuidanceEnabled,
|
|
12
12
|
providerBaseUrlConfigError,
|
|
13
13
|
providerHeadersConfigError,
|
|
14
|
+
validateConfigCandidate,
|
|
14
15
|
} from "../../config";
|
|
16
|
+
import { canonicalServerOrigin, serverTlsConfigError } from "../../lib/server-tls";
|
|
15
17
|
import {
|
|
16
18
|
clearLoginState,
|
|
17
19
|
getLoginStatus,
|
|
@@ -91,7 +93,7 @@ import { drainAndShutdown } from "../lifecycle";
|
|
|
91
93
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
92
94
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
93
95
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
94
|
-
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
96
|
+
import { assertServerAuthConfig, configuredApiAuthToken, isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
95
97
|
import { withProviderServiceTierDTO } from "./provider-capability-config";
|
|
96
98
|
import { applySystemEnvToggle } from "../system-env";
|
|
97
99
|
import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
|
|
@@ -131,6 +133,40 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
|
|
|
131
133
|
return { model, reasoning, models };
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
function aiStudioExtensionOrigin(config: Pick<OcxConfig, "corsAllowOrigins">): string | null {
|
|
137
|
+
return config.corsAllowOrigins?.find(origin => origin.startsWith("chrome-extension://")) ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function serverSettings(
|
|
141
|
+
config: OcxConfig,
|
|
142
|
+
activeOrigin?: string,
|
|
143
|
+
activeConfig?: Pick<OcxConfig, "hostname" | "port" | "tls">,
|
|
144
|
+
) {
|
|
145
|
+
const active = activeOrigin ?? canonicalServerOrigin(config, config.port);
|
|
146
|
+
const configuredOrigin = canonicalServerOrigin(config, config.port);
|
|
147
|
+
const configuredListener = {
|
|
148
|
+
hostname: config.hostname ?? "127.0.0.1",
|
|
149
|
+
port: config.port,
|
|
150
|
+
tls: config.tls ?? null,
|
|
151
|
+
};
|
|
152
|
+
const activeListener = activeConfig && {
|
|
153
|
+
hostname: activeConfig.hostname ?? "127.0.0.1",
|
|
154
|
+
port: activeConfig.port,
|
|
155
|
+
tls: activeConfig.tls ?? null,
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
configured: {
|
|
159
|
+
...configuredListener,
|
|
160
|
+
aiStudioOrigin: aiStudioExtensionOrigin(config),
|
|
161
|
+
},
|
|
162
|
+
activeOrigin: active,
|
|
163
|
+
credentialConfigured: !!configuredApiAuthToken(config)
|
|
164
|
+
|| (config.apiKeys ?? []).some(entry => !!entry.key.trim()),
|
|
165
|
+
restartRequired: active !== configuredOrigin
|
|
166
|
+
|| !!activeListener && JSON.stringify(activeListener) !== JSON.stringify(configuredListener),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
134
170
|
/** One client's outcome from a fan-out sync. Absent from the list means "left alone". */
|
|
135
171
|
interface ClientIntegrationSyncOutcome {
|
|
136
172
|
readonly client: "grok" | "claude-desktop" | "mcode";
|
|
@@ -304,6 +340,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
304
340
|
// Absent means the historical auto-open, so the GUI can render the toggle
|
|
305
341
|
// without having to know that `undefined` and `true` mean the same thing.
|
|
306
342
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
343
|
+
server: serverSettings(config, deps.activeServerOrigin, deps.activeServerConfig),
|
|
307
344
|
startupHealth: await readStartupHealth(config),
|
|
308
345
|
codexRuntime: {
|
|
309
346
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
@@ -390,14 +427,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
390
427
|
codexAccountPickerEnabled?: unknown;
|
|
391
428
|
oauthOpenBrowser?: unknown;
|
|
392
429
|
showCodexSparkQuota?: unknown;
|
|
430
|
+
server?: unknown;
|
|
393
431
|
};
|
|
394
432
|
if (body.codexAutoStart === undefined
|
|
395
433
|
&& body.streamMode === undefined
|
|
396
434
|
&& body.appOwnedMemoryBudgetMb === undefined
|
|
397
435
|
&& body.codexAccountPickerEnabled === undefined
|
|
398
436
|
&& body.oauthOpenBrowser === undefined
|
|
399
|
-
&& body.showCodexSparkQuota === undefined
|
|
400
|
-
|
|
437
|
+
&& body.showCodexSparkQuota === undefined
|
|
438
|
+
&& body.server === undefined) {
|
|
439
|
+
return jsonResponse({ error: "provide a supported settings field" }, 400);
|
|
401
440
|
}
|
|
402
441
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
403
442
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -415,6 +454,30 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
415
454
|
if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") {
|
|
416
455
|
return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400);
|
|
417
456
|
}
|
|
457
|
+
let nextServer: {
|
|
458
|
+
hostname: string;
|
|
459
|
+
port: number;
|
|
460
|
+
tls: OcxConfig["tls"];
|
|
461
|
+
aiStudioOrigin: string | null;
|
|
462
|
+
} | undefined;
|
|
463
|
+
if (body.server !== undefined) {
|
|
464
|
+
if (!isPlainRecord(body.server)) return jsonResponse({ error: "server must be an object" }, 400);
|
|
465
|
+
const raw = body.server;
|
|
466
|
+
if (typeof raw.hostname !== "string" || !raw.hostname.trim()) return jsonResponse({ error: "server.hostname must be a non-empty string" }, 400);
|
|
467
|
+
if (typeof raw.port !== "number" || !Number.isInteger(raw.port) || raw.port < 0 || raw.port > 65535) return jsonResponse({ error: "server.port must be an integer from 0 to 65535" }, 400);
|
|
468
|
+
const tlsError = raw.tls === null ? null : serverTlsConfigError(raw.tls);
|
|
469
|
+
if (tlsError) return jsonResponse({ error: tlsError }, 400);
|
|
470
|
+
if (raw.aiStudioOrigin !== null && (
|
|
471
|
+
typeof raw.aiStudioOrigin !== "string"
|
|
472
|
+
|| !/^chrome-extension:\/\/[a-z]{32}$/.test(raw.aiStudioOrigin)
|
|
473
|
+
)) return jsonResponse({ error: "server.aiStudioOrigin must be an exact chrome-extension origin or null" }, 400);
|
|
474
|
+
nextServer = {
|
|
475
|
+
hostname: raw.hostname.trim(),
|
|
476
|
+
port: raw.port,
|
|
477
|
+
tls: raw.tls === null ? undefined : raw.tls as OcxConfig["tls"],
|
|
478
|
+
aiStudioOrigin: raw.aiStudioOrigin,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
418
481
|
if (body.appOwnedMemoryBudgetMb !== undefined && (
|
|
419
482
|
typeof body.appOwnedMemoryBudgetMb !== "number"
|
|
420
483
|
|| !Number.isInteger(body.appOwnedMemoryBudgetMb)
|
|
@@ -438,36 +501,67 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
438
501
|
hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
|
|
439
502
|
showCodexSparkQuota: config.showCodexSparkQuota,
|
|
440
503
|
hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"),
|
|
504
|
+
hostname: config.hostname,
|
|
505
|
+
hasHostname: Object.hasOwn(config, "hostname"),
|
|
506
|
+
port: config.port,
|
|
507
|
+
tls: config.tls,
|
|
508
|
+
hasTls: Object.hasOwn(config, "tls"),
|
|
509
|
+
corsAllowOrigins: config.corsAllowOrigins,
|
|
510
|
+
hasCorsAllowOrigins: Object.hasOwn(config, "corsAllowOrigins"),
|
|
441
511
|
};
|
|
442
512
|
const pickerWasEnabled = codexAccountPickerEnabled(config);
|
|
443
|
-
|
|
444
|
-
try {
|
|
513
|
+
const applySettings = (target: OcxConfig): boolean => {
|
|
445
514
|
if (typeof body.codexAutoStart === "boolean") {
|
|
446
|
-
|
|
515
|
+
target.codexAutoStart = body.codexAutoStart;
|
|
447
516
|
}
|
|
448
517
|
if (body.streamMode !== undefined) {
|
|
449
518
|
if (body.streamMode === "auto") {
|
|
450
|
-
deleteConfigTopLevelKey(
|
|
519
|
+
deleteConfigTopLevelKey(target, "streamMode");
|
|
451
520
|
} else {
|
|
452
|
-
|
|
521
|
+
target.streamMode = body.streamMode as "legacy-tee" | "eager-relay";
|
|
453
522
|
}
|
|
454
523
|
}
|
|
455
524
|
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
456
|
-
|
|
525
|
+
target.appOwnedMemoryBudgetMb = body.appOwnedMemoryBudgetMb;
|
|
457
526
|
}
|
|
458
527
|
if (body.codexAccountPickerEnabled === true) {
|
|
459
|
-
|
|
460
|
-
initializeDefaultCodexAccountNamespaces(
|
|
528
|
+
target.codexAccountPickerEnabled = true;
|
|
529
|
+
initializeDefaultCodexAccountNamespaces(target);
|
|
461
530
|
} else if (body.codexAccountPickerEnabled === false) {
|
|
462
|
-
|
|
531
|
+
target.codexAccountPickerEnabled = false;
|
|
463
532
|
}
|
|
464
533
|
if (typeof body.oauthOpenBrowser === "boolean") {
|
|
465
|
-
|
|
534
|
+
target.oauthOpenBrowser = body.oauthOpenBrowser;
|
|
466
535
|
}
|
|
467
536
|
if (typeof body.showCodexSparkQuota === "boolean") {
|
|
468
|
-
|
|
537
|
+
target.showCodexSparkQuota = body.showCodexSparkQuota;
|
|
538
|
+
}
|
|
539
|
+
if (nextServer) {
|
|
540
|
+
target.hostname = nextServer.hostname;
|
|
541
|
+
target.port = nextServer.port;
|
|
542
|
+
if (nextServer.tls) target.tls = nextServer.tls;
|
|
543
|
+
else deleteConfigTopLevelKey(target, "tls");
|
|
544
|
+
const origins = (target.corsAllowOrigins ?? []).filter(origin => !origin.startsWith("chrome-extension://"));
|
|
545
|
+
if (nextServer.aiStudioOrigin) origins.push(nextServer.aiStudioOrigin);
|
|
546
|
+
if (origins.length) target.corsAllowOrigins = origins;
|
|
547
|
+
else deleteConfigTopLevelKey(target, "corsAllowOrigins");
|
|
469
548
|
}
|
|
470
|
-
|
|
549
|
+
return codexAccountPickerEnabled(target);
|
|
550
|
+
};
|
|
551
|
+
if (nextServer) {
|
|
552
|
+
const candidate = structuredClone(config);
|
|
553
|
+
applySettings(candidate);
|
|
554
|
+
const validation = validateConfigCandidate(candidate);
|
|
555
|
+
if (!validation.ok) return jsonResponse({ error: validation.error }, 400);
|
|
556
|
+
try {
|
|
557
|
+
assertServerAuthConfig(candidate);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
let pickerIsEnabled = pickerWasEnabled;
|
|
563
|
+
try {
|
|
564
|
+
pickerIsEnabled = applySettings(config);
|
|
471
565
|
saveManagementConfig(deps, config);
|
|
472
566
|
} catch (error) {
|
|
473
567
|
if (previousSettings.hasCodexAutoStart) config.codexAutoStart = previousSettings.codexAutoStart;
|
|
@@ -489,6 +583,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
489
583
|
if (previousSettings.hasShowCodexSparkQuota) {
|
|
490
584
|
config.showCodexSparkQuota = previousSettings.showCodexSparkQuota;
|
|
491
585
|
} else deleteConfigTopLevelKey(config, "showCodexSparkQuota");
|
|
586
|
+
if (previousSettings.hasHostname) config.hostname = previousSettings.hostname;
|
|
587
|
+
else deleteConfigTopLevelKey(config, "hostname");
|
|
588
|
+
config.port = previousSettings.port;
|
|
589
|
+
if (previousSettings.hasTls) config.tls = previousSettings.tls;
|
|
590
|
+
else deleteConfigTopLevelKey(config, "tls");
|
|
591
|
+
if (previousSettings.hasCorsAllowOrigins) config.corsAllowOrigins = previousSettings.corsAllowOrigins;
|
|
592
|
+
else deleteConfigTopLevelKey(config, "corsAllowOrigins");
|
|
492
593
|
throw error;
|
|
493
594
|
}
|
|
494
595
|
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
@@ -512,6 +613,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
512
613
|
catalogRefreshPending,
|
|
513
614
|
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
514
615
|
startupHealth: await readStartupHealth(config),
|
|
616
|
+
server: serverSettings(config, deps.activeServerOrigin, deps.activeServerConfig),
|
|
515
617
|
});
|
|
516
618
|
}
|
|
517
619
|
|
|
@@ -18,6 +18,10 @@ import type {
|
|
|
18
18
|
} from "../../codex/app-server-restart-service";
|
|
19
19
|
|
|
20
20
|
export interface ManagementApiDeps {
|
|
21
|
+
/** Canonical origin captured from the running listener; management writes must not change it. */
|
|
22
|
+
activeServerOrigin?: string;
|
|
23
|
+
/** Listener settings captured at bind time so same-origin certificate/path edits still require restart. */
|
|
24
|
+
activeServerConfig?: Pick<OcxConfig, "hostname" | "port" | "tls">;
|
|
21
25
|
/** Platform seam for capability projections; does not alter host-level startup behavior. */
|
|
22
26
|
platform?: NodeJS.Platform;
|
|
23
27
|
toggleCodexMultiAgentV2?: (enabled: boolean) => void;
|
|
@@ -677,6 +677,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
677
677
|
// Presence only (#959 review): header names and values never leave the process.
|
|
678
678
|
hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
|
|
679
679
|
allowPrivateNetwork: p.allowPrivateNetwork === true,
|
|
680
|
+
replayTransientFailures: p.replayTransientFailures === true,
|
|
680
681
|
liveModels: p.liveModels !== false,
|
|
681
682
|
requestPacing: p.requestPacing,
|
|
682
683
|
models: p.models ?? [],
|
|
@@ -58,7 +58,6 @@ import {
|
|
|
58
58
|
type CodexUpstreamOutcome,
|
|
59
59
|
} from "../../codex/routing";
|
|
60
60
|
import {
|
|
61
|
-
fetchWithResetRetry,
|
|
62
61
|
fetchWithTransientRetry,
|
|
63
62
|
applyUpstreamRecoveryInit,
|
|
64
63
|
type UpstreamSendRecovery,
|
|
@@ -533,7 +532,11 @@ export async function handleResponsesCompact(
|
|
|
533
532
|
});
|
|
534
533
|
return recovery === "single"
|
|
535
534
|
? doFetch()
|
|
536
|
-
: fetchWithTransientRetry(doFetch, {
|
|
535
|
+
: fetchWithTransientRetry(doFetch, {
|
|
536
|
+
abortSignal: req.signal,
|
|
537
|
+
label: safeHostLabel(compactUrl),
|
|
538
|
+
replayTransientFailures: sendProvider.replayTransientFailures,
|
|
539
|
+
});
|
|
537
540
|
};
|
|
538
541
|
|
|
539
542
|
// The account each outcome belongs to. Reassigned only when the alternate send below
|
|
@@ -196,7 +196,6 @@ import {
|
|
|
196
196
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
197
197
|
import {
|
|
198
198
|
applyUpstreamRecoveryInit,
|
|
199
|
-
fetchWithResetRetry,
|
|
200
199
|
fetchWithTransientRetry,
|
|
201
200
|
prepareSameTarget429Wait,
|
|
202
201
|
sleepWithAbort,
|
|
@@ -3702,6 +3701,7 @@ async function handleResponsesInner(
|
|
|
3702
3701
|
linkAbortSignal(upstream, options.abortSignal);
|
|
3703
3702
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
3704
3703
|
let upstreamResponse: Response;
|
|
3704
|
+
const replayBudget = route.provider.replayTransientFailures ? { remaining: 2 } : undefined;
|
|
3705
3705
|
const transportFailureResponse = (err: unknown): Response => {
|
|
3706
3706
|
upstream.abort();
|
|
3707
3707
|
if (options.abortSignal?.aborted) {
|
|
@@ -3768,7 +3768,7 @@ async function handleResponsesInner(
|
|
|
3768
3768
|
return res;
|
|
3769
3769
|
});
|
|
3770
3770
|
},
|
|
3771
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
3771
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), replayTransientFailures: route.provider.replayTransientFailures, replayBudget },
|
|
3772
3772
|
);
|
|
3773
3773
|
} catch (err) {
|
|
3774
3774
|
return transportFailureResponse(err);
|
|
@@ -3843,7 +3843,7 @@ async function handleResponsesInner(
|
|
|
3843
3843
|
return response;
|
|
3844
3844
|
});
|
|
3845
3845
|
},
|
|
3846
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
3846
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), replayTransientFailures: route.provider.replayTransientFailures, replayBudget },
|
|
3847
3847
|
);
|
|
3848
3848
|
} catch (err) {
|
|
3849
3849
|
return { failed: transportFailureResponse(err) };
|
|
@@ -3959,7 +3959,7 @@ async function handleResponsesInner(
|
|
|
3959
3959
|
return res;
|
|
3960
3960
|
});
|
|
3961
3961
|
},
|
|
3962
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
3962
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), replayTransientFailures: route.provider.replayTransientFailures, replayBudget },
|
|
3963
3963
|
);
|
|
3964
3964
|
} catch (err) {
|
|
3965
3965
|
return transportFailureResponse(err);
|
|
@@ -4021,7 +4021,7 @@ async function handleResponsesInner(
|
|
|
4021
4021
|
return res;
|
|
4022
4022
|
});
|
|
4023
4023
|
},
|
|
4024
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
4024
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), replayTransientFailures: route.provider.replayTransientFailures, replayBudget },
|
|
4025
4025
|
);
|
|
4026
4026
|
} catch (err) {
|
|
4027
4027
|
return transportFailureResponse(err);
|
|
@@ -5353,6 +5353,7 @@ async function handleResponsesInner(
|
|
|
5353
5353
|
*/
|
|
5354
5354
|
const invalidateSameTargetRequest = (): void => { transportToken += 1; };
|
|
5355
5355
|
let upstreamResponse: Response;
|
|
5356
|
+
const replayBudget = route.provider.replayTransientFailures ? { remaining: 2 } : undefined;
|
|
5356
5357
|
try {
|
|
5357
5358
|
if (activeAdapter.fetchResponse) {
|
|
5358
5359
|
noteDiagnosticAttempt(logCtx.activeAttempt, inputTokenEstimate, undefined, activeAdapter.name);
|
|
@@ -5361,20 +5362,17 @@ async function handleResponsesInner(
|
|
|
5361
5362
|
abortSignal: upstream.signal,
|
|
5362
5363
|
timeoutMs: connectMs,
|
|
5363
5364
|
stream: parsed.stream,
|
|
5364
|
-
|
|
5365
|
+
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
5365
5366
|
providerName: route.providerName,
|
|
5366
5367
|
modelId: route.modelId,
|
|
5367
5368
|
pacingSlotAcquired: true,
|
|
5368
|
-
|
|
5369
|
+
}),
|
|
5370
|
+
replayBudget,
|
|
5369
5371
|
...(antigravityAccountId ? { accountId: antigravityAccountId } : {}),
|
|
5370
5372
|
});
|
|
5371
5373
|
} else {
|
|
5372
|
-
//
|
|
5373
|
-
|
|
5374
|
-
// adapters keep reset-only retry so combo failover still hops on the first 5xx
|
|
5375
|
-
// instead of burning ~1.2s of same-target retries per hop.
|
|
5376
|
-
const fetchWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
|
|
5377
|
-
upstreamResponse = await fetchWithRetryPolicy(
|
|
5374
|
+
// One coordinator owns reset and transient-5xx replay for every generic adapter.
|
|
5375
|
+
upstreamResponse = await fetchWithTransientRetry(
|
|
5378
5376
|
recovery => {
|
|
5379
5377
|
noteDiagnosticAttempt(logCtx.activeAttempt, inputTokenEstimate, recovery, activeAdapter.name);
|
|
5380
5378
|
return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
|
|
@@ -5387,7 +5385,7 @@ async function handleResponsesInner(
|
|
|
5387
5385
|
modelId: route.modelId,
|
|
5388
5386
|
}));
|
|
5389
5387
|
},
|
|
5390
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(builtInitialRequest.url) },
|
|
5388
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(builtInitialRequest.url), replayTransientFailures: route.provider.replayTransientFailures, replayBudget },
|
|
5391
5389
|
);
|
|
5392
5390
|
}
|
|
5393
5391
|
} catch (err) {
|
|
@@ -6055,13 +6053,12 @@ async function handleResponsesInner(
|
|
|
6055
6053
|
modelId: nextParsed.modelId,
|
|
6056
6054
|
pacingSlotAcquired: true,
|
|
6057
6055
|
}),
|
|
6056
|
+
replayBudget,
|
|
6058
6057
|
...(antigravityAccountId ? { accountId: antigravityAccountId } : {}),
|
|
6059
6058
|
});
|
|
6060
6059
|
}
|
|
6061
|
-
//
|
|
6062
|
-
|
|
6063
|
-
const fetchContinuationWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
|
|
6064
|
-
return await fetchContinuationWithRetryPolicy(
|
|
6060
|
+
// Continuations consume the same generation-scoped transient replay allowance.
|
|
6061
|
+
return await fetchWithTransientRetry(
|
|
6065
6062
|
recovery => {
|
|
6066
6063
|
noteDiagnosticAttempt(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind, activeAdapter.name);
|
|
6067
6064
|
return fetchWithHeaderTimeout(
|
|
@@ -6080,7 +6077,7 @@ async function handleResponsesInner(
|
|
|
6080
6077
|
}),
|
|
6081
6078
|
);
|
|
6082
6079
|
},
|
|
6083
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url) },
|
|
6080
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url), replayTransientFailures: route.provider.replayTransientFailures, replayBudget },
|
|
6084
6081
|
);
|
|
6085
6082
|
} finally {
|
|
6086
6083
|
builtContinuationRequest.releaseBodyObservation?.();
|