From c091534ffcde1279767060dd5f9bb6735a99d65a Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 3 Sep 2026 18:00:27 +0200 Subject: [PATCH] fix(providers): stop an unrelated-provider tiktoken bundling failure from crashing /api/providers (#12355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark. --- src/app/(dashboard)/dashboard/combos/page.tsx | 16 +++++++++----- src/app/api/providers/route.ts | 21 ++++++++++++++++--- .../providers/validation/chatgptWebCodex.ts | 13 +++++++++++- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 978a5f1321..0105dd8723 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -761,15 +761,21 @@ function CombosPageContent() { const [proxyConfig, setProxyConfig] = useState(null); const { comboProxyAssignedIds, fetchComboProxyAssignments } = useComboProxyAssignments(); const [providerNodes, setProviderNodes] = useState([]); - const [showUsageGuide, setShowUsageGuide] = useState(() => { - // Lazy initializer instead of a mount effect (react-hooks/set-state-in-effect). + // SSR has no localStorage, so a lazy initializer reading it here returns a + // different value server-side (always "not dismissed") than the client's + // real stored value -- exactly the kind of source React's hydration + // mismatch check is built to catch, and in dev mode a mismatch forces a + // full client-only re-render of this tree, discarding whatever the fetch + // effects below had already populated. Start with the SSR-safe default on + // both passes and correct it client-only, after hydration, in an effect. + const [showUsageGuide, setShowUsageGuide] = useState(true); + useEffect(() => { try { - return globalThis.localStorage?.getItem(COMBO_USAGE_GUIDE_STORAGE_KEY) !== "1"; + setShowUsageGuide(globalThis.localStorage?.getItem(COMBO_USAGE_GUIDE_STORAGE_KEY) !== "1"); } catch { // Ignore storage access errors (privacy mode / restricted environments) - return true; } - }); + }, []); const [recentlyCreatedCombo, setRecentlyCreatedCombo] = useState(""); const [creatingKimiPreset, setCreatingKimiPreset] = useState(false); const [comboDragIndex, setComboDragIndex] = useState(null); diff --git a/src/app/api/providers/route.ts b/src/app/api/providers/route.ts index bb5d1ded72..409f7445ab 100644 --- a/src/app/api/providers/route.ts +++ b/src/app/api/providers/route.ts @@ -47,7 +47,15 @@ import { fetchModelSyncInternal, getModelSyncInternalBaseUrl, } from "@/shared/services/modelSyncScheduler"; -import { finalizeValidatedChatGptWebCodexSecrets } from "@omniroute/open-sse/services/chatgptWebCodexAdmin.ts"; +// Dynamically imported below, inside the one `provider === "chatgpt-web-codex"` +// branch that needs it: this module's transitive chain pulls in tiktoken's +// WASM tokenizer, which Turbopack dev mode fails to resolve for this graph +// even with `tiktoken` listed in serverExternalPackages (the standalone +// Node require works fine; only Turbopack's bundling of this import path +// doesn't). A static top-level import evaluates that whole chain on EVERY +// /api/providers request regardless of provider, turning an unrelated- +// provider bug into a route-wide 500. Loading it lazily, only when actually +// needed, avoids paying that cost (and that risk) on the common path. import { isAutoFetchModelsEnabled } from "@/lib/providerModels/modelDiscovery"; import { testSingleConnection } from "./[id]/test/route"; import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse"; @@ -204,6 +212,8 @@ export async function POST(request: Request) { ? providerSpecificData.validationId : ""; try { + const { finalizeValidatedChatGptWebCodexSecrets } = + await import("@omniroute/open-sse/services/chatgptWebCodexAdmin.ts"); const finalized = finalizeValidatedChatGptWebCodexSecrets(apiKey || "", validationId); persistedApiKey = finalized.encodedCredential; providerSpecificData = { ...(providerSpecificData || {}) }; @@ -324,11 +334,16 @@ export async function POST(request: Request) { }) .then((syncRes) => { if (!syncRes.ok) { - console.log(`[providers] Auto-sync failed for ${newConnection.id}: ${syncRes.status}`); + console.log( + `[providers] Auto-sync failed for ${newConnection.id}: ${syncRes.status}` + ); } }) .catch((err) => { - console.log(`[providers] Auto-sync error for ${newConnection.id}:`, err?.message || err); + console.log( + `[providers] Auto-sync error for ${newConnection.id}:`, + err?.message || err + ); }); } catch (syncSetupError) { // Defensive: if URL parsing or header construction itself throws, do diff --git a/src/lib/providers/validation/chatgptWebCodex.ts b/src/lib/providers/validation/chatgptWebCodex.ts index 3dd6eda748..d95c1475f3 100644 --- a/src/lib/providers/validation/chatgptWebCodex.ts +++ b/src/lib/providers/validation/chatgptWebCodex.ts @@ -4,7 +4,6 @@ import { rmSync } from "node:fs"; import { CHATGPT_WEB_CODEX_CONNECTOR_NAME } from "@/shared/constants/chatgptWebCodex"; import { inspectBrowserLoginCapabilities } from "@omniroute/open-sse/vendor/codex-chatgpt-web/browser-login.ts"; import { decodeChatGptWebCodexSecrets } from "@omniroute/open-sse/executors/chatgpt-web-codex/credentials.ts"; -import { detectChromeExecutable } from "@omniroute/open-sse/executors/chatgpt-web-codex.ts"; import { connectionRuntimePaths, ensureConnectionStorageState, @@ -12,6 +11,16 @@ import { } from "@omniroute/open-sse/executors/chatgpt-web-codex/storageState.ts"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; +// detectChromeExecutable (executors/chatgpt-web-codex.ts) is imported +// dynamically below, not statically here: this module is re-exported through +// the shared `@/lib/providers/validation` barrel that every provider +// validator's callers pull in, and executors/chatgpt-web-codex.ts's own +// import chain (its vendor browser adapter -> token-estimate.ts -> tiktoken's +// WASM tokenizer) fails to bundle under Turbopack dev mode even with +// `tiktoken` server-externalized -- turning validation of an unrelated +// provider into a route-wide crash for anyone who merely imports the barrel. +// A static import here evaluates that whole chain unconditionally. + export async function validateChatGptWebCodexProvider({ apiKey, providerSpecificData = {}, @@ -54,6 +63,8 @@ export async function validateChatGptWebCodexProvider({ }; } const cdpEndpoint = process.env.CHATGPT_WEB_CODEX_CDP_URL?.trim(); + const { detectChromeExecutable } = + await import("@omniroute/open-sse/executors/chatgpt-web-codex.ts"); const chromeExecutablePath = detectChromeExecutable( typeof providerSpecificData.chromeExecutablePath === "string" ? providerSpecificData.chromeExecutablePath