From 5ead198eb4dff2cc697a4fc3e2436d30e164b28f Mon Sep 17 00:00:00 2001 From: Jan Leon Date: Thu, 30 Jul 2026 06:56:09 +0200 Subject: [PATCH] Add ChatGPT Web setup and doctor UI --- .../[id]/components/modals/AddApiKeyModal.tsx | 98 +++++++++++++- .../components/modals/EditConnectionModal.tsx | 124 +++++++++++++++++- .../modals/connectionProviderSpecificData.ts | 11 ++ src/shared/components/lobeProviderIcons.ts | 1 + 4 files changed, 231 insertions(+), 3 deletions(-) diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index cc8e7df52c..476794b4ac 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -93,6 +93,7 @@ export default function AddApiKeyModal({ const localProviderMetadata = getLocalProviderMetadata(provider); const isLocalSelfHostedProvider = !!localProviderMetadata; const isGooglePse = provider === "google-pse-search"; + const isChatGptWebCodex = provider === "chatgpt-web-codex"; const webSessionCredential = getWebSessionCredentialRequirement(provider); const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none"; const isWebSessionCredential = !!webSessionCredential && webSessionCredential.kind !== "none"; @@ -130,9 +131,16 @@ export default function AddApiKeyModal({ ccCompatibleSummarizeThinking: false, passthroughModels: false, importFreeModelsOnly: false, + tunnelId: "", + runtimeKey: "", + connectorName: "OmniRoute Codex", }); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); + const [validationCapabilities, setValidationCapabilities] = useState | null>(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); @@ -233,12 +241,18 @@ export default function AddApiKeyModal({ baseUrl: formData.baseUrl.trim() || undefined, region: showsRegion ? formData.region.trim() || defaultRegion : undefined, cx: formData.cx.trim() || undefined, + runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined, + tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined, + connectorName: isChatGptWebCodex ? formData.connectorName.trim() || undefined : undefined, }), }); const data = await res.json(); const ok = !!data.valid; const unsupported = !!data.unsupported; setValidationResult(ok ? "success" : unsupported ? "unsupported" : "failed"); + setValidationCapabilities( + ok && data.capabilities && typeof data.capabilities === "object" ? data.capabilities : null + ); // #5088: surface backend reason (e.g. TLS/EACCES) instead of bare "invalid". if (!ok && !unsupported && typeof data.error === "string" && data.error) { setSaveError(data.error); @@ -285,6 +299,7 @@ export default function AddApiKeyModal({ let isValid = Boolean(isNoAuthWebSessionCredential && !credentialInput); let validationError: string | null = null; let isUnsupported = false; // #5565/#5567: no live validator → save anyway + let validatedProviderSpecificData: Record | undefined; if (!isValid) { try { setValidating(true); @@ -300,6 +315,11 @@ export default function AddApiKeyModal({ baseUrl: formData.baseUrl.trim() || undefined, region: showsRegion ? formData.region.trim() || defaultRegion : undefined, cx: formData.cx.trim() || undefined, + runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined, + tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined, + connectorName: isChatGptWebCodex + ? formData.connectorName.trim() || undefined + : undefined, }), }); const data = await res.json(); @@ -308,6 +328,13 @@ export default function AddApiKeyModal({ if (!isValid && data.error) { validationError = data.error; } + if ( + isValid && + data.providerSpecificData && + typeof data.providerSpecificData === "object" + ) { + validatedProviderSpecificData = data.providerSpecificData; + } setValidationResult(isValid ? "success" : isUnsupported ? "unsupported" : "failed"); } catch { setValidationResult("failed"); @@ -339,14 +366,28 @@ export default function AddApiKeyModal({ isCloudflare, isCcCompatible, }); + const mergedProviderSpecificData = { + ...(providerSpecificData || {}), + ...(validatedProviderSpecificData || {}), + }; + const encodedCredential = isChatGptWebCodex + ? JSON.stringify({ + version: 1, + cookie: credentialInput.trim().replace(/^cookie\s*:\s*/i, ""), + runtimeKey: formData.runtimeKey.trim(), + }) + : credentialInput.trim(); const payload = { name: formData.name, - apiKey: credentialInput.trim() || undefined, + apiKey: encodedCredential || undefined, priority: formData.priority, testStatus: "active", defaultModel: isCompatible ? formData.defaultModel.trim() || undefined : undefined, - providerSpecificData, + providerSpecificData: + Object.keys(mergedProviderSpecificData).length > 0 + ? mergedProviderSpecificData + : undefined, }; const error = await onSave(payload); @@ -736,6 +777,59 @@ export default function AddApiKeyModal({ )} + {isChatGptWebCodex && ( +
+
+

Codex-Toolverbindung

+

+ Der Tunnel bleibt ausschließlich ausgehend. Lokale Tools werden weiterhin nur + von Codex gemäß dessen Sandbox- und Freigaberichtlinie ausgeführt. +

+
+ setFormData({ ...formData, tunnelId: e.target.value })} + placeholder="tunnel_0123456789abcdef0123456789abcdef" + autoComplete="off" + spellCheck={false} + /> + setFormData({ ...formData, runtimeKey: e.target.value })} + placeholder="Runtime-Key" + hint="Wird zusammen mit dem Cookie verschlüsselt gespeichert und nie in Logs ausgegeben." + autoComplete="off" + spellCheck={false} + /> + setFormData({ ...formData, connectorName: e.target.value })} + placeholder="OmniRoute Codex" + /> + {validationCapabilities && ( +
+
Browser: bereit
+
Storage-State: geprüft
+
ChatGPT-Anmeldung: bestätigt
+
Temporary Chat: bereit
+
+ Pro:{" "} + {validationCapabilities.proAvailable === true ? "verfügbar" : "nicht erkannt"} +
+
+ Toolmodus:{" "} + {formData.tunnelId.trim() && formData.runtimeKey.trim() + ? "konfiguriert" + : "global oder read-only"} +
+
+ )} +
+ )} {isModal && ( | undefined + >(); + const [doctorStatus, setDoctorStatus] = useState | null>(null); + const [doctorLoading, setDoctorLoading] = useState(false); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [extraApiKeys, setExtraApiKeys] = useState([]); @@ -196,6 +204,7 @@ export default function EditConnectionModal({ const localProviderMetadata = getLocalProviderMetadata(provider); const isLocalSelfHostedProvider = !!localProviderMetadata; const isGooglePse = provider === "google-pse-search"; + const isChatGptWebCodex = provider === "chatgpt-web-codex"; const isM365TierCapable = isM365TierCapableProvider(provider); const webSessionCredential = getWebSessionCredentialRequirement(provider); const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none"; @@ -334,6 +343,10 @@ export default function EditConnectionModal({ passthroughModels: connection?.providerSpecificData?.passthroughModels === true, disableCooling: connection?.providerSpecificData?.disableCooling === true, importFreeModelsOnly: connection?.providerSpecificData?.importFreeModelsOnly === true, + tunnelId: stringField(connection.providerSpecificData?.tunnelId), + runtimeKey: "", + connectorName: + stringField(connection.providerSpecificData?.connectorName) || "OmniRoute Codex", m365Tier: normalizeM365TierValue(connection.providerSpecificData?.tier) as M365TierValue, }); const existing = connection.providerSpecificData?.extraApiKeys; @@ -359,6 +372,7 @@ export default function EditConnectionModal({ ); setTestResult(null); setValidationResult(null); + setValidatedProviderSpecificData(undefined); setSaveError(null); } }, [ @@ -422,10 +436,20 @@ export default function EditConnectionModal({ baseUrl: formData.baseUrl.trim() || undefined, region: showsRegion ? formData.region.trim() || defaultRegion : undefined, cx: formData.cx.trim() || undefined, + runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined, + tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined, + connectorName: isChatGptWebCodex ? formData.connectorName.trim() || undefined : undefined, }), }); const data = await res.json(); setValidationResult(data.valid ? "success" : "failed"); + if ( + data.valid && + data.providerSpecificData && + typeof data.providerSpecificData === "object" + ) { + setValidatedProviderSpecificData(data.providerSpecificData); + } } catch { setValidationResult("failed"); } finally { @@ -502,7 +526,7 @@ export default function EditConnectionModal({ } if (!isOAuth && formData.apiKey) { - updates.apiKey = formData.apiKey; + let validationPsd = validatedProviderSpecificData; let isValid = validationResult === "success"; if (!isValid) { try { @@ -519,11 +543,24 @@ export default function EditConnectionModal({ baseUrl: formData.baseUrl.trim() || undefined, region: showsRegion ? formData.region.trim() || defaultRegion : undefined, cx: formData.cx.trim() || undefined, + runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined, + tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined, + connectorName: isChatGptWebCodex + ? formData.connectorName.trim() || undefined + : undefined, }), }); const data = await res.json(); isValid = !!data.valid; setValidationResult(isValid ? "success" : "failed"); + if ( + isValid && + data.providerSpecificData && + typeof data.providerSpecificData === "object" + ) { + setValidatedProviderSpecificData(data.providerSpecificData); + validationPsd = data.providerSpecificData; + } } catch { setValidationResult("failed"); } finally { @@ -531,6 +568,13 @@ export default function EditConnectionModal({ } } if (isValid) { + updates.apiKey = isChatGptWebCodex + ? JSON.stringify({ + version: 1, + cookie: formData.apiKey.trim().replace(/^cookie\s*:\s*/i, ""), + ...(formData.runtimeKey.trim() ? { runtimeKey: formData.runtimeKey.trim() } : {}), + }) + : formData.apiKey; updates.testStatus = "active"; updates.lastError = null; updates.lastErrorAt = null; @@ -543,6 +587,7 @@ export default function EditConnectionModal({ if (!isOAuth) { updates.providerSpecificData = { ...(connection.providerSpecificData || {}), + ...(validationPsd || {}), }; assignEditApiKeyProviderSpecificData({ provider, @@ -872,6 +917,83 @@ export default function EditConnectionModal({ )} + {isChatGptWebCodex && ( +
+

Codex-Toolverbindung

+ setFormData({ ...formData, tunnelId: event.target.value })} + placeholder="tunnel_0123456789abcdef0123456789abcdef" + /> + setFormData({ ...formData, runtimeKey: event.target.value })} + hint="Nur zusammen mit einem frischen Cookie eingeben. Der Wert wird verschlüsselt gespeichert." + autoComplete="off" + /> + + setFormData({ ...formData, connectorName: event.target.value }) + } + /> + + {doctorStatus && ( +
+ {[ + ["Browser", doctorStatus.browser?.ready], + ["Storage-State", doctorStatus.storageState?.ready], + ["ChatGPT-Anmeldung", doctorStatus.login?.ready], + ["Temporary Chat", doctorStatus.temporaryChats?.ready], + ["Tunnel-Binary", doctorStatus.tunnelBinary?.ready], + ["Tunnel", doctorStatus.tunnel?.ready], + ["Connector", doctorStatus.connector?.ready], + ["Tool-Roundtrip", doctorStatus.toolRoundtrip?.ready], + ["Aktive Turns", doctorStatus.runtime?.activeTurns], + ["Wartende Turns", doctorStatus.runtime?.waitingTurns], + ].map(([label, ready]) => ( +
+ {label}:{" "} + {typeof ready === "number" ? ready : ready ? "bereit" : "nicht bereit"} +
+ ))} + {doctorStatus.recovery?.interactiveLoginRequired && ( +
+ Interaktive Anmeldung erforderlich. Nutze den geschützten + Browser-/VNC-Recovery-Pfad. +
+ )} + {doctorStatus.lastError && ( +
+ Letzter Fehler: {String(doctorStatus.lastError)} +
+ )} +
+ )} +
+ )} {isGooglePse && ( ; @@ -92,6 +95,10 @@ export function buildAddProviderSpecificData(options: { assignGlmTeamQuotaProviderData(isGlm, formData, data); } else if (isCloudflare && formData.accountId.trim()) data.accountId = formData.accountId.trim(); if (isCcCompatible) assignCcCompatibleRequestDefaults(data, formData); + if (provider === "chatgpt-web-codex") { + if (formData.tunnelId.trim()) data.tunnelId = formData.tunnelId.trim(); + if (formData.connectorName.trim()) data.connectorName = formData.connectorName.trim(); + } return Object.keys(data).length > 0 ? data : undefined; } @@ -147,4 +154,8 @@ export function assignEditApiKeyProviderSpecificData(options: { o.formData ); } + if (o.provider === "chatgpt-web-codex") { + o.target.tunnelId = o.formData.tunnelId.trim() || undefined; + o.target.connectorName = o.formData.connectorName.trim() || undefined; + } } diff --git a/src/shared/components/lobeProviderIcons.ts b/src/shared/components/lobeProviderIcons.ts index 71b751ff07..0cb41519ca 100644 --- a/src/shared/components/lobeProviderIcons.ts +++ b/src/shared/components/lobeProviderIcons.ts @@ -329,6 +329,7 @@ const LOBE_PROVIDER_ALIASES = { "black-forest-labs": "Bfl", cerebras: "Cerebras", "chatgpt-web": "OpenAI", + "chatgpt-web-codex": "OpenAI", claude: "ClaudeCode", "claude-web": "Claude", cline: "Cline",