diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index fc70d51b..2082be2f 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -34,21 +34,6 @@ interface LazyProviderModule< ) => AsyncIterable; } -type DynamicImport = (specifier: string) => Promise; - -const dynamicImport: DynamicImport = (specifier) => import(specifier); - -const ANTHROPIC_PROVIDER_SPECIFIER = "./anthropic.js"; -const AZURE_OPENAI_RESPONSES_PROVIDER_SPECIFIER = "./azure-openai-responses.js"; -const GOOGLE_PROVIDER_SPECIFIER = "./google.js"; -const GOOGLE_GEMINI_CLI_PROVIDER_SPECIFIER = "./google-gemini-cli.js"; -const GOOGLE_VERTEX_PROVIDER_SPECIFIER = "./google-vertex.js"; -const MISTRAL_PROVIDER_SPECIFIER = "./mistral.js"; -const OPENAI_CODEX_RESPONSES_PROVIDER_SPECIFIER = "./openai-codex-responses.js"; -const OPENAI_COMPLETIONS_PROVIDER_SPECIFIER = "./openai-completions.js"; -const OPENAI_RESPONSES_PROVIDER_SPECIFIER = "./openai-responses.js"; -const BEDROCK_PROVIDER_SPECIFIER = "./amazon-" + "bedrock.js"; - interface AnthropicProviderModule { streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>; streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>; @@ -107,6 +92,8 @@ interface BedrockProviderModule { ) => AsyncIterable; } +const importNodeOnlyProvider = (specifier: string): Promise => import(specifier); + let anthropicProviderModulePromise: | Promise> | undefined; @@ -225,7 +212,7 @@ function createLazySimpleStream< function loadAnthropicProviderModule(): Promise< LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> > { - anthropicProviderModulePromise ||= dynamicImport(ANTHROPIC_PROVIDER_SPECIFIER).then((module) => { + anthropicProviderModulePromise ||= import("./anthropic.js").then((module) => { const provider = module as AnthropicProviderModule; return { stream: provider.streamAnthropic, @@ -238,22 +225,20 @@ function loadAnthropicProviderModule(): Promise< function loadAzureOpenAIResponsesProviderModule(): Promise< LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> > { - azureOpenAIResponsesProviderModulePromise ||= dynamicImport(AZURE_OPENAI_RESPONSES_PROVIDER_SPECIFIER).then( - (module) => { - const provider = module as AzureOpenAIResponsesProviderModule; - return { - stream: provider.streamAzureOpenAIResponses, - streamSimple: provider.streamSimpleAzureOpenAIResponses, - }; - }, - ); + azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.js").then((module) => { + const provider = module as AzureOpenAIResponsesProviderModule; + return { + stream: provider.streamAzureOpenAIResponses, + streamSimple: provider.streamSimpleAzureOpenAIResponses, + }; + }); return azureOpenAIResponsesProviderModulePromise; } function loadGoogleProviderModule(): Promise< LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> > { - googleProviderModulePromise ||= dynamicImport(GOOGLE_PROVIDER_SPECIFIER).then((module) => { + googleProviderModulePromise ||= import("./google.js").then((module) => { const provider = module as GoogleProviderModule; return { stream: provider.streamGoogle, @@ -266,7 +251,7 @@ function loadGoogleProviderModule(): Promise< function loadGoogleGeminiCliProviderModule(): Promise< LazyProviderModule<"google-gemini-cli", GoogleGeminiCliOptions, SimpleStreamOptions> > { - googleGeminiCliProviderModulePromise ||= dynamicImport(GOOGLE_GEMINI_CLI_PROVIDER_SPECIFIER).then((module) => { + googleGeminiCliProviderModulePromise ||= import("./google-gemini-cli.js").then((module) => { const provider = module as GoogleGeminiCliProviderModule; return { stream: provider.streamGoogleGeminiCli, @@ -279,7 +264,7 @@ function loadGoogleGeminiCliProviderModule(): Promise< function loadGoogleVertexProviderModule(): Promise< LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> > { - googleVertexProviderModulePromise ||= dynamicImport(GOOGLE_VERTEX_PROVIDER_SPECIFIER).then((module) => { + googleVertexProviderModulePromise ||= import("./google-vertex.js").then((module) => { const provider = module as GoogleVertexProviderModule; return { stream: provider.streamGoogleVertex, @@ -292,7 +277,7 @@ function loadGoogleVertexProviderModule(): Promise< function loadMistralProviderModule(): Promise< LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> > { - mistralProviderModulePromise ||= dynamicImport(MISTRAL_PROVIDER_SPECIFIER).then((module) => { + mistralProviderModulePromise ||= import("./mistral.js").then((module) => { const provider = module as MistralProviderModule; return { stream: provider.streamMistral, @@ -305,22 +290,20 @@ function loadMistralProviderModule(): Promise< function loadOpenAICodexResponsesProviderModule(): Promise< LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> > { - openAICodexResponsesProviderModulePromise ||= dynamicImport(OPENAI_CODEX_RESPONSES_PROVIDER_SPECIFIER).then( - (module) => { - const provider = module as OpenAICodexResponsesProviderModule; - return { - stream: provider.streamOpenAICodexResponses, - streamSimple: provider.streamSimpleOpenAICodexResponses, - }; - }, - ); + openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.js").then((module) => { + const provider = module as OpenAICodexResponsesProviderModule; + return { + stream: provider.streamOpenAICodexResponses, + streamSimple: provider.streamSimpleOpenAICodexResponses, + }; + }); return openAICodexResponsesProviderModulePromise; } function loadOpenAICompletionsProviderModule(): Promise< LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> > { - openAICompletionsProviderModulePromise ||= dynamicImport(OPENAI_COMPLETIONS_PROVIDER_SPECIFIER).then((module) => { + openAICompletionsProviderModulePromise ||= import("./openai-completions.js").then((module) => { const provider = module as OpenAICompletionsProviderModule; return { stream: provider.streamOpenAICompletions, @@ -333,7 +316,7 @@ function loadOpenAICompletionsProviderModule(): Promise< function loadOpenAIResponsesProviderModule(): Promise< LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> > { - openAIResponsesProviderModulePromise ||= dynamicImport(OPENAI_RESPONSES_PROVIDER_SPECIFIER).then((module) => { + openAIResponsesProviderModulePromise ||= import("./openai-responses.js").then((module) => { const provider = module as OpenAIResponsesProviderModule; return { stream: provider.streamOpenAIResponses, @@ -349,7 +332,7 @@ function loadBedrockProviderModule(): Promise< if (bedrockProviderModuleOverride) { return Promise.resolve(bedrockProviderModuleOverride); } - bedrockProviderModulePromise ||= dynamicImport(BEDROCK_PROVIDER_SPECIFIER).then((module) => { + bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.js").then((module) => { const provider = module as BedrockProviderModule; return { stream: provider.streamBedrock, diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 3a1fba3f..3a7db8be 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -32,7 +32,7 @@ "clean": "shx rm -rf dist", "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets", - "build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets", + "build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets", "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/", "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/", "test": "vitest --run", diff --git a/packages/coding-agent/src/bun/cli.ts b/packages/coding-agent/src/bun/cli.ts new file mode 100644 index 00000000..e25c297b --- /dev/null +++ b/packages/coding-agent/src/bun/cli.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +process.title = "pi"; + +await import("./register-bedrock.js"); +await import("../cli.js"); diff --git a/packages/coding-agent/src/bun/register-bedrock.ts b/packages/coding-agent/src/bun/register-bedrock.ts new file mode 100644 index 00000000..a89061ad --- /dev/null +++ b/packages/coding-agent/src/bun/register-bedrock.ts @@ -0,0 +1,118 @@ +import { + type AssistantMessage, + type AssistantMessageEvent, + createAssistantMessageEventStream, + type Model, + registerApiProvider, + type SimpleStreamOptions, + type StreamFunction, + type StreamOptions, +} from "@mariozechner/pi-ai"; + +interface LazyBedrockProviderModule { + streamBedrock: StreamFunction<"bedrock-converse-stream", StreamOptions>; + streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions>; +} + +let bedrockProviderModulePromise: Promise | undefined; + +function forwardStream( + target: ReturnType, + source: AsyncIterable, +): void { + (async () => { + for await (const event of source) { + target.push(event); + } + target.end(); + })(); +} + +function createLazyLoadErrorMessage(model: Model<"bedrock-converse-stream">, error: unknown): AssistantMessage { + return { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; +} + +function loadBedrockProviderModule(): Promise { + if (!bedrockProviderModulePromise) { + bedrockProviderModulePromise = import("@mariozechner/pi-ai/bedrock-provider").then( + ({ bedrockProviderModule }) => { + return { + streamBedrock: (model, context, options) => { + const outer = createAssistantMessageEventStream(); + const inner = bedrockProviderModule.streamBedrock(model, context, options); + forwardStream(outer, inner); + return outer; + }, + streamSimpleBedrock: (model, context, options) => { + const outer = createAssistantMessageEventStream(); + const inner = bedrockProviderModule.streamSimpleBedrock(model, context, options); + forwardStream(outer, inner); + return outer; + }, + }; + }, + ); + } + return bedrockProviderModulePromise; +} + +const streamBedrockLazy: StreamFunction<"bedrock-converse-stream", StreamOptions> = (model, context, options) => { + const outer = createAssistantMessageEventStream(); + + loadBedrockProviderModule() + .then((module) => { + const inner = module.streamBedrock(model, context, options); + forwardStream(outer, inner); + }) + .catch((error) => { + const message = createLazyLoadErrorMessage(model, error); + outer.push({ type: "error", reason: "error", error: message }); + outer.end(message); + }); + + return outer; +}; + +const streamSimpleBedrockLazy: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions> = ( + model, + context, + options, +) => { + const outer = createAssistantMessageEventStream(); + + loadBedrockProviderModule() + .then((module) => { + const inner = module.streamSimpleBedrock(model, context, options); + forwardStream(outer, inner); + }) + .catch((error) => { + const message = createLazyLoadErrorMessage(model, error); + outer.push({ type: "error", reason: "error", error: message }); + outer.end(message); + }); + + return outer; +}; + +registerApiProvider({ + api: "bedrock-converse-stream", + stream: streamBedrockLazy, + streamSimple: streamSimpleBedrockLazy, +}); diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index d05106c8..131fb6ad 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -7,12 +7,9 @@ */ process.title = "pi"; -import { setBedrockProviderModule } from "@mariozechner/pi-ai"; -import { bedrockProviderModule } from "@mariozechner/pi-ai/bedrock-provider"; import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; import { main } from "./main.js"; setGlobalDispatcher(new EnvHttpProxyAgent()); -setBedrockProviderModule(bedrockProviderModule); main(process.argv.slice(2)); diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index fd9a60f5..166f3dd2 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -107,9 +107,9 @@ for platform in "${PLATFORMS[@]}"; do # call site has a try/catch fallback. For Windows builds, we copy the # appropriate .node file alongside the binary below. if [[ "$platform" == "windows-x64" ]]; then - bun build --compile --external koffi --target=bun-$platform ./dist/cli.js --outfile binaries/$platform/pi.exe + bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe else - bun build --compile --external koffi --target=bun-$platform ./dist/cli.js --outfile binaries/$platform/pi + bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi fi done