# pi-other-provider

[![pi-extension](https://img.shields.io/badge/pi-extension-blue)](https://github.com/earendil-works/pi)

> ⚠️ **UNOFFICIAL.** This is a community package. It is **not** affiliated with,
> endorsed by, or maintained by OpenCode, Command Code, Factory, or the pi core team.

Custom model providers for [pi](https://github.com/earendil-works/pi) that fix
protocol-routing bugs in the built-in `opencode` / `opencode-go` providers,
vendor the Command Code provider, and add a session-based Factory Droid
adapter — all in one package.

## Why this exists

pi-ai ships built-in `opencode` and `opencode-go` providers, but several models
are routed to the **wrong wire protocol**, causing 400 errors:

| Model        | Tier | Built-in routes to              | Correct route |
| ------------ | ---- | ------------------------------- | ------------- |
| Qwen3.x      | Go   | `/chat/completions` (rejected!) | `/messages`   |
| MiniMax M2.7 | Go   | `/chat/completions` (rejected!) | `/messages`   |
| MiniMax M3   | Go   | `/chat/completions` (rejected!) | `/messages`   |

This package registers **new** provider IDs (`oc-zen`, `oc-go`) — it does **not**
clobber the built-ins — with a data-driven routing table that pins each model to
the protocol its upstream actually accepts. It also injects 5 `onPayload` hooks
that patch known upstream incompatibilities, and vendors the full Command Code
provider so both model sources work from one package.

## Features

- **`oc-zen` / `oc-go`** — corrected OpenCode Zen + Go (see
  [docs/OPENCODE-BACKEND.md](docs/OPENCODE-BACKEND.md))
- **`commandcode`** — vendored Command Code provider, hardened with upstream
  knowledge from OmniRoute (see [docs/COMMANDCODE-BACKEND.md](docs/COMMANDCODE-BACKEND.md))
- **`factory-droid`** — session-based **agent adapter** for Factory Droid (the
  API is NOT OpenAI-compatible; see [docs/FACTORYDROID-BACKEND.md](docs/FACTORYDROID-BACKEND.md))
- **Live model discovery** — catalogs refresh from the upstream `/models`
  endpoint through pi's own model-store cache (`refreshModels` hook) — new
  models appear without code updates
- **Usage-aware pricing** — cache-hit (`cacheRead` / `cacheWrite`) rates included
  for cost display; Command Code requests use a stable `threadId` per
  conversation so upstream prefix caching can hit across turns (see
  [docs/CACHING.md](docs/CACHING.md))
- **`/providers` command + on/off visibility** — an interactive TUI
  (`/providers`) to turn the four providers and their individual models on/off
  in `/model`; see [Model visibility](#model-visibility-onoff-in-model) below
- **Zero runtime dependencies** — `pi-ai` / `pi-coding-agent` / `pi-tui` are
  optional peers

## Installation

```bash
# Local development (from this directory)
npx pi install .

# Or from npm (once published)
npx pi install npm:pi-other-provider
```

Restart/reload pi, then run `/model` to pick a model. Opening `/model` also
triggers the live catalog refresh (see [Caching](docs/CACHING.md)).

## Providers

| Provider        | Backend       | Base URL                        | Models               |
| --------------- | ------------- | ------------------------------- | -------------------- |
| `oc-zen`        | OpenCode Zen  | `https://opencode.ai/zen/v1`    | 43 static · ~64 live |
| `oc-go`         | OpenCode Go   | `https://opencode.ai/zen/go/v1` | 44 static · ~48 live |
| `commandcode`   | Command Code  | `https://api.commandcode.ai`    | live fetch (52+)     |
| `factory-droid` | Factory Droid | `https://api.factory.ai`        | 18 static            |

Static counts are the curated baselines; the live counts are what the upstream
`/models` endpoints return at refresh time (they grow over time).

The `factory-droid` provider is **different from the others**: Factory Droid is
an **agentic coding assistant**, not a raw LLM endpoint. When pi sends a
message, the **Droid runs its own full agent loop** (its own tools) on a
Factory computer and returns the final text. pi's tools are ignored; each pi
turn is one Factory agent turn. See
[docs/FACTORYDROID-BACKEND.md](docs/FACTORYDROID-BACKEND.md) before using it.

## Authentication

### OpenCode (Zen + Go — one key for both)

1. **Environment variable** (recommended):

    ```bash
    export OPENCODE_API_KEY="your-key"
    ```

2. **Auth file** — entries are keyed by provider id, so add one per custom
   provider (or reuse the built-in `opencode-go` key by copying it):

    ```json
    {
    	"oc-zen": { "type": "api_key", "key": "user_..." },
    	"oc-go": { "type": "api_key", "key": "user_..." }
    }
    ```

    If the key is missing (no env var, no auth entry), the provider will not
    appear in `/model` and pi reports `Could not refresh <provider>` when the
    model selector tries to refresh its catalog.

### Command Code

1. Run `/login`, select **Use a subscription** → **Command Code** (browser flow),
   or
2. Set `COMMANDCODE_API_KEY`, or
3. Place the key in one of:
    - `~/.commandcode/auth.json` — `{ "apiKey": "user_..." }`
    - `~/.pi/agent/auth.json` — `{ "commandcode": { "type": "api_key", "key": "user_..." } }`

### Factory Droid

Factory Droid requires a **Factory computer** (a session cannot run without
one). It is auto-created from a cloud template when none exists; see
[docs/FACTORYDROID-BACKEND.md](docs/FACTORYDROID-BACKEND.md#computer-lifecycle-option-b--auto-create)
for the full lifecycle.

1. **API key** — create one at <https://app.factory.ai/settings/api-keys> (keys
   start with `fk-`), then either:

    ```bash
    export FACTORY_API_KEY="fk-your-api-key"
    ```

    or place it in `~/.factory/settings.json` as `{ "apiKey": "fk-..." }`.

2. **Computer id** (recommended) — set `FACTORY_DROID_COMPUTER_ID` to an
   existing Factory computer for stability; otherwise the provider reuses an
   active `e2b` computer or auto-creates one.

## Environment variables

| Variable                            | Provider      | Purpose                                              |
| ----------------------------------- | ------------- | ---------------------------------------------------- |
| `OPENCODE_API_KEY`                  | oc-zen/oc-go  | OpenCode key (shared by both tiers)                  |
| `COMMANDCODE_API_KEY`               | commandcode   | Command Code key                                     |
| `COMMANDCODE_API_BASE`              | commandcode   | Override API base (tests/mocks)                      |
| `COMMANDCODE_MODELS_URL`            | commandcode   | Override model catalog URL                           |
| `COMMANDCODE_MODELS_CACHE`          | commandcode   | Override model cache file path                       |
| `COMMANDCODE_AUTH_TIMEOUT_MS`       | commandcode   | Browser-login callback timeout (default 15s)         |
| `FACTORY_API_KEY`                   | factory-droid | Factory API key (`fk-…`, required)                   |
| `FACTORY_DROID_COMPUTER_ID`         | factory-droid | Reuse a specific Factory computer (recommended)      |
| `FACTORY_DROID_MACHINE_TEMPLATE_ID` | factory-droid | Template to auto-create a computer from              |
| `FACTORY_DROID_BASE`                | factory-droid | Override API base (default `https://api.factory.ai`) |
| `PI_OTHER_PROVIDER_CONFIG`          | all           | Override the visibility config file path             |
| `PI_OTHER_PROVIDER_DISABLE`         | all           | Comma list of provider ids to disable (`*` = all)    |

## Model visibility (on/off in `/model`)

You control which of the **four providers registered by this package** —
`oc-zen`, `oc-go`, `commandcode`, `factory-droid` — show up in pi's `/model`
selector (and in `/login`). This only affects these providers; every other pi
provider is left alone.

There are two layers. Both are optional — by default everything is visible.

### 1. Provider on/off

Disable a whole provider so it is not registered at all (it then disappears
from both `/model` and `/login`):

```jsonc
// ~/.pi/agent/pi-other-provider.json
{
	"oc-go": { "enabled": false },
}
```

Or, faster, with an env var (no file needed):

```bash
# Disable oc-go and commandcode:
export PI_OTHER_PROVIDER_DISABLE=oc-go,commandcode
# Disable all three of this package's providers:
export PI_OTHER_PROVIDER_DISABLE='*'
```

### 2. Model-level filtering

Keep only some models, or hide a few, using glob patterns (`*` matches any
run of characters):

```jsonc
// ~/.pi/agent/pi-other-provider.json
{
	"oc-zen": { "showOnly": ["claude*", "deepseek*"] },
	"oc-go": { "hide": ["mimo-*"] },
	"commandcode": { "showOnly": ["claude*", "qwen*"] },
}
```

- `showOnly` — if set and non-empty, **only** models whose id matches at
  least one pattern are shown.
- `hide` — models matching any pattern are hidden. Applied after `showOnly`.
- Matching is **case-sensitive** and applies to the **model id** only.
- `*` = any characters; e.g. `claude*`, `*-pro`, `deepseek-*-low`.

### How it works

pi's own `filterModels` provider hook is only forwarded from pi-ai _base_
providers, never from extensions, so this package controls visibility at the
**source**: the model list it passes to `registerProvider` and the lists its
`refreshModels` hook returns (which pi persists to its model-store cache).
The live catalog is also filtered before it is persisted, so a refreshed
`/model` list already respects the rules.

To pick up a config change you can either restart pi or open `/model` again
(the refresh hook re-reads the config file each time).

### `/providers` slash command (interactive TUI)

Instead of editing the JSON file by hand, run the built-in slash command inside
pi:

```
/providers
```

It opens an interactive panel:

- **Main menu** — pick a provider to toggle its models, or "Enable/disable
  providers", or "Done".
- **Per-provider model panel** — a multi-toggle list of every model
  (`on` = visible in `/model`, `off` = hidden). Toggle with the keyboard; each
  change is saved immediately. Jump straight in with `/providers oc-zen`.
- **Provider on/off panel** — enable/disable whole providers
  (`/providers onoff`).

Effects:

- Model on/off → rewrites the `hide` list → **reopen `/model`** to apply.
- Provider on/off → rewrites `enabled` → **restart pi** to apply (providers
  are registered at load time).

The rich `SettingsList` UI is used in TUI mode; in RPC mode (or if `pi-tui` is
unavailable) it automatically falls back to `ctx.ui.select` dialogs, so the
command always works. The command only affects these three providers.

## How the routing fix works

Each OpenCode model is pinned to one of four wire protocols:

| Protocol               | Endpoint path       | pi-ai streamer                  |
| ---------------------- | ------------------- | ------------------------------- |
| `openai-responses`     | `/responses`        | `streamSimpleOpenAIResponses`   |
| `anthropic-messages`   | `/messages`         | `streamSimpleAnthropic`         |
| `openai-completions`   | `/chat/completions` | `streamSimpleOpenAICompletions` |
| `google-generative-ai` | `/models/{id}`      | `streamSimpleGoogle`            |

The custom `streamSimple` (`src/backends/opencode/stream.ts`) re-derives the
protocol from the catalog, overrides the model's `api` + `baseUrl`, injects the
gotcha hooks, and delegates to the matching pi-ai built-in streamer — so
streaming, abort, usage parsing and **prompt-cache hints** (`cache_control`)
keep working exactly like the built-in providers.

> **How pi actually reaches this `streamSimple`:** every registered model
> carries `api: "opencode-custom"` (the `OPENCODE_CUSTOM_API` marker), matching
> the provider's `api`. pi's stream router only invokes an extension's
> `streamSimple` when `model.api === provider.api`, so the marker is what makes
> the gotcha hooks run — `stream.ts` then re-derives the real protocol from the
> catalog. Setting `model.api` to the real protocol instead would silently
> bypass every gotcha (a previous regression, now guarded by
> `tests/test-stream.ts`).

## Gotcha hooks (`onPayload`)

Five transforms applied to every OpenCode request, ported from OmniRoute's
`OpencodeExecutor`:

1. **`stripClientMetadata`** — removes `client_metadata` (upstream 400 "Extra inputs not permitted")
2. **`limitTools`** — caps `tools` at 128
3. **`rewriteEffortTier`** — expands effort aliases (`deepseek-v4-pro-low` → base + `reasoning_effort: low`)
4. **`stripBooleanReasoning`** — removes boolean `reasoning` fields some upstreams reject
5. **`injectReasoningContent`** — echoes `reasoning_content` on assistant messages for thinking models (DeepSeek, Kimi, MiniMax, MiMo, big-pickle) — OpenAI-completions transport only

## Command Code hardening

The vendored backend was aligned with OmniRoute's commandCode executor:

- `x-cli-environment: "external"`, `x-taste-learning: "false"` (privacy opt-out)
- `max_tokens` is **not fabricated** — only forwarded when pi sets it, clamped
  to the 200k endpoint ceiling (avoids the 400 "Too big" rejection)
- Passthrough of `reasoning_effort` / `reasoning` / `thinking` / `effort` /
  `output_config` / `extra_body` (payload-rule overrides are not dropped)
- `memory`/`taste`/`skills` sent as `""` (not `null`), plus `permissionMode: "standard"`
- Stable `threadId` per pi conversation (from `options.sessionId`) so upstream
  prompt caching can hit across turns

## Caching

Two distinct cache concepts, both supported (details: [docs/CACHING.md](docs/CACHING.md)):

1. **Model catalog cache** — pi's `models-store.json` persists the live model
   list; refreshed when you open `/model`, restored on offline startups.
2. **Usage cache hits** — provider-side prompt caching while chatting:
    - OpenCode: pi-ai streamers send `cache_control` / prompt-cache keys
      automatically; pricing tables include `cacheRead`/`cacheWrite` rates.
    - Command Code: stable `threadId` per conversation enables cross-turn
      prefix caching; `cacheRead`/`cacheWrite` tokens are parsed from responses
      and costed at their discounted rates.

## Development

```bash
npm test            # typecheck + full unit suite (mocked HTTP, no real calls)
npm run typecheck   # tsc --noEmit
```

See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for conventions and gotchas
(e.g. never import `@earendil-works/pi-ai/<subpath>` — pi loads extensions via
tsx and ESM-only subpath exports mis-resolve).

## Documentation

- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — module layout & data flow
- [docs/OPENCODE-BACKEND.md](docs/OPENCODE-BACKEND.md) — oc-zen / oc-go details
- [docs/COMMANDCODE-BACKEND.md](docs/COMMANDCODE-BACKEND.md) — commandcode details
- [docs/FACTORYDROID-BACKEND.md](docs/FACTORYDROID-BACKEND.md) — factory-droid session adapter
- [docs/CACHING.md](docs/CACHING.md) — catalog cache & usage cache hits
- [docs/COMPARISON.md](docs/COMPARISON.md) — vs pi-ai built-ins & OmniRoute
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) — contributing / release
- [docs/COMMANDCODE-ANALYSIS.md](docs/COMMANDCODE-ANALYSIS.md) — deep analysis of
  the upstream `pi-commandcode-provider` this package vendors

## Update / remove

```bash
npx pi update --extensions   # update installed packages
npx pi remove pi-other-provider
```

## License

MIT
