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/.github/FUNDING.yml +4 -0
- package/AGENTS.md +58 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/custom-models.json +1 -0
- package/deprecated-models.json +1 -0
- package/index.ts +1110 -0
- package/models.json +89 -0
- package/package.json +40 -0
- package/patch.json +1 -0
- package/pnpm-workspace.yaml +15 -0
- package/scripts/update-models.js +387 -0
- package/status.ts +298 -0
- package/tests/status.smoke.ts +164 -0
- package/tsconfig.json +15 -0
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();
|