From da95cd79c6adc4f273dee84b26f2255b4f215658 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 5 Nov 2026 01:27:21 +0100 Subject: [PATCH] feat(ai): lazy OAuth flow and Bedrock loaders for code-split Node bundles registerLazyOAuthFlows (@earendil-works/pi-ai/lazy-oauth) and registerLazyBedrockProvider (@earendil-works/pi-ai/lazy-bedrock-provider, built on setBedrockProviderLoader) load the flows and Bedrock through literal dynamic imports, so a splitting bundler keeps them inside the bundle as lazy chunks. The variable-specifier default is unchanged. --- packages/ai/CHANGELOG.md | 1 + packages/ai/README.md | 12 ++++++++++ packages/ai/package.json | 8 +++++++ .../src/api/bedrock-converse-stream.lazy.ts | 21 +++++++++++++---- packages/ai/src/lazy-bedrock-provider.ts | 13 +++++++++++ packages/ai/src/lazy-oauth.ts | 23 +++++++++++++++++++ 6 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 packages/ai/src/lazy-bedrock-provider.ts create mode 100644 packages/ai/src/lazy-oauth.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0461e0645..369cb68f5 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,7 @@ - Anthropic requests with a `sessionId` (and `cacheRetention` not `"none"`) now use message threads (`message-threads-2026-08-12` beta): the server continues from the previous reply and pi sends only the new messages instead of the whole conversation. When anything earlier in the request changed, or the server can't continue the thread, the full request is sent. Token counts and prompt caching are unchanged. On by default only for the `anthropic` provider on `api.anthropic.com`; set `compat.supportsMessageThreads` to change that, for example for gateways that forward the `thread` field. Call `cleanupSessionResources(sessionId)` when a session ends to free the per-session state ([#11545](https://github.com/earendil-works/pi/pull/11545)) - Added `StreamOptions.disableContinuation`, which keeps a request from using or replacing server-side continuation state such as Anthropic message threads. Set it on requests that repeat an earlier one, such as cache warming ([#11545](https://github.com/earendil-works/pi/pull/11545)) +- Added `@earendil-works/pi-ai/lazy-oauth` (`registerLazyOAuthFlows()`) and `@earendil-works/pi-ai/lazy-bedrock-provider` (`registerLazyBedrockProvider()`, built on the new `setBedrockProviderLoader()`) for Node bundles that split at dynamic imports: the OAuth login flows and Bedrock load through literal dynamic imports, so the bundler keeps them inside the bundle as chunks that load on first use, instead of through the variable-specifier imports that load pi-ai's own files at runtime. Nothing changes without calling them. ### Fixed diff --git a/packages/ai/README.md b/packages/ai/README.md index bc085705e..01e3166bb 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -1771,6 +1771,18 @@ setBedrockProviderModule(bedrockProviderModule); That explicit override bundles the AWS SDK. Without it, Bedrock's opaque runtime import expects the package's Bedrock implementation file to be available at runtime. +Node bundles that split at dynamic imports (esbuild `splitting: true`) can keep Bedrock and the OAuth login flows inside the bundle but lazy: register loaders with literal dynamic imports, which the bundler turns into chunks that load on first use: + +```typescript +import { registerLazyBedrockProvider } from '@earendil-works/pi-ai/lazy-bedrock-provider'; +import { registerLazyOAuthFlows } from '@earendil-works/pi-ai/lazy-oauth'; + +registerLazyBedrockProvider(); +registerLazyOAuthFlows(); +``` + +Both modules are Node-only; never import them into a browser bundle. + ### Provider-Scoped Environment Overrides Pass `env` in stream options to scope provider configuration to a request. Values in `env` are used before process environment variables for provider auth and configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`. diff --git a/packages/ai/package.json b/packages/ai/package.json index 57e8f262c..8aab510eb 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -46,6 +46,14 @@ "./bun-oauth": { "types": "./dist/bun-oauth.d.ts", "import": "./dist/bun-oauth.js" + }, + "./lazy-bedrock-provider": { + "types": "./dist/lazy-bedrock-provider.d.ts", + "import": "./dist/lazy-bedrock-provider.js" + }, + "./lazy-oauth": { + "types": "./dist/lazy-oauth.d.ts", + "import": "./dist/lazy-oauth.js" } }, "bin": { diff --git a/packages/ai/src/api/bedrock-converse-stream.lazy.ts b/packages/ai/src/api/bedrock-converse-stream.lazy.ts index b0530a1fb..1f8a904b6 100644 --- a/packages/ai/src/api/bedrock-converse-stream.lazy.ts +++ b/packages/ai/src/api/bedrock-converse-stream.lazy.ts @@ -14,7 +14,7 @@ const importNodeOnlyApi = (specifier: string): Promise => { return import(new URL(runtimeSpecifier, import.meta.url).href); }; -let bedrockModuleOverride: ProviderStreams | undefined; +let bedrockModuleLoader: (() => Promise) | undefined; /** * Overrides the dynamically imported bedrock implementation. Used by the Bun @@ -22,11 +22,22 @@ let bedrockModuleOverride: ProviderStreams | undefined; * build registers a statically imported module instead. */ export function setBedrockProviderModule(module: ProviderStreams): void { - bedrockModuleOverride = module; + bedrockModuleLoader = async () => module; +} + +/** + * Overrides how the bedrock implementation is imported, loaded on the first + * bedrock request. Used by Node bundles that split at dynamic imports (see + * `@earendil-works/pi-ai/lazy-bedrock-provider`): the loader's literal + * `import()` lets the bundler keep the implementation in a lazy chunk. + */ +export function setBedrockProviderLoader(load: () => Promise): void { + bedrockModuleLoader = load; } export const bedrockConverseStreamApi = (): ProviderStreams => - lazyApi( - async () => - bedrockModuleOverride ?? ((await importNodeOnlyApi("./bedrock-converse-stream.ts")) as ProviderStreams), + lazyApi(async () => + bedrockModuleLoader + ? await bedrockModuleLoader() + : ((await importNodeOnlyApi("./bedrock-converse-stream.ts")) as ProviderStreams), ); diff --git a/packages/ai/src/lazy-bedrock-provider.ts b/packages/ai/src/lazy-bedrock-provider.ts new file mode 100644 index 000000000..9d1a1243b --- /dev/null +++ b/packages/ai/src/lazy-bedrock-provider.ts @@ -0,0 +1,13 @@ +import { setBedrockProviderLoader } from "./api/bedrock-converse-stream.lazy.ts"; + +/** + * Registers a bedrock loader with a literal dynamic import, for Node bundles + * that split at dynamic imports (esbuild `splitting`, Chord's worker base): + * the implementation and the AWS SDK land in a chunk loaded on the first + * bedrock request, inside the bundle. Without it, pi-ai imports bedrock + * through a variable specifier that bundlers cannot follow, so a bundle loads + * it from pi-ai's own files. Browser bundles must not import this module. + */ +export function registerLazyBedrockProvider(): void { + setBedrockProviderLoader(() => import("./api/bedrock-converse-stream.ts")); +} diff --git a/packages/ai/src/lazy-oauth.ts b/packages/ai/src/lazy-oauth.ts new file mode 100644 index 000000000..e05bd3bc2 --- /dev/null +++ b/packages/ai/src/lazy-oauth.ts @@ -0,0 +1,23 @@ +import { registerBundledOAuthFlowLoaders } from "./auth/oauth/load.ts"; + +/** + * Registers OAuth flow loaders with literal dynamic imports, for Node bundles + * that split at dynamic imports (esbuild `splitting`, Chord's worker base): + * each flow lands in a chunk loaded on its first use, inside the bundle. + * Without it, pi-ai imports the flows through a variable specifier that + * bundlers cannot follow, so a bundle loads them from pi-ai's own files. + * Browser bundles must not import this module (the flows are Node-only). + */ +export function registerLazyOAuthFlows(): void { + registerBundledOAuthFlowLoaders({ + anthropic: async () => (await import("./auth/oauth/anthropic.ts")).anthropicOAuth, + openaiCodex: async () => (await import("./auth/oauth/openai-codex.ts")).openaiCodexOAuth, + openaiChatGPT: async () => (await import("./auth/oauth/openai-chatgpt.ts")).openaiChatGPTOAuth, + githubCopilot: async () => (await import("./auth/oauth/github-copilot.ts")).githubCopilotOAuth, + openrouter: async () => (await import("./auth/oauth/openrouter.ts")).openRouterOAuth, + kimiCoding: async () => (await import("./auth/oauth/kimi-coding.ts")).kimiCodingOAuth, + meta: async () => (await import("./auth/oauth/meta.ts")).metaOAuth, + xai: async () => (await import("./auth/oauth/xai.ts")).xaiOAuth, + radius: async (options) => (await import("./auth/oauth/radius.ts")).createRadiusOAuth(options), + }); +}