import type { IntegrationClientId } from "../../integrations/registry"; import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import type { CatalogModel } from "../../codex/catalog"; import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog"; import { DEFAULT_SUBAGENT_MODELS, codexAutoStartEnabled, deleteConfigTopLevelKey, hasOwnProvider, isValidProviderName, loadConfig, multiAgentGuidanceEnabled, providerBaseUrlConfigError, providerHeadersConfigError, validateConfigCandidate, } from "../../config"; import { canonicalServerOrigin, serverTlsConfigError } from "../../lib/server-tls"; import { clearLoginState, getLoginStatus, isPublicOAuthProvider, listOAuthProviders, startLoginFlow, submitManualLoginCode, upsertOAuthProvider, } from "../../oauth"; import { removeCredential } from "../../oauth/store"; import { providerDestinationResolvedError } from "../../lib/destination-policy"; import { isStreamMode } from "../../lib/bun-stream-caps"; import { shadowSourceModels } from "../../lib/shadow-call"; import { configureAppOwnedMemoryBudget, enforceAppOwnedMemoryBudget, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB, resolveAppOwnedMemoryBudgetBytes, } from "../../lib/app-owned-memory"; import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers"; import { deriveProviderPresets } from "../../providers/derive"; import { providerCodexAccountMode } from "../../providers/registry"; import { routedSlug, slugEquals } from "../../providers/slug-codec"; import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota"; import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers"; import { clearThreadAccountMap } from "../../codex/routing"; import { primeCodexPoolQuotas } from "../../codex/auth-api"; import { isSelectableCodexPoolAccount } from "../../codex/account-id"; import { isCodexAccountPriorityKey } from "../../codex/account-priority"; import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account"; import { getAccountQuota } from "../../codex/quota"; import { codexQuotaAutoRefreshStatus, runCodexQuotaAutoRefresh, } from "../../codex/quota-auto-refresh"; import { getMainAccountHardLockStatus } from "../../codex/main-account-hard-lock"; import { codexAccountPickerEnabled, initializeDefaultCodexAccountNamespaces, } from "../../codex/account-namespaces"; import { catalogRefreshIsPending } from "../../codex/catalog-refresh-status"; import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap"; import { resolveCodexHomeDir } from "../../codex/home"; import { readUsageEntries } from "../../usage/log"; import { getUsageDebugLogEntries } from "../../usage/debug"; import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary"; import { stripCodexRuntimeProviderFields } from "../../codex/auth-context"; import { getProviderRegistryEntry } from "../../providers/registry"; import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort"; import { normalizeVisionReasoningForModel } from "../../vision/reasoning"; import { findAnthropicVisionProvider, isValidVisionTimeoutMs, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS, resolveEffectiveVisionModel, resolveMaxDescriptionsPerTurn, resolveVisionBackend, resolveVisionTimeoutMs, } from "../../vision"; import { visionCandidateRows, visionDescriberIsProvablyBlind, visionDescriberRejection, visionModelOptionsFor, } from "./vision-sidecar-options"; import { webSearchCandidateRows, webSearchModelIsRejected, webSearchModelOptionsFrom, webSearchModelRejection, } from "./web-search-sidecar-options"; import { validateXaiSearchOptions } from "../../web-search/xai-executor"; import { getDebugLogEntries } from "../../lib/debug-log-buffer"; import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log"; import { clearDebugSettings, clearDebugSetting, getDebugSettings, setDebugSettings, type DebugFlag, } from "../../lib/debug-settings"; import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types"; import { shadowCallTargetError } from "./shadow-call-validation"; import { drainAndShutdown } from "../lifecycle"; import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log"; import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost"; import type { PersistedUsageAttempt } from "../../usage/log"; import { assertServerAuthConfig, configuredApiAuthToken, isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; import { withProviderServiceTierDTO } from "./provider-capability-config"; import { applySystemEnvToggle } from "../system-env"; import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache"; import { runWindowsTrayAction } from "../windows-tray-control"; import { runStartupInstallAction, type StartupInstallAction } from "../startup-action-control"; import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../../codex/runtime"; import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared"; import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared"; import { mutateManagementConfig, saveManagementConfig, type ManagementContext } from "./context"; import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body"; function quotaAutoRefreshSettings(config: OcxConfig) { return Object.fromEntries(Object.entries(config.codexQuotaAutoRefresh ?? {}).map(([id, setting]) => [ id, { fiveHour: setting.fiveHour === true, weekly: setting.weekly === true }, ])); } async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{ model: string; reasoning: string; models: Awaited>; }> { const vs = config.visionSidecar ?? {}; // Match the runtime's one selected Anthropic executor for both backend fallback // and catalog reachability; resolving it once prevents the two projections drifting. const anthropicSidecar = findAnthropicVisionProvider(config); // The routed backend reports its own namespaced model verbatim: it is the // dispatched value, and collapsing it through the legacy resolver would // display a describer the runtime is not using (roadmap 190). const routedActive = vs.backend === "routed" && !!vs.model && vs.model.includes("/"); const backend = routedActive ? "routed" as const : resolveVisionBackend(vs.backend, anthropicSidecar); const model = routedActive && vs.model ? vs.model : resolveEffectiveVisionModel(config, backend === "routed" ? resolveVisionBackend(undefined, anthropicSidecar) : backend); const reasoning = normalizeVisionReasoningForModel(model, vs.reasoning) ?? "low"; const models = await visionModelOptionsFor(config, anthropicSidecar); // Display-only grandfather: a persisted id stays selectable, but the write gate // remains stricter and rejects a model that is positively proven blind. if (!models.some(option => option.value === model)) { models.unshift({ value: model, label: model, backend }); } return { model, reasoning, models }; } function aiStudioExtensionOrigin(config: Pick): string | null { return config.corsAllowOrigins?.find(origin => origin.startsWith("chrome-extension://")) ?? null; } function serverSettings( config: OcxConfig, activeOrigin?: string, activeConfig?: Pick, ) { const active = activeOrigin ?? canonicalServerOrigin(config, config.port); const configuredOrigin = canonicalServerOrigin(config, config.port); const configuredListener = { hostname: config.hostname ?? "127.0.0.1", port: config.port, tls: config.tls ?? null, }; const activeListener = activeConfig && { hostname: activeConfig.hostname ?? "127.0.0.1", port: activeConfig.port, tls: activeConfig.tls ?? null, }; return { configured: { ...configuredListener, aiStudioOrigin: aiStudioExtensionOrigin(config), }, activeOrigin: active, credentialConfigured: !!configuredApiAuthToken(config) || (config.apiKeys ?? []).some(entry => !!entry.key.trim()), restartRequired: active !== configuredOrigin || !!activeListener && JSON.stringify(activeListener) !== JSON.stringify(configuredListener), }; } /** One client's outcome from a fan-out sync. Absent from the list means "left alone". */ interface ClientIntegrationSyncOutcome { readonly client: "grok" | "claude-desktop" | IntegrationClientId; readonly ok: boolean; readonly changed?: boolean; readonly reason?: string; readonly profileId?: number; } /** * Re-inject native clients that are switched ON and every file integration whose * OpenCodex ownership record is the operator's durable opt-in. * * Only Codex used to run here, so a catalog change reached Codex and nothing else: a Grok * fence or a written Desktop profile kept the context windows it was created with until the * next `ocx start`. The startup path already gates each client on its own toggle * (`src/cli/index.ts`), and this is that same fan-out for the on-demand command. * * File integrations use the catalog-refresh coordinator so owned blocks are * updated without claiming unowned files. Aside remains on its multi-profile * server-owned path inside that coordinator. * * A client that is OFF or never connected is omitted from the result rather than reported as skipped — the * caller has to be able to tell "not touched" from "tried and failed". A client that fails * does not fail the sync: Codex is the one that matters for routing, and a broken Grok file * should surface as a warning, not as a 500 on a command that did its main job. */ export async function syncEnabledClientIntegrations( port: number | undefined, config: OcxConfig, deps: Pick = {}, ): Promise { if (port === undefined) return []; const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state"); const out: ClientIntegrationSyncOutcome[] = []; if (grokIntegrationEnabled(config)) { try { const { syncGrokConfig } = await import("../../grok/sync"); const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {}); out.push(r.ok ? { client: "grok", ok: true, changed: r.changed === true } : { client: "grok", ok: false, reason: r.message }); } catch (error) { out.push({ client: "grok", ok: false, reason: error instanceof Error ? error.message : String(error) }); } } if (claudeDesktopIntegrationEnabled(config)) { try { const { writeDesktop3pConfig } = await import("../../claude/desktop-3p"); const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog"); const { fetchAllModels } = await import("../management-api"); const models = await (deps.fetchAllModels ?? fetchAllModels)(config); // Discovery admits a concurrent OFF or settings edit. Re-read outside C: // the writer facade owns L and its final desired-state check under L→C. const latest = loadConfig(); if (claudeDesktopIntegrationEnabled(latest)) { const routed = filterCatalogVisibleModels(models, latest) .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow })); const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)( port, [...desktopVisibleNativeSlugs(latest)], routed, latest.apiKeys?.[0]?.key, "static", latest.claudeCode?.desktopProfile, nativeContextLimits(latest), ); out.push(r.written ? { client: "claude-desktop", ok: true, changed: true } : { client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" }); } } catch (error) { out.push({ client: "claude-desktop", ok: false, reason: error instanceof Error ? error.message : String(error) }); } } const { refreshOwnedCatalogIntegrations } = await import("../../integrations/catalog-refresh"); out.push(...await refreshOwnedCatalogIntegrations({ models: async () => { const { loadExportModels } = await import("./model-rows"); return loadExportModels(config); }, config, port, }, ["mcode", "pi", "aside", "raycast"])); return out; } function publicVisionSidecarSettings( config: OcxConfig, vision: Awaited>, ) { const vs = config.visionSidecar ?? {}; return { enabled: vs.enabled !== false, model: vision.model, backend: vs.backend, reasoning: vision.reasoning, maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(vs.maxDescriptionsPerTurn), timeoutMs: resolveVisionTimeoutMs(vs.timeoutMs), }; } export async function handleConfigRoutes(ctx: ManagementContext): Promise { const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx; const readStartupHealth = deps.getCachedStartupHealth ?? getCachedStartupHealth; if (url.pathname === "/api/config" && req.method === "GET") { return jsonResponse(withProviderServiceTierDTO(safeConfigDTO(config), config)); } if (url.pathname === "/api/config" && req.method === "PUT") { return jsonResponse({ error: "Full config PUT is disabled. Use /api/providers POST for provider changes." }, 405); } if (url.pathname === "/api/settings" && req.method === "GET") { let resolved: ReturnType; try { // Full alternative discovery (memoized) so newerAvailable warnings work. resolved = resolveCodexRuntime(); } catch { resolved = { runtime: { command: "codex", version: null, source: "fallback" }, failures: [], }; } const lastClamp = loadLastEffortClamp(); const clampActive = effortClampAppliesToRuntime(lastClamp, resolved.runtime); const warningParts: string[] = []; if (resolved.replacedConfigured) { warningParts.push( `Preferred Codex runtime is unavailable; using ${displayCodexRuntimePath(resolved.runtime.command)} instead.`, ); } else if ( resolved.runtime.source === "fallback" && resolved.failures.length > 0 && !resolved.runtime.version ) { warningParts.push("No validated Codex runtime found; falling back to `codex`."); } if (clampActive) { const clampVersion = lastClamp?.runtimeVersion ?? resolved.runtime.version ?? "an older binary"; warningParts.push( `Some reasoning effort options were hidden because OpenCodex used Codex ${clampVersion}.${resolved.newerAvailable ? " A newer Codex installation is available." : ""}`, ); } else if (resolved.newerAvailable) { warningParts.push( `OpenCodex is using an older Codex binary (${resolved.runtime.version ?? "unknown"}). A newer Codex installation is available.`, ); } return jsonResponse({ // The dashboard renders request-log timestamps. Without this it formats them in the // BROWSER's zone, so a KST proxy viewed from a UTC browser reports every request nine // hours off (#725). Carried on settings rather than /api/logs because that route's // array response has four consumers that would have to change with it. timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, codexAutoStart: codexAutoStartEnabled(config), port: config.port, hostname: config.hostname ?? "127.0.0.1", streamMode: config.streamMode ?? "auto", appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256, codexAccountPickerEnabled: codexAccountPickerEnabled(config), codexQuotaAutoRefresh: quotaAutoRefreshSettings(config), // Absent means hidden, so the GUI renders the switch without having to know that // `undefined` and `false` mean the same thing. showCodexSparkQuota: config.showCodexSparkQuota === true, // Absent means off, same convention: the GUI renders a plain switch without // needing to know that `undefined` and `false` mean the same thing here. ultraFastTier: config.ultraFastTier === true, codexMainAccountHardLock: config.codexMainAccountHardLock === true, mainAccountHardLock: getMainAccountHardLockStatus(config), // Absent means the historical auto-open, so the GUI can render the toggle // without having to know that `undefined` and `true` mean the same thing. oauthOpenBrowser: config.oauthOpenBrowser !== false, server: serverSettings(config, deps.activeServerOrigin, deps.activeServerConfig), codexDesktopAuthless: config.codexDesktopAuthless === true, startupHealth: await readStartupHealth(config), codexRuntime: { path: displayCodexRuntimePath(resolved.runtime.command), version: resolved.runtime.version, source: resolved.runtime.source, newerAvailable: resolved.newerAvailable ? { path: displayCodexRuntimePath(resolved.newerAvailable.command), version: resolved.newerAvailable.version, } : null, catalogClamp: { active: clampActive, removedEfforts: clampActive ? (lastClamp?.removedEfforts ?? []) : [], runtimeVersion: clampActive ? (lastClamp?.runtimeVersion ?? null) : null, }, warning: warningParts.length > 0 ? warningParts.join(" ") : null, }, }); } if (url.pathname === "/api/startup-health" && req.method === "GET") { return jsonResponse(await readStartupHealth(config)); } if (url.pathname === "/api/startup-action" && req.method === "POST") { let body: { action?: unknown; repair?: unknown }; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } if (!body || !["install-service", "install-shim"].includes(String(body.action))) { return jsonResponse({ error: "action must be install-service or install-shim" }, 400); } if (body.repair !== undefined && typeof body.repair !== "boolean") { return jsonResponse({ error: "repair must be a boolean when provided" }, 400); } try { const action = body.action as StartupInstallAction; const repair = body.repair === true; const result = await (deps.runStartupInstallAction ?? runStartupInstallAction)(action, { repair }); invalidateStartupHealthCache(); return jsonResponse({ ok: true, action, repair, message: result.message }); } catch (error) { return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500); } } if (url.pathname === "/api/windows-tray" && req.method === "GET") { if (process.platform !== "win32") return jsonResponse({ supported: false, installed: false, running: false, stale: false, summary: `unsupported on ${process.platform}` }); try { return jsonResponse(await runWindowsTrayAction("status")); } catch (error) { return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500); } } if (url.pathname === "/api/windows-tray" && req.method === "POST") { let body: { action?: unknown }; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } if (!body || !["install", "start", "stop", "uninstall"].includes(String(body.action))) { return jsonResponse({ error: "action must be install, start, stop, or uninstall" }, 400); } if (process.platform !== "win32") return jsonResponse({ error: "Windows tray is only supported on Windows" }, 400); try { const status = await runWindowsTrayAction(body.action as "install" | "start" | "stop" | "uninstall"); return jsonResponse({ ok: true, status }); } catch (error) { return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500); } } if (url.pathname === "/api/settings" && req.method === "PUT") { // Each field is optional but at least one must be present; fields are // validated when present. streamMode-only PUTs must work: Windows/macOS // memory troubleshooting can use this persisted stream-shape escape hatch // (a Windows service does not inherit shell env). A stream-shape // change applies to NEW turns only — the config object is shared by // reference with the request handlers, no restart needed. let parsedBody: unknown; try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "settings body must be an object" }, 400); const body = parsedBody as { codexAutoStart?: unknown; streamMode?: unknown; appOwnedMemoryBudgetMb?: unknown; codexAccountPickerEnabled?: unknown; codexQuotaAutoRefresh?: unknown; oauthOpenBrowser?: unknown; showCodexSparkQuota?: unknown; server?: unknown; ultraFastTier?: unknown; codexMainAccountHardLock?: unknown; codexDesktopAuthless?: unknown; }; if (body.codexAutoStart === undefined && body.streamMode === undefined && body.appOwnedMemoryBudgetMb === undefined && body.codexAccountPickerEnabled === undefined && body.codexQuotaAutoRefresh === undefined && body.oauthOpenBrowser === undefined && body.showCodexSparkQuota === undefined && body.server === undefined && body.ultraFastTier === undefined && body.codexMainAccountHardLock === undefined && body.codexDesktopAuthless === undefined) { return jsonResponse({ error: "provide a supported settings field" }, 400); } if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") { return jsonResponse({ error: "codexAutoStart boolean is required" }, 400); } if (body.oauthOpenBrowser !== undefined && typeof body.oauthOpenBrowser !== "boolean") { return jsonResponse({ error: "oauthOpenBrowser boolean is required" }, 400); } if (body.streamMode !== undefined && !isStreamMode(body.streamMode)) { return jsonResponse({ error: "streamMode must be auto, legacy-tee, or eager-relay" }, 400); } if (body.codexAccountPickerEnabled !== undefined && typeof body.codexAccountPickerEnabled !== "boolean") { return jsonResponse({ error: "codexAccountPickerEnabled boolean is required" }, 400); } if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") { return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400); } if (body.ultraFastTier !== undefined && typeof body.ultraFastTier !== "boolean") { return jsonResponse({ error: "ultraFastTier boolean is required" }, 400); } if (body.codexMainAccountHardLock !== undefined && typeof body.codexMainAccountHardLock !== "boolean") { return jsonResponse({ error: "codexMainAccountHardLock boolean is required" }, 400); } if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") { return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400); } let nextServer: { hostname: string; port: number; tls: OcxConfig["tls"]; aiStudioOrigin: string | null; } | undefined; if (body.server !== undefined) { if (!isPlainRecord(body.server)) return jsonResponse({ error: "server must be an object" }, 400); const raw = body.server; if (typeof raw.hostname !== "string" || !raw.hostname.trim()) return jsonResponse({ error: "server.hostname must be a non-empty string" }, 400); 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); const tlsError = raw.tls === null ? null : serverTlsConfigError(raw.tls); if (tlsError) return jsonResponse({ error: tlsError }, 400); if (raw.aiStudioOrigin !== null && ( typeof raw.aiStudioOrigin !== "string" || !/^chrome-extension:\/\/[a-z]{32}$/.test(raw.aiStudioOrigin) )) return jsonResponse({ error: "server.aiStudioOrigin must be an exact chrome-extension origin or null" }, 400); nextServer = { hostname: raw.hostname.trim(), port: raw.port, tls: raw.tls === null ? undefined : raw.tls as OcxConfig["tls"], aiStudioOrigin: raw.aiStudioOrigin, }; } let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined; if (body.codexQuotaAutoRefresh !== undefined) { if (!isPlainRecord(body.codexQuotaAutoRefresh)) { return jsonResponse({ error: "codexQuotaAutoRefresh must be an object" }, 400); } const change = body.codexQuotaAutoRefresh; const id = typeof change.id === "string" ? change.id.trim() : ""; if (!isCodexAccountPriorityKey(id)) return jsonResponse({ error: "Invalid account id format" }, 400); if (change.window !== "fiveHour" && change.window !== "weekly") { return jsonResponse({ error: "window must be fiveHour or weekly" }, 400); } if (typeof change.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400); const exists = id === MAIN_CODEX_ACCOUNT_ID || (config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id); if (!exists) return jsonResponse({ error: "Account not found" }, 404); const status = codexQuotaAutoRefreshStatus(config, id, getAccountQuota(id)); if (change.enabled && !status[change.window === "fiveHour" ? "fiveHourAvailable" : "weeklyAvailable"]) { return jsonResponse({ error: "Quota window is not available for this account" }, 409); } quotaAutoRefreshChange = { id, window: change.window, enabled: change.enabled }; } if (body.appOwnedMemoryBudgetMb !== undefined && ( typeof body.appOwnedMemoryBudgetMb !== "number" || !Number.isInteger(body.appOwnedMemoryBudgetMb) || body.appOwnedMemoryBudgetMb < MIN_APP_OWNED_MEMORY_BUDGET_MB || body.appOwnedMemoryBudgetMb > MAX_APP_OWNED_MEMORY_BUDGET_MB )) { return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400); } const previousSettings = { codexAutoStart: config.codexAutoStart, hasCodexAutoStart: Object.hasOwn(config, "codexAutoStart"), streamMode: config.streamMode, hasStreamMode: Object.hasOwn(config, "streamMode"), appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb, hasAppOwnedMemoryBudgetMb: Object.hasOwn(config, "appOwnedMemoryBudgetMb"), codexAccountNamespaces: config.codexAccountNamespaces, hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"), codexAccountPickerEnabled: config.codexAccountPickerEnabled, hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"), codexQuotaAutoRefresh: config.codexQuotaAutoRefresh, hasCodexQuotaAutoRefresh: Object.hasOwn(config, "codexQuotaAutoRefresh"), oauthOpenBrowser: config.oauthOpenBrowser, hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"), showCodexSparkQuota: config.showCodexSparkQuota, hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"), hostname: config.hostname, hasHostname: Object.hasOwn(config, "hostname"), port: config.port, tls: config.tls, hasTls: Object.hasOwn(config, "tls"), corsAllowOrigins: config.corsAllowOrigins, hasCorsAllowOrigins: Object.hasOwn(config, "corsAllowOrigins"), ultraFastTier: config.ultraFastTier, hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"), codexMainAccountHardLock: config.codexMainAccountHardLock, hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"), codexDesktopAuthless: config.codexDesktopAuthless, hasCodexDesktopAuthless: Object.hasOwn(config, "codexDesktopAuthless"), }; const pickerWasEnabled = codexAccountPickerEnabled(config); const authlessWasEnabled = config.codexDesktopAuthless === true; const applySettings = (target: OcxConfig): boolean => { if (typeof body.codexAutoStart === "boolean") { target.codexAutoStart = body.codexAutoStart; } if (body.streamMode !== undefined) { if (body.streamMode === "auto") { deleteConfigTopLevelKey(target, "streamMode"); } else { target.streamMode = body.streamMode as "legacy-tee" | "eager-relay"; } } if (typeof body.appOwnedMemoryBudgetMb === "number") { target.appOwnedMemoryBudgetMb = body.appOwnedMemoryBudgetMb; } if (body.codexAccountPickerEnabled === true) { target.codexAccountPickerEnabled = true; initializeDefaultCodexAccountNamespaces(target); } else if (body.codexAccountPickerEnabled === false) { target.codexAccountPickerEnabled = false; } if (typeof body.oauthOpenBrowser === "boolean") { target.oauthOpenBrowser = body.oauthOpenBrowser; } if (typeof body.showCodexSparkQuota === "boolean") { target.showCodexSparkQuota = body.showCodexSparkQuota; } // Off deletes opt-in keys rather than persisting false, preserving the documented // absent-is-default representation used by the rest of the settings surface. if (body.ultraFastTier === true) target.ultraFastTier = true; else if (body.ultraFastTier === false) deleteConfigTopLevelKey(target, "ultraFastTier"); if (body.codexMainAccountHardLock === true) target.codexMainAccountHardLock = true; else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(target, "codexMainAccountHardLock"); if (body.codexDesktopAuthless === true) target.codexDesktopAuthless = true; else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(target, "codexDesktopAuthless"); if (quotaAutoRefreshChange) { const { id, window, enabled } = quotaAutoRefreshChange; const setting = { ...(target.codexQuotaAutoRefresh?.[id] ?? {}) }; if (enabled) setting[window] = true; else delete setting[window]; const all = { ...(target.codexQuotaAutoRefresh ?? {}) }; if (Object.keys(setting).length > 0) all[id] = setting; else delete all[id]; if (Object.keys(all).length > 0) target.codexQuotaAutoRefresh = all; else deleteConfigTopLevelKey(target, "codexQuotaAutoRefresh"); } if (nextServer) { target.hostname = nextServer.hostname; target.port = nextServer.port; if (nextServer.tls) target.tls = nextServer.tls; else deleteConfigTopLevelKey(target, "tls"); const origins = (target.corsAllowOrigins ?? []).filter(origin => !origin.startsWith("chrome-extension://")); if (nextServer.aiStudioOrigin) origins.push(nextServer.aiStudioOrigin); if (origins.length) target.corsAllowOrigins = origins; else deleteConfigTopLevelKey(target, "corsAllowOrigins"); } return codexAccountPickerEnabled(target); }; if (nextServer) { const candidate = structuredClone(config); applySettings(candidate); const validation = validateConfigCandidate(candidate); if (!validation.ok) return jsonResponse({ error: validation.error }, 400); try { assertServerAuthConfig(candidate); } catch (error) { return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400); } } let pickerIsEnabled = pickerWasEnabled; try { pickerIsEnabled = applySettings(config); saveManagementConfig(deps, config); } catch (error) { if (previousSettings.hasCodexAutoStart) config.codexAutoStart = previousSettings.codexAutoStart; else deleteConfigTopLevelKey(config, "codexAutoStart"); if (previousSettings.hasStreamMode) config.streamMode = previousSettings.streamMode; else deleteConfigTopLevelKey(config, "streamMode"); if (previousSettings.hasAppOwnedMemoryBudgetMb) { config.appOwnedMemoryBudgetMb = previousSettings.appOwnedMemoryBudgetMb; } else deleteConfigTopLevelKey(config, "appOwnedMemoryBudgetMb"); if (previousSettings.hasCodexAccountNamespaces) { config.codexAccountNamespaces = previousSettings.codexAccountNamespaces; } else deleteConfigTopLevelKey(config, "codexAccountNamespaces"); if (previousSettings.hasCodexAccountPickerEnabled) { config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled; } else deleteConfigTopLevelKey(config, "codexAccountPickerEnabled"); if (previousSettings.hasCodexQuotaAutoRefresh) { config.codexQuotaAutoRefresh = previousSettings.codexQuotaAutoRefresh; } else deleteConfigTopLevelKey(config, "codexQuotaAutoRefresh"); if (previousSettings.hasOauthOpenBrowser) { config.oauthOpenBrowser = previousSettings.oauthOpenBrowser; } else deleteConfigTopLevelKey(config, "oauthOpenBrowser"); if (previousSettings.hasShowCodexSparkQuota) { config.showCodexSparkQuota = previousSettings.showCodexSparkQuota; } else deleteConfigTopLevelKey(config, "showCodexSparkQuota"); if (previousSettings.hasHostname) config.hostname = previousSettings.hostname; else deleteConfigTopLevelKey(config, "hostname"); config.port = previousSettings.port; if (previousSettings.hasTls) config.tls = previousSettings.tls; else deleteConfigTopLevelKey(config, "tls"); if (previousSettings.hasCorsAllowOrigins) config.corsAllowOrigins = previousSettings.corsAllowOrigins; else deleteConfigTopLevelKey(config, "corsAllowOrigins"); if (previousSettings.hasUltraFastTier) { config.ultraFastTier = previousSettings.ultraFastTier; } else deleteConfigTopLevelKey(config, "ultraFastTier"); if (previousSettings.hasCodexMainAccountHardLock) { config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock; } else deleteConfigTopLevelKey(config, "codexMainAccountHardLock"); if (previousSettings.hasCodexDesktopAuthless) { config.codexDesktopAuthless = previousSettings.codexDesktopAuthless; } else deleteConfigTopLevelKey(config, "codexDesktopAuthless"); throw error; } if (typeof body.appOwnedMemoryBudgetMb === "number") { configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb)); enforceAppOwnedMemoryBudget(); } const authlessIsEnabled = config.codexDesktopAuthless === true; const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || authlessWasEnabled !== authlessIsEnabled ? await convergeCodexCatalog() : undefined; const catalogRefreshPending = catalogRefresh ? catalogRefreshIsPending(catalogRefresh) : false; invalidateStartupHealthCache(); if (quotaAutoRefreshChange) void runCodexQuotaAutoRefresh(config); return jsonResponse({ ok: true, codexAutoStart: codexAutoStartEnabled(config), streamMode: config.streamMode ?? "auto", appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256, codexAccountPickerEnabled: pickerIsEnabled, codexQuotaAutoRefresh: quotaAutoRefreshSettings(config), oauthOpenBrowser: config.oauthOpenBrowser !== false, catalogRefreshPending, showCodexSparkQuota: config.showCodexSparkQuota === true, codexDesktopAuthless: authlessIsEnabled, codexMainAccountHardLock: config.codexMainAccountHardLock === true, mainAccountHardLock: getMainAccountHardLockStatus(config), startupHealth: await readStartupHealth(config), server: serverSettings(config, deps.activeServerOrigin, deps.activeServerConfig), }); } if (url.pathname === "/api/diagnostics/project-config" && req.method === "GET") { const { getCachedProjectConfigDiagnostics } = await import("../../codex/project-config-warnings"); const { warnings, grouped } = getCachedProjectConfigDiagnostics(); return jsonResponse({ warnings, grouped }); } if (url.pathname === "/api/sync" && req.method === "POST") { const { syncModelsToCodex } = await import("../../codex/sync"); const { attachStaleAppServerHint } = await import("../../codex/app-server-processes"); const [{ readRuntimePort }, { loadConfig }] = await Promise.all([ import("../../config/process-state"), import("../../config"), ]); // Never use the server-captured startup object for a durable integration // decision. A toggle may have persisted while this process was gathering. const runtime = readRuntimePort(process.pid); const config = loadConfig(); const result = await syncModelsToCodex(runtime?.port, config, null); // A sync used to stop here, so a Grok fence or a Desktop profile kept whatever // context windows it was written with while the Codex catalog moved on. The // startup path already fans out to every enabled client; this is the same fan-out // for the on-demand command. Codex goes first because the others read its catalog. const integrations = result.status === "refused" ? [] : await syncEnabledClientIntegrations(runtime?.port, config, deps); const status = result.status === "refused" ? 409 : (result.status === "skipped" || result.ok ? 200 : 500); return jsonResponse({ ...attachStaleAppServerHint(result), ...(integrations.length > 0 ? { integrations } : {}), ...(result.ok ? {} : { error: result.message }), }, status); } if (url.pathname === "/api/update/check" && req.method === "GET") { const { checkForUpdate, normalizeUpdateChannel } = await import("../../update/job"); const rawTag = url.searchParams.get("tag"); if (rawTag && rawTag !== "latest" && rawTag !== "preview") { return jsonResponse({ error: "tag must be latest or preview" }, 400); } return jsonResponse(checkForUpdate(normalizeUpdateChannel(rawTag))); } if (url.pathname === "/api/update/run" && req.method === "POST") { const { normalizeUpdateChannel, startUpdateJob, UpdateJobError } = await import("../../update/job"); let body: { tag?: unknown; restart?: unknown }; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } if (body.tag !== undefined && body.tag !== "latest" && body.tag !== "preview") { return jsonResponse({ error: "tag must be latest or preview" }, 400); } if (body.restart !== undefined && typeof body.restart !== "boolean") { return jsonResponse({ error: "restart boolean is required" }, 400); } try { return jsonResponse({ ok: true, job: startUpdateJob(normalizeUpdateChannel(body.tag as string | undefined), body.restart !== false) }); } catch (err) { if (err instanceof UpdateJobError) { return jsonResponse({ error: err.message, code: err.code }, err.status); } return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 500); } } if (url.pathname === "/api/update/status" && req.method === "GET") { const { readUpdateJob } = await import("../../update/job"); const job = readUpdateJob(url.searchParams.get("jobId")); if (!job) return jsonResponse({ error: "update job not found" }, 404); return jsonResponse({ ok: true, job }); } if (url.pathname === "/api/sidecar-settings" && req.method === "GET") { const ws = config.webSearchSidecar ?? {}; const vision = await sidecarVisionResponseSettings(config); const webSearchCandidates = await webSearchCandidateRows(config); return jsonResponse({ webSearch: { enabled: ws.enabled !== false, model: ws.model ?? "gpt-5.6-luna", backend: ws.backend, streamRoutedModelOutput: ws.streamRoutedModelOutput === true, ...(ws.xSearch ? { xSearch: ws.xSearch } : {}), }, vision: publicVisionSidecarSettings(config, vision), visionModels: vision.models, // ALWAYS present: the dashboard treats an omitted list as "no filter" and // falls back to the full model union, so empty must be [] (review B3). webSearchModels: webSearchModelOptionsFrom(config, webSearchCandidates), }); } if (url.pathname === "/api/sidecar-settings" && req.method === "PUT") { let raw: unknown; try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } // Strict shape (review F2): reject non-object bodies and non-object sections instead of throwing // on `null` or silently accepting arrays/strings as no-op updates. if (!isPlainRecord(raw)) return jsonResponse({ error: "body must be a JSON object" }, 400); if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400); if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400); const body = raw as { webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown }; vision?: { model?: unknown; backend?: unknown; reasoning?: unknown; maxDescriptionsPerTurn?: unknown; enabled?: unknown; timeoutMs?: unknown; }; }; const WEB_SEARCH_BACKENDS_UNION = ["openai", "anthropic", "xai", "gemini", "exa"] as const; if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null && !WEB_SEARCH_BACKENDS_UNION.includes(body.webSearch.backend as never)) { return jsonResponse({ error: "webSearch.backend must be openai, anthropic, xai, gemini, exa, or null" }, 400); } if (body.webSearch?.model !== undefined && typeof body.webSearch.model !== "string") { return jsonResponse({ error: "webSearch.model must be a string" }, 400); } if (body.webSearch && body.webSearch.streamRoutedModelOutput !== undefined && typeof body.webSearch.streamRoutedModelOutput !== "boolean") { return jsonResponse({ error: "webSearch.streamRoutedModelOutput must be a boolean" }, 400); } if (body.vision && body.vision.backend !== undefined && body.vision.backend !== null && body.vision.backend !== "openai" && body.vision.backend !== "anthropic" && body.vision.backend !== "routed") { return jsonResponse({ error: "vision.backend must be openai, anthropic, routed, or null" }, 400); } if (body.vision && body.vision.maxDescriptionsPerTurn !== undefined && (typeof body.vision.maxDescriptionsPerTurn !== "number" || !Number.isInteger(body.vision.maxDescriptionsPerTurn) || body.vision.maxDescriptionsPerTurn <= 0)) { return jsonResponse({ error: "vision.maxDescriptionsPerTurn must be a positive integer" }, 400); } if (body.vision && body.vision.enabled !== undefined && typeof body.vision.enabled !== "boolean") { return jsonResponse({ error: "vision.enabled must be a boolean" }, 400); } if (body.vision && body.vision.timeoutMs !== undefined && !isValidVisionTimeoutMs(body.vision.timeoutMs)) { return jsonResponse({ error: `vision.timeoutMs must be an integer from ${MIN_VISION_TIMEOUT_MS} to ${MAX_VISION_TIMEOUT_MS}`, }, 400); } if (body.vision?.reasoning !== undefined && !isVisionReasoningEffort(body.vision.reasoning)) { return jsonResponse({ error: `vision.reasoning must be ${VISION_REASONING_EFFORTS.join(", ")}` }, 400); } // Reject ONLY a model we can prove is blind. An id nothing knows about stays // allowed: the operator may be ahead of our catalog, and the runtime never // required catalog membership (`tests/vision/vision-reasoning-contract.test.ts` // pins `custom-vision` → 200). The catalog is read ONCE and reused for the // rejection body, so a 400 cannot cost two provider fetches. if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") { const requested = body.vision.model; const candidates = await visionCandidateRows(config); const hint = body.vision.backend === "anthropic" || body.vision.backend === "openai" || body.vision.backend === "routed" ? body.vision.backend : config.visionSidecar?.backend; // Coherence (roadmap 170 r2): the forward/OAuth executors POST the model // string VERBATIM, so a namespaced id on those backends persists a wire // id they cannot run; and "routed" without a namespace cannot route. const effectiveBackend = hint ?? "openai"; const namespaced = requested.includes("/"); if (namespaced && effectiveBackend !== "routed") { return jsonResponse({ error: `vision.model "${requested}" is provider-namespaced; it requires vision.backend "routed"` }, 400); } if (!namespaced && effectiveBackend === "routed") { return jsonResponse({ error: `vision.backend "routed" requires a provider-namespaced vision.model ("provider/model"); got "${requested}"` }, 400); } if (visionDescriberIsProvablyBlind(config, requested, candidates, hint)) { return jsonResponse(visionDescriberRejection("vision.model", requested, config, candidates), 400); } } let normalizedVisionReasoning: ReturnType; let visionReasoningTouched = false; if (body.vision && (body.vision.model !== undefined || body.vision.reasoning !== undefined)) { visionReasoningTouched = true; const model = typeof body.vision.model === "string" ? (body.vision.model === "" ? "gpt-5.4-mini" : body.vision.model) : (config.visionSidecar?.model || "gpt-5.4-mini"); const sourceReasoning = body.vision.reasoning ?? config.visionSidecar?.reasoning; normalizedVisionReasoning = sourceReasoning === undefined ? undefined : normalizeVisionReasoningForModel(model, sourceReasoning); } const nextConfig = structuredClone(config); if (body.webSearch) { const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined; // Validate against the backend the caller SUBMITTED, across the whole // union — not just openai/anthropic (#2457). The union check above has // already refused unknown literals, so a surviving string is a member; // Array.includes does not narrow, hence the cast. Falling back to the // stored backend for xai/gemini/exa both rejected legal pairs and // accepted illegal ones: a submitted gemini was checked against a stored // openai. null means "unset the backend", and unset resolves to openai. const submittedBackend = body.webSearch.backend; const effectiveBackend = typeof submittedBackend === "string" && WEB_SEARCH_BACKENDS_UNION.includes(submittedBackend as never) ? submittedBackend as typeof WEB_SEARCH_BACKENDS_UNION[number] : submittedBackend === null ? "openai" : config.webSearchSidecar?.backend ?? "openai"; const effectiveModel = typeof body.webSearch.model === "string" ? body.webSearch.model || undefined : config.webSearchSidecar?.model; if (pairTouched && effectiveModel) { const candidates = await webSearchCandidateRows(config); if (webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) { return jsonResponse(webSearchModelRejection("webSearch.model", effectiveBackend, effectiveModel, candidates), 400); } } const webSearchCandidate = { ...nextConfig.webSearchSidecar }; if (typeof body.webSearch.model === "string") { if (body.webSearch.model === "") delete webSearchCandidate.model; else webSearchCandidate.model = body.webSearch.model; } if (body.webSearch.backend === null) delete webSearchCandidate.backend; else if (WEB_SEARCH_BACKENDS_UNION.includes(body.webSearch.backend as never)) { webSearchCandidate.backend = body.webSearch.backend as typeof WEB_SEARCH_BACKENDS_UNION[number]; } if (typeof body.webSearch.reasoning === "string") webSearchCandidate.reasoning = body.webSearch.reasoning; // Operator secret for the exa backend: string sets, empty string clears. The GET // payload deliberately never carries it and redact.ts strips the key from logs. if (typeof body.webSearch.exaApiKey === "string") { if (body.webSearch.exaApiKey === "") delete webSearchCandidate.exaApiKey; else webSearchCandidate.exaApiKey = body.webSearch.exaApiKey; } // Opt-in x_search block (L7): null clears; an object is doc-validated before persisting. if (body.webSearch.xSearch === null) delete webSearchCandidate.xSearch; else if (body.webSearch.xSearch !== undefined) { if (!isPlainRecord(body.webSearch.xSearch)) { return jsonResponse({ error: "webSearch.xSearch must be an object or null" }, 400); } const x = body.webSearch.xSearch as Record; const allowedXSearchKeys = new Set([ "enabled", "allowedXHandles", "excludedXHandles", "fromDate", "toDate", ]); const unknownKey = Object.keys(x).find(key => !allowedXSearchKeys.has(key)); if (unknownKey !== undefined) { return jsonResponse({ error: `webSearch.xSearch.${unknownKey} is not a supported field` }, 400); } if (x.enabled !== undefined && typeof x.enabled !== "boolean") { return jsonResponse({ error: "webSearch.xSearch.enabled must be a boolean" }, 400); } for (const field of ["allowedXHandles", "excludedXHandles"] as const) { const value = x[field]; if (value !== undefined && (!Array.isArray(value) || !value.every(handle => typeof handle === "string"))) { return jsonResponse({ error: `webSearch.xSearch.${field} must be an array of strings` }, 400); } } for (const field of ["fromDate", "toDate"] as const) { if (x[field] !== undefined && typeof x[field] !== "string") { return jsonResponse({ error: `webSearch.xSearch.${field} must be an ISO-8601 date (YYYY-MM-DD)` }, 400); } } const candidate = { ...(x.enabled === true ? { enabled: true } : {}), ...(x.allowedXHandles !== undefined ? { allowedXHandles: x.allowedXHandles as string[] } : {}), ...(x.excludedXHandles !== undefined ? { excludedXHandles: x.excludedXHandles as string[] } : {}), ...(x.fromDate !== undefined ? { fromDate: x.fromDate as string } : {}), ...(x.toDate !== undefined ? { toDate: x.toDate as string } : {}), }; const invalid = validateXaiSearchOptions({ xSearch: candidate.enabled, allowedXHandles: candidate.allowedXHandles, excludedXHandles: candidate.excludedXHandles, fromDate: candidate.fromDate, toDate: candidate.toDate, }); if (invalid) return jsonResponse({ error: `webSearch.xSearch invalid: ${invalid}` }, 400); webSearchCandidate.xSearch = candidate; } if (typeof body.webSearch.streamRoutedModelOutput === "boolean") { // `false` is the default — drop the key so config files stay minimal. if (body.webSearch.streamRoutedModelOutput) webSearchCandidate.streamRoutedModelOutput = true; else delete webSearchCandidate.streamRoutedModelOutput; } nextConfig.webSearchSidecar = webSearchCandidate; } if (body.vision) { nextConfig.visionSidecar = { ...nextConfig.visionSidecar }; if (typeof body.vision.model === "string") { if (body.vision.model === "") delete nextConfig.visionSidecar.model; else nextConfig.visionSidecar.model = body.vision.model; } if (body.vision.backend === null) delete nextConfig.visionSidecar.backend; else if (body.vision.backend === "openai" || body.vision.backend === "anthropic" || body.vision.backend === "routed") { nextConfig.visionSidecar.backend = body.vision.backend; } if (typeof body.vision.maxDescriptionsPerTurn === "number") { nextConfig.visionSidecar.maxDescriptionsPerTurn = body.vision.maxDescriptionsPerTurn; } if (typeof body.vision.enabled === "boolean") { // `true` is the default — drop the key so disable/re-enable does not rewrite the file. if (body.vision.enabled) delete nextConfig.visionSidecar.enabled; else nextConfig.visionSidecar.enabled = false; } if (typeof body.vision.timeoutMs === "number") { nextConfig.visionSidecar.timeoutMs = body.vision.timeoutMs; } if (visionReasoningTouched) { if (normalizedVisionReasoning === undefined) delete nextConfig.visionSidecar.reasoning; else nextConfig.visionSidecar.reasoning = normalizedVisionReasoning; } } let committedWebSearch: OcxConfig["webSearchSidecar"]; let committedVision: OcxConfig["visionSidecar"]; const persisted = mutateManagementConfig(deps, disk => { if (body.webSearch) { const latest = { ...disk.webSearchSidecar }; for (const key of ["model", "backend", "reasoning", "streamRoutedModelOutput", "exaApiKey", "xSearch"] as const) { if (!Object.hasOwn(body.webSearch, key)) continue; if (Object.hasOwn(nextConfig.webSearchSidecar ?? {}, key)) latest[key] = nextConfig.webSearchSidecar![key] as never; else delete latest[key]; } disk.webSearchSidecar = latest; committedWebSearch = structuredClone(latest); } if (body.vision) { const latest = { ...disk.visionSidecar }; for (const key of ["model", "backend", "reasoning", "maxDescriptionsPerTurn", "enabled", "timeoutMs"] as const) { if (!Object.hasOwn(body.vision, key) && !(key === "reasoning" && visionReasoningTouched)) continue; if (Object.hasOwn(nextConfig.visionSidecar ?? {}, key)) latest[key] = nextConfig.visionSidecar![key] as never; else delete latest[key]; } disk.visionSidecar = latest; committedVision = structuredClone(latest); } return { changed: true, value: true }; }); if (persisted.status === "unavailable") return jsonResponse({ error: "management persistence unavailable" }, 500, req, config); if (body.webSearch) config.webSearchSidecar = committedWebSearch; if (body.vision) config.visionSidecar = committedVision; const ws = config.webSearchSidecar ?? {}; const vision = await sidecarVisionResponseSettings(config); const savedWebSearchCandidates = await webSearchCandidateRows(config); return jsonResponse({ ok: true, webSearch: { enabled: ws.enabled !== false, model: ws.model ?? "gpt-5.6-luna", backend: ws.backend, streamRoutedModelOutput: ws.streamRoutedModelOutput === true, ...(ws.xSearch ? { xSearch: ws.xSearch } : {}), }, vision: publicVisionSidecarSettings(config, vision), visionModels: vision.models, // Echoed for the same reason GET always carries it: the dashboard rebuilds // its sidecar state from this body, and an omitted key reads as "old // server" and falls back to the full union (review F1). webSearchModels: webSearchModelOptionsFrom(config, savedWebSearchCandidates), }); } if (url.pathname === "/api/shadow-call-settings" && req.method === "GET") { const sci = config.shadowCallIntercept ?? {}; return jsonResponse({ enabled: sci.enabled === true, model: sci.model ?? "", sourceModels: shadowSourceModels(sci.sourceModels), }); } if (url.pathname === "/api/shadow-call-settings" && req.method === "PUT") { let raw: unknown; try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } if (!isPlainRecord(raw)) return jsonResponse({ error: "body must be a JSON object" }, 400); const body = raw as { enabled?: unknown; model?: unknown }; if (body.enabled !== undefined && typeof body.enabled !== "boolean") { return jsonResponse({ error: "enabled must be a boolean" }, 400); } if (body.model !== undefined && typeof body.model !== "string") { return jsonResponse({ error: "model must be a string" }, 400); } const candidateModel = typeof body.model === "string" ? body.model : body.enabled === true ? config.shadowCallIntercept?.model : undefined; const targetError = shadowCallTargetError(config, candidateModel); if (targetError) return jsonResponse({ error: targetError }, 400); config.shadowCallIntercept = { ...config.shadowCallIntercept }; if (typeof body.enabled === "boolean") config.shadowCallIntercept.enabled = body.enabled; if (typeof body.model === "string") { if (body.model === "") delete config.shadowCallIntercept.model; else config.shadowCallIntercept.model = body.model; } saveManagementConfig(deps, config); const sci = config.shadowCallIntercept; return jsonResponse({ ok: true, enabled: sci.enabled === true, model: sci.model ?? "", sourceModels: shadowSourceModels(sci.sourceModels), }); } return null; }