Provider bridge for GSD2. Lets you build provider extensions that use auth models and source types the vendored GSD2 fork of Pi doesn't natively support.
GSD2 uses a vendored fork of Pi as its AI provider system. That fork only supports two auth modes natively: apiKey and oauth. If your provider doesn't authenticate with one of those two patterns — a CLI tool like claude or ollama, a local model with no auth, an SDK that manages its own credentials — you're stuck. The vendored Pi fork has no mechanism for it, and GSD core doesn't patch around it.
This package fills that gap.
gsd-provider-api is an external bridge library that sits between your provider extension and the vendored GSD2 Pi fork. It:
-
Extends the auth model. Adds
externalCliandnoneauth modes on top of Pi'sapiKeyandoauth. Your provider declares which mode it uses, and the bridge handles the rest. -
Defines a stable provider contract. You implement
GsdProviderInfo— a single interface that describes your provider's identity, models, auth mode, and acreateStream()function. That's the entire surface area you need to touch. -
Translates your stream into Pi's event format. Your
createStream()yields simpleGsdEventobjects (text_delta,thinking_delta,progress_delta,tool_call_*,tool_result,completion,error). The adapter converts these into Pi'sAssistantMessageEventStreamso the vendored fork's orchestration layer consumes them like any native provider. -
Shares runtime state without compile-time coupling. GSD orchestration publishes supervisor config, tool definitions, and context callbacks via process-global symbols. Your provider consumes them through this package's registry APIs. No direct imports from GSD internals.
┌─────────────────────────┐
│ Your Provider Extension │
│ │
│ implements: │
│ GsdProviderInfo │
│ createStream() │
│ → yields GsdEvent │
└────────┬────────────────┘
│
│ registerProviderInfo()
│
┌────────▼────────────────┐
│ gsd-provider-api │
│ │
│ Provider Registry │ ← process-global Symbol store
│ Tool Registry │ ← shared tool defs from GSD
│ Deps Registry │ ← supervisor config, callbacks
│ Pi Adapter │ ← GsdEvent → Pi stream translation
│ Local Discovery │ ← auto-loads info.ts files
│ Onboarding │ ← default or custom onboarding
└────────┬────────────────┘
│
│ wireProvidersToPI(pi)
│
┌────────▼────────────────┐
│ Vendored GSD2 Pi Fork │
│ │
│ pi.registerProvider() │
│ AssistantMessageEvent │
│ Stream │
└─────────────────────────┘
npm install @thereaperjay/gsd-provider-apiYour extension also needs @gsd/pi-ai and @gsd/pi-coding-agent as peer dependencies (these come from the vendored GSD2 Pi fork).
This file self-registers your provider as a side effect of being imported. The local discovery system picks it up automatically.
import { registerProviderInfo } from "@thereaperjay/gsd-provider-api";
import type {
GsdProviderInfo,
GsdStreamContext,
GsdProviderDeps,
GsdEventStream,
} from "@thereaperjay/gsd-provider-api";
function createStream(context: GsdStreamContext, deps: GsdProviderDeps): GsdEventStream {
return (async function* () {
// Call your provider's API, SDK, CLI, local model — whatever you need.
const response = await callYourProvider(context.modelId, context.userPrompt);
// Yield GsdEvent objects. The adapter handles Pi translation.
yield { type: "text_delta" as const, text: response.text };
yield {
type: "completion" as const,
usage: { inputTokens: response.inputTokens, outputTokens: response.outputTokens },
stopReason: "stop",
};
})();
}
const provider: GsdProviderInfo = {
id: "my-provider",
displayName: "My Provider",
authMode: "externalCli", // or "apiKey", "oauth", "none"
models: [
{
id: "my-provider:my-model",
displayName: "My Model",
reasoning: false,
contextWindow: 128000,
maxTokens: 8192,
},
],
createStream,
};
registerProviderInfo(provider);import { wireProvidersToPI } from "@thereaperjay/gsd-provider-api";
import "./info.js"; // side-effect: registers the provider
export async function activate(pi: ExtensionAPI) {
await wireProvidersToPI(pi);
}That's it. GSD discovers your extension, imports your info.ts, and wireProvidersToPI translates your GsdEvent stream into Pi's native format.
Place your provider in one of these locations (scanned in order, last write wins on ID conflict):
| Location | Scope |
|---|---|
~/.gsd/agent/extensions/<name>/ |
Bundled/installed extensions |
~/.gsd/providers/<name>/ |
Global providers |
<project>/.gsd/providers/<name>/ |
Project-local (overrides global) |
Each directory must contain an info.ts or info.js that calls registerProviderInfo() on import.
Your createStream() yields these events:
| Event | Fields | Purpose |
|---|---|---|
text_delta |
text: string |
Streamed text output |
thinking_delta |
thinking: string |
Streamed reasoning/thinking output |
progress_delta |
text: string |
Ephemeral progress/status text (UI status channel, not transcript) |
tool_call_start |
toolCallId, toolName, detail? |
Starts a tool call block |
tool_call_delta |
toolCallId, delta |
Streams tool arguments JSON |
tool_call_end |
toolCallId |
Ends the tool call block |
tool_result |
toolCallId, toolName, result |
Attaches executed tool result to the tool call block |
completion |
usage: GsdUsage, stopReason: string |
Stream finished successfully |
error |
message, category, retryAfterMs? |
Stream failed. Categories: rate_limit, auth, timeout, unknown |
| Mode | Use Case |
|---|---|
apiKey |
Provider authenticates with an API key (native Pi support) |
oauth |
Provider authenticates via OAuth flow (native Pi support) |
externalCli |
Provider delegates auth to an external CLI tool (e.g., claude, gcloud) |
none |
No authentication required (local models, open APIs) |
For externalCli providers, you can declare an onboarding field with a check() function that verifies the CLI is installed and authenticated:
const provider: GsdProviderInfo = {
// ...
authMode: "externalCli",
onboarding: {
kind: "externalCli",
hint: "Run `my-cli auth login` to authenticate",
check: (spawnFn) => {
const result = (spawnFn ?? spawnSync)("my-cli", ["auth", "status"]);
if (result.status === 0) return { ok: true, email: "[email protected]" };
return { ok: false, reason: "Not authenticated", instruction: "Run: my-cli auth login" };
},
},
};| Function | Description |
|---|---|
registerProviderInfo(info) |
Register or replace a provider by ID |
getRegisteredProviderInfos() |
Get all registered providers |
removeProviderInfo(id) |
Remove a provider by ID |
clearRegisteredProviderInfos() |
Remove all providers |
GSD orchestration publishes these once per active runtime context. Your provider receives them as the second argument to createStream().
| Function | Description |
|---|---|
setProviderDeps(deps) |
Publish runtime deps (called by GSD core) |
getProviderDeps() |
Get current deps (or null if not yet set) |
waitForProviderDeps(timeoutMs?) |
Async wait for deps to be published (default 3s timeout) |
clearProviderDeps() |
Clear deps |
GsdProviderDeps gives your provider access to supervisor config, context-write blocking, milestone tracking, tool lifecycle hooks, and unit info — all without importing GSD internals.
GSD publishes available tools here. Providers can read them to expose tools to their underlying model.
| Function | Description |
|---|---|
registerGsdTool(def) |
Register a single tool |
replaceGsdTools(defs) |
Replace all tools atomically |
getGsdTools() |
Get current tool definitions |
clearGsdTools() |
Clear all tools |
defineGsdTool(name, desc, schema, execute) |
Type-safe tool definition helper (infers arg types from Zod schema) |
| Function | Description |
|---|---|
wireProvidersToPI(pi) |
Registers all discovered providers with the vendored Pi fork. Translates GsdEvent streams to AssistantMessageEventStream. |
| Function | Description |
|---|---|
discoverLocalProviders(projectRoot?) |
Scans extension/provider directories for info.ts/info.js files and imports them. Returns list of loaded provider directory names. |
| Function | Description |
|---|---|
runPluginOnboarding(provider) |
Self-contained onboarding. If onboard() is set on the provider, it runs with clack/pico passed from the library. If onboarding.kind === "externalCli", runs spinner + check(). Otherwise logs a generic install message. Returns { ok: boolean }. The library owns @clack/prompts and picocolors — extensions do not need to install them. |
See INTEGRATION.md for the full two-phase lifecycle (CLI install + session start) and how to structure your extension's onboarding.
State is shared across module boundaries via process-global symbols:
Symbol.for("gsd-provider-registry")— registeredGsdProviderInfoentriesSymbol.for("gsd-provider-deps")— runtime deps from GSD orchestrationSymbol.for("gsd-tool-registry")— shared tool definitions
This means your extension doesn't need a compile-time dependency on GSD core. GSD publishes state to these symbols, your extension reads from them through this package's APIs, and everything shares the same process-global store.
npm install
npm run typecheck
npm run build