diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0c24d042..01ae3e10 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- `pi.unregisterProvider(name)` removes a dynamically registered provider and its models from the registry without requiring `/reload`. Built-in models that were overridden by the provider are restored. + +### Fixed + +- `pi.registerProvider()` now takes effect immediately when called after the initial extension load phase (e.g. from a command handler). Previously the registration sat in a pending queue that was never flushed until the next `/reload`. + ## [0.53.0] - 2026-02-17 ### Breaking Changes diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index a019f007..6a6983b2 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -1161,6 +1161,8 @@ pi.events.emit("my:event", { ... }); Register or override a model provider dynamically. Useful for proxies, custom endpoints, or team-wide model configurations. +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`. + ```typescript // Register a new provider with custom models pi.registerProvider("my-proxy", { @@ -1221,6 +1223,21 @@ pi.registerProvider("corporate-ai", { See [custom-provider.md](custom-provider.md) for advanced topics: custom streaming APIs, OAuth details, model definition reference. +### pi.unregisterProvider(name) + +Remove a previously registered provider and its models. Built-in models that were overridden by the provider are restored. Has no effect if the provider was not registered. + +Like `registerProvider`, this takes effect immediately when called after the initial load phase, so a `/reload` is not required. + +```typescript +pi.registerCommand("my-setup-teardown", { + description: "Remove the custom proxy provider", + handler: async (_args, _ctx) => { + pi.unregisterProvider("my-proxy"); + }, +}); +``` + ## State Management Extensions with state should store it in tool result `details` for proper branching support: diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 12f7c82c..0cf1293b 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -109,7 +109,7 @@ export function createExtensionRuntime(): ExtensionRuntime { throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading."); }; - return { + const runtime: ExtensionRuntime = { sendMessage: notInitialized, sendUserMessage: notInitialized, appendEntry: notInitialized, @@ -125,7 +125,18 @@ export function createExtensionRuntime(): ExtensionRuntime { setThinkingLevel: notInitialized, flagValues: new Map(), pendingProviderRegistrations: [], + // Pre-bind: queue registrations so bindCore() can flush them once the + // model registry is available. bindCore() replaces both with direct calls. + registerProvider: (name, config) => { + runtime.pendingProviderRegistrations.push({ name, config }); + }, + unregisterProvider: (name) => { + const idx = runtime.pendingProviderRegistrations.findIndex((r) => r.name === name); + if (idx !== -1) runtime.pendingProviderRegistrations.splice(idx, 1); + }, }; + + return runtime; } /** @@ -246,7 +257,11 @@ function createExtensionAPI( }, registerProvider(name: string, config: ProviderConfig) { - runtime.pendingProviderRegistrations.push({ name, config }); + runtime.registerProvider(name, config); + }, + + unregisterProvider(name: string) { + runtime.unregisterProvider(name); }, events: eventBus, diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index 06ee38bc..0f2ae67f 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -259,11 +259,16 @@ export class ExtensionRunner { this.compactFn = contextActions.compact; this.getSystemPromptFn = contextActions.getSystemPrompt; - // Process provider registrations queued during extension loading + // Flush provider registrations queued during extension loading for (const { name, config } of this.runtime.pendingProviderRegistrations) { this.modelRegistry.registerProvider(name, config); } this.runtime.pendingProviderRegistrations = []; + + // From this point on, provider registration/unregistration takes effect immediately + // without requiring a /reload. + this.runtime.registerProvider = (name, config) => this.modelRegistry.registerProvider(name, config); + this.runtime.unregisterProvider = (name) => this.modelRegistry.unregisterProvider(name); } bindCommandContext(actions?: ExtensionCommandContextActions): void { diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index c3e3183a..501af8fa 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -1071,6 +1071,11 @@ export interface ExtensionAPI { * If `oauth` is provided: registers OAuth provider for /login support. * If `streamSimple` is provided: registers a custom API stream handler. * + * During initial extension load this call is queued and applied once the + * runner has bound its context. After that it takes effect immediately, so + * it is safe to call from command handlers or event callbacks without + * requiring a `/reload`. + * * @example * // Register a new provider with custom models * pi.registerProvider("my-proxy", { @@ -1112,6 +1117,21 @@ export interface ExtensionAPI { */ registerProvider(name: string, config: ProviderConfig): void; + /** + * Unregister a previously registered provider. + * + * Removes all models belonging to the named provider and restores any + * built-in models that were overridden by it. Has no effect if the provider + * was not registered by this extension API. + * + * Like `registerProvider`, this takes effect immediately when called after + * the initial load phase. + * + * @example + * pi.unregisterProvider("my-proxy"); + */ + unregisterProvider(name: string): void; + /** Shared event bus for extension communication. */ events: EventBus; } @@ -1247,6 +1267,14 @@ export interface ExtensionRuntimeState { flagValues: Map; /** Provider registrations queued during extension loading, processed when runner binds */ pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig }>; + /** + * Register or unregister a provider. + * + * Before bindCore(): queues registrations / removes from queue. + * After bindCore(): calls ModelRegistry directly for immediate effect. + */ + registerProvider: (name: string, config: ProviderConfig) => void; + unregisterProvider: (name: string) => void; } /** diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 574f4e98..bf304b09 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -540,6 +540,20 @@ export class ModelRegistry { this.applyProviderConfig(providerName, config); } + /** + * Unregister a previously registered provider. + * + * Removes the provider from the registry and reloads models from disk so that + * built-in models overridden by this provider are restored to their original state. + * Has no effect if the provider was never registered. + */ + unregisterProvider(providerName: string): void { + if (!this.registeredProviders.has(providerName)) return; + this.registeredProviders.delete(providerName); + this.customProviderApiKeys.delete(providerName); + this.refresh(); + } + private applyProviderConfig(providerName: string, config: ProviderConfigInput): void { // Register OAuth provider if provided if (config.oauth) {