pi-opencode-go-provider 1.0.10 → 1.0.11

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,57 @@
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
+ | `deprecated-models.json` | Graveyard for models the API delisted. update-models.js stamps them with deprecatedAt and pi keeps serving them for a 2-week grace period, then evicts them. Never edit by hand. |
11
+ | `README.md` (model table) | The table under `## Available Models` is replaced in-place by `update-models.js` after merging base models → patch → custom models. |
12
+
13
+ ## Correct Files to Edit
14
+
15
+ 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.
16
+
17
+ | File | Purpose |
18
+ |------|---------|
19
+ | `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. |
20
+ | `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). |
21
+ | `index.ts` | Provider extension code. |
22
+ | `scripts/update-models.js` | The sync script itself (edit only if changing how models are fetched/transformed). |
23
+
24
+ ## Data Flow
25
+
26
+ ```
27
+ Provider API ──fetch──► models.json ──apply──► patch.json ──merge──► custom-models.json
28
+ │ │ │
29
+ └────────────────────────────┴──────────────────────┘
30
+
31
+ README model table
32
+ ```
33
+
34
+ 1. `models.json` — base data from the provider API (auto-generated, DO NOT EDIT)
35
+ 2. `patch.json` — overrides applied on top (EDIT THIS for corrections/enrichments)
36
+ 3. `custom-models.json` — additional models not in the API (EDIT THIS for new models)
37
+ 4. README table — rendered from the merged result of all three (auto-generated, DO NOT EDIT)
38
+
39
+ ## Common Tasks
40
+
41
+ ### Add a compat setting or override pricing for an existing model
42
+ → Edit `patch.json`. Add an entry keyed by the model's `id`.
43
+
44
+ ### Add a model not available in the provider API
45
+ → Edit `custom-models.json`. Add a full model object to the array.
46
+
47
+ ### Update models from the provider API
48
+ → Run `node scripts/update-models.js` (may require an API key env var).
49
+
50
+ ### Regenerate the README model table
51
+ → Run `node scripts/update-models.js` — it updates both `models.json` and the README table.
52
+
53
+ ## TL;DR
54
+
55
+ - **Never edit `models.json`** — edit `patch.json` instead.
56
+ - **Never edit the README model table** — run the update script instead.
57
+ - `patch.json` and `custom-models.json` are the source files you should modify.
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ {}
package/models.json ADDED
@@ -0,0 +1,354 @@
1
+ [
2
+ {
3
+ "id": "kimi-k2.7-code",
4
+ "name": "Kimi K2.7 Code",
5
+ "api": "openai-completions",
6
+ "baseUrl": "https://opencode.ai/zen/go/v1",
7
+ "reasoning": true,
8
+ "input": [
9
+ "text",
10
+ "image"
11
+ ],
12
+ "cost": {
13
+ "input": 0.95,
14
+ "output": 4,
15
+ "cacheRead": 0.19,
16
+ "cacheWrite": 0
17
+ },
18
+ "contextWindow": 262144,
19
+ "maxTokens": 262144
20
+ },
21
+ {
22
+ "id": "qwen3.7-max",
23
+ "name": "Qwen3.7 Max",
24
+ "api": "openai-completions",
25
+ "baseUrl": "https://opencode.ai/zen/go/v1",
26
+ "reasoning": true,
27
+ "input": [
28
+ "text"
29
+ ],
30
+ "cost": {
31
+ "input": 2.5,
32
+ "output": 7.5,
33
+ "cacheRead": 0.5,
34
+ "cacheWrite": 3.125
35
+ },
36
+ "contextWindow": 1000000,
37
+ "maxTokens": 65536
38
+ },
39
+ {
40
+ "id": "kimi-k3",
41
+ "name": "Kimi K3",
42
+ "api": "openai-completions",
43
+ "baseUrl": "https://opencode.ai/zen/go/v1",
44
+ "reasoning": true,
45
+ "input": [
46
+ "text",
47
+ "image"
48
+ ],
49
+ "cost": {
50
+ "input": 3,
51
+ "output": 15,
52
+ "cacheRead": 0.3,
53
+ "cacheWrite": 0
54
+ },
55
+ "contextWindow": 1048576,
56
+ "maxTokens": 131072
57
+ },
58
+ {
59
+ "id": "deepseek-v4-flash",
60
+ "name": "DeepSeek V4 Flash",
61
+ "api": "openai-completions",
62
+ "baseUrl": "https://opencode.ai/zen/go/v1",
63
+ "reasoning": true,
64
+ "input": [
65
+ "text"
66
+ ],
67
+ "cost": {
68
+ "input": 0.22,
69
+ "output": 0.66,
70
+ "cacheRead": 0.007,
71
+ "cacheWrite": 0
72
+ },
73
+ "contextWindow": 1000000,
74
+ "maxTokens": 384000
75
+ },
76
+ {
77
+ "id": "mimo-v2.5",
78
+ "name": "MiMo V2.5",
79
+ "api": "openai-completions",
80
+ "baseUrl": "https://opencode.ai/zen/go/v1",
81
+ "reasoning": true,
82
+ "input": [
83
+ "text",
84
+ "image"
85
+ ],
86
+ "cost": {
87
+ "input": 0.14,
88
+ "output": 0.28,
89
+ "cacheRead": 0.0028,
90
+ "cacheWrite": 0
91
+ },
92
+ "contextWindow": 1000000,
93
+ "maxTokens": 128000
94
+ },
95
+ {
96
+ "id": "grok-4.5",
97
+ "name": "Grok 4.5",
98
+ "api": "openai-completions",
99
+ "baseUrl": "https://opencode.ai/zen/go/v1",
100
+ "reasoning": true,
101
+ "input": [
102
+ "text",
103
+ "image"
104
+ ],
105
+ "cost": {
106
+ "input": 2,
107
+ "output": 6,
108
+ "cacheRead": 0.5,
109
+ "cacheWrite": 0
110
+ },
111
+ "contextWindow": 500000,
112
+ "maxTokens": 500000
113
+ },
114
+ {
115
+ "id": "deepseek-v4-pro",
116
+ "name": "DeepSeek V4 Pro (New)",
117
+ "api": "openai-completions",
118
+ "baseUrl": "https://opencode.ai/zen/go/v1",
119
+ "reasoning": true,
120
+ "input": [
121
+ "text"
122
+ ],
123
+ "cost": {
124
+ "input": 0.66,
125
+ "output": 1.98,
126
+ "cacheRead": 0.022,
127
+ "cacheWrite": 0
128
+ },
129
+ "contextWindow": 1000000,
130
+ "maxTokens": 384000
131
+ },
132
+ {
133
+ "id": "gpt-5.6-luna",
134
+ "name": "GPT-5.6 Luna (2x usage)",
135
+ "api": "openai-completions",
136
+ "baseUrl": "https://opencode.ai/zen/go/v1",
137
+ "reasoning": true,
138
+ "input": [
139
+ "text",
140
+ "image"
141
+ ],
142
+ "cost": {
143
+ "input": 0.1,
144
+ "output": 0.6,
145
+ "cacheRead": 0.01,
146
+ "cacheWrite": 0.125
147
+ },
148
+ "contextWindow": 1050000,
149
+ "maxTokens": 128000
150
+ },
151
+ {
152
+ "id": "minimax-m3",
153
+ "name": "MiniMax-M3",
154
+ "api": "anthropic-messages",
155
+ "baseUrl": "https://opencode.ai/zen/go",
156
+ "reasoning": true,
157
+ "input": [
158
+ "text",
159
+ "image"
160
+ ],
161
+ "cost": {
162
+ "input": 0.3,
163
+ "output": 1.2,
164
+ "cacheRead": 0.06,
165
+ "cacheWrite": 0
166
+ },
167
+ "contextWindow": 1000000,
168
+ "maxTokens": 131072
169
+ },
170
+ {
171
+ "id": "minimax-m2.7",
172
+ "name": "MiniMax-M2.7",
173
+ "api": "anthropic-messages",
174
+ "baseUrl": "https://opencode.ai/zen/go",
175
+ "reasoning": true,
176
+ "input": [
177
+ "text"
178
+ ],
179
+ "cost": {
180
+ "input": 0.3,
181
+ "output": 1.2,
182
+ "cacheRead": 0.06,
183
+ "cacheWrite": 0
184
+ },
185
+ "contextWindow": 204800,
186
+ "maxTokens": 131072
187
+ },
188
+ {
189
+ "id": "qwen3.8-max",
190
+ "name": "Qwen3.8 Max",
191
+ "api": "openai-completions",
192
+ "baseUrl": "https://opencode.ai/zen/go/v1",
193
+ "reasoning": true,
194
+ "input": [
195
+ "text",
196
+ "image"
197
+ ],
198
+ "cost": {
199
+ "input": 2,
200
+ "output": 6,
201
+ "cacheRead": 0.25,
202
+ "cacheWrite": 2.5
203
+ },
204
+ "contextWindow": 1000000,
205
+ "maxTokens": 131072
206
+ },
207
+ {
208
+ "id": "qwen3.7-plus",
209
+ "name": "Qwen3.7 Plus",
210
+ "api": "openai-completions",
211
+ "baseUrl": "https://opencode.ai/zen/go/v1",
212
+ "reasoning": true,
213
+ "input": [
214
+ "text",
215
+ "image"
216
+ ],
217
+ "cost": {
218
+ "input": 0.4,
219
+ "output": 1.6,
220
+ "cacheRead": 0.04,
221
+ "cacheWrite": 0.5
222
+ },
223
+ "contextWindow": 1000000,
224
+ "maxTokens": 65536
225
+ },
226
+ {
227
+ "id": "glm-5.3",
228
+ "name": "GLM-5.3",
229
+ "api": "openai-completions",
230
+ "baseUrl": "https://opencode.ai/zen/go/v1",
231
+ "reasoning": true,
232
+ "input": [
233
+ "text"
234
+ ],
235
+ "cost": {
236
+ "input": 1.4,
237
+ "output": 4.4,
238
+ "cacheRead": 0.26,
239
+ "cacheWrite": 0
240
+ },
241
+ "contextWindow": 1000000,
242
+ "maxTokens": 131072
243
+ },
244
+ {
245
+ "id": "glm-5.2",
246
+ "name": "GLM-5.2",
247
+ "api": "openai-completions",
248
+ "baseUrl": "https://opencode.ai/zen/go/v1",
249
+ "reasoning": true,
250
+ "input": [
251
+ "text"
252
+ ],
253
+ "cost": {
254
+ "input": 1.4,
255
+ "output": 4.4,
256
+ "cacheRead": 0.26,
257
+ "cacheWrite": 0
258
+ },
259
+ "contextWindow": 1000000,
260
+ "maxTokens": 131072
261
+ },
262
+ {
263
+ "id": "qwen3.6-plus",
264
+ "name": "Qwen3.6 Plus",
265
+ "api": "openai-completions",
266
+ "baseUrl": "https://opencode.ai/zen/go/v1",
267
+ "reasoning": true,
268
+ "input": [
269
+ "text",
270
+ "image"
271
+ ],
272
+ "cost": {
273
+ "input": 0.5,
274
+ "output": 3,
275
+ "cacheRead": 0.05,
276
+ "cacheWrite": 0.625
277
+ },
278
+ "contextWindow": 1000000,
279
+ "maxTokens": 65536
280
+ },
281
+ {
282
+ "id": "glm-5.1",
283
+ "name": "GLM-5.1",
284
+ "api": "openai-completions",
285
+ "baseUrl": "https://opencode.ai/zen/go/v1",
286
+ "reasoning": true,
287
+ "input": [
288
+ "text"
289
+ ],
290
+ "cost": {
291
+ "input": 1.4,
292
+ "output": 4.4,
293
+ "cacheRead": 0.26,
294
+ "cacheWrite": 0
295
+ },
296
+ "contextWindow": 202752,
297
+ "maxTokens": 32768
298
+ },
299
+ {
300
+ "id": "mimo-v2.5-pro",
301
+ "name": "MiMo V2.5 Pro",
302
+ "api": "openai-completions",
303
+ "baseUrl": "https://opencode.ai/zen/go/v1",
304
+ "reasoning": true,
305
+ "input": [
306
+ "text"
307
+ ],
308
+ "cost": {
309
+ "input": 0.435,
310
+ "output": 0.87,
311
+ "cacheRead": 0.003625,
312
+ "cacheWrite": 0
313
+ },
314
+ "contextWindow": 1048576,
315
+ "maxTokens": 128000
316
+ },
317
+ {
318
+ "id": "hy3",
319
+ "name": "Hy3",
320
+ "api": "openai-completions",
321
+ "baseUrl": "https://opencode.ai/zen/go/v1",
322
+ "reasoning": true,
323
+ "input": [
324
+ "text"
325
+ ],
326
+ "cost": {
327
+ "input": 0.14,
328
+ "output": 0.58,
329
+ "cacheRead": 0.035,
330
+ "cacheWrite": 0
331
+ },
332
+ "contextWindow": 256000,
333
+ "maxTokens": 64000
334
+ },
335
+ {
336
+ "id": "kimi-k2.6",
337
+ "name": "Kimi K2.6",
338
+ "api": "openai-completions",
339
+ "baseUrl": "https://opencode.ai/zen/go/v1",
340
+ "reasoning": true,
341
+ "input": [
342
+ "text",
343
+ "image"
344
+ ],
345
+ "cost": {
346
+ "input": 0.95,
347
+ "output": 4,
348
+ "cacheRead": 0.16,
349
+ "cacheWrite": 0
350
+ },
351
+ "contextWindow": 262144,
352
+ "maxTokens": 65536
353
+ }
354
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-opencode-go-provider",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Opencode Go provider extension for pi - Fast, efficient GLM, Kimi, and MiniMax models through the opencode.ai API",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -23,10 +23,6 @@
23
23
  "./index.ts"
24
24
  ]
25
25
  },
26
- "files": [
27
- "README.md",
28
- "LICENSE"
29
- ],
30
26
  "scripts": {
31
27
  "clean": "echo 'nothing to clean'",
32
28
  "build": "echo 'nothing to build'",
package/patch.json ADDED
@@ -0,0 +1,176 @@
1
+ {
2
+ "glm-5.1": {
3
+ "compat": {
4
+ "supportsStore": false,
5
+ "supportsDeveloperRole": false,
6
+ "maxTokensField": "max_tokens"
7
+ }
8
+ },
9
+ "deepseek-v4-flash": {
10
+ "name": "DeepSeek V4 Flash",
11
+ "thinkingLevelMap": {
12
+ "minimal": null,
13
+ "low": null,
14
+ "medium": null,
15
+ "high": "high",
16
+ "max": "max"
17
+ },
18
+ "compat": {
19
+ "supportsStore": false,
20
+ "supportsDeveloperRole": false,
21
+ "maxTokensField": "max_tokens",
22
+ "requiresReasoningContentOnAssistantMessages": true,
23
+ "thinkingFormat": "deepseek"
24
+ }
25
+ },
26
+ "minimax-m2.7": {
27
+ "api": "openai-completions",
28
+ "baseUrl": "https://opencode.ai/zen/go/v1",
29
+ "compat": {
30
+ "supportsStore": false,
31
+ "supportsDeveloperRole": false,
32
+ "maxTokensField": "max_tokens"
33
+ }
34
+ },
35
+ "glm-5.2": {
36
+ "thinkingLevelMap": {
37
+ "off": null,
38
+ "minimal": null,
39
+ "low": null,
40
+ "medium": null,
41
+ "high": "high",
42
+ "xhigh": null,
43
+ "max": "max"
44
+ },
45
+ "compat": {
46
+ "supportsStore": false,
47
+ "supportsDeveloperRole": false,
48
+ "maxTokensField": "max_tokens"
49
+ }
50
+ },
51
+ "kimi-k2.6": {
52
+ "thinkingLevelMap": {
53
+ "minimal": null,
54
+ "low": null,
55
+ "medium": null
56
+ },
57
+ "compat": {
58
+ "supportsStore": false,
59
+ "supportsDeveloperRole": false,
60
+ "thinkingFormat": "deepseek",
61
+ "supportsReasoningEffort": false,
62
+ "maxTokensField": "max_tokens",
63
+ "supportsLongCacheRetention": false
64
+ }
65
+ },
66
+ "hy3": {
67
+ "thinkingLevelMap": {
68
+ "off": "none",
69
+ "minimal": null,
70
+ "low": "low",
71
+ "medium": null,
72
+ "high": "high",
73
+ "xhigh": null,
74
+ "max": null
75
+ },
76
+ "compat": {
77
+ "supportsStore": false,
78
+ "supportsDeveloperRole": false,
79
+ "maxTokensField": "max_tokens"
80
+ }
81
+ },
82
+ "deepseek-v4-pro": {
83
+ "thinkingLevelMap": {
84
+ "minimal": null,
85
+ "low": null,
86
+ "medium": null,
87
+ "high": "high",
88
+ "max": "max"
89
+ },
90
+ "compat": {
91
+ "supportsStore": false,
92
+ "supportsDeveloperRole": false,
93
+ "maxTokensField": "max_tokens",
94
+ "requiresReasoningContentOnAssistantMessages": true,
95
+ "thinkingFormat": "deepseek"
96
+ }
97
+ },
98
+ "mimo-v2.5": {
99
+ "compat": {
100
+ "supportsStore": false,
101
+ "supportsDeveloperRole": false,
102
+ "maxTokensField": "max_tokens"
103
+ }
104
+ },
105
+ "grok-4.5": {
106
+ "api": "openai-responses",
107
+ "thinkingLevelMap": {
108
+ "off": null,
109
+ "minimal": null,
110
+ "low": "low",
111
+ "medium": "medium",
112
+ "high": "high",
113
+ "xhigh": null,
114
+ "max": null
115
+ },
116
+ "compat": {
117
+ "sessionAffinityFormat": "openai-nosession"
118
+ }
119
+ },
120
+ "kimi-k2.7-code": {
121
+ "compat": {
122
+ "supportsStore": false,
123
+ "supportsDeveloperRole": false,
124
+ "maxTokensField": "max_tokens"
125
+ }
126
+ },
127
+ "kimi-k3": {
128
+ "name": "Kimi K3 (2x usage)",
129
+ "thinkingLevelMap": {
130
+ "off": null,
131
+ "minimal": null,
132
+ "low": null,
133
+ "medium": null,
134
+ "high": null,
135
+ "xhigh": null,
136
+ "max": "max"
137
+ },
138
+ "compat": {
139
+ "supportsStore": false,
140
+ "supportsDeveloperRole": false,
141
+ "maxTokensField": "max_tokens"
142
+ }
143
+ },
144
+ "mimo-v2.5-pro": {
145
+ "compat": {
146
+ "supportsStore": false,
147
+ "supportsDeveloperRole": false,
148
+ "maxTokensField": "max_tokens"
149
+ }
150
+ },
151
+ "gpt-5.6-luna": {
152
+ "api": "openai-responses",
153
+ "thinkingLevelMap": {
154
+ "off": null,
155
+ "minimal": null,
156
+ "low": "low",
157
+ "medium": "medium",
158
+ "high": "high",
159
+ "xhigh": "xhigh",
160
+ "max": "max"
161
+ },
162
+ "compat": {
163
+ "sessionAffinityFormat": "openai-nosession"
164
+ }
165
+ },
166
+ "qwen3.6-plus": {
167
+ "api": "openai-completions",
168
+ "baseUrl": "https://opencode.ai/zen/go/v1",
169
+ "compat": {
170
+ "supportsStore": false,
171
+ "supportsDeveloperRole": false,
172
+ "thinkingFormat": "qwen",
173
+ "maxTokensField": "max_tokens"
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,319 @@
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 '—';
48
+ if (cost === null || cost === undefined) return '—';
49
+ return '$' + cost.toFixed(2);
50
+ }
51
+
52
+ // Format number with K/M suffix
53
+ function formatNumber(num) {
54
+ if (num === null || num === undefined) return '-';
55
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
56
+ if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
57
+ return num.toString();
58
+ }
59
+
60
+ // Get input types from modalities (pi supports "text" and "image" only)
61
+ function getInputTypes(modalities) {
62
+ const raw = modalities?.input || ['text'];
63
+ const filtered = raw.filter(m => m === 'text' || m === 'image');
64
+ if (!filtered.includes('text')) filtered.unshift('text');
65
+ return filtered;
66
+ }
67
+
68
+ // Get API label for display
69
+ function getApiLabel(api) {
70
+ const labels = {
71
+ 'anthropic-messages': 'Anthropic',
72
+ 'openai-responses': 'Responses',
73
+ 'openai-completions': 'Completions',
74
+ 'google-generative-ai': 'Gemini',
75
+ };
76
+ return labels[api] || api;
77
+ }
78
+
79
+ // Convert API model to Pi-native format with per-model api/baseUrl
80
+ function convertModel(model) {
81
+ const npm = model.provider?.npm;
82
+ const { api, baseUrl } = (npm && NPM_TO_API[npm]) || DEFAULT_API;
83
+ const inputTypes = getInputTypes(model.modalities);
84
+ const cost = model.cost || {};
85
+ const limit = model.limit || {};
86
+
87
+ return {
88
+ id: model.id,
89
+ name: model.name,
90
+ api,
91
+ baseUrl,
92
+ reasoning: model.reasoning || false,
93
+ input: inputTypes,
94
+ cost: {
95
+ input: cost.input || 0,
96
+ output: cost.output || 0,
97
+ cacheRead: cost.cache_read || 0,
98
+ cacheWrite: cost.cache_write || 0,
99
+ },
100
+ contextWindow: limit.context || 0,
101
+ maxTokens: limit.output || 0,
102
+ };
103
+ }
104
+
105
+ // Load a layered JSON model file.
106
+ function loadJson(fileName) {
107
+ try {
108
+ return JSON.parse(fs.readFileSync(path.join(process.cwd(), fileName), 'utf8'));
109
+ } catch {
110
+ return fileName === 'patch.json' ? {} : [];
111
+ }
112
+ }
113
+
114
+ // Deep-merge patch overrides into a model for README documentation. The
115
+ // generated models.json remains API-derived; only the displayed model map uses patches.
116
+ function applyPatch(model, patch) {
117
+ if (!patch) return model;
118
+ const result = { ...model };
119
+ for (const [key, value] of Object.entries(patch)) {
120
+ if (key === 'cost' || key === 'compat' || key === 'thinkingLevelMap') {
121
+ result[key] = { ...(result[key] || {}), ...value };
122
+ } else {
123
+ result[key] = value;
124
+ }
125
+ }
126
+ if (!result.reasoning) {
127
+ delete result.thinkingLevelMap;
128
+ if (result.compat?.thinkingFormat) delete result.compat.thinkingFormat;
129
+ }
130
+ return result;
131
+ }
132
+
133
+ // Merge API models, patches, and custom models in the same order as index.ts.
134
+ function buildModels(baseModels, customModels, patch) {
135
+ const byId = new Map(baseModels.map(model => [model.id, model]));
136
+ for (const [id, entry] of Object.entries(patch)) {
137
+ if (byId.has(id)) byId.set(id, applyPatch(byId.get(id), entry));
138
+ }
139
+ for (const model of customModels) {
140
+ byId.set(model.id, applyPatch(model, patch[model.id]));
141
+ }
142
+ return Array.from(byId.values());
143
+ }
144
+
145
+ // Generate README model table row
146
+ function generateReadmeRow(model) {
147
+ const cost = model.cost || {};
148
+ const hasImage = model.input.includes('image');
149
+ const typeLabel = hasImage ? 'Text + Image' : 'Text';
150
+ return `| ${model.name} | ${getApiLabel(model.api)} | ${typeLabel} | ${formatNumber(model.contextWindow)} | ${formatNumber(model.maxTokens)} | ${formatCost(cost.input)} | ${formatCost(cost.output)} |`;
151
+ }
152
+
153
+ // Update README model table
154
+ function updateReadme(models) {
155
+ const readmePath = path.join(process.cwd(), 'README.md');
156
+ let readme;
157
+
158
+ try {
159
+ readme = fs.readFileSync(readmePath, 'utf8');
160
+ } catch {
161
+ console.log(' No README.md found, skipping README update');
162
+ return;
163
+ }
164
+
165
+ // Sort models by name
166
+ const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name));
167
+
168
+ // Generate table rows
169
+ const tableRows = sortedModels.map(generateReadmeRow).join('\n');
170
+ const newTable = `| Model | API | Type | Context | Max Tokens | Input Cost | Output Cost |
171
+ |-------|-----|------|---------|------------|------------|-------------|
172
+ ${tableRows}`;
173
+
174
+ // Replace table in README
175
+ const tableRegex = /\| Model \| API \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
176
+ if (readme.match(tableRegex)) {
177
+ readme = readme.replace(tableRegex, newTable);
178
+ } else {
179
+ // Fallback: replace old 6-column table format
180
+ const oldTableRegex = /\| Model \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
181
+ if (readme.match(oldTableRegex)) {
182
+ readme = readme.replace(oldTableRegex, newTable);
183
+ }
184
+ }
185
+
186
+ // Update model count in features
187
+ readme = readme.replace(/\*\*\d+ Optimized AI Models\*\*/, `**${models.length} Optimized AI Models**`);
188
+
189
+ fs.writeFileSync(readmePath, readme);
190
+ console.log(` Updated README.md with ${models.length} models`);
191
+ }
192
+
193
+ // Grace period for delisted models: update-models.js moves models the API no
194
+ // longer lists into deprecated-models.json (stamped with deprecatedAt) instead
195
+ // of dropping them; the runtime appends them back so sessions and saved model
196
+ // settings keep working, and after 14 days they are evicted permanently.
197
+ const DEPRECATED_MODEL_TTL_MS = 14 * 24 * 60 * 60 * 1000;
198
+
199
+ /**
200
+ * Reconcile deprecated-models.json against the freshly fetched model list.
201
+ * - in old models.json but not the API: moved into the deprecated file
202
+ * (deprecatedAt = now; preserved on repeat runs so the grace clock is not reset)
203
+ * - back in the API: resurrected (dropped from the deprecated file)
204
+ * - deprecatedAt older than 14 days: evicted permanently
205
+ * Must run BEFORE the new models.json is written; it reads the old file itself.
206
+ */
207
+ function updateDeprecatedModels(modelsJsonPath, newModels) {
208
+ const deprecatedPath = path.join(path.dirname(modelsJsonPath), 'deprecated-models.json');
209
+
210
+ let oldModels = [];
211
+ try {
212
+ const parsed = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf8'));
213
+ if (Array.isArray(parsed)) oldModels = parsed;
214
+ } catch { /* first run: no previous models.json */ }
215
+
216
+ let deprecated = {};
217
+ try {
218
+ const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
219
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
220
+ } catch { /* no graveyard yet */ }
221
+
222
+ const currentIds = new Set(newModels.map(m => m.id));
223
+ const now = new Date().toISOString();
224
+ const added = [];
225
+ const resurrected = [];
226
+ const evicted = [];
227
+
228
+ for (const old of oldModels) {
229
+ if (old && old.id && !currentIds.has(old.id) && !deprecated[old.id]) {
230
+ deprecated[old.id] = { ...old, deprecatedAt: now };
231
+ added.push(old.id);
232
+ }
233
+ }
234
+
235
+ for (const [id, entry] of Object.entries(deprecated)) {
236
+ if (currentIds.has(id)) {
237
+ delete deprecated[id];
238
+ resurrected.push(id);
239
+ continue;
240
+ }
241
+ const removedAt = Date.parse(entry && entry.deprecatedAt ? entry.deprecatedAt : '');
242
+ if (Number.isNaN(removedAt) || Date.now() - removedAt > DEPRECATED_MODEL_TTL_MS) {
243
+ delete deprecated[id];
244
+ evicted.push(id);
245
+ }
246
+ }
247
+
248
+ if (added.length > 0 || resurrected.length > 0 || evicted.length > 0) {
249
+ fs.writeFileSync(deprecatedPath, JSON.stringify(deprecated, null, 2) + '\n');
250
+ console.log('Updated deprecated-models.json ' + JSON.stringify({ added, resurrected, evicted }));
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Grace-period deprecated models (deprecatedAt within TTL) with metadata stripped.
256
+ * Keeps the README table serving models that are delisted but still within their
257
+ * 14-day grace window.
258
+ */
259
+ function withDeprecatedForReadme(models) {
260
+ const deprecatedPath = path.join(process.cwd(), 'deprecated-models.json');
261
+ let deprecated = {};
262
+ try {
263
+ const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
264
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
265
+ } catch { /* no graveyard yet */ }
266
+ const now = Date.now();
267
+ const seen = new Set(models.map(m => m.id));
268
+ const extras = [];
269
+ for (const entry of Object.values(deprecated)) {
270
+ if (!entry || !entry.id || seen.has(entry.id)) continue;
271
+ const removedAt = Date.parse(entry.deprecatedAt || '');
272
+ if (Number.isNaN(removedAt) || now - removedAt > DEPRECATED_MODEL_TTL_MS) continue;
273
+ const m = { ...entry };
274
+ delete m.deprecatedAt;
275
+ extras.push(m);
276
+ }
277
+ return extras.length > 0 ? [...models, ...extras] : models;
278
+ }
279
+ async function main() {
280
+ console.log('Fetching models from API...');
281
+
282
+ try {
283
+ const data = await fetchJSON(API_URL);
284
+ const provider = data[PROVIDER_ID];
285
+
286
+ if (!provider) {
287
+ throw new Error(`Provider "${PROVIDER_ID}" not found in API`);
288
+ }
289
+
290
+ if (!provider.models) {
291
+ throw new Error(`No models found for provider "${PROVIDER_ID}"`);
292
+ }
293
+
294
+ // Convert models object to array and filter out deprecated
295
+ const apiModels = Object.values(provider.models).filter(m => m.status !== 'deprecated');
296
+
297
+ console.log(`Found ${apiModels.length} active models`);
298
+
299
+ // Convert to Pi-native format and save to models.json
300
+ const models = apiModels.map(convertModel);
301
+ const modelsPath = path.join(process.cwd(), 'models.json');
302
+ // Move delisted models to deprecated-models.json BEFORE models.json is overwritten
303
+ updateDeprecatedModels(modelsPath, models);
304
+ fs.writeFileSync(modelsPath, JSON.stringify(models, null, 2) + '\n');
305
+ console.log(` Saved ${models.length} models to models.json`);
306
+
307
+ // Apply layered overrides for documentation: models.json → patch.json → custom-models.json.
308
+ const patch = loadJson('patch.json');
309
+ const customModels = Array.isArray(loadJson('custom-models.json')) ? loadJson('custom-models.json') : [];
310
+ updateReadme(buildModels(models, customModels, patch));
311
+
312
+ console.log('\nDone!');
313
+ } catch (error) {
314
+ console.error('Error:', error.message);
315
+ process.exit(1);
316
+ }
317
+ }
318
+
319
+ main();