# Development

## Setup

```bash
cd pi-other-provider
npm install        # dev deps only: tsx, typescript, prettier, @types/node
npm test           # typecheck + full unit suite (mocked HTTP — no real calls)
npm run typecheck  # tsc --noEmit
```

Runtime dependencies: **none**. `@earendil-works/pi-ai`,
`@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` are **optional
peer dependencies** — they are provided by the pi runtime.

## Conventions

- **Tabs** for indentation (project-wide).
- TypeScript **strict**; `.ts` import paths (NodeNext).
- Tests use `node:test` + `tsx`; every test file is `tests/test-*.ts` and is
  picked up automatically by `npm test` (`xargs -n1 tsx`).
- All HTTP is mocked — tests must never hit real endpoints.

## Critical gotchas

### 1. Never import `@earendil-works/pi-ai/<subpath>`

pi loads extensions through **tsx**, and tsx mis-resolves the ESM-only subpath
exports of pi-ai:

```
import { streamSimpleAnthropic } from "@earendil-works/pi-ai/anthropic"   // ❌
Cannot find module '.../pi-ai/dist/index.js/anthropic'
```

Import everything from the **main entry** — it re-exports the streamers:

```ts
import { streamSimpleAnthropic } from "@earendil-works/pi-ai" // ✅
```

### 2. `refreshModels` must not throw

pi treats a throwing hook as "Could not refresh <provider>" in the model
selector. Always return `null` on failure (the static baseline stays), check
`context.allowNetwork` / `context.signal.aborted`, and persist explicitly via
`context.publish({ persist: ... })` — pi does **not** persist the return value
for extension providers (see `docs/CACHING.md`).

### 3. Type resolution for pi-coding-agent

The workspace may resolve an older pi-coding-agent copy for typechecking
(e.g. v0.77 vs the runtime v0.84). When using newer provider hooks
(`refreshModels`), extend the config type locally instead of relying on the
resolved peer:

```ts
type ProviderConfigWithRefresh = ProviderConfig & {
	refreshModels?: ReturnType<typeof createRefreshModels>
}
```

### 4. Don't break the streamer delegation

`src/backends/opencode/stream.ts` re-derives the protocol from the catalog and
delegates to pi-ai streamers. Keep the switch exhaustive; add new protocols to
`ProtocolType` and the switch together.

### 5. `/providers` command: import `pi-tui` dynamically, never at top level

The slash command (`src/command.ts`) renders a `SettingsList` from `pi-tui`.
`pi-tui` is a transitive dependency of `pi-coding-agent` and resolves at runtime,
but it is **not** guaranteed to resolve from every extension load path. Import
it dynamically and guard it so a failure can never break extension load:

```ts
const tui = await import("@earendil-works/pi-tui").catch(() => null)
```

Type-only imports (`import type { SettingItem } from "@earendil-works/pi-tui"`)
are safe — they are erased at runtime, so they do not trigger resolution.

The command also uses a local `CmdCtx` interface (cast at the handler boundary)
instead of `ExtensionCommandContext` directly, because typecheck resolves
`pi-coding-agent` v0.77 (which lacks `ctx.mode` and types `select` as
`string[]`) while the runtime is v0.84. This is the same version-skew pattern as
the `ProviderConfigWithRefresh` cast in `index.ts`.

### 6. Visibility tests must isolate from the real agent dir

`~/.pi/agent/pi-other-provider.json` is created by live `/providers` usage.
Tests that read config via `loadVisibilityConfig()` without an env override would
pick up that real file and become environment-dependent. Always point
`PI_OTHER_PROVIDER_CONFIG` at a temp path in `beforeEach` (see
`tests/test-visibility.ts`, `tests/test-refresh.ts`).

### 7. Registered `model.api` must be the `OPENCODE_CUSTOM_API` marker

pi's `streamWith` invokes an extension's `streamSimple` only when
`model.api === provider.api`. `buildProviderModels` therefore stamps every
model with `api: OPENCODE_CUSTOM_API` (matching the provider's `api`), NOT the
real protocol — `stream.ts` re-derives the real protocol from the catalog at
stream time. Setting `model.api` to the real protocol silently bypasses every
gotcha hook (pi calls pi-ai's streamer directly). This is guarded by
`tests/test-stream.ts` and the marker assertion in `tests/test-catalog.ts`.

## Adding a model

1. Add the entry to `ZEN_MODELS` / `GO_MODELS` in
   `src/backends/opencode/catalog.ts` with the **correct** protocol pin
   (cross-check against OmniRoute's
   `open-sse/config/providers/registry/opencode/{zen,go}/index.ts` if unsure).
2. Add pricing to `src/pricing.ts` (input/output/cacheRead/cacheWrite).
3. Add a routing assertion to `tests/test-routing.ts`.

Models already served by the live `/models` endpoint appear automatically via
`refreshModels` even before step 1–2 (with generic metadata) — steps 1–2 make
their pricing/context accurate.

## Releasing

1. Update `CHANGELOG.md` (move changes into a new version section).
2. Bump `package.json` version.
3. `npm test` + `npm run format` (prettier).
4. Commit; tag the release.

## Verifying a live pi load

```bash
# From the package dir — extension must load without errors:
npx pi -p hi 2>&1 | grep -iE "error|cannot find|is required"
# Expect: no pi-other-provider related errors.

# With a key set, the custom providers must appear:
OPENCODE_API_KEY=... npx pi --list-models | grep -cE "^oc-zen |^oc-go "
```
