|
| 1 | +# 001 — External client config survey |
| 2 | + |
| 3 | +Research only. No diffs here; implementation designs live in the decade docs. |
| 4 | + |
| 5 | +Scope of the survey: where a user (or an agent) drops a custom OpenAI-compatible |
| 6 | +provider so that Pi and OpenCode can call the opencodex proxy, what JSON those |
| 7 | +files require, and whether any cross-vendor standard governs the shape. |
| 8 | + |
| 9 | +## 1. Is there a standard? |
| 10 | + |
| 11 | +No. There is no formal specification body governing agent-client provider |
| 12 | +configuration. What exists instead is a stack of de-facto layers: |
| 13 | + |
| 14 | +| Layer | What it standardizes | Who honors it | |
| 15 | +|-------|----------------------|---------------| |
| 16 | +| OpenAI `/v1` wire (`/chat/completions`, `/responses`, `/models`) | request/response bytes | effectively everyone | |
| 17 | +| Anthropic `/v1/messages` wire | request/response bytes | Claude clients, our Messages surface | |
| 18 | +| `@ai-sdk/openai-compatible` (Vercel AI SDK) | adapter package name + option names | OpenCode, several TS agents | |
| 19 | +| models.dev | model metadata registry (context, cost, modality) | OpenCode, Pi metadata seeds | |
| 20 | +| per-client JSON Schema (`https://opencode.ai/config.json`) | one client's config file only | that client | |
| 21 | + |
| 22 | +The consequence for this unit: the transport half is already solved — the proxy |
| 23 | +speaks `/v1` and clients speak `/v1`. Everything left is **metadata translation** |
| 24 | +into per-client dialects, and each dialect is a moving target owned by one vendor. |
| 25 | + |
| 26 | +Critically, **models.dev does not populate custom providers.** A user-defined |
| 27 | +provider block gets no inherited context window, no pricing, no modality flags. |
| 28 | +Whatever we do not emit ourselves, the client simply does not know. |
| 29 | + |
| 30 | +## 2. Where custom providers are injected |
| 31 | + |
| 32 | +### OpenCode |
| 33 | + |
| 34 | +Three layers, highest priority first: |
| 35 | + |
| 36 | +| Layer | Location | Notes | |
| 37 | +|-------|----------|-------| |
| 38 | +| Inline runtime | `OPENCODE_CONFIG_CONTENT` env var | outranks every file; what `ocx opencode` uses today | |
| 39 | +| Project | `<git root>/opencode.json` or `.jsonc` | committed per repo | |
| 40 | +| Global | `~/.config/opencode/opencode.json`, XDG-aware via `XDG_CONFIG_HOME` | the file most users hand-edit | |
| 41 | + |
| 42 | +JSONC is accepted for the file layers (comments must survive a round trip, which |
| 43 | +is why we never rewrite a user's file — see `002`). |
| 44 | + |
| 45 | +V1 schema (current, what we generate): |
| 46 | + |
| 47 | +```json |
| 48 | +{ |
| 49 | + "$schema": "https://opencode.ai/config.json", |
| 50 | + "provider": { |
| 51 | + "opencodex": { |
| 52 | + "npm": "@ai-sdk/openai-compatible", |
| 53 | + "name": "opencodex", |
| 54 | + "options": { "baseURL": "http://127.0.0.1:10100/v1", "apiKey": "{env:OPENCODEX_OPENCODE_API_KEY}" }, |
| 55 | + "models": { |
| 56 | + "anthropic/claude-opus-5": { "name": "Claude Opus 5", "limit": { "context": 200000, "output": 32000 } } |
| 57 | + } |
| 58 | + } |
| 59 | + } |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +V2 schema (documented at `opencode.ai/v2/docs/models`) renames nearly every key: |
| 64 | + |
| 65 | +| Concept | V1 | V2 | |
| 66 | +|---------|----|----| |
| 67 | +| container | `provider` | `providers` | |
| 68 | +| adapter | `npm: "@ai-sdk/openai-compatible"` | `package: "aisdk:@ai-sdk/openai-compatible"` | |
| 69 | +| endpoint | `options.baseURL` | `settings.baseURL` | |
| 70 | +| upstream id | `models.<k>.id` | `models.<k>.modelID` | |
| 71 | +| credentials | `options.apiKey` / auth store | `env[]` | |
| 72 | +| new in V2 | — | `capabilities`, `variants`, `disabled` | |
| 73 | + |
| 74 | +**Decision: stay on V1.** The shipped launcher already emits V1 and that path is |
| 75 | +exercised by tests; switching would break it in exchange for `capabilities` and |
| 76 | +`variants` fields we have no authoritative data to fill. V2 is recorded here so a |
| 77 | +later migration starts from a written diff rather than a re-survey. |
| 78 | + |
| 79 | +### Pi |
| 80 | + |
| 81 | +Single global file: `~/.pi/agent/models.json`. |
| 82 | + |
| 83 | +```json |
| 84 | +{ |
| 85 | + "providers": { |
| 86 | + "opencodex": { |
| 87 | + "baseUrl": "http://127.0.0.1:10100/v1", |
| 88 | + "api": "openai-completions", |
| 89 | + "apiKey": "$OPENCODEX_API_KEY", |
| 90 | + "models": [ |
| 91 | + { |
| 92 | + "id": "anthropic/claude-opus-5", |
| 93 | + "name": "Claude Opus 5", |
| 94 | + "reasoning": false, |
| 95 | + "input": ["text"], |
| 96 | + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, |
| 97 | + "contextWindow": 200000, |
| 98 | + "maxTokens": 32000 |
| 99 | + } |
| 100 | + ] |
| 101 | + } |
| 102 | + } |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +Shape differences that matter for the serializer: |
| 107 | + |
| 108 | +- `models` is an **array**, not a keyed object. Model identity lives in `id`. |
| 109 | +- `cost` demands all four fields when present. |
| 110 | +- `apiKey` supports `$ENV_VAR` interpolation (bare `$NAME`, not `{env:NAME}`). |
| 111 | +- `api` selects the wire dialect; `openai-completions` is ours. |
| 112 | + |
| 113 | +**Verification status: UNVERIFIED against a real installation.** The shape above |
| 114 | +comes from Pi's published `models.md` and `pi.dev/docs/latest/custom-provider`. |
| 115 | +No `~/.pi/agent/models.json` was read on this machine. The first implementation |
| 116 | +cycle touching Pi must diff this against a real file before shipping. |
| 117 | + |
| 118 | +### Cross-client field map |
| 119 | + |
| 120 | +| Concept | Pi | OpenCode V1 | |
| 121 | +|---------|----|-------------| |
| 122 | +| container | `providers` | `provider` | |
| 123 | +| adapter | `api: "openai-completions"` | `npm: "@ai-sdk/openai-compatible"` | |
| 124 | +| endpoint | `baseUrl` | `options.baseURL` | |
| 125 | +| model container | array | keyed object | |
| 126 | +| context | `contextWindow` | `limit.context` | |
| 127 | +| max output | `maxTokens` | `limit.output` | |
| 128 | +| price | `cost{4}` | absent | |
| 129 | +| modality | `input[]` | absent | |
| 130 | +| key reference | `$VAR` | `{env:VAR}` | |
| 131 | + |
| 132 | +Only four concepts are universal: endpoint, adapter kind, model id, and the |
| 133 | +context/output pair. Everything else is one client's luxury. |
| 134 | + |
| 135 | +## 3. Credential handling |
| 136 | + |
| 137 | +All three injection paths support an environment reference, so no exporter needs |
| 138 | +to serialize a live `ocx_...` key. Pi takes `$VAR`, OpenCode takes `{env:VAR}`, |
| 139 | +and OpenCode additionally has an auth store. AGENTS.md treats token serialization |
| 140 | +as a release blocker, so emitting a reference is not merely polite — writing the |
| 141 | +plaintext key into an exported file would be a security-review failure. |
| 142 | + |
| 143 | +The exported artifact therefore carries a variable name, and every surface that |
| 144 | +hands the file over must also tell the user which variable to export. |
| 145 | + |
| 146 | +## 4. Sources |
| 147 | + |
| 148 | +- Pi: `github.com/earendil-works/pi` `packages/coding-agent/docs/models.md`; `pi.dev/docs/latest/custom-provider` |
| 149 | +- OpenCode V1: `opencode.ai/docs/providers`, `opencode.ai/config.json` |
| 150 | +- OpenCode V2: `opencode.ai/v2/docs/models` |
| 151 | +- AI SDK adapter: `@ai-sdk/openai-compatible` |
| 152 | +- Metadata registry: `models.dev` |
| 153 | + |
| 154 | +All external claims were read during the 2026-07-31 P phase. Client schemas drift; |
| 155 | +re-verify before any cycle that changes serializer output. |
0 commit comments