pi-opencode-go-provider 1.0.9 → 1.0.10

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.
package/README.md CHANGED
@@ -68,8 +68,8 @@ pi
68
68
 
69
69
  | Model | API | Type | Context | Max Tokens | Input Cost | Output Cost |
70
70
  |-------|-----|------|---------|------------|------------|-------------|
71
- | DeepSeek V4 Flash | Completions | Text | 1.0M | 384K | $0.07 | $0.14 |
72
- | DeepSeek V4 Pro (New) | Completions | Text | 1.0M | 384K | $0.43 | $0.87 |
71
+ | DeepSeek V4 Flash | Completions | Text | 1.0M | 384K | $0.22 | $0.66 |
72
+ | DeepSeek V4 Pro (New) | Completions | Text | 1.0M | 384K | $0.66 | $1.98 |
73
73
  | GLM-5.1 | Completions | Text | 203K | 33K | $1.40 | $4.40 |
74
74
  | GLM-5.2 | Completions | Text | 1.0M | 131K | $1.40 | $4.40 |
75
75
  | GLM-5.3 | Completions | Text | 1.0M | 131K | $1.40 | $4.40 |
@@ -84,9 +84,9 @@ pi
84
84
  | MiniMax-M2.7 | Completions | Text | 205K | 131K | $0.30 | $1.20 |
85
85
  | MiniMax-M3 | Anthropic | Text + Image | 1.0M | 131K | $0.30 | $1.20 |
86
86
  | Qwen3.6 Plus | Completions | Text + Image | 1.0M | 66K | $0.50 | $3.00 |
87
- | Qwen3.7 Max | Anthropic | Text | 1.0M | 66K | $2.50 | $7.50 |
88
- | Qwen3.7 Plus | Anthropic | Text + Image | 1.0M | 66K | $0.40 | $1.60 |
89
- | Qwen3.8 Max | Anthropic | Text + Image | 1.0M | 131K | $2.00 | $6.00 |
87
+ | Qwen3.7 Max | Completions | Text | 1.0M | 66K | $2.50 | $7.50 |
88
+ | Qwen3.7 Plus | Completions | Text + Image | 1.0M | 66K | $0.40 | $1.60 |
89
+ | Qwen3.8 Max | Completions | Text + Image | 1.0M | 131K | $2.00 | $6.00 |
90
90
  *Costs are per million tokens. Prices subject to change - check [opencode.ai](https://opencode.ai) for current pricing.*
91
91
 
92
92
  ## Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-opencode-go-provider",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
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,6 +23,10 @@
23
23
  "./index.ts"
24
24
  ]
25
25
  },
26
+ "files": [
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
26
30
  "scripts": {
27
31
  "clean": "echo 'nothing to clean'",
28
32
  "build": "echo 'nothing to build'",
@@ -1,4 +0,0 @@
1
- github: monotykamary
2
- ko_fi: monotykamary
3
- buy_me_a_coffee: monotykamary
4
- polar: monotykamary
package/AGENTS.md DELETED
@@ -1,57 +0,0 @@
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.
@@ -1 +0,0 @@
1
- []
@@ -1 +0,0 @@
1
- {}
package/models.json DELETED
@@ -1,354 +0,0 @@
1
- [
2
- {
3
- "id": "glm-5.3",
4
- "name": "GLM-5.3",
5
- "api": "openai-completions",
6
- "baseUrl": "https://opencode.ai/zen/go/v1",
7
- "reasoning": true,
8
- "input": [
9
- "text"
10
- ],
11
- "cost": {
12
- "input": 1.4,
13
- "output": 4.4,
14
- "cacheRead": 0.26,
15
- "cacheWrite": 0
16
- },
17
- "contextWindow": 1000000,
18
- "maxTokens": 131072
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": "glm-5.1",
41
- "name": "GLM-5.1",
42
- "api": "openai-completions",
43
- "baseUrl": "https://opencode.ai/zen/go/v1",
44
- "reasoning": true,
45
- "input": [
46
- "text"
47
- ],
48
- "cost": {
49
- "input": 1.4,
50
- "output": 4.4,
51
- "cacheRead": 0.26,
52
- "cacheWrite": 0
53
- },
54
- "contextWindow": 202752,
55
- "maxTokens": 32768
56
- },
57
- {
58
- "id": "deepseek-v4-flash",
59
- "name": "DeepSeek V4 Flash (2x usage)",
60
- "api": "openai-completions",
61
- "baseUrl": "https://opencode.ai/zen/go/v1",
62
- "reasoning": true,
63
- "input": [
64
- "text"
65
- ],
66
- "cost": {
67
- "input": 0.07,
68
- "output": 0.14,
69
- "cacheRead": 0.0014,
70
- "cacheWrite": 0
71
- },
72
- "contextWindow": 1000000,
73
- "maxTokens": 384000
74
- },
75
- {
76
- "id": "minimax-m2.7",
77
- "name": "MiniMax-M2.7",
78
- "api": "anthropic-messages",
79
- "baseUrl": "https://opencode.ai/zen/go",
80
- "reasoning": true,
81
- "input": [
82
- "text"
83
- ],
84
- "cost": {
85
- "input": 0.3,
86
- "output": 1.2,
87
- "cacheRead": 0.06,
88
- "cacheWrite": 0
89
- },
90
- "contextWindow": 204800,
91
- "maxTokens": 131072
92
- },
93
- {
94
- "id": "glm-5.2",
95
- "name": "GLM-5.2",
96
- "api": "openai-completions",
97
- "baseUrl": "https://opencode.ai/zen/go/v1",
98
- "reasoning": true,
99
- "input": [
100
- "text"
101
- ],
102
- "cost": {
103
- "input": 1.4,
104
- "output": 4.4,
105
- "cacheRead": 0.26,
106
- "cacheWrite": 0
107
- },
108
- "contextWindow": 1000000,
109
- "maxTokens": 131072
110
- },
111
- {
112
- "id": "qwen3.7-max",
113
- "name": "Qwen3.7 Max",
114
- "api": "anthropic-messages",
115
- "baseUrl": "https://opencode.ai/zen/go",
116
- "reasoning": true,
117
- "input": [
118
- "text"
119
- ],
120
- "cost": {
121
- "input": 2.5,
122
- "output": 7.5,
123
- "cacheRead": 0.5,
124
- "cacheWrite": 3.125
125
- },
126
- "contextWindow": 1000000,
127
- "maxTokens": 65536
128
- },
129
- {
130
- "id": "kimi-k2.6",
131
- "name": "Kimi K2.6",
132
- "api": "openai-completions",
133
- "baseUrl": "https://opencode.ai/zen/go/v1",
134
- "reasoning": true,
135
- "input": [
136
- "text",
137
- "image"
138
- ],
139
- "cost": {
140
- "input": 0.95,
141
- "output": 4,
142
- "cacheRead": 0.16,
143
- "cacheWrite": 0
144
- },
145
- "contextWindow": 262144,
146
- "maxTokens": 65536
147
- },
148
- {
149
- "id": "minimax-m3",
150
- "name": "MiniMax-M3",
151
- "api": "anthropic-messages",
152
- "baseUrl": "https://opencode.ai/zen/go",
153
- "reasoning": true,
154
- "input": [
155
- "text",
156
- "image"
157
- ],
158
- "cost": {
159
- "input": 0.3,
160
- "output": 1.2,
161
- "cacheRead": 0.06,
162
- "cacheWrite": 0
163
- },
164
- "contextWindow": 1000000,
165
- "maxTokens": 131072
166
- },
167
- {
168
- "id": "hy3",
169
- "name": "Hy3",
170
- "api": "openai-completions",
171
- "baseUrl": "https://opencode.ai/zen/go/v1",
172
- "reasoning": true,
173
- "input": [
174
- "text"
175
- ],
176
- "cost": {
177
- "input": 0.14,
178
- "output": 0.58,
179
- "cacheRead": 0.035,
180
- "cacheWrite": 0
181
- },
182
- "contextWindow": 256000,
183
- "maxTokens": 64000
184
- },
185
- {
186
- "id": "deepseek-v4-pro",
187
- "name": "DeepSeek V4 Pro (New)",
188
- "api": "openai-completions",
189
- "baseUrl": "https://opencode.ai/zen/go/v1",
190
- "reasoning": true,
191
- "input": [
192
- "text"
193
- ],
194
- "cost": {
195
- "input": 0.435,
196
- "output": 0.87,
197
- "cacheRead": 0.003625,
198
- "cacheWrite": 0
199
- },
200
- "contextWindow": 1000000,
201
- "maxTokens": 384000
202
- },
203
- {
204
- "id": "qwen3.8-max",
205
- "name": "Qwen3.8 Max",
206
- "api": "anthropic-messages",
207
- "baseUrl": "https://opencode.ai/zen/go",
208
- "reasoning": true,
209
- "input": [
210
- "text",
211
- "image"
212
- ],
213
- "cost": {
214
- "input": 2,
215
- "output": 6,
216
- "cacheRead": 0.25,
217
- "cacheWrite": 2.5
218
- },
219
- "contextWindow": 1000000,
220
- "maxTokens": 131072
221
- },
222
- {
223
- "id": "mimo-v2.5",
224
- "name": "MiMo V2.5",
225
- "api": "openai-completions",
226
- "baseUrl": "https://opencode.ai/zen/go/v1",
227
- "reasoning": true,
228
- "input": [
229
- "text",
230
- "image"
231
- ],
232
- "cost": {
233
- "input": 0.14,
234
- "output": 0.28,
235
- "cacheRead": 0.0028,
236
- "cacheWrite": 0
237
- },
238
- "contextWindow": 1000000,
239
- "maxTokens": 128000
240
- },
241
- {
242
- "id": "gpt-5.6-luna",
243
- "name": "GPT-5.6 Luna (2x usage)",
244
- "api": "openai-completions",
245
- "baseUrl": "https://opencode.ai/zen/go/v1",
246
- "reasoning": true,
247
- "input": [
248
- "text",
249
- "image"
250
- ],
251
- "cost": {
252
- "input": 0.1,
253
- "output": 0.6,
254
- "cacheRead": 0.01,
255
- "cacheWrite": 0.125
256
- },
257
- "contextWindow": 1050000,
258
- "maxTokens": 128000
259
- },
260
- {
261
- "id": "grok-4.5",
262
- "name": "Grok 4.5",
263
- "api": "openai-completions",
264
- "baseUrl": "https://opencode.ai/zen/go/v1",
265
- "reasoning": true,
266
- "input": [
267
- "text",
268
- "image"
269
- ],
270
- "cost": {
271
- "input": 2,
272
- "output": 6,
273
- "cacheRead": 0.5,
274
- "cacheWrite": 0
275
- },
276
- "contextWindow": 500000,
277
- "maxTokens": 500000
278
- },
279
- {
280
- "id": "kimi-k2.7-code",
281
- "name": "Kimi K2.7 Code",
282
- "api": "openai-completions",
283
- "baseUrl": "https://opencode.ai/zen/go/v1",
284
- "reasoning": true,
285
- "input": [
286
- "text",
287
- "image"
288
- ],
289
- "cost": {
290
- "input": 0.95,
291
- "output": 4,
292
- "cacheRead": 0.19,
293
- "cacheWrite": 0
294
- },
295
- "contextWindow": 262144,
296
- "maxTokens": 262144
297
- },
298
- {
299
- "id": "kimi-k3",
300
- "name": "Kimi K3",
301
- "api": "openai-completions",
302
- "baseUrl": "https://opencode.ai/zen/go/v1",
303
- "reasoning": true,
304
- "input": [
305
- "text",
306
- "image"
307
- ],
308
- "cost": {
309
- "input": 3,
310
- "output": 15,
311
- "cacheRead": 0.3,
312
- "cacheWrite": 0
313
- },
314
- "contextWindow": 1048576,
315
- "maxTokens": 131072
316
- },
317
- {
318
- "id": "mimo-v2.5-pro",
319
- "name": "MiMo V2.5 Pro",
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.435,
328
- "output": 0.87,
329
- "cacheRead": 0.003625,
330
- "cacheWrite": 0
331
- },
332
- "contextWindow": 1048576,
333
- "maxTokens": 128000
334
- },
335
- {
336
- "id": "qwen3.6-plus",
337
- "name": "Qwen3.6 Plus",
338
- "api": "anthropic-messages",
339
- "baseUrl": "https://opencode.ai/zen/go",
340
- "reasoning": true,
341
- "input": [
342
- "text",
343
- "image"
344
- ],
345
- "cost": {
346
- "input": 0.5,
347
- "output": 3,
348
- "cacheRead": 0.05,
349
- "cacheWrite": 0.625
350
- },
351
- "contextWindow": 1000000,
352
- "maxTokens": 65536
353
- }
354
- ]
package/patch.json DELETED
@@ -1,176 +0,0 @@
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
- }
@@ -1,293 +0,0 @@
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
- // Load a layered JSON model file.
105
- function loadJson(fileName) {
106
- try {
107
- return JSON.parse(fs.readFileSync(path.join(process.cwd(), fileName), 'utf8'));
108
- } catch {
109
- return fileName === 'patch.json' ? {} : [];
110
- }
111
- }
112
-
113
- // Deep-merge patch overrides into a model for README documentation. The
114
- // generated models.json remains API-derived; only the displayed model map uses patches.
115
- function applyPatch(model, patch) {
116
- if (!patch) return model;
117
- const result = { ...model };
118
- for (const [key, value] of Object.entries(patch)) {
119
- if (key === 'cost' || key === 'compat' || key === 'thinkingLevelMap') {
120
- result[key] = { ...(result[key] || {}), ...value };
121
- } else {
122
- result[key] = value;
123
- }
124
- }
125
- if (!result.reasoning) {
126
- delete result.thinkingLevelMap;
127
- if (result.compat?.thinkingFormat) delete result.compat.thinkingFormat;
128
- }
129
- return result;
130
- }
131
-
132
- // Merge API models, patches, and custom models in the same order as index.ts.
133
- function buildModels(baseModels, customModels, patch) {
134
- const byId = new Map(baseModels.map(model => [model.id, model]));
135
- for (const [id, entry] of Object.entries(patch)) {
136
- if (byId.has(id)) byId.set(id, applyPatch(byId.get(id), entry));
137
- }
138
- for (const model of customModels) {
139
- byId.set(model.id, applyPatch(model, patch[model.id]));
140
- }
141
- return Array.from(byId.values());
142
- }
143
-
144
- // Generate README model table row
145
- function generateReadmeRow(model) {
146
- const cost = model.cost || {};
147
- const hasImage = model.input.includes('image');
148
- const typeLabel = hasImage ? 'Text + Image' : 'Text';
149
- return `| ${model.name} | ${getApiLabel(model.api)} | ${typeLabel} | ${formatNumber(model.contextWindow)} | ${formatNumber(model.maxTokens)} | ${formatCost(cost.input)} | ${formatCost(cost.output)} |`;
150
- }
151
-
152
- // Update README model table
153
- function updateReadme(models) {
154
- const readmePath = path.join(process.cwd(), 'README.md');
155
- let readme;
156
-
157
- try {
158
- readme = fs.readFileSync(readmePath, 'utf8');
159
- } catch {
160
- console.log(' No README.md found, skipping README update');
161
- return;
162
- }
163
-
164
- // Sort models by name
165
- const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name));
166
-
167
- // Generate table rows
168
- const tableRows = sortedModels.map(generateReadmeRow).join('\n');
169
- const newTable = `| Model | API | Type | Context | Max Tokens | Input Cost | Output Cost |
170
- |-------|-----|------|---------|------------|------------|-------------|
171
- ${tableRows}`;
172
-
173
- // Replace table in README
174
- const tableRegex = /\| Model \| API \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
175
- if (readme.match(tableRegex)) {
176
- readme = readme.replace(tableRegex, newTable);
177
- } else {
178
- // Fallback: replace old 6-column table format
179
- const oldTableRegex = /\| Model \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
180
- if (readme.match(oldTableRegex)) {
181
- readme = readme.replace(oldTableRegex, newTable);
182
- }
183
- }
184
-
185
- // Update model count in features
186
- readme = readme.replace(/\*\*\d+ Optimized AI Models\*\*/, `**${models.length} Optimized AI Models**`);
187
-
188
- fs.writeFileSync(readmePath, readme);
189
- console.log(` Updated README.md with ${models.length} models`);
190
- }
191
-
192
- // Grace period for delisted models: update-models.js moves models the API no
193
- // longer lists into deprecated-models.json (stamped with deprecatedAt) instead
194
- // of dropping them; the runtime appends them back so sessions and saved model
195
- // settings keep working, and after 14 days they are evicted permanently.
196
- const DEPRECATED_MODEL_TTL_MS = 14 * 24 * 60 * 60 * 1000;
197
-
198
- /**
199
- * Reconcile deprecated-models.json against the freshly fetched model list.
200
- * - in old models.json but not the API: moved into the deprecated file
201
- * (deprecatedAt = now; preserved on repeat runs so the grace clock is not reset)
202
- * - back in the API: resurrected (dropped from the deprecated file)
203
- * - deprecatedAt older than 14 days: evicted permanently
204
- * Must run BEFORE the new models.json is written; it reads the old file itself.
205
- */
206
- function updateDeprecatedModels(modelsJsonPath, newModels) {
207
- const deprecatedPath = path.join(path.dirname(modelsJsonPath), 'deprecated-models.json');
208
-
209
- let oldModels = [];
210
- try {
211
- const parsed = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf8'));
212
- if (Array.isArray(parsed)) oldModels = parsed;
213
- } catch { /* first run: no previous models.json */ }
214
-
215
- let deprecated = {};
216
- try {
217
- const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
218
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
219
- } catch { /* no graveyard yet */ }
220
-
221
- const currentIds = new Set(newModels.map(m => m.id));
222
- const now = new Date().toISOString();
223
- const added = [];
224
- const resurrected = [];
225
- const evicted = [];
226
-
227
- for (const old of oldModels) {
228
- if (old && old.id && !currentIds.has(old.id) && !deprecated[old.id]) {
229
- deprecated[old.id] = { ...old, deprecatedAt: now };
230
- added.push(old.id);
231
- }
232
- }
233
-
234
- for (const [id, entry] of Object.entries(deprecated)) {
235
- if (currentIds.has(id)) {
236
- delete deprecated[id];
237
- resurrected.push(id);
238
- continue;
239
- }
240
- const removedAt = Date.parse(entry && entry.deprecatedAt ? entry.deprecatedAt : '');
241
- if (Number.isNaN(removedAt) || Date.now() - removedAt > DEPRECATED_MODEL_TTL_MS) {
242
- delete deprecated[id];
243
- evicted.push(id);
244
- }
245
- }
246
-
247
- if (added.length > 0 || resurrected.length > 0 || evicted.length > 0) {
248
- fs.writeFileSync(deprecatedPath, JSON.stringify(deprecated, null, 2) + '\n');
249
- console.log('Updated deprecated-models.json ' + JSON.stringify({ added, resurrected, evicted }));
250
- }
251
- }
252
-
253
- async function main() {
254
- console.log('Fetching models from API...');
255
-
256
- try {
257
- const data = await fetchJSON(API_URL);
258
- const provider = data[PROVIDER_ID];
259
-
260
- if (!provider) {
261
- throw new Error(`Provider "${PROVIDER_ID}" not found in API`);
262
- }
263
-
264
- if (!provider.models) {
265
- throw new Error(`No models found for provider "${PROVIDER_ID}"`);
266
- }
267
-
268
- // Convert models object to array and filter out deprecated
269
- const apiModels = Object.values(provider.models).filter(m => m.status !== 'deprecated');
270
-
271
- console.log(`Found ${apiModels.length} active models`);
272
-
273
- // Convert to Pi-native format and save to models.json
274
- const models = apiModels.map(convertModel);
275
- const modelsPath = path.join(process.cwd(), 'models.json');
276
- // Move delisted models to deprecated-models.json BEFORE models.json is overwritten
277
- updateDeprecatedModels(modelsPath, models);
278
- fs.writeFileSync(modelsPath, JSON.stringify(models, null, 2) + '\n');
279
- console.log(` Saved ${models.length} models to models.json`);
280
-
281
- // Apply layered overrides for documentation: models.json → patch.json → custom-models.json.
282
- const patch = loadJson('patch.json');
283
- const customModels = Array.isArray(loadJson('custom-models.json')) ? loadJson('custom-models.json') : [];
284
- updateReadme(buildModels(models, customModels, patch));
285
-
286
- console.log('\nDone!');
287
- } catch (error) {
288
- console.error('Error:', error.message);
289
- process.exit(1);
290
- }
291
- }
292
-
293
- main();