pi-tensorix-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.
@@ -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,19 @@
1
+ {
2
+ "deepseek/deepseek-v4-flash": {
3
+ "id": "deepseek/deepseek-v4-flash",
4
+ "name": "Deepseek V4 Flash",
5
+ "reasoning": false,
6
+ "input": [
7
+ "text"
8
+ ],
9
+ "cost": {
10
+ "input": 0,
11
+ "output": 0,
12
+ "cacheRead": 0,
13
+ "cacheWrite": 0
14
+ },
15
+ "contextWindow": 131072,
16
+ "maxTokens": 131072,
17
+ "deprecatedAt": "2026-08-09T02:00:20.006Z"
18
+ }
19
+ }
package/models.json ADDED
@@ -0,0 +1,338 @@
1
+ [
2
+ {
3
+ "id": "chatterbox-turbo",
4
+ "name": "Chatterbox Turbo",
5
+ "reasoning": false,
6
+ "input": [
7
+ "text"
8
+ ],
9
+ "cost": {
10
+ "input": 0,
11
+ "output": 0,
12
+ "cacheRead": 0,
13
+ "cacheWrite": 0
14
+ },
15
+ "contextWindow": 131072,
16
+ "maxTokens": 131072
17
+ },
18
+ {
19
+ "id": "deepseek/deepseek-r1-0528",
20
+ "name": "Deepseek R1 0528",
21
+ "reasoning": false,
22
+ "input": [
23
+ "text"
24
+ ],
25
+ "cost": {
26
+ "input": 0,
27
+ "output": 0,
28
+ "cacheRead": 0,
29
+ "cacheWrite": 0
30
+ },
31
+ "contextWindow": 131072,
32
+ "maxTokens": 131072
33
+ },
34
+ {
35
+ "id": "deepseek/deepseek-v3.2",
36
+ "name": "Deepseek V3.2",
37
+ "reasoning": false,
38
+ "input": [
39
+ "text"
40
+ ],
41
+ "cost": {
42
+ "input": 0,
43
+ "output": 0,
44
+ "cacheRead": 0,
45
+ "cacheWrite": 0
46
+ },
47
+ "contextWindow": 131072,
48
+ "maxTokens": 131072
49
+ },
50
+ {
51
+ "id": "deepseek/deepseek-v4-flash-0731",
52
+ "name": "Deepseek V4 Flash 0731",
53
+ "reasoning": false,
54
+ "input": [
55
+ "text"
56
+ ],
57
+ "cost": {
58
+ "input": 0,
59
+ "output": 0,
60
+ "cacheRead": 0,
61
+ "cacheWrite": 0
62
+ },
63
+ "contextWindow": 131072,
64
+ "maxTokens": 131072
65
+ },
66
+ {
67
+ "id": "deepseek/deepseek-v4-pro",
68
+ "name": "Deepseek V4 Pro",
69
+ "reasoning": false,
70
+ "input": [
71
+ "text"
72
+ ],
73
+ "cost": {
74
+ "input": 0,
75
+ "output": 0,
76
+ "cacheRead": 0,
77
+ "cacheWrite": 0
78
+ },
79
+ "contextWindow": 131072,
80
+ "maxTokens": 131072
81
+ },
82
+ {
83
+ "id": "minimax/minimax-m2.5",
84
+ "name": "Minimax M2.5",
85
+ "reasoning": false,
86
+ "input": [
87
+ "text"
88
+ ],
89
+ "cost": {
90
+ "input": 0,
91
+ "output": 0,
92
+ "cacheRead": 0,
93
+ "cacheWrite": 0
94
+ },
95
+ "contextWindow": 131072,
96
+ "maxTokens": 131072
97
+ },
98
+ {
99
+ "id": "minimax/minimax-m3",
100
+ "name": "Minimax M3",
101
+ "reasoning": false,
102
+ "input": [
103
+ "text"
104
+ ],
105
+ "cost": {
106
+ "input": 0,
107
+ "output": 0,
108
+ "cacheRead": 0,
109
+ "cacheWrite": 0
110
+ },
111
+ "contextWindow": 131072,
112
+ "maxTokens": 131072
113
+ },
114
+ {
115
+ "id": "moonshotai/kimi-k2.5",
116
+ "name": "Kimi K2.5",
117
+ "reasoning": false,
118
+ "input": [
119
+ "text"
120
+ ],
121
+ "cost": {
122
+ "input": 0,
123
+ "output": 0,
124
+ "cacheRead": 0,
125
+ "cacheWrite": 0
126
+ },
127
+ "contextWindow": 131072,
128
+ "maxTokens": 131072
129
+ },
130
+ {
131
+ "id": "moonshotai/kimi-k2.6",
132
+ "name": "Kimi K2.6",
133
+ "reasoning": false,
134
+ "input": [
135
+ "text"
136
+ ],
137
+ "cost": {
138
+ "input": 0,
139
+ "output": 0,
140
+ "cacheRead": 0,
141
+ "cacheWrite": 0
142
+ },
143
+ "contextWindow": 131072,
144
+ "maxTokens": 131072
145
+ },
146
+ {
147
+ "id": "moonshotai/kimi-k2.7-code",
148
+ "name": "Kimi K2.7 Code",
149
+ "reasoning": false,
150
+ "input": [
151
+ "text"
152
+ ],
153
+ "cost": {
154
+ "input": 0,
155
+ "output": 0,
156
+ "cacheRead": 0,
157
+ "cacheWrite": 0
158
+ },
159
+ "contextWindow": 131072,
160
+ "maxTokens": 131072
161
+ },
162
+ {
163
+ "id": "moonshotai/kimi-k3",
164
+ "name": "Kimi K3",
165
+ "reasoning": false,
166
+ "input": [
167
+ "text"
168
+ ],
169
+ "cost": {
170
+ "input": 0,
171
+ "output": 0,
172
+ "cacheRead": 0,
173
+ "cacheWrite": 0
174
+ },
175
+ "contextWindow": 131072,
176
+ "maxTokens": 131072
177
+ },
178
+ {
179
+ "id": "qwen/qwen3-235b-a22b-2507",
180
+ "name": "Qwen3 235b A22b 2507",
181
+ "reasoning": false,
182
+ "input": [
183
+ "text"
184
+ ],
185
+ "cost": {
186
+ "input": 0,
187
+ "output": 0,
188
+ "cacheRead": 0,
189
+ "cacheWrite": 0
190
+ },
191
+ "contextWindow": 131072,
192
+ "maxTokens": 131072
193
+ },
194
+ {
195
+ "id": "qwen/qwen3.5-122b-a10b",
196
+ "name": "Qwen3.5 122b A10b",
197
+ "reasoning": false,
198
+ "input": [
199
+ "text"
200
+ ],
201
+ "cost": {
202
+ "input": 0,
203
+ "output": 0,
204
+ "cacheRead": 0,
205
+ "cacheWrite": 0
206
+ },
207
+ "contextWindow": 131072,
208
+ "maxTokens": 131072
209
+ },
210
+ {
211
+ "id": "qwen/qwen3.5-9b",
212
+ "name": "Qwen3.5 9b",
213
+ "reasoning": false,
214
+ "input": [
215
+ "text"
216
+ ],
217
+ "cost": {
218
+ "input": 0,
219
+ "output": 0,
220
+ "cacheRead": 0,
221
+ "cacheWrite": 0
222
+ },
223
+ "contextWindow": 131072,
224
+ "maxTokens": 131072
225
+ },
226
+ {
227
+ "id": "qwen/qwen3.8-2.4t-a95b",
228
+ "name": "Qwen3.8 2.4t A95b",
229
+ "reasoning": false,
230
+ "input": [
231
+ "text"
232
+ ],
233
+ "cost": {
234
+ "input": 0,
235
+ "output": 0,
236
+ "cacheRead": 0,
237
+ "cacheWrite": 0
238
+ },
239
+ "contextWindow": 131072,
240
+ "maxTokens": 131072
241
+ },
242
+ {
243
+ "id": "qwen/qwen3.8-27b",
244
+ "name": "Qwen3.8 27b",
245
+ "reasoning": false,
246
+ "input": [
247
+ "text"
248
+ ],
249
+ "cost": {
250
+ "input": 0,
251
+ "output": 0,
252
+ "cacheRead": 0,
253
+ "cacheWrite": 0
254
+ },
255
+ "contextWindow": 131072,
256
+ "maxTokens": 131072
257
+ },
258
+ {
259
+ "id": "z-ai/glm-5",
260
+ "name": "Glm 5",
261
+ "reasoning": false,
262
+ "input": [
263
+ "text"
264
+ ],
265
+ "cost": {
266
+ "input": 0,
267
+ "output": 0,
268
+ "cacheRead": 0,
269
+ "cacheWrite": 0
270
+ },
271
+ "contextWindow": 131072,
272
+ "maxTokens": 131072
273
+ },
274
+ {
275
+ "id": "z-ai/glm-5-turbo",
276
+ "name": "Glm 5 Turbo",
277
+ "reasoning": false,
278
+ "input": [
279
+ "text"
280
+ ],
281
+ "cost": {
282
+ "input": 0,
283
+ "output": 0,
284
+ "cacheRead": 0,
285
+ "cacheWrite": 0
286
+ },
287
+ "contextWindow": 131072,
288
+ "maxTokens": 131072
289
+ },
290
+ {
291
+ "id": "z-ai/glm-5.1",
292
+ "name": "Glm 5.1",
293
+ "reasoning": false,
294
+ "input": [
295
+ "text"
296
+ ],
297
+ "cost": {
298
+ "input": 0,
299
+ "output": 0,
300
+ "cacheRead": 0,
301
+ "cacheWrite": 0
302
+ },
303
+ "contextWindow": 131072,
304
+ "maxTokens": 131072
305
+ },
306
+ {
307
+ "id": "z-ai/glm-5.2",
308
+ "name": "Glm 5.2",
309
+ "reasoning": false,
310
+ "input": [
311
+ "text"
312
+ ],
313
+ "cost": {
314
+ "input": 0,
315
+ "output": 0,
316
+ "cacheRead": 0,
317
+ "cacheWrite": 0
318
+ },
319
+ "contextWindow": 131072,
320
+ "maxTokens": 131072
321
+ },
322
+ {
323
+ "id": "z-ai/glm-5v-turbo",
324
+ "name": "Glm 5v Turbo",
325
+ "reasoning": false,
326
+ "input": [
327
+ "text"
328
+ ],
329
+ "cost": {
330
+ "input": 0,
331
+ "output": 0,
332
+ "cacheRead": 0,
333
+ "cacheWrite": 0
334
+ },
335
+ "contextWindow": 131072,
336
+ "maxTokens": 131072
337
+ }
338
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tensorix-provider",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Tensorix provider extension for pi - Access DeepSeek, GLM, Kimi, Qwen, MiniMax, Llama, and more through the Tensorix API",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -25,10 +25,6 @@
25
25
  "./index.ts"
26
26
  ]
27
27
  },
28
- "files": [
29
- "README.md",
30
- "LICENSE"
31
- ],
32
28
  "scripts": {
33
29
  "clean": "echo 'nothing to clean'",
34
30
  "build": "echo 'nothing to build'",
package/patch.json ADDED
@@ -0,0 +1,251 @@
1
+ {
2
+ "deepseek/deepseek-r1-0528": {
3
+ "reasoning": true,
4
+ "compat": {
5
+ "thinkingFormat": "openai",
6
+ "supportsDeveloperRole": true,
7
+ "supportsReasoningEffort": true
8
+ }
9
+ },
10
+ "deepseek/deepseek-v3.2": {
11
+ "reasoning": true,
12
+ "contextWindow": 163840,
13
+ "maxTokens": 163840,
14
+ "compat": {
15
+ "thinkingFormat": "openai",
16
+ "supportsDeveloperRole": true,
17
+ "supportsReasoningEffort": true
18
+ }
19
+ },
20
+ "deepseek/deepseek-v4-flash": {
21
+ "reasoning": true,
22
+ "contextWindow": 1000000,
23
+ "maxTokens": 131072,
24
+ "compat": {
25
+ "thinkingFormat": "qwen-chat-template",
26
+ "requiresReasoningContentOnAssistantMessages": true,
27
+ "supportsDeveloperRole": true,
28
+ "supportsReasoningEffort": true
29
+ }
30
+ },
31
+ "deepseek/deepseek-v4-pro": {
32
+ "reasoning": true,
33
+ "contextWindow": 1000000,
34
+ "maxTokens": 131072,
35
+ "compat": {
36
+ "thinkingFormat": "openai",
37
+ "supportsDeveloperRole": true,
38
+ "supportsReasoningEffort": true
39
+ }
40
+ },
41
+ "z-ai/glm-5": {
42
+ "reasoning": true,
43
+ "contextWindow": 202752,
44
+ "maxTokens": 131072,
45
+ "compat": {
46
+ "thinkingFormat": "openai",
47
+ "supportsDeveloperRole": true,
48
+ "supportsReasoningEffort": true
49
+ }
50
+ },
51
+ "z-ai/glm-5-turbo": {
52
+ "reasoning": true,
53
+ "contextWindow": 202752,
54
+ "maxTokens": 131072,
55
+ "compat": {
56
+ "thinkingFormat": "openai",
57
+ "supportsDeveloperRole": true,
58
+ "supportsReasoningEffort": true
59
+ }
60
+ },
61
+ "z-ai/glm-5.1": {
62
+ "reasoning": true,
63
+ "contextWindow": 202752,
64
+ "maxTokens": 131072,
65
+ "compat": {
66
+ "thinkingFormat": "openai",
67
+ "supportsDeveloperRole": true,
68
+ "supportsReasoningEffort": true
69
+ }
70
+ },
71
+ "z-ai/glm-5v-turbo": {
72
+ "reasoning": true,
73
+ "input": [
74
+ "text",
75
+ "image"
76
+ ],
77
+ "contextWindow": 202752,
78
+ "maxTokens": 131072,
79
+ "compat": {
80
+ "thinkingFormat": "openai",
81
+ "supportsDeveloperRole": true,
82
+ "supportsReasoningEffort": true
83
+ }
84
+ },
85
+ "minimax/minimax-m2.5": {
86
+ "reasoning": true,
87
+ "contextWindow": 204800,
88
+ "maxTokens": 131072,
89
+ "compat": {
90
+ "thinkingFormat": "openai",
91
+ "supportsDeveloperRole": true,
92
+ "supportsReasoningEffort": true
93
+ }
94
+ },
95
+ "moonshotai/kimi-k2.5": {
96
+ "reasoning": true,
97
+ "input": [
98
+ "text",
99
+ "image"
100
+ ],
101
+ "contextWindow": 262144,
102
+ "maxTokens": 262144,
103
+ "compat": {
104
+ "thinkingFormat": "openai",
105
+ "supportsDeveloperRole": true,
106
+ "supportsReasoningEffort": true
107
+ }
108
+ },
109
+ "moonshotai/kimi-k2.6": {
110
+ "reasoning": true,
111
+ "input": [
112
+ "text",
113
+ "image"
114
+ ],
115
+ "contextWindow": 262144,
116
+ "maxTokens": 262144,
117
+ "compat": {
118
+ "thinkingFormat": "openai",
119
+ "supportsDeveloperRole": true,
120
+ "supportsReasoningEffort": true
121
+ }
122
+ },
123
+ "qwen/qwen3-235b-a22b-2507": {
124
+ "reasoning": true,
125
+ "contextWindow": 262144,
126
+ "maxTokens": 262144,
127
+ "compat": {
128
+ "thinkingFormat": "openai",
129
+ "supportsDeveloperRole": true,
130
+ "supportsReasoningEffort": true
131
+ }
132
+ },
133
+ "qwen/qwen3-coder-30b-a3b-instruct": {
134
+ "reasoning": true,
135
+ "contextWindow": 262144,
136
+ "maxTokens": 262144,
137
+ "compat": {
138
+ "thinkingFormat": "openai",
139
+ "supportsDeveloperRole": true,
140
+ "supportsReasoningEffort": true
141
+ }
142
+ },
143
+ "qwen/qwen3-vl-235b-a22b-instruct": {
144
+ "reasoning": true,
145
+ "input": [
146
+ "text",
147
+ "image"
148
+ ],
149
+ "contextWindow": 262144,
150
+ "maxTokens": 262144,
151
+ "compat": {
152
+ "thinkingFormat": "openai",
153
+ "supportsDeveloperRole": true,
154
+ "supportsReasoningEffort": true
155
+ }
156
+ },
157
+ "qwen/qwen3.5-122b-a10b": {
158
+ "reasoning": true,
159
+ "contextWindow": 262144,
160
+ "maxTokens": 262144,
161
+ "compat": {
162
+ "thinkingFormat": "openai",
163
+ "supportsDeveloperRole": true,
164
+ "supportsReasoningEffort": true
165
+ }
166
+ },
167
+ "qwen/qwen3.5-9b": {
168
+ "reasoning": true,
169
+ "contextWindow": 262144,
170
+ "maxTokens": 262144,
171
+ "compat": {
172
+ "thinkingFormat": "openai",
173
+ "supportsDeveloperRole": true,
174
+ "supportsReasoningEffort": true
175
+ }
176
+ },
177
+ "openai/gpt-oss-120b": {
178
+ "reasoning": true,
179
+ "contextWindow": 131072,
180
+ "maxTokens": 32768,
181
+ "compat": {
182
+ "supportsDeveloperRole": true,
183
+ "supportsReasoningEffort": true
184
+ }
185
+ },
186
+ "nvidia/nemotron-3-super-120b-a12b": {
187
+ "contextWindow": 4096,
188
+ "maxTokens": 4096
189
+ },
190
+ "deepseek/deepseek-v4-flash-0731": {
191
+ "reasoning": true,
192
+ "compat": {
193
+ "thinkingFormat": "openai",
194
+ "supportsDeveloperRole": true,
195
+ "supportsReasoningEffort": true
196
+ }
197
+ },
198
+ "minimax/minimax-m3": {
199
+ "reasoning": true,
200
+ "input": [
201
+ "text",
202
+ "image"
203
+ ],
204
+ "compat": {
205
+ "thinkingFormat": "openai",
206
+ "supportsDeveloperRole": true,
207
+ "supportsReasoningEffort": true
208
+ }
209
+ },
210
+ "moonshotai/kimi-k2.7-code": {
211
+ "reasoning": true,
212
+ "input": [
213
+ "text",
214
+ "image"
215
+ ],
216
+ "compat": {
217
+ "thinkingFormat": "openai",
218
+ "supportsDeveloperRole": true,
219
+ "supportsReasoningEffort": true
220
+ }
221
+ },
222
+ "moonshotai/kimi-k3": {
223
+ "reasoning": true,
224
+ "input": [
225
+ "text",
226
+ "image"
227
+ ],
228
+ "thinkingLevelMap": {
229
+ "off": null,
230
+ "minimal": null,
231
+ "low": "low",
232
+ "medium": null,
233
+ "high": "high",
234
+ "xhigh": null,
235
+ "max": "max"
236
+ },
237
+ "compat": {
238
+ "thinkingFormat": "openai",
239
+ "supportsDeveloperRole": true,
240
+ "supportsReasoningEffort": true
241
+ }
242
+ },
243
+ "z-ai/glm-5.2": {
244
+ "reasoning": true,
245
+ "compat": {
246
+ "thinkingFormat": "openai",
247
+ "supportsDeveloperRole": true,
248
+ "supportsReasoningEffort": true
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Update Tensorix models from API
4
+ *
5
+ * Fetches models from https://api.tensorix.ai/v1/models and updates:
6
+ * - models.json: API model definitions with preserved curated data
7
+ * - README.md: Model table with patch.json overrides applied
8
+ *
9
+ * The Tensorix /v1/models API returns minimal metadata (id only — no pricing,
10
+ * context_length, or reasoning flags). models.json preserves curated specs for
11
+ * known models and creates skeleton entries for new ones. patch.json corrects
12
+ * reasoning capability and adds compatibility overrides at runtime.
13
+ *
14
+ * Merge order for README: models.json → apply patch.json → merge custom-models.json
15
+ *
16
+ * API key: the stored `tensorix` credential in ~/.pi/agent/auth.json wins, then
17
+ * the TENSORIX_API_KEY environment variable. The script refuses to run without one.
18
+ */
19
+
20
+ import fs from 'fs';
21
+ import os from 'os';
22
+ import { execSync } from 'child_process';
23
+ import path from 'path';
24
+ import { fileURLToPath } from 'url';
25
+
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+
28
+ // pi's agent directory: PI_CODING_AGENT_DIR (with ~ expansion) or ~/.pi/agent.
29
+ function piAgentDir() {
30
+ const envDir = process.env.PI_CODING_AGENT_DIR;
31
+ if (envDir) {
32
+ return envDir.startsWith('~/') || envDir === '~'
33
+ ? path.join(os.homedir(), envDir.slice(1))
34
+ : envDir;
35
+ }
36
+ return path.join(os.homedir(), '.pi', 'agent');
37
+ }
38
+
39
+ const AUTH_JSON_PATH = path.join(piAgentDir(), 'auth.json');
40
+
41
+ /**
42
+ * Resolve a configured value using pi's semantics (resolve-config-value.ts in
43
+ * pi-mono): "!command" runs via the shell (10s timeout) and uses trimmed
44
+ * stdout; "$VAR" / "${VAR}" interpolate environment variables ("$$" escapes a
45
+ * literal "$", "$!" a literal "!"); anything else is a literal. Returns
46
+ * undefined when a referenced env var is unset or a command fails.
47
+ */
48
+ function resolveConfigValue(config, env) {
49
+ if (typeof config !== 'string' || config.length === 0) return undefined;
50
+ if (config.startsWith('!')) {
51
+ try {
52
+ const out = execSync(config.slice(1), {
53
+ encoding: 'utf8',
54
+ timeout: 10000,
55
+ stdio: ['ignore', 'pipe', 'ignore'],
56
+ });
57
+ return out.trim() || undefined;
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
63
+ let resolved = '';
64
+ let index = 0;
65
+ while (index < config.length) {
66
+ const dollar = config.indexOf('$', index);
67
+ if (dollar < 0) {
68
+ resolved += config.slice(index);
69
+ break;
70
+ }
71
+ resolved += config.slice(index, dollar);
72
+ const next = config[dollar + 1];
73
+ let name;
74
+ if (next === '$' || next === '!') {
75
+ resolved += next;
76
+ index = dollar + 2;
77
+ continue;
78
+ } else if (next === '{') {
79
+ const end = config.indexOf('}', dollar + 2);
80
+ if (end < 0) {
81
+ resolved += '$';
82
+ index = dollar + 1;
83
+ continue;
84
+ }
85
+ const inner = config.slice(dollar + 2, end);
86
+ if (!ENV_NAME_RE.test(inner)) {
87
+ resolved += config.slice(dollar, end + 1);
88
+ index = end + 1;
89
+ continue;
90
+ }
91
+ name = inner;
92
+ index = end + 1;
93
+ } else {
94
+ const match = config.slice(dollar + 1).match(/^[A-Za-z_][A-Za-z0-9_]*/);
95
+ if (!match) {
96
+ resolved += '$';
97
+ index = dollar + 1;
98
+ continue;
99
+ }
100
+ name = match[0];
101
+ index = dollar + 1 + name.length;
102
+ }
103
+ const value = (env && env[name]) || process.env[name] || undefined;
104
+ if (value === undefined) return undefined;
105
+ resolved += value;
106
+ }
107
+ return resolved;
108
+ }
109
+
110
+ /**
111
+ * The API key, resolved the way pi itself resolves it for this provider: the
112
+ * stored `tensorix` credential in ~/.pi/agent/auth.json wins, then
113
+ * the TENSORIX_API_KEY environment variable.
114
+ */
115
+ function resolveApiKey() {
116
+ try {
117
+ const auth = JSON.parse(fs.readFileSync(AUTH_JSON_PATH, 'utf8'));
118
+ const credential = auth?.tensorix;
119
+ if (credential && credential.type === 'api_key' && typeof credential.key === 'string') {
120
+ const key = resolveConfigValue(credential.key, credential.env);
121
+ if (key) return key;
122
+ }
123
+ } catch {
124
+ // Missing or unparseable auth.json: fall through to the env var.
125
+ }
126
+ return process.env.TENSORIX_API_KEY || undefined;
127
+ }
128
+
129
+ const MODELS_API_URL = 'https://api.tensorix.ai/v1/models';
130
+ const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
131
+ const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
132
+ const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
133
+ const README_PATH = path.join(__dirname, '..', 'README.md');
134
+
135
+ const MODELS_TO_SKIP = new Set([
136
+ 'qwen/qwen3-embedding-8b',
137
+ 'Systran/faster-whisper-large-v3',
138
+ ]);
139
+
140
+ // ─── Patch application ────────────────────────────────────────────────────────
141
+
142
+ function applyPatch(model, patch) {
143
+ const result = { ...model };
144
+
145
+ if (patch.name !== undefined) result.name = patch.name;
146
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
147
+ if (patch.input !== undefined) result.input = patch.input;
148
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
149
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
150
+ if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = { ...patch.thinkingLevelMap };
151
+
152
+ if (patch.cost) {
153
+ result.cost = {
154
+ input: patch.cost.input ?? result.cost.input,
155
+ output: patch.cost.output ?? result.cost.output,
156
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
157
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
158
+ };
159
+ }
160
+ if (patch.compat) {
161
+ result.compat = { ...(result.compat || {}), ...patch.compat };
162
+ }
163
+
164
+ if (!result.reasoning && result.compat?.thinkingFormat) {
165
+ delete result.compat.thinkingFormat;
166
+ }
167
+ if (result.compat && Object.keys(result.compat).length === 0) {
168
+ delete result.compat;
169
+ }
170
+
171
+ return result;
172
+ }
173
+
174
+ function buildModels(baseModels, customModels, patchData) {
175
+ const modelMap = new Map();
176
+
177
+ for (const model of baseModels) {
178
+ modelMap.set(model.id, model);
179
+ }
180
+
181
+ for (const [id, patchEntry] of Object.entries(patchData)) {
182
+ const existing = modelMap.get(id);
183
+ if (existing) {
184
+ modelMap.set(id, applyPatch(existing, patchEntry));
185
+ }
186
+ }
187
+
188
+ for (const model of customModels) {
189
+ const existing = modelMap.get(model.id);
190
+ const patchEntry = patchData[model.id];
191
+ if (existing && patchEntry) {
192
+ modelMap.set(model.id, applyPatch(model, patchEntry));
193
+ } else if (existing) {
194
+ modelMap.set(model.id, model);
195
+ } else if (patchEntry) {
196
+ modelMap.set(model.id, applyPatch(model, patchEntry));
197
+ } else {
198
+ modelMap.set(model.id, model);
199
+ }
200
+ }
201
+
202
+ return Array.from(modelMap.values());
203
+ }
204
+
205
+ // ─── Model transformation ─────────────────────────────────────────────────────
206
+
207
+ function buildDisplayName(apiModel) {
208
+ const id = apiModel.id || '';
209
+ const parts = id.split('/');
210
+ const rawName = parts.length > 1 ? parts.slice(1).join('/') : id;
211
+ return rawName
212
+ .replace(/-/g, ' ')
213
+ .replace(/\b\w/g, c => c.toUpperCase());
214
+ }
215
+
216
+ function transformModel(apiModel, existingModelsMap) {
217
+ const modelId = apiModel.id;
218
+
219
+ // Preserve existing curated data (reasoning, vision, compat, context, etc.)
220
+ if (existingModelsMap[modelId]) {
221
+ const existing = { ...existingModelsMap[modelId] };
222
+ existing._meta = { isFree: true };
223
+ return existing;
224
+ }
225
+
226
+ // New model — build from API data + sensible defaults
227
+ return {
228
+ id: modelId,
229
+ name: buildDisplayName(apiModel),
230
+ reasoning: false,
231
+ input: ['text'],
232
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
233
+ contextWindow: 131072,
234
+ maxTokens: 131072,
235
+ _meta: { isFree: true },
236
+ };
237
+ }
238
+
239
+ // ─── File I/O ─────────────────────────────────────────────────────────────────
240
+
241
+ function loadJson(filePath) {
242
+ try {
243
+ const data = fs.readFileSync(filePath, 'utf8');
244
+ return JSON.parse(data);
245
+ } catch {
246
+ return {};
247
+ }
248
+ }
249
+
250
+ // ─── README generation ────────────────────────────────────────────────────────
251
+
252
+ function formatCost(cost, isFree) {
253
+ if (isFree) return '**Free**';
254
+ if (cost === 0) return '—';
255
+ if (cost === null || cost === undefined) return '—';
256
+ return '$' + cost.toFixed(2);
257
+ }
258
+
259
+ function formatContextWindow(n) {
260
+ if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
261
+ if (n >= 1000) return `${Math.round(n / 1000)}K`;
262
+ return n.toString();
263
+ }
264
+
265
+ function generateReadmeTable(models) {
266
+ const lines = [
267
+ '| Model | Context | Vision | Reasoning | Input $/M | Output $/M |',
268
+ '|-------|---------|--------|-----------|-----------|------------|',
269
+ ];
270
+
271
+ for (const model of models) {
272
+ const name = model.name;
273
+ const context = formatContextWindow(model.contextWindow);
274
+ const vision = model.input.includes('image') ? '✅' : '❌';
275
+ const reasoning = model.reasoning ? '✅' : '❌';
276
+ const isFree = model._meta?.isFree ?? (model.cost.input === 0 && model.cost.output === 0);
277
+ const inputCost = formatCost(model.cost.input, isFree);
278
+ const outputCost = formatCost(model.cost.output, isFree);
279
+
280
+ lines.push(`| ${name} | ${context} | ${vision} | ${reasoning} | ${inputCost} | ${outputCost} |`);
281
+ }
282
+
283
+ return lines.join('\n');
284
+ }
285
+
286
+ function updateReadme(models) {
287
+ let readme = fs.readFileSync(README_PATH, 'utf8');
288
+ const newTable = generateReadmeTable(models);
289
+
290
+ const tableRegex = /(## Available Models\n\n)\| Model \| Context \| Vision \| Reasoning \| Input \$\/M \| Output \$\/M \|\n\|[-| ]+\|(\n\|[^\n]+\|)*\n*/;
291
+
292
+ if (tableRegex.test(readme)) {
293
+ readme = readme.replace(tableRegex, (match, header) => `${header}${newTable}\n\n`);
294
+ fs.writeFileSync(README_PATH, readme);
295
+ console.log('✓ Updated README.md');
296
+ } else {
297
+ console.warn('⚠ Could not find model table in "## Available Models" section');
298
+ }
299
+ }
300
+
301
+ function cleanModelForJson(model) {
302
+ const { _meta, ...cleanModel } = model;
303
+ return cleanModel;
304
+ }
305
+
306
+ // ─── Main ──────────────────────────────────────────────────────────────────────
307
+
308
+ // Grace period for delisted models: update-models.js moves models the API no
309
+ // longer lists into deprecated-models.json (stamped with deprecatedAt) instead
310
+ // of dropping them; the runtime appends them back so sessions and saved model
311
+ // settings keep working, and after 14 days they are evicted permanently.
312
+ const DEPRECATED_MODEL_TTL_MS = 14 * 24 * 60 * 60 * 1000;
313
+
314
+ /**
315
+ * Reconcile deprecated-models.json against the freshly fetched model list.
316
+ * - in old models.json but not the API: moved into the deprecated file
317
+ * (deprecatedAt = now; preserved on repeat runs so the grace clock is not reset)
318
+ * - back in the API: resurrected (dropped from the deprecated file)
319
+ * - deprecatedAt older than 14 days: evicted permanently
320
+ * Must run BEFORE the new models.json is written; it reads the old file itself.
321
+ */
322
+ function updateDeprecatedModels(modelsJsonPath, newModels) {
323
+ const deprecatedPath = path.join(path.dirname(modelsJsonPath), 'deprecated-models.json');
324
+
325
+ let oldModels = [];
326
+ try {
327
+ const parsed = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf8'));
328
+ if (Array.isArray(parsed)) oldModels = parsed;
329
+ } catch { /* first run: no previous models.json */ }
330
+
331
+ let deprecated = {};
332
+ try {
333
+ const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
334
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
335
+ } catch { /* no graveyard yet */ }
336
+
337
+ const currentIds = new Set(newModels.map(m => m.id));
338
+ const now = new Date().toISOString();
339
+ const added = [];
340
+ const resurrected = [];
341
+ const evicted = [];
342
+
343
+ for (const old of oldModels) {
344
+ if (old && old.id && !currentIds.has(old.id) && !deprecated[old.id]) {
345
+ deprecated[old.id] = { ...old, deprecatedAt: now };
346
+ added.push(old.id);
347
+ }
348
+ }
349
+
350
+ for (const [id, entry] of Object.entries(deprecated)) {
351
+ if (currentIds.has(id)) {
352
+ delete deprecated[id];
353
+ resurrected.push(id);
354
+ continue;
355
+ }
356
+ const removedAt = Date.parse(entry && entry.deprecatedAt ? entry.deprecatedAt : '');
357
+ if (Number.isNaN(removedAt) || Date.now() - removedAt > DEPRECATED_MODEL_TTL_MS) {
358
+ delete deprecated[id];
359
+ evicted.push(id);
360
+ }
361
+ }
362
+
363
+ if (added.length > 0 || resurrected.length > 0 || evicted.length > 0) {
364
+ fs.writeFileSync(deprecatedPath, JSON.stringify(deprecated, null, 2) + '\n');
365
+ console.log('Updated deprecated-models.json ' + JSON.stringify({ added, resurrected, evicted }));
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Grace-period deprecated models (deprecatedAt within TTL) with metadata stripped.
371
+ * Keeps the README table serving models that are delisted but still within their
372
+ * 14-day grace window.
373
+ */
374
+ function withDeprecatedForReadme(models) {
375
+ const deprecatedPath = path.join(path.dirname(MODELS_JSON_PATH), 'deprecated-models.json');
376
+ let deprecated = {};
377
+ try {
378
+ const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
379
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
380
+ } catch { /* no graveyard yet */ }
381
+ const now = Date.now();
382
+ const seen = new Set(models.map(m => m.id));
383
+ const extras = [];
384
+ for (const entry of Object.values(deprecated)) {
385
+ if (!entry || !entry.id || seen.has(entry.id)) continue;
386
+ const removedAt = Date.parse(entry.deprecatedAt || '');
387
+ if (Number.isNaN(removedAt) || now - removedAt > DEPRECATED_MODEL_TTL_MS) continue;
388
+ const m = { ...entry };
389
+ delete m.deprecatedAt;
390
+ extras.push(m);
391
+ }
392
+ return extras.length > 0 ? [...models, ...extras] : models;
393
+ }
394
+ async function main() {
395
+ console.log(`Fetching models from ${MODELS_API_URL}...`);
396
+
397
+ try {
398
+ // Without a key the API answers 200 with only the PUBLIC model list, so an
399
+ // unauthenticated run does not fail: it silently rewrites models.json (and
400
+ // the README table) with every gated model missing. Refuse to run rather
401
+ // than corrupt the generated files.
402
+ const apiKey = resolveApiKey();
403
+ if (!apiKey) {
404
+ console.error('❌ No API key found: no `tensorix` credential resolved from ' + AUTH_JSON_PATH + ' and TENSORIX_API_KEY is not set.');
405
+ process.exit(1);
406
+ }
407
+ const headers = { Authorization: `Bearer ${apiKey}` };
408
+
409
+ const response = await fetch(MODELS_API_URL, { headers });
410
+ if (!response.ok) {
411
+ throw new Error(`HTTP error! status: ${response.status}`);
412
+ }
413
+
414
+ const apiResponse = await response.json();
415
+ const apiModels = apiResponse.data || apiResponse;
416
+
417
+ if (!Array.isArray(apiModels)) {
418
+ throw new Error('API response does not contain an array of models');
419
+ }
420
+
421
+ console.log(`✓ Fetched ${apiModels.length} models from API`);
422
+
423
+ // Filter out non-chat models
424
+ const chatModels = apiModels.filter(m => !MODELS_TO_SKIP.has(m.id));
425
+ console.log(`✓ Filtered to ${chatModels.length} chat models (skipped ${apiModels.length - chatModels.length} non-chat)`);
426
+
427
+ // Load existing models.json — source of truth for curated specs
428
+ let existingModels = [];
429
+ try {
430
+ existingModels = JSON.parse(fs.readFileSync(MODELS_JSON_PATH, 'utf8'));
431
+ } catch {
432
+ // File might not exist or be invalid
433
+ }
434
+ const existingModelsMap = {};
435
+ for (const m of existingModels) {
436
+ existingModelsMap[m.id] = m;
437
+ }
438
+
439
+ // Transform models from API, preserving existing curated data
440
+ let apiTransformed = chatModels.map(m => transformModel(m, existingModelsMap));
441
+
442
+ // Sort by id
443
+ apiTransformed.sort((a, b) => a.id.localeCompare(b.id));
444
+
445
+ // Update models.json — API-derived model list with curated specs preserved
446
+ const cleanModels = apiTransformed.map(cleanModelForJson);
447
+ // Move delisted models to deprecated-models.json BEFORE models.json is overwritten
448
+ updateDeprecatedModels(MODELS_JSON_PATH, cleanModels);
449
+ fs.writeFileSync(MODELS_JSON_PATH, JSON.stringify(cleanModels, null, 2) + '\n');
450
+ console.log('✓ Updated models.json (API model list with curated specs)');
451
+
452
+ // Log new models not yet in patch.json
453
+ const patch = loadJson(PATCH_JSON_PATH);
454
+ for (const m of apiTransformed) {
455
+ if (!patch[m.id] && !existingModelsMap[m.id]) {
456
+ console.log(` 🆕 New model: ${m.id} (${m.name}) — add to patch.json for reasoning/vision/specs`);
457
+ }
458
+ }
459
+
460
+ // Build full model list for README: base → patch → custom
461
+ const customModels = loadJson(CUSTOM_MODELS_JSON_PATH);
462
+ const readmeModels = buildModels(
463
+ withDeprecatedForReadme(apiTransformed),
464
+ Array.isArray(customModels) ? customModels : [],
465
+ patch
466
+ );
467
+ readmeModels.sort((a, b) => a.name.localeCompare(b.name));
468
+ console.log('✓ Built model list (base → patch → custom) for README');
469
+
470
+ // Update README.md with patched data
471
+ updateReadme(readmeModels);
472
+
473
+ // Summary
474
+ console.log('\n--- Summary ---');
475
+ console.log(`Total models: ${readmeModels.length}`);
476
+ console.log(`Reasoning models (patched): ${readmeModels.filter(m => m.reasoning).length}`);
477
+ console.log(`Vision models: ${readmeModels.filter(m => m.input.includes('image')).length}`);
478
+
479
+ const newIds = new Set(apiTransformed.map(m => m.id));
480
+ const oldIds = new Set(existingModels.map(m => m.id));
481
+
482
+ const added = [...newIds].filter(id => !oldIds.has(id));
483
+ const removed = [...oldIds].filter(id => !newIds.has(id));
484
+
485
+ if (added.length > 0) {
486
+ console.log(`\nNew models: ${added.join(', ')}`);
487
+ }
488
+ if (removed.length > 0) {
489
+ console.log(`\nRemoved models: ${removed.join(', ')}`);
490
+ }
491
+ } catch (error) {
492
+ console.error('Error:', error.message);
493
+ process.exit(1);
494
+ }
495
+ }
496
+
497
+ main();