fix(ai): preload provider modules at process startup
Upgrading a global install rotates the hashed bundle chunk files under a still-running process. Provider adapters load lazily via dynamic import, so the first request for a not-yet-loaded adapter after an upgrade resolves against the mutated install and fails with "Cannot find module". preloadApiProviderModules() warms every provider module at boot in every process that enters main() (interactive client, daemon worker, daemon supervisor), so a running process never resolves provider chunks from disk again. Load failures still surface per-stream through the lazy wrappers.
88 unmodified lines89909192939495969798991001011021031046 unmodified lines88 unmodified lines
expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); });
it("loads every provider SDK when preloading provider modules", () => { const result = runProbe(` await mod.preloadApiProviderModules(); `);
expect([...result.loadedSpecifiers].sort()).toEqual([...SDK_SPECIFIERS].sort()); });
it("loads only the Anthropic SDK when dispatching through streamSimple", () => { const result = runProbe(` const model = mod.getModel("anthropic", "claude-sonnet-4-6"); const context = { messages: [{ role: "user", content: "hi" }] };6 unmodified lines5 unmodified lines67891010111213141516171819201033 unmodified linesExpand all105410551056105710581059106010611062656 unmodified linesExpand all5 unmodified lines */
import { join, resolve } from "node:path";import { createInterface } from "node:readline";import { type Api, type ImageContent, type Model, modelsAreEqual } from "@earendil-works/pi-ai";import { type Api, type ImageContent, type Model, modelsAreEqual, preloadApiProviderModules,} from "@earendil-works/pi-ai";import { registerBuiltinMcpOAuthProviders } from "@earendil-works/pi-ai/mcp";import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";import chalk from "chalk";import { type Args, type Mode, parseArgs } from "./cli/args.js";1033 unmodified linesExpand all return; } // Client and daemon are separate processes; both need these in their registry. registerBuiltinMcpOAuthProviders(); void preloadApiProviderModules(); const offlineMode = args.includes("--offline") || isTruthyEnvFlag(process.env.PI_OFFLINE); if (offlineMode) { process.env.PI_OFFLINE = "1"; process.env.PI_SKIP_VERSION_CHECK = "1";656 unmodified linesExpand all318 unmodified linesExpand all31932032132232332432532632732832933033133233333433533633733833934034134234334434577 unmodified lines318 unmodified linesExpand all }); return bedrockProviderModulePromise;}
const providerModuleLoaders = [ loadAnthropicProviderModule, loadAzureOpenAIResponsesProviderModule, loadGoogleProviderModule, loadGoogleVertexProviderModule, loadMistralProviderModule, loadOpenAICodexResponsesProviderModule, loadOpenAICompletionsProviderModule, loadOpenAIResponsesProviderModule, loadBedrockProviderModule,];
// Long-lived processes must warm every provider module at startup: a lazy// import resolves against the install directory, whose hashed chunk files an// upgrade rotates out from under a still-running process.export async function preloadApiProviderModules(): Promise<void> { await Promise.allSettled(providerModuleLoaders.map((load) => load()));}
export const streamAnthropic = createLazyStream(loadAnthropicProviderModule);export const streamSimpleAnthropic = createLazySimpleStream(loadAnthropicProviderModule);export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIResponsesProviderModule);export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule);77 unmodified lines