pi-opencode-go-provider 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ github: monotykamary
2
+ ko_fi: monotykamary
3
+ buy_me_a_coffee: monotykamary
4
+ polar: monotykamary
package/AGENTS.md ADDED
@@ -0,0 +1,56 @@
1
+ # AGENTS.md
2
+
3
+ ## DO NOT EDIT — Auto-generated Files
4
+
5
+ The following files are **idempotent** and regenerated by `scripts/update-models.js`. Never edit them directly — your changes will be overwritten on the next model sync.
6
+
7
+ | File | Why it's auto-generated |
8
+ |------|------------------------|
9
+ | `models.json` | Built from the provider API. `update-models.js` fetches models, preserves curated data for known IDs, and writes this file. |
10
+ | `README.md` (model table) | The table under `## Available Models` is replaced in-place by `update-models.js` after merging base models → patch → custom models. |
11
+
12
+ ## Correct Files to Edit
13
+
14
+ When a model needs overrides, new properties, or corrections, edit the appropriate source file below. These are the **source of truth** that the update script reads but never writes.
15
+
16
+ | File | Purpose |
17
+ |------|---------|
18
+ | `patch.json` | Per-model overrides keyed by model ID. Add reasoning flags, compat settings, pricing corrections, thinking level maps, etc. Applied on top of `models.json` at runtime and for README generation. |
19
+ | `custom-models.json` | Models that don't exist in the provider API (hidden models, router endpoints, cross-provider aliases). Merged after patch. Format: array of full model objects (same schema as `models.json` entries). |
20
+ | `index.ts` | Provider extension code. |
21
+ | `scripts/update-models.js` | The sync script itself (edit only if changing how models are fetched/transformed). |
22
+
23
+ ## Data Flow
24
+
25
+ ```
26
+ Provider API ──fetch──► models.json ──apply──► patch.json ──merge──► custom-models.json
27
+ │ │ │
28
+ └────────────────────────────┴──────────────────────┘
29
+
30
+ README model table
31
+ ```
32
+
33
+ 1. `models.json` — base data from the provider API (auto-generated, DO NOT EDIT)
34
+ 2. `patch.json` — overrides applied on top (EDIT THIS for corrections/enrichments)
35
+ 3. `custom-models.json` — additional models not in the API (EDIT THIS for new models)
36
+ 4. README table — rendered from the merged result of all three (auto-generated, DO NOT EDIT)
37
+
38
+ ## Common Tasks
39
+
40
+ ### Add a compat setting or override pricing for an existing model
41
+ → Edit `patch.json`. Add an entry keyed by the model's `id`.
42
+
43
+ ### Add a model not available in the provider API
44
+ → Edit `custom-models.json`. Add a full model object to the array.
45
+
46
+ ### Update models from the provider API
47
+ → Run `node scripts/update-models.js` (may require an API key env var).
48
+
49
+ ### Regenerate the README model table
50
+ → Run `node scripts/update-models.js` — it updates both `models.json` and the README table.
51
+
52
+ ## TL;DR
53
+
54
+ - **Never edit `models.json`** — edit `patch.json` instead.
55
+ - **Never edit the README model table** — run the update script instead.
56
+ - `patch.json` and `custom-models.json` are the source files you should modify.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ <div align="center">
2
+
3
+ # 🟢 pi-opencode-go-provider
4
+
5
+ **Fast GLM, Kimi & MiniMax via [opencode-go](https://opencode.ai/)**
6
+
7
+ _Go-optimized endpoints for lower latency — 14+ models for [pi](https://github.com/earendil-works/pi-coding-agent)._
8
+
9
+ [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://github.com/earendil-works/pi-coding-agent)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## Features
17
+
18
+ - **14+ Optimized AI Models** - DeepSeek, GLM, Kimi, MiMo, MiniMax, and Qwen
19
+ - **Multi-API Support** — uses the correct API protocol per model (Anthropic, OpenAI Completions)
20
+ - **Fast & Efficient** - Go-optimized endpoints for lower latency
21
+ - **Cost Tracking** with per-model pricing for budget management
22
+ - **Reasoning Models** with thinking level maps for proper effort control
23
+
24
+ ## Installation
25
+
26
+ ### Option 1: Using `pi install` (Recommended)
27
+
28
+ Install directly from GitHub:
29
+
30
+ ```bash
31
+ pi install https://github.com/monotykamary/pi-opencode-go-provider
32
+ ```
33
+
34
+ Then set your API key and run pi:
35
+ ```bash
36
+ # Recommended: add to auth.json
37
+ # See Authentication section below
38
+
39
+ # Or set as environment variable
40
+ export OPENCODE_API_KEY=your-api-key-here
41
+
42
+ pi
43
+ ```
44
+
45
+ ### Option 2: Manual Clone
46
+
47
+ 1. Clone this repository:
48
+ ```bash
49
+ git clone https://github.com/monotykamary/pi-opencode-go-provider.git
50
+ cd pi-opencode-go-provider
51
+ ```
52
+
53
+ 2. Set your opencode API key:
54
+ ```bash
55
+ # Recommended: add to auth.json
56
+ # See Authentication section below
57
+
58
+ # Or set as environment variable
59
+ export OPENCODE_API_KEY=your-api-key-here
60
+ ```
61
+
62
+ 3. Run pi with the extension:
63
+ ```bash
64
+ pi -e /path/to/pi-opencode-go-provider
65
+ ```
66
+
67
+ ## Available Models
68
+
69
+ | Model | API | Type | Context | Max Tokens | Input Cost | Output Cost |
70
+ |-------|-----|------|---------|------------|------------|-------------|
71
+ | DeepSeek V4 Flash | Completions | Text | 1.0M | 384K | $0.14 | $0.28 |
72
+ | DeepSeek V4 Pro | Completions | Text | 1.0M | 384K | $1.74 | $3.48 |
73
+ | GLM-5.1 | Completions | Text | 203K | 33K | $1.40 | $4.40 |
74
+ | GLM-5.2 | Completions | Text | 1.0M | 131K | $1.40 | $4.40 |
75
+ | Kimi K2.6 | Completions | Text + Image | 262K | 66K | $0.95 | $4.00 |
76
+ | Kimi K2.7 Code | Completions | Text + Image | 262K | 262K | $0.95 | $4.00 |
77
+ | MiMo V2.5 | Completions | Text + Image | 1.0M | 128K | $0.14 | $0.28 |
78
+ | MiMo V2.5 Pro | Completions | Text | 1.0M | 128K | $1.74 | $3.48 |
79
+ | MiniMax-M2.7 | Anthropic | Text | 205K | 131K | $0.30 | $1.20 |
80
+ | MiniMax-M3 | Anthropic | Text + Image | 1.0M | 131K | $0.30 | $1.20 |
81
+ | Qwen3.6 Plus | Anthropic | Text + Image | 1.0M | 66K | $0.50 | $3.00 |
82
+ | Qwen3.7 Max | Anthropic | Text | 1.0M | 66K | $2.50 | $7.50 |
83
+ | Qwen3.7 Plus | Anthropic | Text + Image | 1.0M | 66K | $0.40 | $1.60 |
84
+ *Costs are per million tokens. Prices subject to change - check [opencode.ai](https://opencode.ai) for current pricing.*
85
+
86
+ ## Usage
87
+
88
+ After loading the extension, use the `/model` command in pi to select your preferred model:
89
+
90
+ ```
91
+ /model
92
+ ```
93
+
94
+ Then select "opencode-go" as the provider and choose from the available models.
95
+
96
+ ## Authentication
97
+
98
+ The opencode-go API key can be configured in multiple ways (resolved in this order):
99
+
100
+ 1. **`auth.json`** (recommended) — Add to `~/.pi/agent/auth.json`:
101
+ ```json
102
+ { "opencode-go": { "type": "api_key", "key": "your-api-key" } }
103
+ ```
104
+ The `key` field supports literal values, env var names, and shell commands (prefix with `!`). See [pi's auth file docs](https://github.com/badlogic/pi-mono) for details.
105
+ 2. **Runtime override** — Use the `--api-key` CLI flag
106
+ 3. **Environment variable** — Set `OPENCODE_API_KEY`
107
+
108
+ ## Environment Variables
109
+
110
+ | Variable | Required | Description |
111
+ |----------|----------|-------------|
112
+ | `OPENCODE_API_KEY` | No | Your opencode.ai API key (fallback if not in auth.json) |
113
+
114
+ ## Configuration
115
+
116
+ Add to your pi configuration for automatic loading:
117
+
118
+ ```json
119
+ {
120
+ "extensions": [
121
+ "/path/to/pi-opencode-go-provider"
122
+ ]
123
+ }
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1 @@
1
+ []
package/index.ts ADDED
@@ -0,0 +1,354 @@
1
+ /**
2
+ * opencode-go Provider Extension
3
+ *
4
+ * Registers opencode-go as a custom provider with multi-API support.
5
+ * Models use anthropic-messages or openai-completions as appropriate,
6
+ * with per-model baseUrl/api.
7
+ *
8
+ * Model resolution strategy: Stale-While-Revalidate
9
+ * 1. Serve stale immediately: disk cache → embedded models.json (zero-latency)
10
+ * 2. Revalidate in background: live API /models → merge with embedded → cache → hot-swap
11
+ * 3. patch.json + custom-models.json applied on top of whichever source won
12
+ *
13
+ * Merge order: [live|cache|embedded] → apply patch.json → merge custom-models.json
14
+ *
15
+ * Usage:
16
+ * # Set your API key
17
+ * export OPENCODE_API_KEY=your-api-key
18
+ *
19
+ * # Run pi with the extension
20
+ * pi -e /path/to/pi-opencode-go-provider
21
+ *
22
+ * Then use /model to select from available models
23
+ */
24
+
25
+ import { getAgentDir, type ExtensionAPI, type ModelRegistry } from "@earendil-works/pi-coding-agent";
26
+ import modelsData from "./models.json" with { type: "json" };
27
+ import customModelsData from "./custom-models.json" with { type: "json" };
28
+ import patchData from "./patch.json" with { type: "json" };
29
+ import fs from "fs";
30
+ import path from "path";
31
+
32
+ // ─── Types ────────────────────────────────────────────────────────────────────
33
+
34
+ type Api = "openai-completions" | "openai-responses" | "anthropic-messages" | "google-generative-ai";
35
+
36
+ interface ThinkingLevelMapValue {
37
+ [level: string]: string | null;
38
+ }
39
+
40
+ interface JsonModel {
41
+ id: string;
42
+ name: string;
43
+ api?: Api;
44
+ baseUrl?: string;
45
+ reasoning: boolean;
46
+ thinkingLevelMap?: ThinkingLevelMapValue;
47
+ input: ("text" | "image")[];
48
+ cost: {
49
+ input: number;
50
+ output: number;
51
+ cacheRead: number;
52
+ cacheWrite: number;
53
+ };
54
+ contextWindow: number;
55
+ maxTokens: number;
56
+ compat?: Record<string, unknown>;
57
+ }
58
+
59
+ interface PatchEntry {
60
+ name?: string;
61
+ api?: Api;
62
+ baseUrl?: string;
63
+ reasoning?: boolean;
64
+ thinkingLevelMap?: ThinkingLevelMapValue;
65
+ input?: ("text" | "image")[];
66
+ cost?: {
67
+ input?: number;
68
+ output?: number;
69
+ cacheRead?: number;
70
+ cacheWrite?: number;
71
+ };
72
+ contextWindow?: number;
73
+ maxTokens?: number;
74
+ compat?: Record<string, unknown>;
75
+ }
76
+
77
+ type PatchData = Record<string, PatchEntry>;
78
+
79
+ // ─── Patch Application ────────────────────────────────────────────────────────
80
+
81
+ function applyPatch(model: JsonModel, patch: PatchEntry): JsonModel {
82
+ const result = { ...model };
83
+
84
+ if (patch.name !== undefined) result.name = patch.name;
85
+ if (patch.api !== undefined) result.api = patch.api;
86
+ if (patch.baseUrl !== undefined) result.baseUrl = patch.baseUrl;
87
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
88
+ if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = patch.thinkingLevelMap;
89
+ if (patch.input !== undefined) result.input = patch.input;
90
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
91
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
92
+
93
+ if (patch.cost) {
94
+ result.cost = {
95
+ input: patch.cost.input ?? result.cost.input,
96
+ output: patch.cost.output ?? result.cost.output,
97
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
98
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
99
+ };
100
+ }
101
+ if (patch.compat) {
102
+ result.compat = { ...(result.compat || {}), ...patch.compat };
103
+ }
104
+
105
+ if (!result.reasoning && result.compat?.thinkingFormat) {
106
+ delete result.compat.thinkingFormat;
107
+ }
108
+ if (result.compat && Object.keys(result.compat).length === 0) {
109
+ delete result.compat;
110
+ }
111
+
112
+ return result;
113
+ }
114
+
115
+ /** Full pipeline: base models → patch → custom → result */
116
+ function buildModels(base: JsonModel[], custom: JsonModel[], patch: PatchData): JsonModel[] {
117
+ const modelMap = new Map<string, JsonModel>();
118
+
119
+ for (const model of base) {
120
+ modelMap.set(model.id, model);
121
+ }
122
+
123
+ for (const [id, patchEntry] of Object.entries(patch)) {
124
+ const existing = modelMap.get(id);
125
+ if (existing) {
126
+ modelMap.set(id, applyPatch(existing, patchEntry));
127
+ }
128
+ }
129
+
130
+ for (const model of custom) {
131
+ const existing = modelMap.get(model.id);
132
+ const patchEntry = patch[model.id];
133
+ if (existing && patchEntry) {
134
+ modelMap.set(model.id, applyPatch(model, patchEntry));
135
+ } else if (existing) {
136
+ modelMap.set(model.id, model);
137
+ } else if (patchEntry) {
138
+ modelMap.set(model.id, applyPatch(model, patchEntry));
139
+ } else {
140
+ modelMap.set(model.id, model);
141
+ }
142
+ }
143
+
144
+ return Array.from(modelMap.values());
145
+ }
146
+
147
+ // ─── Stale-While-Revalidate Model Sync ────────────────────────────────────────
148
+
149
+ const PROVIDER_ID = "opencode-go";
150
+ const BASE_URL = "https://opencode.ai/zen/go/v1";
151
+ const MODELS_URL = `${BASE_URL}/models`;
152
+ const CACHE_DIR = path.join(getAgentDir(), "cache");
153
+ const CACHE_PATH = path.join(CACHE_DIR, `${PROVIDER_ID}-models.json`);
154
+ const LIVE_FETCH_TIMEOUT_MS = 8000;
155
+
156
+ /** Map models.dev provider.npm to pi API type and base URL. */
157
+ const NPM_TO_API: Record<string, { api: Api; baseUrl: string }> = {
158
+ "@ai-sdk/anthropic": { api: "anthropic-messages", baseUrl: "https://opencode.ai/zen/go" },
159
+ };
160
+ const DEFAULT_API: { api: Api; baseUrl: string } = { api: "openai-completions", baseUrl: "https://opencode.ai/zen/go/v1" };
161
+
162
+ /** Transform a model from the opencode-go /v1/models API. Returns minimal data. */
163
+ function transformApiModel(apiModel: any): JsonModel {
164
+ const npm = apiModel.provider?.npm;
165
+ const { api, baseUrl } = (npm && NPM_TO_API[npm]) || DEFAULT_API;
166
+ const rawInput = apiModel.modalities?.input || ["text"];
167
+ // Pi Model type only supports "text" and "image"
168
+ const input: ("text" | "image")[] = rawInput.filter((m: string) => m === "text" || m === "image") as ("text" | "image")[];
169
+ if (!input.includes("text")) input.unshift("text");
170
+
171
+ return {
172
+ id: apiModel.id,
173
+ name: apiModel.name || apiModel.id,
174
+ api,
175
+ baseUrl,
176
+ reasoning: apiModel.reasoning || false,
177
+ input,
178
+ cost: {
179
+ input: apiModel.cost?.input || 0,
180
+ output: apiModel.cost?.output || 0,
181
+ cacheRead: apiModel.cost?.cache_read || 0,
182
+ cacheWrite: apiModel.cost?.cache_write || 0,
183
+ },
184
+ contextWindow: apiModel.limit?.context || apiModel.context_length || 131072,
185
+ maxTokens: apiModel.limit?.output || 0,
186
+ };
187
+ }
188
+
189
+ async function fetchLiveModels(apiKey: string, signal?: AbortSignal): Promise<JsonModel[] | null> {
190
+ try {
191
+ const response = await fetch(MODELS_URL, {
192
+ headers: { Authorization: `Bearer ${apiKey}` },
193
+ signal: signal ? AbortSignal.any([AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS),
194
+ });
195
+ if (!response.ok) return null;
196
+ const data = await response.json();
197
+ const apiModels = Array.isArray(data) ? data : (data.data || []);
198
+ if (!Array.isArray(apiModels) || apiModels.length === 0) return null;
199
+ return apiModels.map(transformApiModel);
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function loadCachedModels(): JsonModel[] | null {
206
+ try {
207
+ const data = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
208
+ return Array.isArray(data) ? data : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function cacheModels(models: JsonModel[]): void {
215
+ try {
216
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
217
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(models, null, 2) + "\n");
218
+ } catch {
219
+ // Cache write failure is non-fatal
220
+ }
221
+ }
222
+
223
+ function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[]): JsonModel[] {
224
+ const embeddedMap = new Map(embeddedModels.map(m => [m.id, m]));
225
+ const seen = new Set<string>();
226
+ const result: JsonModel[] = [];
227
+ for (const liveModel of liveModels) {
228
+ const embedded = embeddedMap.get(liveModel.id);
229
+ seen.add(liveModel.id);
230
+ if (embedded) {
231
+ // Self-heal: live API pricing is authoritative field-by-field. Prefer the
232
+ // live cost when the API reports it (non-zero); fall back to embedded when
233
+ // the API is silent (0) so curated cacheRead/cacheWrite isn't clobbered and
234
+ // providers whose /models endpoint exposes no pricing keep their curated
235
+ // cost. Curation (reasoning/input/compat/name) still wins via ...embedded.
236
+ result.push({
237
+ ...liveModel,
238
+ ...embedded,
239
+ cost: {
240
+ input: liveModel.cost.input || embedded.cost.input,
241
+ output: liveModel.cost.output || embedded.cost.output,
242
+ cacheRead: liveModel.cost.cacheRead || embedded.cost.cacheRead,
243
+ cacheWrite: liveModel.cost.cacheWrite || embedded.cost.cacheWrite,
244
+ },
245
+ contextWindow: liveModel.contextWindow || embedded.contextWindow,
246
+ });
247
+ } else {
248
+ result.push(liveModel);
249
+ }
250
+ }
251
+ // Append any embedded models that the live API didn't return
252
+ for (const em of embeddedModels) {
253
+ if (!seen.has(em.id)) {
254
+ result.push(em);
255
+ }
256
+ }
257
+ return result;
258
+ }
259
+
260
+ function loadStaleModels(embeddedModels: JsonModel[]): JsonModel[] {
261
+ const cached = loadCachedModels();
262
+ if (!cached || cached.length === 0) return embeddedModels;
263
+
264
+ // Merge embedded models that are missing from cache (newly added models)
265
+ const cachedMap = new Map(cached.map(m => [m.id, m]));
266
+ for (const em of embeddedModels) {
267
+ if (!cachedMap.has(em.id)) {
268
+ cached.push(em);
269
+ }
270
+ }
271
+ return cached;
272
+ }
273
+
274
+ async function revalidateModels(apiKey: string | undefined, embeddedModels: JsonModel[], signal?: AbortSignal): Promise<JsonModel[] | null> {
275
+ if (!apiKey) return null;
276
+ const liveModels = await fetchLiveModels(apiKey, signal);
277
+ if (!liveModels || liveModels.length === 0) return null;
278
+ const merged = mergeWithEmbedded(liveModels, embeddedModels);
279
+ cacheModels(merged);
280
+ return merged;
281
+ }
282
+
283
+ // ─── API Key Resolution (via ModelRegistry) ────────────────────────────────────
284
+
285
+ let cachedApiKey: string | undefined;
286
+ let revalidateAbort: AbortController | null = null;
287
+
288
+ async function resolveApiKey(modelRegistry: ModelRegistry): Promise<void> {
289
+ cachedApiKey = await modelRegistry.getApiKeyForProvider("opencode-go") ?? undefined;
290
+ }
291
+
292
+ // ─── Extension Entry Point ────────────────────────────────────────────────────
293
+
294
+ export default function (pi: ExtensionAPI) {
295
+ const embeddedModels = modelsData as JsonModel[];
296
+ const customModels = customModelsData as JsonModel[];
297
+ const patches = patchData as PatchData;
298
+
299
+ const staleBase = loadStaleModels(embeddedModels);
300
+ const staleModels = buildModels(staleBase, customModels, patches);
301
+
302
+ pi.registerProvider("opencode-go", {
303
+ baseUrl: BASE_URL,
304
+ apiKey: "$OPENCODE_API_KEY",
305
+ api: "openai-completions",
306
+ models: staleModels.map(m => ({
307
+ id: m.id,
308
+ name: m.name,
309
+ api: m.api || "openai-completions",
310
+ baseUrl: m.baseUrl,
311
+ reasoning: m.reasoning,
312
+ thinkingLevelMap: m.thinkingLevelMap,
313
+ input: m.input,
314
+ cost: m.cost,
315
+ contextWindow: m.contextWindow,
316
+ maxTokens: m.maxTokens,
317
+ compat: m.compat,
318
+ })),
319
+ });
320
+
321
+ pi.on("session_start", async (_event, ctx) => {
322
+ revalidateAbort?.abort();
323
+ revalidateAbort = new AbortController();
324
+ const signal = revalidateAbort.signal;
325
+ resolveApiKey(ctx.modelRegistry).then(() => {
326
+ revalidateModels(cachedApiKey, embeddedModels, signal).then((freshBase) => {
327
+ if (freshBase && !signal.aborted) {
328
+ pi.registerProvider("opencode-go", {
329
+ baseUrl: BASE_URL,
330
+ apiKey: "$OPENCODE_API_KEY",
331
+ api: "openai-completions",
332
+ models: buildModels(freshBase, customModels, patches).map(m => ({
333
+ id: m.id,
334
+ name: m.name,
335
+ api: m.api || "openai-completions",
336
+ baseUrl: m.baseUrl,
337
+ reasoning: m.reasoning,
338
+ thinkingLevelMap: m.thinkingLevelMap,
339
+ input: m.input,
340
+ cost: m.cost,
341
+ contextWindow: m.contextWindow,
342
+ maxTokens: m.maxTokens,
343
+ compat: m.compat,
344
+ })),
345
+ });
346
+ }
347
+ });
348
+ });
349
+ });
350
+
351
+ pi.on("session_shutdown", () => {
352
+ revalidateAbort?.abort();
353
+ });
354
+ }
package/models.json ADDED
@@ -0,0 +1,242 @@
1
+ [
2
+ {
3
+ "id": "deepseek-v4-flash",
4
+ "name": "DeepSeek V4 Flash",
5
+ "api": "openai-completions",
6
+ "baseUrl": "https://opencode.ai/zen/go/v1",
7
+ "reasoning": true,
8
+ "input": [
9
+ "text"
10
+ ],
11
+ "cost": {
12
+ "input": 0.14,
13
+ "output": 0.28,
14
+ "cacheRead": 0.0028,
15
+ "cacheWrite": 0
16
+ },
17
+ "contextWindow": 1000000,
18
+ "maxTokens": 384000
19
+ },
20
+ {
21
+ "id": "qwen3.7-plus",
22
+ "name": "Qwen3.7 Plus",
23
+ "api": "anthropic-messages",
24
+ "baseUrl": "https://opencode.ai/zen/go",
25
+ "reasoning": true,
26
+ "input": [
27
+ "text",
28
+ "image"
29
+ ],
30
+ "cost": {
31
+ "input": 0.4,
32
+ "output": 1.6,
33
+ "cacheRead": 0.04,
34
+ "cacheWrite": 0.5
35
+ },
36
+ "contextWindow": 1000000,
37
+ "maxTokens": 65536
38
+ },
39
+ {
40
+ "id": "qwen3.7-max",
41
+ "name": "Qwen3.7 Max",
42
+ "api": "anthropic-messages",
43
+ "baseUrl": "https://opencode.ai/zen/go",
44
+ "reasoning": true,
45
+ "input": [
46
+ "text"
47
+ ],
48
+ "cost": {
49
+ "input": 2.5,
50
+ "output": 7.5,
51
+ "cacheRead": 0.5,
52
+ "cacheWrite": 3.125
53
+ },
54
+ "contextWindow": 1000000,
55
+ "maxTokens": 65536
56
+ },
57
+ {
58
+ "id": "kimi-k2.7-code",
59
+ "name": "Kimi K2.7 Code",
60
+ "api": "openai-completions",
61
+ "baseUrl": "https://opencode.ai/zen/go/v1",
62
+ "reasoning": true,
63
+ "input": [
64
+ "text",
65
+ "image"
66
+ ],
67
+ "cost": {
68
+ "input": 0.95,
69
+ "output": 4,
70
+ "cacheRead": 0.19,
71
+ "cacheWrite": 0
72
+ },
73
+ "contextWindow": 262144,
74
+ "maxTokens": 262144
75
+ },
76
+ {
77
+ "id": "glm-5.1",
78
+ "name": "GLM-5.1",
79
+ "api": "openai-completions",
80
+ "baseUrl": "https://opencode.ai/zen/go/v1",
81
+ "reasoning": true,
82
+ "input": [
83
+ "text"
84
+ ],
85
+ "cost": {
86
+ "input": 1.4,
87
+ "output": 4.4,
88
+ "cacheRead": 0.26,
89
+ "cacheWrite": 0
90
+ },
91
+ "contextWindow": 202752,
92
+ "maxTokens": 32768
93
+ },
94
+ {
95
+ "id": "deepseek-v4-pro",
96
+ "name": "DeepSeek V4 Pro",
97
+ "api": "openai-completions",
98
+ "baseUrl": "https://opencode.ai/zen/go/v1",
99
+ "reasoning": true,
100
+ "input": [
101
+ "text"
102
+ ],
103
+ "cost": {
104
+ "input": 1.74,
105
+ "output": 3.48,
106
+ "cacheRead": 0.0145,
107
+ "cacheWrite": 0
108
+ },
109
+ "contextWindow": 1000000,
110
+ "maxTokens": 384000
111
+ },
112
+ {
113
+ "id": "glm-5.2",
114
+ "name": "GLM-5.2",
115
+ "api": "openai-completions",
116
+ "baseUrl": "https://opencode.ai/zen/go/v1",
117
+ "reasoning": true,
118
+ "input": [
119
+ "text"
120
+ ],
121
+ "cost": {
122
+ "input": 1.4,
123
+ "output": 4.4,
124
+ "cacheRead": 0.26,
125
+ "cacheWrite": 0
126
+ },
127
+ "contextWindow": 1000000,
128
+ "maxTokens": 131072
129
+ },
130
+ {
131
+ "id": "minimax-m3",
132
+ "name": "MiniMax-M3",
133
+ "api": "anthropic-messages",
134
+ "baseUrl": "https://opencode.ai/zen/go",
135
+ "reasoning": true,
136
+ "input": [
137
+ "text",
138
+ "image"
139
+ ],
140
+ "cost": {
141
+ "input": 0.3,
142
+ "output": 1.2,
143
+ "cacheRead": 0.06,
144
+ "cacheWrite": 0
145
+ },
146
+ "contextWindow": 1000000,
147
+ "maxTokens": 131072
148
+ },
149
+ {
150
+ "id": "minimax-m2.7",
151
+ "name": "MiniMax-M2.7",
152
+ "api": "anthropic-messages",
153
+ "baseUrl": "https://opencode.ai/zen/go",
154
+ "reasoning": true,
155
+ "input": [
156
+ "text"
157
+ ],
158
+ "cost": {
159
+ "input": 0.3,
160
+ "output": 1.2,
161
+ "cacheRead": 0.06,
162
+ "cacheWrite": 0
163
+ },
164
+ "contextWindow": 204800,
165
+ "maxTokens": 131072
166
+ },
167
+ {
168
+ "id": "mimo-v2.5",
169
+ "name": "MiMo V2.5",
170
+ "api": "openai-completions",
171
+ "baseUrl": "https://opencode.ai/zen/go/v1",
172
+ "reasoning": true,
173
+ "input": [
174
+ "text",
175
+ "image"
176
+ ],
177
+ "cost": {
178
+ "input": 0.14,
179
+ "output": 0.28,
180
+ "cacheRead": 0.0028,
181
+ "cacheWrite": 0
182
+ },
183
+ "contextWindow": 1000000,
184
+ "maxTokens": 128000
185
+ },
186
+ {
187
+ "id": "kimi-k2.6",
188
+ "name": "Kimi K2.6",
189
+ "api": "openai-completions",
190
+ "baseUrl": "https://opencode.ai/zen/go/v1",
191
+ "reasoning": true,
192
+ "input": [
193
+ "text",
194
+ "image"
195
+ ],
196
+ "cost": {
197
+ "input": 0.95,
198
+ "output": 4,
199
+ "cacheRead": 0.16,
200
+ "cacheWrite": 0
201
+ },
202
+ "contextWindow": 262144,
203
+ "maxTokens": 65536
204
+ },
205
+ {
206
+ "id": "mimo-v2.5-pro",
207
+ "name": "MiMo V2.5 Pro",
208
+ "api": "openai-completions",
209
+ "baseUrl": "https://opencode.ai/zen/go/v1",
210
+ "reasoning": true,
211
+ "input": [
212
+ "text"
213
+ ],
214
+ "cost": {
215
+ "input": 1.74,
216
+ "output": 3.48,
217
+ "cacheRead": 0.0145,
218
+ "cacheWrite": 0
219
+ },
220
+ "contextWindow": 1048576,
221
+ "maxTokens": 128000
222
+ },
223
+ {
224
+ "id": "qwen3.6-plus",
225
+ "name": "Qwen3.6 Plus",
226
+ "api": "anthropic-messages",
227
+ "baseUrl": "https://opencode.ai/zen/go",
228
+ "reasoning": true,
229
+ "input": [
230
+ "text",
231
+ "image"
232
+ ],
233
+ "cost": {
234
+ "input": 0.5,
235
+ "output": 3,
236
+ "cacheRead": 0.05,
237
+ "cacheWrite": 0.625
238
+ },
239
+ "contextWindow": 1000000,
240
+ "maxTokens": 65536
241
+ }
242
+ ]
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "pi-opencode-go-provider",
3
+ "version": "1.0.1",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "pi-opencode-go-provider",
9
+ "version": "1.0.1",
10
+ "license": "MIT"
11
+ }
12
+ }
13
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "pi-opencode-go-provider",
3
+ "version": "1.0.2",
4
+ "description": "Opencode Go provider extension for pi - Fast, efficient GLM, Kimi, and MiniMax models through the opencode.ai API",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "keywords": [
8
+ "pi",
9
+ "extension",
10
+ "provider",
11
+ "opencode",
12
+ "opencode-go",
13
+ "ai",
14
+ "llm",
15
+ "glm",
16
+ "kimi",
17
+ "minimax"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "pi": {
22
+ "extensions": [
23
+ "./index.ts"
24
+ ]
25
+ },
26
+ "scripts": {
27
+ "clean": "echo 'nothing to clean'",
28
+ "build": "echo 'nothing to build'",
29
+ "check": "echo 'nothing to check'",
30
+ "update-models": "node scripts/update-models.js"
31
+ }
32
+ }
package/patch.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "deepseek-v4-flash": {
3
+ "compat": { "requiresReasoningContentOnAssistantMessages": true, "thinkingFormat": "deepseek" },
4
+ "thinkingLevelMap": { "minimal": null, "low": null, "medium": null, "high": "high", "xhigh": "max" }
5
+ },
6
+ "deepseek-v4-pro": {
7
+ "compat": { "requiresReasoningContentOnAssistantMessages": true, "thinkingFormat": "deepseek" },
8
+ "thinkingLevelMap": { "minimal": null, "low": null, "medium": null, "high": "high", "xhigh": "max" }
9
+ },
10
+ "kimi-k2.6": {
11
+ "compat": { "thinkingFormat": "deepseek", "supportsReasoningEffort": false },
12
+ "thinkingLevelMap": { "minimal": null, "low": null, "medium": null }
13
+ },
14
+ "qwen3.6-plus": {
15
+ "compat": { "thinkingFormat": "qwen" }
16
+ }
17
+ }
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Script to update opencode-go models from models.dev API
5
+ * Writes to models.json (Pi-native format) and updates README.md.
6
+ *
7
+ * Pipeline: models.dev API → models.json (idempotent, re-runnable)
8
+ * patch.json + custom-models.json for our layered overrides
9
+ */
10
+
11
+ import https from 'https';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ const API_URL = 'https://models.dev/api.json';
20
+ const PROVIDER_ID = 'opencode-go';
21
+
22
+ // Map models.dev provider.npm to pi API type and base URL
23
+ const NPM_TO_API = {
24
+ '@ai-sdk/anthropic': { api: 'anthropic-messages', baseUrl: 'https://opencode.ai/zen/go' },
25
+ };
26
+ const DEFAULT_API = { api: 'openai-completions', baseUrl: 'https://opencode.ai/zen/go/v1' };
27
+
28
+ // Fetch JSON from URL
29
+ function fetchJSON(url) {
30
+ return new Promise((resolve, reject) => {
31
+ https.get(url, (res) => {
32
+ let data = '';
33
+ res.on('data', chunk => data += chunk);
34
+ res.on('end', () => {
35
+ try {
36
+ resolve(JSON.parse(data));
37
+ } catch (e) {
38
+ reject(new Error(`Failed to parse JSON: ${e.message}`));
39
+ }
40
+ });
41
+ }).on('error', reject);
42
+ });
43
+ }
44
+
45
+ // Format cost for display
46
+ function formatCost(cost) {
47
+ if (cost === 0) return 'Free';
48
+ return `$${cost.toFixed(2)}`;
49
+ }
50
+
51
+ // Format number with K/M suffix
52
+ function formatNumber(num) {
53
+ if (num === null || num === undefined) return '-';
54
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
55
+ if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
56
+ return num.toString();
57
+ }
58
+
59
+ // Get input types from modalities (pi supports "text" and "image" only)
60
+ function getInputTypes(modalities) {
61
+ const raw = modalities?.input || ['text'];
62
+ const filtered = raw.filter(m => m === 'text' || m === 'image');
63
+ if (!filtered.includes('text')) filtered.unshift('text');
64
+ return filtered;
65
+ }
66
+
67
+ // Get API label for display
68
+ function getApiLabel(api) {
69
+ const labels = {
70
+ 'anthropic-messages': 'Anthropic',
71
+ 'openai-responses': 'Responses',
72
+ 'openai-completions': 'Completions',
73
+ 'google-generative-ai': 'Gemini',
74
+ };
75
+ return labels[api] || api;
76
+ }
77
+
78
+ // Convert API model to Pi-native format with per-model api/baseUrl
79
+ function convertModel(model) {
80
+ const npm = model.provider?.npm;
81
+ const { api, baseUrl } = (npm && NPM_TO_API[npm]) || DEFAULT_API;
82
+ const inputTypes = getInputTypes(model.modalities);
83
+ const cost = model.cost || {};
84
+ const limit = model.limit || {};
85
+
86
+ return {
87
+ id: model.id,
88
+ name: model.name,
89
+ api,
90
+ baseUrl,
91
+ reasoning: model.reasoning || false,
92
+ input: inputTypes,
93
+ cost: {
94
+ input: cost.input || 0,
95
+ output: cost.output || 0,
96
+ cacheRead: cost.cache_read || 0,
97
+ cacheWrite: cost.cache_write || 0,
98
+ },
99
+ contextWindow: limit.context || 0,
100
+ maxTokens: limit.output || 0,
101
+ };
102
+ }
103
+
104
+ // Generate README model table row
105
+ function generateReadmeRow(model) {
106
+ const cost = model.cost || {};
107
+ const hasImage = model.input.includes('image');
108
+ const typeLabel = hasImage ? 'Text + Image' : 'Text';
109
+ return `| ${model.name} | ${getApiLabel(model.api)} | ${typeLabel} | ${formatNumber(model.contextWindow)} | ${formatNumber(model.maxTokens)} | ${formatCost(cost.input)} | ${formatCost(cost.output)} |`;
110
+ }
111
+
112
+ // Update README model table
113
+ function updateReadme(models) {
114
+ const readmePath = path.join(process.cwd(), 'README.md');
115
+ let readme;
116
+
117
+ try {
118
+ readme = fs.readFileSync(readmePath, 'utf8');
119
+ } catch {
120
+ console.log(' No README.md found, skipping README update');
121
+ return;
122
+ }
123
+
124
+ // Sort models by name
125
+ const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name));
126
+
127
+ // Generate table rows
128
+ const tableRows = sortedModels.map(generateReadmeRow).join('\n');
129
+ const newTable = `| Model | API | Type | Context | Max Tokens | Input Cost | Output Cost |
130
+ |-------|-----|------|---------|------------|------------|-------------|
131
+ ${tableRows}`;
132
+
133
+ // Replace table in README
134
+ const tableRegex = /\| Model \| API \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
135
+ if (readme.match(tableRegex)) {
136
+ readme = readme.replace(tableRegex, newTable);
137
+ } else {
138
+ // Fallback: replace old 6-column table format
139
+ const oldTableRegex = /\| Model \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
140
+ if (readme.match(oldTableRegex)) {
141
+ readme = readme.replace(oldTableRegex, newTable);
142
+ }
143
+ }
144
+
145
+ // Update model count in features
146
+ readme = readme.replace(/\*\*\d+ Optimized AI Models\*\*/, `**${models.length} Optimized AI Models**`);
147
+
148
+ fs.writeFileSync(readmePath, readme);
149
+ console.log(` Updated README.md with ${models.length} models`);
150
+ }
151
+
152
+ async function main() {
153
+ console.log('Fetching models from API...');
154
+
155
+ try {
156
+ const data = await fetchJSON(API_URL);
157
+ const provider = data[PROVIDER_ID];
158
+
159
+ if (!provider) {
160
+ throw new Error(`Provider "${PROVIDER_ID}" not found in API`);
161
+ }
162
+
163
+ if (!provider.models) {
164
+ throw new Error(`No models found for provider "${PROVIDER_ID}"`);
165
+ }
166
+
167
+ // Convert models object to array and filter out deprecated
168
+ const apiModels = Object.values(provider.models).filter(m => m.status !== 'deprecated');
169
+
170
+ console.log(`Found ${apiModels.length} active models`);
171
+
172
+ // Convert to Pi-native format and save to models.json
173
+ const models = apiModels.map(convertModel);
174
+ const modelsPath = path.join(process.cwd(), 'models.json');
175
+ fs.writeFileSync(modelsPath, JSON.stringify(models, null, 2) + '\n');
176
+ console.log(` Saved ${models.length} models to models.json`);
177
+
178
+ // Update README
179
+ updateReadme(models);
180
+
181
+ console.log('\nDone!');
182
+ } catch (error) {
183
+ console.error('Error:', error.message);
184
+ process.exit(1);
185
+ }
186
+ }
187
+
188
+ main();