/** * pi-other-provider — UNOFFICIAL custom providers for pi. * * Registers three providers: * - oc-zen OpenCode Zen (custom protocol-routing fix) * - oc-go OpenCode Go (custom protocol-routing fix) * - commandcode Command Code (vendored from pi-commandcode-provider) * * The OpenCode providers fix the built-in pi-ai `opencode`/`opencode-go` * providers which route several models to the wrong wire protocol. See * README.md for details. */ import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions, } from "@earendil-works/pi-ai" import { getAgentDir, type ExtensionAPI, type ProviderConfig, } from "@earendil-works/pi-coding-agent" import { join } from "node:path" import { GO_BASE, ZEN_BASE } from "./src/config.ts" import { buildProviderModels, GO_MODELS, OPENCODE_CUSTOM_API, ZEN_MODELS, } from "./src/backends/opencode/catalog.ts" import { createRefreshModels } from "./src/backends/opencode/refresh.ts" import { createOpencodeStreamSimple } from "./src/backends/opencode/stream.ts" import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE, calculateCommandCodeCost, getApiKey, loadCommandCodeModels, DEFAULT_MODELS_URL, login, refreshToken, } from "./src/backends/commandcode/index.ts" import { countHidden, filterModels, isProviderEnabled } from "./src/visibility.ts" import { registerProvidersCommand } from "./src/command.ts" import { loadedCommandCodeModelIds, loadedFactoryDroidModelIds, } from "./src/provider-state.ts" import type { AssistantMessageEventStreamLike } from "./src/backends/commandcode/types.ts" import { COMMANDCODE_MODEL_COSTS, ZERO_COMMANDCODE_MODEL_COST, } from "./src/backends/commandcode/pricing.ts" import { createMiniEventStream } from "./src/event-stream-shim.ts" import { createStreamFactoryDroid, calculateFactoryDroidCost, FACTORY_DROID_MODELS, ZERO_FACTORY_DROID_MODEL_COST, FACTORY_DROID_BASE, } from "./src/backends/factorydroid/index.ts" // --------------------------------------------------------------------------- // OpenCode providers // --------------------------------------------------------------------------- // The dev-time typecheck resolves pi-coding-agent v0.77.x (peer resolution from // the workspace), which predates the `refreshModels` provider hook. The runtime // pi (v0.84+) supports it; extend locally so the registration stays typed. type ProviderConfigWithRefresh = ProviderConfig & { refreshModels?: ReturnType } function registerOpencodeProviders(pi: ExtensionAPI): void { // oc-zen — Zen tier with a comprehensive static catalog. (pi-ai's // registerProvider has no native passthroughModels, so we ship the full // known list; getProtocolForModel pattern-matches any live id as a fallback.) // api: nominal marker (custom). The streamSimple re-derives the real // per-model protocol (responses/messages/chat-completions/google) at runtime. // refreshModels: fetch the live /models catalog and hand it to pi's own // model-store cache (persisted + throttled + restored on startup); null // keeps the static baseline. // Visibility: each provider registers only when enabled, and its model list // is filtered by the showOnly/hide rules (see src/visibility.ts). if (isProviderEnabled("oc-zen")) { const models = buildProviderModels("zen", "oc-zen", ZEN_MODELS) const { visible } = countHidden("oc-zen", models) pi.registerProvider("oc-zen", { name: "OpenCode Zen (Custom)", baseUrl: ZEN_BASE, apiKey: "$OPENCODE_API_KEY", api: OPENCODE_CUSTOM_API, streamSimple: createOpencodeStreamSimple("zen"), refreshModels: createRefreshModels("zen", "oc-zen"), models: visible, } as ProviderConfigWithRefresh) } // oc-go — Go tier, static catalog only. if (isProviderEnabled("oc-go")) { const models = buildProviderModels("go", "oc-go", GO_MODELS) const { visible } = countHidden("oc-go", models) pi.registerProvider("oc-go", { name: "OpenCode Go (Custom)", baseUrl: GO_BASE, apiKey: "$OPENCODE_API_KEY", api: OPENCODE_CUSTOM_API, streamSimple: createOpencodeStreamSimple("go"), refreshModels: createRefreshModels("go", "oc-go"), models: visible, } as ProviderConfigWithRefresh) } } // --------------------------------------------------------------------------- // Command Code provider (vendored) // --------------------------------------------------------------------------- interface CommandCodeModelShape { id: string name: string reasoning: boolean contextWindow: number maxTokens: number } function registerCommandCodeProvider( pi: ExtensionAPI, models: readonly CommandCodeModelShape[], ): void { const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE const streamCommandCode = createStreamCommandCode({ // MiniEventStream is structurally an async-iterable push/end queue; cast to // the vendored AssistantMessageEventStreamLike (the commandcode backend // pushes real AssistantMessageEvent objects at runtime). createStream: createMiniEventStream as unknown as () => AssistantMessageEventStreamLike, calculateCost: calculateCommandCodeCost, apiBase, }) pi.registerProvider("commandcode", { name: "Command Code", baseUrl: apiBase, apiKey: "$COMMANDCODE_API_KEY", authHeader: true, api: "commandcode-custom", // The vendored backend uses its own parallel types (ModelLike, ContextLike, // …) that are structurally compatible with pi-ai at runtime. Cast at the // boundary to satisfy registerProvider's stricter signatures. streamSimple: streamCommandCode as unknown as ( model: Model, context: Context, options?: SimpleStreamOptions, ) => AssistantMessageEventStream, headers: { "x-command-code-version": COMMAND_CODE_CLI_VERSION, "x-cli-environment": "production", }, oauth: { name: "Command Code", login: login as never, refreshToken: refreshToken as never, getApiKey, }, models: filterModels("commandcode", models).map((model) => ({ id: model.id, name: model.name, reasoning: model.reasoning, input: ["text"] as const, cost: COMMANDCODE_MODEL_COSTS[model.id] ?? ZERO_COMMANDCODE_MODEL_COST, contextWindow: model.contextWindow, maxTokens: model.maxTokens, })), }) } /** * Factory Droid — session-based agent provider (Path B). * * Factory is NOT OpenAI-compatible; the adapter drives the Droid Sessions API * (create session → send newest user message → poll until idle → surface final * assistant text). Requires a Factory computer: either the * FACTORY_DROID_COMPUTER_ID env var, an existing active e2b computer, or an * auto-created computer from a cloud template (option B). */ function registerFactoryDroidProvider(pi: ExtensionAPI): void { if (!isProviderEnabled("factory-droid")) return const streamFactoryDroid = createStreamFactoryDroid({ createStream: createMiniEventStream as unknown as () => AssistantMessageEventStreamLike, calculateCost: calculateFactoryDroidCost, apiBase: process.env.FACTORY_DROID_BASE ?? FACTORY_DROID_BASE, }) const models = filterModels("factory-droid", FACTORY_DROID_MODELS).map((model) => ({ id: model.id, name: model.name, reasoning: model.reasoning, input: ["text"] as ("text" | "image")[], cost: model.cost ?? ZERO_FACTORY_DROID_MODEL_COST, contextWindow: model.contextWindow, maxTokens: model.maxTokens, })) loadedFactoryDroidModelIds.push(...models.map((model) => model.id)) pi.registerProvider("factory-droid", { name: "Factory Droid (session adapter)", baseUrl: process.env.FACTORY_DROID_BASE ?? FACTORY_DROID_BASE, apiKey: "$FACTORY_API_KEY", authHeader: true, api: "factory-droid-custom", streamSimple: streamFactoryDroid as unknown as ( model: Model, context: Context, options?: SimpleStreamOptions, ) => AssistantMessageEventStream, models, }) } // --------------------------------------------------------------------------- // Extension entry point // --------------------------------------------------------------------------- export default async function (pi: ExtensionAPI): Promise { registerOpencodeProviders(pi) // Factory Droid — static catalog, no network fetch at load time. registerFactoryDroidProvider(pi) // Slash command `/providers` — interactive on/off TUI for the providers // registered by this package. Registered unconditionally so the command is // always available even when individual providers are disabled. registerProvidersCommand(pi) if (!isProviderEnabled("commandcode")) { console.info( "[commandcode] provider disabled by visibility config — not registered", ) return } const modelsCachePath = process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json") const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL let commandcodeModels: CommandCodeModelShape[] = [] try { const { models, warning } = await loadCommandCodeModels({ url: modelsUrl, cachePath: modelsCachePath, }) if (warning) console.warn(`[commandcode] ${warning}`) commandcodeModels = models as CommandCodeModelShape[] loadedCommandCodeModelIds.push(...commandcodeModels.map((model) => model.id)) } catch (error) { // Model discovery is best-effort; the provider still registers with an // empty list so the extension loads even when offline. console.warn( `[commandcode] model discovery failed: ${error instanceof Error ? error.message : String(error)}`, ) } registerCommandCodeProvider(pi, commandcodeModels) }