diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b54776c0..434b5b18 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,6 +7,10 @@ - Added extension support for customizing the interactive streaming working indicator via `ctx.ui.setWorkingIndicator()`, including custom animated frames, static indicators, hidden indicators, a new `working-indicator.ts` example extension, and updated extension/TUI/RPC docs ([#3413](https://github.com/badlogic/pi-mono/issues/3413)) - Added `/clone` to duplicate the current active branch into a new session, while keeping `/fork` focused on forking from a previous user message ([#2962](https://github.com/badlogic/pi-mono/issues/2962)) +### Changed + +- Documented async extension factory functions in the extensions and custom-provider docs, including startup ordering and dynamic model/provider discovery via async initialization ([#3469](https://github.com/badlogic/pi-mono/issues/3469)) + ### Fixed - Fixed `pi update` reinstalling npm packages that are already at the latest published version by checking the installed package version before running `npm install @latest` ([#3000](https://github.com/badlogic/pi-mono/issues/3000)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 6c30f995..a2f2045d 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -335,6 +335,8 @@ export default function (pi: ExtensionAPI) { } ``` +The default export can also be `async`. pi waits for async extension factories before startup continues, which is useful for one-time initialization such as fetching remote model lists before calling `pi.registerProvider()`. + **What's possible:** - Custom tools (or replace built-in tools entirely) - Sub-agents and plan mode diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index be22acc9..31c1ef75 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -59,6 +59,8 @@ export default function (pi: ExtensionAPI) { } ``` +The extension factory can also be `async`. For dynamic model discovery, fetch and register models in the factory instead of `session_start`. pi waits for the factory before startup continues, so the provider is available during interactive startup and to `pi --list-models`. + ## Override Existing Provider The simplest use case: redirect an existing provider through a proxy. @@ -91,6 +93,41 @@ When only `baseUrl` and/or `headers` are provided (no `models`), all existing mo To add a completely new provider, specify `models` along with the required configuration. +If the model list comes from a remote endpoint, use an async extension factory: + +```typescript +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +export default async function (pi: ExtensionAPI) { + const response = await fetch("http://localhost:1234/v1/models"); + const payload = (await response.json()) as { + data: Array<{ + id: string; + name?: string; + context_window?: number; + max_tokens?: number; + }>; + }; + + pi.registerProvider("local-openai", { + baseUrl: "http://localhost:1234/v1", + apiKey: "LOCAL_OPENAI_API_KEY", + api: "openai-completions", + models: payload.data.map((model) => ({ + id: model.id, + name: model.name ?? model.id, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.context_window ?? 128000, + maxTokens: model.max_tokens ?? 4096, + })), + }); +} +``` + +This registers the fetched models before startup finishes. + ```typescript pi.registerProvider("my-llm", { baseUrl: "https://api.my-llm.com/v1", diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index d64ebf16..09b58d31 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -151,7 +151,7 @@ Node.js built-ins (`node:fs`, `node:path`, etc.) are also available. ## Writing an Extension -An extension exports a default function that receives `ExtensionAPI`: +An extension exports a default factory function that receives `ExtensionAPI`. The factory can be synchronous or asynchronous: ```typescript import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; @@ -176,6 +176,45 @@ export default function (pi: ExtensionAPI) { Extensions are loaded via [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation. +If the factory returns a `Promise`, pi awaits it before continuing startup. That means async initialization completes before `session_start`, before `resources_discover`, and before provider registrations queued via `pi.registerProvider()` are flushed. + +### Async factory functions + +Use an async factory for one-time startup work such as fetching remote configuration or dynamically discovering available models. + +```typescript +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +export default async function (pi: ExtensionAPI) { + const response = await fetch("http://localhost:1234/v1/models"); + const payload = (await response.json()) as { + data: Array<{ + id: string; + name?: string; + context_window?: number; + max_tokens?: number; + }>; + }; + + pi.registerProvider("local-openai", { + baseUrl: "http://localhost:1234/v1", + apiKey: "LOCAL_OPENAI_API_KEY", + api: "openai-completions", + models: payload.data.map((model) => ({ + id: model.id, + name: model.name ?? model.id, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.context_window ?? 128000, + maxTokens: model.max_tokens ?? 4096, + })), + }); +} +``` + +This pattern makes the fetched models available during normal startup and to `pi --list-models`. + ### Extension Styles **Single file** - simplest, for small extensions: @@ -1371,6 +1410,8 @@ Register or override a model provider dynamically. Useful for proxies, custom en Calls made during the extension factory function are queued and applied once the runner initialises. Calls made after that — for example from a command handler following a user setup flow — take effect immediately without requiring a `/reload`. +If you need to discover models from a remote endpoint, prefer an async extension factory over deferring the fetch to `session_start`. pi waits for the factory before startup continues, so the registered models are available immediately, including to `pi --list-models`. + ```typescript // Register a new provider with custom models pi.registerProvider("my-proxy", {