diff --git a/gui/src/components/provider-workspace/ProviderSettings.tsx b/gui/src/components/provider-workspace/ProviderSettings.tsx index 5436651f54..4763e0eda3 100644 --- a/gui/src/components/provider-workspace/ProviderSettings.tsx +++ b/gui/src/components/provider-workspace/ProviderSettings.tsx @@ -13,6 +13,7 @@ import { readJsonIfOk } from "../../fetch-json"; import { useT } from "../../i18n/shared"; import { IconLock } from "../../icons"; import { isCatalogProviderId } from "../../provider-icons"; +import { openAiAccountProviderState } from "../../provider-payload"; import type { CatalogPreset } from "../provider-catalog/provider-presets"; import { authModeLabel } from "./ProviderRail"; import type { WorkspaceItem, ProviderUpdatePatch } from "./types"; @@ -46,6 +47,9 @@ export default function ProviderSettings({ const [liveModels, setLiveModels] = useState(item.liveModels !== false); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); + const [accountMode, setAccountMode] = useState<"pool" | "direct">(item.codexAccountMode ?? "pool"); + const [modeSaving, setModeSaving] = useState(false); + const [modeMsg, setModeMsg] = useState<{ ok: boolean; text: string } | null>(null); const [baseUrlChoices, setBaseUrlChoices] = useState(); const [choicesStatus, setChoicesStatus] = useState(apiBase ? "loading" : "idle"); const [endpointChoice, setEndpointChoice] = useState(() => "custom"); @@ -61,10 +65,19 @@ export default function ProviderSettings({ setAllowPrivateNetwork(item.allowPrivateNetwork ?? false); setLiveModels(item.liveModels !== false); setMsg(null); + setModeMsg(null); queueMicrotask(() => setEndpointChoice(matchChoiceId(baseUrlChoices, item.baseUrl))); }, [item.adapter, item.baseUrl, item.defaultModel, item.authMode, item.apiKeyTransport, item.keyOptional, item.note, item.allowPrivateNetwork, item.liveModels, baseUrlChoices]); /* eslint-enable react-hooks/set-state-in-effect */ + // Account mode syncs on its own: a mode PATCH refresh must not reset an in-progress + // draft, so it is deliberately kept out of the form-reset effect above. + /* eslint-disable react-hooks/set-state-in-effect -- intentional split from the form reset */ + useEffect(() => { + setAccountMode(item.codexAccountMode ?? "pool"); + }, [item.codexAccountMode]); + /* eslint-enable react-hooks/set-state-in-effect */ + useEffect(() => { if (!apiBase) return; let cancelled = false; @@ -122,12 +135,15 @@ export default function ProviderSettings({ const isPreset = isCatalogProviderId(item.name); const hasEndpointPicker = choicesStatus === "ready" && !!(baseUrlChoices && baseUrlChoices.length > 0); const supportsApiKeyTransport = adapter.trim() === "anthropic" && authMode === "key"; + const openAiState = item.name === "openai" ? openAiAccountProviderState(item) : "invalid"; + const isCanonicalOpenAi = openAiState === "ready" || openAiState === "disabled"; // Lock plain baseUrl for presets while loading or when there is no picker. // On fetch error, keep it editable so allowBaseUrlOverride providers are not trapped. const plainBaseUrlLocked = isPreset && choicesStatus !== "error"; const save = async (): Promise => { if (!onUpdateProvider) { setMsg({ ok: false, text: t("pws.updatesUnavailable") }); return false; } + if (modeSaving) return false; const nextBaseUrl = hasEndpointPicker ? resolvedBaseUrlForChoice(baseUrlChoices, endpointChoice, baseUrl) : baseUrl.trim(); @@ -159,6 +175,26 @@ export default function ProviderSettings({ return () => onRegisterSave(null); }, [onRegisterSave]); + const applyAccountMode = async (next: "pool" | "direct") => { + if (modeSaving || saving || next === accountMode) return; + if (!onUpdateProvider) { setModeMsg({ ok: false, text: t("pws.updatesUnavailable") }); return; } + setModeSaving(true); + setModeMsg(null); + try { + const res = await onUpdateProvider("openai", { codexAccountMode: next }); + if (res.ok) { + setAccountMode(next); + setModeMsg({ ok: true, text: t("pws.accountModeSaved") }); + } else { + setModeMsg({ ok: false, text: res.error || t("pws.accountModeFailed") }); + } + } catch { + setModeMsg({ ok: false, text: t("pws.accountModeFailed") }); + } finally { + setModeSaving(false); + } + }; + const discard = () => { setAdapter(item.adapter); setBaseUrl(item.baseUrl); setDefaultModel(item.defaultModel ?? ""); setAuthMode(initialAuth); @@ -243,6 +279,43 @@ export default function ProviderSettings({ )} + {isCanonicalOpenAi && ( + + )} {supportsApiKeyTransport && (