fix(providers): stop an unrelated-provider tiktoken bundling failure from crashing /api/providers (#12355)

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.
This commit is contained in:
Markus Hartung
2026-09-03 18:00:27 +02:00
committed by GitHub
parent d353870342
commit c091534ffc
3 changed files with 41 additions and 9 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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