# Architecture

`pi-other-provider` is a single pi extension that registers **three providers**:
`oc-zen`, `oc-go` and `commandcode`. It has **zero runtime dependencies** —
everything is built on the optional peers `@earendil-works/pi-ai` (streaming
primitives) and `@earendil-works/pi-coding-agent` (extension API).

## Module map

```
index.ts                          entry point — registers the 3 providers +
│                                 the /providers slash command; applies
│                                 visibility gating/filtering at registration
├── src/config.ts                 all base URLs, env var names, auth paths
├── src/types.ts                  shared OpenCode model + provider config types
├── src/pricing.ts                ZEN_PRICING / GO_PRICING (incl. cache rates)
├── src/event-stream-shim.ts      MiniEventStream — minimal async-queue stream
│                                 (pi-ai v0.77+ no longer exports the concrete
│                                 AssistantMessageEventStream from its root)
├── src/visibility.ts             on/off control for the 3 providers + their
│                                 models: config-file loader (lenient),
│                                 glob matching, isProviderEnabled,
│                                 filterModels, saveVisibilityConfig
├── src/provider-state.ts         neutral holder for loaded commandcode model
│                                 ids (shared by index.ts + command.ts to
│                                 avoid a circular import)
├── src/command.ts                /providers slash command: interactive TUI
│                                 (SettingsList) to toggle models/providers;
│                                 dynamic import of pi-tui + dialog fallback
│
├── src/backends/opencode/        ← CORE FIX (Zen + Go)
│   ├── catalog.ts                data-driven routing table: model id → protocol
│   │                             (ZEN_MODELS/GO_MODELS baselines, EFFORT_TIERS,
│   │                             getProtocolForModel pattern fallback,
│   │                             buildProviderModels → registerProvider shape)
│   ├── stream.ts                 custom streamSimple: re-derives the REAL
│   │                             protocol from the catalog (models are
│   │                             registered with the OPENCODE_CUSTOM_API
│   │                             marker so pi routes here), overrides
│   │                             api+baseUrl, injects gotcha hooks, then
│   │                             delegates to the matching pi-ai streamer
│   │                             (imported from the MAIN entry — never
│   │                             subpaths). Injectable `streamers` for tests.
│   ├── gotchas.ts                5 onPayload transforms (stripClientMetadata,
│   │                             limitTools, rewriteEffortTier,
│   │                             stripBooleanReasoning, injectReasoningContent)
│   ├── refresh.ts                refreshModels hook: live /models fetch →
│   │                             merge with static baseline → filter via
│   │                             visibility → pi model-store cache (both phases)
│   └── auth.ts                   OPENCODE_API_KEY resolution (env + auth files)
│
└── src/backends/commandcode/     vendored from pi-commandcode-provider v0.4.3
    ├── core.ts                   custom stream engine (factory + DI), headers,
    │                             max_tokens policy, passthrough fields,
    │                             retry/abort, SSE parsing
    ├── converters.ts             pure functions (getApiKey, messagesToCC,
    │                             toolsToJson, toJsonSchema, SSE line parser)
    ├── oauth.ts / auth-server.ts browser-assisted login + local callback server
    ├── models.ts                 model discovery + versioned file cache
    ├── cost.ts / pricing.ts      cost calculation + static pricing table
    └── types.ts                  vendored type definitions
```

## Data flow

### Extension load

```
pi loads index.ts (strip-types via tsx)
  → default export (pi: ExtensionAPI)
  → registerProvidersCommand(pi)            // /providers always registered
  → for each of oc-zen / oc-go / commandcode:
      isProviderEnabled(id)?                 // skip if disabled (config/env)
      → registerProvider(id, { streamSimple, refreshModels,
                               models: filterModels(id, …) })  // hide list applied
  → commandcode: loadCommandCodeModels() (live fetch → cache fallback)
```

The visibility filter is applied at the **source** (the `models` array handed
to `registerProvider`), because pi's own `filterModels` provider hook is only
forwarded from pi-ai _base_ providers, never from extensions
(`provider-composer.js`). See "Visibility" below.

### Chat request (OpenCode)

```
pi calls oc-zen/oc-go streamSimple(model, context, options)
  → stream.ts: getProtocolForModel(tier, model.id)      // re-derive REAL protocol
  → getBaseUrlForProtocol(tier, protocol)               // zen vs go base
  → composeOnPayload(protocol, model.id)                // gotcha hooks
  → switch(protocol):
      anthropic-messages   → streamSimpleAnthropic
      openai-responses     → streamSimpleOpenAIResponses
      openai-completions   → streamSimpleOpenAICompletions
      google-generative-ai → streamSimpleGoogle
  → pi-ai handles streaming, abort, usage parsing,
    AND prompt-cache hints (cache_control / session affinity)
```

### Chat request (Command Code)

```
pi calls commandcode streamSimple(model, context, options)
  → resolve api key (host → env → auth files)
  → build body:
      config {workingDir, date, environment, git placeholders}
      memory/taste/skills: ""  ·  permissionMode: "standard"
      params {model, messages, tools, system, temperature: 0.3,
              max_tokens? (only when pi sets one, clamped ≤ 200k),
              reasoning_effort? (from options.reasoningEffort)}
      threadId = options.sessionId ?? uuid()    // stable per conversation
  → options.onPayload(body) → host may rewrite the body
  → passthrough fields forwarded into params
  → headers: Authorization Bearer, x-command-code-version,
             x-cli-environment: external, x-taste-learning: false, …
  → POST {apiBase}/alpha/generate  (retry loop: 429/5xx, Retry-After,
    exponential backoff + jitter, per-attempt timeout, abort propagation)
  → SSE parse → AssistantMessageEvent mapping
    (text-delta → text_delta, reasoning-delta → thinking_delta,
     tool-call → toolcall_*, finish → usage + done)
```

### Model catalog refresh (OpenCode)

```
user opens /model (pi's model selector)
  → pi calls refreshModels(context) in TWO phases:
    phase 1 (allowNetwork=false): return filterModels(stored.models)
              (restore the last persisted catalog — offline friendly;
               visibility re-applied so a config change takes effect now)
    phase 2 (allowNetwork=true):  fetch https://opencode.ai/zen/{go,}v1/models
              → merge: static baseline ∪ live ids (unknown ids synthesized
                via getProtocolForModel pattern fallback)
              → filterModels(merged) applied BEFORE publish
              → context.publish({ persist: {models: visible, checkedAt} })
                (persists the already-filtered list to models-store.json)
              → return visible models
```

### Visibility & the `/providers` command

```
config: ~/.pi/agent/pi-other-provider.json   (override: PI_OTHER_PROVIDER_CONFIG)
         { "oc-zen": { enabled, showOnly[], hide[] }, … }
env:    PI_OTHER_PROVIDER_DISABLE=oc-zen,commandcode   (“*” = all three)

Two layers, both optional (default = everything visible):
  1. Provider on/off   → isProviderEnabled(id): config.enabled + env disable
  2. Model filtering    → filterModels(id, models): showOnly glob ∩ then − hide glob

Applied at:
  • index.ts    — gate registerProvider + filter the static `models` array
  • refresh.ts  — filter both restore (phase 1) and persist (phase 2)

/providers command (src/command.ts):
  /providers                → main menu (pick provider / enable-disable / done)
  /providers oc-zen         → jump to that provider's model panel
  /providers onoff          → provider enable/disable panel
  TUI:  SettingsList (multi-toggle) when pi-tui loads + mode=="tui"
  else: ctx.ui.select dialog fallback (RPC / no pi-tui)
  writes: saveVisibilityConfig → pi-other-provider.json
  effect: model toggle → reopen /model ; provider toggle → restart pi

pi-tui is imported DYNAMICALLY and guarded, so a resolution failure never
breaks extension load (worst case: the dialog fallback runs).

Scope: ONLY the three package providers. pi's built-in providers cannot be
filtered this way — see docs/COMPARISON.md “Built-in provider visibility”.
```

## Design decisions

| Decision                                              | Rationale                                                                                                                                                                                                                                                                                            |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| New provider IDs `oc-zen`/`oc-go`                     | do not clobber pi-ai built-in `opencode`/`opencode-go`; user can A/B them                                                                                                                                                                                                                            |
| Data-driven routing table                             | single source of truth; fixing a model = editing one entry                                                                                                                                                                                                                                           |
| Delegate to pi-ai streamers                           | streaming/abort/usage/cache-hints stay identical to built-ins                                                                                                                                                                                                                                        |
| Static baseline + live merge                          | offline-first; new models appear without code updates                                                                                                                                                                                                                                                |
| Import pi-ai from main entry only                     | pi loads extensions via tsx; ESM-only subpath exports mis-resolve (`Cannot find module .../dist/index.js/anthropic`)                                                                                                                                                                                 |
| Vendored commandcode, not a dependency                | package stays self-contained; only pi-ai/pi-coding-agent/pi-tui are peers                                                                                                                                                                                                                            |
| `threadId = options.sessionId ?? uuid()`              | stable per-conversation thread enables upstream prompt-cache hits                                                                                                                                                                                                                                    |
| Visibility applied at the source                      | pi's `filterModels` hook isn’t forwarded from extensions; filtering the `models` array + refresh output is the only reliable path                                                                                                                                                                    |
| Registered `model.api` = `OPENCODE_CUSTOM_API` marker | pi's `streamWith` routes to our `streamSimple` only when `model.api === provider.api`; the marker guarantees this and `stream.ts` re-derives the real protocol from the catalog. Using the real protocol here silently bypassed every gotcha hook (regression now guarded by `tests/test-stream.ts`) |
| `/providers` uses dynamic `import("pi-tui")`          | a resolution failure can never break extension load — it falls back to `ctx.ui.select` dialogs                                                                                                                                                                                                       |
| Local `CmdCtx` interface + boundary cast              | typecheck resolves pi-coding-agent v0.77 (no `ctx.mode`, `select` is `string[]`) while runtime is v0.84; the cast bridges the skew (same pattern as `ProviderConfigWithRefresh`)                                                                                                                     |
