From c479a0d54d45301843ef5a7ed6e8da979fef9c20 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Wed, 24 Jun 2026 09:15:32 +0200 Subject: [PATCH] fix: support pi-ai provider compat entrypoint --- .changeset/sessiond-pi-ai-compat.md | 5 ++ src/server/sessions/authProviderOptions.ts | 5 +- src/server/sessions/sessionNameGenerator.ts | 54 ++++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 .changeset/sessiond-pi-ai-compat.md diff --git a/.changeset/sessiond-pi-ai-compat.md b/.changeset/sessiond-pi-ai-compat.md new file mode 100644 index 0000000..43470e1 --- /dev/null +++ b/.changeset/sessiond-pi-ai-compat.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Fix the session daemon startup when PI WEB runs with compatible Pi packages that moved legacy provider registry exports to the Pi AI compatibility entrypoint. diff --git a/src/server/sessions/authProviderOptions.ts b/src/server/sessions/authProviderOptions.ts index 61940db..58211d8 100644 --- a/src/server/sessions/authProviderOptions.ts +++ b/src/server/sessions/authProviderOptions.ts @@ -1,8 +1,6 @@ -import { getProviders } from "@earendil-works/pi-ai"; import type { AuthProviderOption, AuthProviderStatus, AuthType } from "../../shared/apiTypes.js"; const OAUTH_ONLY_PROVIDERS = new Set(["github-copilot", "openai-codex"]); -const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders()); export interface AuthProviderModelRegistry { authStorage: { @@ -54,11 +52,10 @@ export function getLogoutProviderOptions(modelRegistry: AuthProviderModelRegistr return filterAndSort(options); } -export function isApiKeyLoginProvider(providerId: string, oauthProviderIds: ReadonlySet, builtInProviderIds: ReadonlySet = BUILT_IN_MODEL_PROVIDERS): boolean { +export function isApiKeyLoginProvider(providerId: string, oauthProviderIds: ReadonlySet): boolean { if (OAUTH_ONLY_PROVIDERS.has(providerId)) return false; if (providerId === "anthropic") return true; if (oauthProviderIds.has(providerId)) return false; - if (builtInProviderIds.has(providerId)) return true; return true; } diff --git a/src/server/sessions/sessionNameGenerator.ts b/src/server/sessions/sessionNameGenerator.ts index d214ea3..77c7c15 100644 --- a/src/server/sessions/sessionNameGenerator.ts +++ b/src/server/sessions/sessionNameGenerator.ts @@ -1,13 +1,27 @@ -import { getApiProvider, type Api, type AssistantMessage, type Model } from "@earendil-works/pi-ai"; +import type { Api, AssistantMessage, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; import type { ModelRegistry } from "@earendil-works/pi-coding-agent"; const SESSION_NAME_TIMEOUT_MS = 10_000; const SESSION_NAME_MAX_INPUT_CHARS = 4_000; const SESSION_NAME_MAX_LENGTH = 60; const FALLBACK_SESSION_NAME_MAX_WORDS = 6; +const PI_AI_COMPAT_MODULE = ["@earendil-works/pi-ai", "compat"].join("/"); + +interface SessionNameApiProvider { + streamSimple(model: Model, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream; +} + +interface PiAiProviderRegistryModule { + getApiProvider?: (api: Api) => SessionNameApiProvider | undefined; +} + +type ModuleImporter = (specifier: string) => Promise; + +let piAiProviderRegistryModulePromise: Promise | undefined; export async function generateShortSessionName(modelRegistry: ModelRegistry, model: Model, firstMessage: string): Promise { - const provider = getApiProvider(model.api); + const providerRegistry = await getPiAiProviderRegistryModule(); + const provider = providerRegistry.getApiProvider?.(model.api); if (provider === undefined) return undefined; const auth = await modelRegistry.getApiKeyAndHeaders(model); @@ -67,6 +81,42 @@ export function cleanSessionName(value: string): string | undefined { return title === "" ? undefined : title; } +async function getPiAiProviderRegistryModule(importer: ModuleImporter = (specifier) => import(specifier)): Promise { + piAiProviderRegistryModulePromise ??= loadPiAiProviderRegistryModule(importer); + return piAiProviderRegistryModulePromise; +} + +async function loadPiAiProviderRegistryModule(importer: ModuleImporter): Promise { + const compatModule = await importOptionalPiAiModule(PI_AI_COMPAT_MODULE, importer); + if (hasGetApiProvider(compatModule)) return compatModule; + + const rootModule = await importer("@earendil-works/pi-ai"); + if (hasGetApiProvider(rootModule)) return rootModule; + return {}; +} + +async function importOptionalPiAiModule(specifier: string, importer: ModuleImporter): Promise { + try { + return await importer(specifier); + } catch (error) { + if (isModuleUnavailableError(error)) return undefined; + throw error; + } +} + +function hasGetApiProvider(moduleValue: unknown): moduleValue is PiAiProviderRegistryModule { + return typeof moduleValue === "object" + && moduleValue !== null + && "getApiProvider" in moduleValue + && typeof moduleValue.getApiProvider === "function"; +} + +function isModuleUnavailableError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + const code = "code" in error ? error.code : undefined; + return code === "ERR_MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED"; +} + function textFromAssistant(message: AssistantMessage): string { return message.content .filter((part) => part.type === "text")