pi-zro-provider 1.0.0

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/models.json ADDED
@@ -0,0 +1,89 @@
1
+ [
2
+ {
3
+ "id": "deepseek-v4-flash-0731",
4
+ "name": "DeepSeek V4 Flash",
5
+ "reasoning": true,
6
+ "thinkingLevelMap": {
7
+ "off": "none",
8
+ "minimal": null,
9
+ "low": null,
10
+ "medium": null,
11
+ "high": "high",
12
+ "xhigh": null
13
+ },
14
+ "input": [
15
+ "text"
16
+ ],
17
+ "cost": {
18
+ "input": 0,
19
+ "output": 0,
20
+ "cacheRead": 0,
21
+ "cacheWrite": 0
22
+ },
23
+ "contextWindow": 1048576,
24
+ "maxTokens": 384000,
25
+ "compat": {
26
+ "supportsDeveloperRole": false,
27
+ "supportsReasoningEffort": true,
28
+ "maxTokensField": "max_tokens"
29
+ }
30
+ },
31
+ {
32
+ "id": "glm-5.2",
33
+ "name": "GLM-5.2",
34
+ "reasoning": true,
35
+ "thinkingLevelMap": {
36
+ "off": "none",
37
+ "minimal": null,
38
+ "low": null,
39
+ "medium": null,
40
+ "high": "high",
41
+ "xhigh": "max"
42
+ },
43
+ "input": [
44
+ "text"
45
+ ],
46
+ "cost": {
47
+ "input": 0,
48
+ "output": 0,
49
+ "cacheRead": 0,
50
+ "cacheWrite": 0
51
+ },
52
+ "contextWindow": 524288,
53
+ "maxTokens": 64000,
54
+ "compat": {
55
+ "supportsDeveloperRole": false,
56
+ "supportsReasoningEffort": true,
57
+ "maxTokensField": "max_tokens"
58
+ }
59
+ },
60
+ {
61
+ "id": "kimi-k3",
62
+ "name": "Kimi K3",
63
+ "reasoning": true,
64
+ "thinkingLevelMap": {
65
+ "off": null,
66
+ "minimal": null,
67
+ "low": "low",
68
+ "medium": null,
69
+ "high": "high",
70
+ "xhigh": "max"
71
+ },
72
+ "input": [
73
+ "text"
74
+ ],
75
+ "cost": {
76
+ "input": 0,
77
+ "output": 0,
78
+ "cacheRead": 0,
79
+ "cacheWrite": 0
80
+ },
81
+ "contextWindow": 1048576,
82
+ "maxTokens": 1048576,
83
+ "compat": {
84
+ "supportsDeveloperRole": false,
85
+ "supportsReasoningEffort": true,
86
+ "maxTokensField": "max_tokens"
87
+ }
88
+ }
89
+ ]
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "pi-zro-provider",
3
+ "version": "1.0.0",
4
+ "description": "Zro provider extension for pi - GLM-5.2, Kimi K3 & DeepSeek V4 Flash through the Zro inference endpoint",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "keywords": [
8
+ "pi",
9
+ "extension",
10
+ "provider",
11
+ "zro",
12
+ "moonmath",
13
+ "glm",
14
+ "kimi",
15
+ "deepseek",
16
+ "glm-5.2",
17
+ "ai",
18
+ "llm"
19
+ ],
20
+ "author": "",
21
+ "license": "MIT",
22
+ "pi": {
23
+ "extensions": [
24
+ "./index.ts"
25
+ ]
26
+ },
27
+ "devDependencies": {
28
+ "@earendil-works/pi-ai": "0.84.2",
29
+ "@earendil-works/pi-coding-agent": "0.84.2",
30
+ "@types/node": "^24.10.1",
31
+ "typescript": "6.0.3"
32
+ },
33
+ "scripts": {
34
+ "clean": "echo 'nothing to clean'",
35
+ "build": "echo 'nothing to build'",
36
+ "check": "tsc --noEmit && node tests/status.smoke.ts",
37
+ "smoke": "node tests/status.smoke.ts",
38
+ "update-models": "node scripts/update-models.js"
39
+ }
40
+ }
package/patch.json ADDED
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,15 @@
1
+ allowBuilds:
2
+ '@google/genai': set this to true or false
3
+ protobufjs: set this to true or false
4
+ onlyBuiltDependencies:
5
+ - '@google/genai'
6
+ - protobufjs
7
+
8
+ minimumReleaseAgeExclude:
9
+ - '@earendil-works/pi-agent-core@0.84.0 || 0.84.1 || 0.84.2'
10
+ - '@earendil-works/pi-ai@0.84.0 || 0.84.1 || 0.84.2'
11
+ - '@earendil-works/pi-client@0.84.0 || 0.84.1 || 0.84.2'
12
+ - '@earendil-works/pi-coding-agent@0.84.0 || 0.84.1 || 0.84.2'
13
+ - '@earendil-works/pi-protocol@0.84.0 || 0.84.1 || 0.84.2'
14
+ - '@earendil-works/pi-telemetry@0.84.0 || 0.84.1 || 0.84.2'
15
+ - '@earendil-works/pi-tui@0.84.0 || 0.84.1 || 0.84.2'
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Zro model catalog sync.
4
+ *
5
+ * Fetches the live model catalog from https://zro.moonmath.ai/api/cli/models
6
+ * (the same endpoint the zro CLI uses), writes models.json, moves delisted
7
+ * models into deprecated-models.json, and regenerates the README model table.
8
+ *
9
+ * Requires a credential, resolved in order:
10
+ * 1. ZRO_API_KEY environment variable
11
+ * 2. ~/.config/zro/credentials.json (written by `zro login`)
12
+ *
13
+ * Usage:
14
+ * node scripts/update-models.js
15
+ * ZRO_API_KEY=your-key node scripts/update-models.js
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import os from 'node:os';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const MODELS_JSON_PATH = path.join(PROJECT_ROOT, 'models.json');
24
+ const README_PATH = path.join(PROJECT_ROOT, 'README.md');
25
+ const PATCH_JSON_PATH = path.join(PROJECT_ROOT, 'patch.json');
26
+ const CUSTOM_MODELS_JSON_PATH = path.join(PROJECT_ROOT, 'custom-models.json');
27
+
28
+ const ENDPOINT_ROOT = (process.env.ZRO_ENDPOINT_ROOT || 'https://zro.moonmath.ai').replace(/\/+$/, '');
29
+ const MODELS_API_URL = `${ENDPOINT_ROOT}/api/cli/models`;
30
+
31
+ const AUTH_JSON_PATH = path.join(os.homedir(), '.config', 'zro', 'credentials.json');
32
+
33
+ function loadJson(filePath) {
34
+ try {
35
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function saveJson(filePath, data) {
42
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
43
+ }
44
+
45
+ function resolveApiKey() {
46
+ if (process.env.ZRO_API_KEY) return process.env.ZRO_API_KEY;
47
+ const configRoot = process.env.XDG_CONFIG_HOME
48
+ ? path.join(process.env.XDG_CONFIG_HOME, 'zro')
49
+ : path.join(os.homedir(), '.config', 'zro');
50
+ try {
51
+ const parsed = JSON.parse(fs.readFileSync(path.join(configRoot, 'credentials.json'), 'utf8'));
52
+ if (parsed && typeof parsed.apiKey === 'string' && parsed.apiKey.trim()) return parsed.apiKey;
53
+ } catch { /* None stored yet */ }
54
+ return null;
55
+ }
56
+
57
+ // ─── Patch application (same pipeline as index.ts) ────────────────────────────
58
+
59
+ function applyPatch(model, patch) {
60
+ const result = { ...model };
61
+ if (patch.name !== undefined) result.name = patch.name;
62
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
63
+ if (patch.input !== undefined) result.input = patch.input;
64
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
65
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
66
+ if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = { ...patch.thinkingLevelMap };
67
+ if (patch.cost) {
68
+ result.cost = {
69
+ input: patch.cost.input ?? result.cost.input,
70
+ output: patch.cost.output ?? result.cost.output,
71
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
72
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
73
+ };
74
+ }
75
+ if (patch.compat) {
76
+ result.compat = { ...(result.compat || {}), ...patch.compat };
77
+ }
78
+ if (!result.reasoning && result.compat?.thinkingFormat) {
79
+ delete result.compat.thinkingFormat;
80
+ }
81
+ if (!result.reasoning && result.thinkingLevelMap) {
82
+ delete result.thinkingLevelMap;
83
+ }
84
+ if (result.compat && Object.keys(result.compat).length === 0) {
85
+ delete result.compat;
86
+ }
87
+ return result;
88
+ }
89
+
90
+ function buildModels(baseModels, customModels, patchData) {
91
+ const modelMap = new Map();
92
+ for (const model of baseModels) modelMap.set(model.id, model);
93
+ for (const [id, patchEntry] of Object.entries(patchData)) {
94
+ const existing = modelMap.get(id);
95
+ if (existing) modelMap.set(id, applyPatch(existing, patchEntry));
96
+ }
97
+ for (const model of customModels) {
98
+ const existing = modelMap.get(model.id);
99
+ const patchEntry = patchData[model.id];
100
+ if (existing && patchEntry) modelMap.set(model.id, applyPatch(model, patchEntry));
101
+ else if (existing) modelMap.set(model.id, model);
102
+ else if (patchEntry) modelMap.set(model.id, applyPatch(model, patchEntry));
103
+ else modelMap.set(model.id, model);
104
+ }
105
+ return Array.from(modelMap.values());
106
+ }
107
+
108
+ // ─── Model transformation ─────────────────────────────────────────────────────
109
+
110
+ const PI_THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
111
+
112
+ // Builds the thinkingLevelMap zro's own pi adapter writes: each catalog
113
+ // reasoning level maps its piLevel (off|minimal|low|medium|high|xhigh) to the
114
+ // id the inference proxy expects (e.g. glm-5.2 → { off: "none", high: "high",
115
+ // xhigh: "max" }). pi levels without a matching catalog level are null.
116
+ function buildThinkingLevelMap(levels) {
117
+ const result = Object.fromEntries(PI_THINKING_LEVELS.map((level) => [level, null]));
118
+ for (const level of levels) {
119
+ if (level && typeof level.id === 'string' && level.id &&
120
+ typeof level.piLevel === 'string' && result[level.piLevel] !== undefined) {
121
+ result[level.piLevel] = level.id;
122
+ }
123
+ }
124
+ return result;
125
+ }
126
+
127
+ function transformModel(apiModel) {
128
+ const levels = Array.isArray(apiModel?.reasoning?.levels) ? apiModel.reasoning.levels : [];
129
+ return {
130
+ id: apiModel.id,
131
+ name: apiModel.displayName || apiModel.id,
132
+ reasoning: true,
133
+ thinkingLevelMap: buildThinkingLevelMap(levels),
134
+ input: ['text'],
135
+ cost: {
136
+ input: 0,
137
+ output: 0,
138
+ cacheRead: 0,
139
+ cacheWrite: 0,
140
+ },
141
+ contextWindow: apiModel.contextWindow || 0,
142
+ maxTokens: apiModel.maxOutputTokens || apiModel.contextWindow || 0,
143
+ compat: {
144
+ supportsDeveloperRole: false,
145
+ supportsReasoningEffort: true,
146
+ maxTokensField: 'max_tokens',
147
+ },
148
+ };
149
+ }
150
+
151
+ // ─── README generation ────────────────────────────────────────────────────────
152
+
153
+ function formatCost(cost) {
154
+ if (cost === 0 || cost === null || cost === undefined) return '—';
155
+ return '$' + cost.toFixed(2);
156
+ }
157
+
158
+ function formatNumber(num) {
159
+ if (num === null || num === undefined) return '-';
160
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
161
+ if (num >= 1000) return `${Math.round(num / 1000)}K`;
162
+ return num.toString();
163
+ }
164
+
165
+ function getInputTypes(inputTypes) {
166
+ const types = inputTypes || ['text'];
167
+ if (types.includes('image') && types.includes('text')) return 'Text + Image';
168
+ if (types.includes('image')) return 'Image';
169
+ return 'Text';
170
+ }
171
+
172
+ function generateReadmeRow(model) {
173
+ const cost = model.cost || {};
174
+ return `| ${model.name} | ${getInputTypes(model.input)} | ${formatNumber(model.contextWindow)} | ${formatNumber(model.maxTokens)} | ${formatCost(cost.input)} | ${formatCost(cost.output)} |`;
175
+ }
176
+
177
+ function updateReadme(models) {
178
+ let readme = fs.readFileSync(README_PATH, 'utf8');
179
+
180
+ const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name));
181
+ const tableRows = sortedModels.map(generateReadmeRow).join('\n');
182
+ const newTable = `| Model | Type | Context | Max Tokens | Input Cost | Output Cost |
183
+ |-------|------|---------|------------|------------|-------------|
184
+ ${tableRows}`;
185
+
186
+ const tableRegex = /\| Model \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
187
+ readme = readme.replace(tableRegex, newTable);
188
+
189
+ readme = readme.replace(/\*\*\d+\+ AI Models\*\*/, `**${models.length}+ AI Models**`);
190
+
191
+ fs.writeFileSync(README_PATH, readme);
192
+ console.log(`✓ Updated README.md with ${models.length} models`);
193
+ }
194
+
195
+ // ─── Deprecated-model reconciliation ──────────────────────────────────────────
196
+
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(PROJECT_ROOT, '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
+
280
+ // ─── Main ─────────────────────────────────────────────────────────────────────
281
+
282
+ async function main() {
283
+ const apiKey = resolveApiKey();
284
+ if (!apiKey) {
285
+ console.error('Error: No API key found. Run `zro login` first, or set ZRO_API_KEY.');
286
+ console.error('Usage: ZRO_API_KEY=your-key node scripts/update-models.js');
287
+ process.exit(1);
288
+ }
289
+
290
+ console.log(`Fetching models from ${MODELS_API_URL}...`);
291
+
292
+ try {
293
+ const response = await fetch(MODELS_API_URL, {
294
+ headers: { Authorization: `Bearer ${apiKey}` },
295
+ });
296
+ if (!response.ok) {
297
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
298
+ }
299
+
300
+ const apiResponse = await response.json();
301
+ const apiModels = Array.isArray(apiResponse)
302
+ ? apiResponse
303
+ : (apiResponse.models || apiResponse.data || []);
304
+
305
+ if (!Array.isArray(apiModels)) {
306
+ throw new Error('API response does not contain an array of models');
307
+ }
308
+
309
+ console.log(`✓ Fetched ${apiModels.length} models from API`);
310
+
311
+ // Load existing models.json — source of truth for curated specs
312
+ let existingModels = [];
313
+ try {
314
+ existingModels = JSON.parse(fs.readFileSync(MODELS_JSON_PATH, 'utf8'));
315
+ } catch {
316
+ // File might not exist yet
317
+ }
318
+ const existingModelsMap = {};
319
+ for (const m of existingModels) {
320
+ existingModelsMap[m.id] = m;
321
+ }
322
+
323
+ // Transform models from API. The live catalog is authoritative for
324
+ // context/output limits; curated name/thinkingLevelMap/compat wins.
325
+ const apiTransformed = apiModels
326
+ .map(m => ({ ...transformModel(m), ...(existingModelsMap[m.id] ?? {}) }))
327
+ .sort((a, b) => a.name.localeCompare(b.name));
328
+
329
+ // Load patch overrides for README rendering.
330
+ const patch = loadJson(PATCH_JSON_PATH);
331
+
332
+ // Update models.json — curated API data
333
+ // Move delisted models to deprecated-models.json BEFORE models.json is overwritten
334
+ updateDeprecatedModels(MODELS_JSON_PATH, apiTransformed);
335
+ fs.writeFileSync(MODELS_JSON_PATH, JSON.stringify(apiTransformed, null, 2) + '\n');
336
+ console.log(`✓ Updated models.json (${apiTransformed.length} models)`);
337
+
338
+ // Load custom-models.json
339
+ const customModels = Array.isArray(loadJson(CUSTOM_MODELS_JSON_PATH))
340
+ ? loadJson(CUSTOM_MODELS_JSON_PATH)
341
+ : [];
342
+
343
+ // Check for custom models now available upstream (remove duplicates)
344
+ const upstreamIds = new Set(apiTransformed.map(m => m.id));
345
+ const duplicates = customModels.filter(m => upstreamIds.has(m.id));
346
+ if (duplicates.length > 0) {
347
+ console.log(`\nFound ${duplicates.length} custom model(s) now available upstream:`);
348
+ for (const dup of duplicates) {
349
+ console.log(` - ${dup.id} (${dup.name})`);
350
+ }
351
+ const cleaned = customModels.filter(m => !upstreamIds.has(m.id));
352
+ saveJson(CUSTOM_MODELS_JSON_PATH, cleaned);
353
+ console.log(`✓ Removed ${duplicates.length} duplicate(s) from custom-models.json`);
354
+ customModels.length = 0;
355
+ customModels.push(...cleaned);
356
+ }
357
+
358
+ // Build merged models with patches for README
359
+ const readmeModels = buildModels(withDeprecatedForReadme(apiTransformed), customModels, patch);
360
+ readmeModels.sort((a, b) => a.name.localeCompare(b.name));
361
+
362
+ // Update README
363
+ updateReadme(readmeModels);
364
+
365
+ // Summary
366
+ console.log('\n--- Summary ---');
367
+ console.log(`Total models: ${readmeModels.length}`);
368
+ console.log(`Reasoning models: ${readmeModels.filter(m => m.reasoning).length}`);
369
+ console.log(`Vision models: ${readmeModels.filter(m => m.input.includes('image')).length}`);
370
+
371
+ const newIds = new Set(apiTransformed.map(m => m.id));
372
+ const oldIds = new Set(existingModels.map(m => m.id));
373
+
374
+ const added = [...newIds].filter(id => !oldIds.has(id));
375
+ const removed = [...oldIds].filter(id => !newIds.has(id));
376
+
377
+ if (added.length > 0) console.log(`\nNew models: ${added.join(', ')}`);
378
+ if (removed.length > 0) console.log(`\nRemoved models: ${removed.join(', ')}`);
379
+
380
+ console.log('\nDone!');
381
+ } catch (error) {
382
+ console.error('Error:', error.message);
383
+ process.exit(1);
384
+ }
385
+ }
386
+
387
+ main();