pi-umans-provider 1.0.2
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/AGENTS.md +56 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/custom-models.json +1 -0
- package/index.ts +700 -0
- package/models.json +102 -0
- package/package.json +37 -0
- package/patch.json +66 -0
- package/scripts/update-models.js +263 -0
- package/tsconfig.json +12 -0
package/models.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "umans-coder",
|
|
4
|
+
"name": "Umans Coder",
|
|
5
|
+
"reasoning": true,
|
|
6
|
+
"input": [
|
|
7
|
+
"text",
|
|
8
|
+
"image"
|
|
9
|
+
],
|
|
10
|
+
"cost": {
|
|
11
|
+
"input": 0,
|
|
12
|
+
"output": 0,
|
|
13
|
+
"cacheRead": 0,
|
|
14
|
+
"cacheWrite": 0
|
|
15
|
+
},
|
|
16
|
+
"contextWindow": 262144,
|
|
17
|
+
"maxTokens": 32768
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "umans-flash",
|
|
21
|
+
"name": "Umans Flash",
|
|
22
|
+
"reasoning": true,
|
|
23
|
+
"input": [
|
|
24
|
+
"text",
|
|
25
|
+
"image"
|
|
26
|
+
],
|
|
27
|
+
"cost": {
|
|
28
|
+
"input": 0,
|
|
29
|
+
"output": 0,
|
|
30
|
+
"cacheRead": 0,
|
|
31
|
+
"cacheWrite": 0
|
|
32
|
+
},
|
|
33
|
+
"contextWindow": 262144,
|
|
34
|
+
"maxTokens": 32768
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "umans-glm-5.2",
|
|
38
|
+
"name": "Umans GLM 5.2",
|
|
39
|
+
"reasoning": true,
|
|
40
|
+
"input": [
|
|
41
|
+
"text"
|
|
42
|
+
],
|
|
43
|
+
"cost": {
|
|
44
|
+
"input": 0,
|
|
45
|
+
"output": 0,
|
|
46
|
+
"cacheRead": 0,
|
|
47
|
+
"cacheWrite": 0
|
|
48
|
+
},
|
|
49
|
+
"contextWindow": 405504,
|
|
50
|
+
"maxTokens": 131071
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "umans-glm-5.2-nvfp4",
|
|
54
|
+
"name": "Umans GLM 5.2 NVFP4 (experimental, short test from Jun 29)",
|
|
55
|
+
"reasoning": true,
|
|
56
|
+
"input": [
|
|
57
|
+
"text"
|
|
58
|
+
],
|
|
59
|
+
"cost": {
|
|
60
|
+
"input": 0,
|
|
61
|
+
"output": 0,
|
|
62
|
+
"cacheRead": 0,
|
|
63
|
+
"cacheWrite": 0
|
|
64
|
+
},
|
|
65
|
+
"contextWindow": 405504,
|
|
66
|
+
"maxTokens": 131071
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "umans-kimi-k2.7",
|
|
70
|
+
"name": "Umans Kimi K2.7 Code",
|
|
71
|
+
"reasoning": true,
|
|
72
|
+
"input": [
|
|
73
|
+
"text",
|
|
74
|
+
"image"
|
|
75
|
+
],
|
|
76
|
+
"cost": {
|
|
77
|
+
"input": 0,
|
|
78
|
+
"output": 0,
|
|
79
|
+
"cacheRead": 0,
|
|
80
|
+
"cacheWrite": 0
|
|
81
|
+
},
|
|
82
|
+
"contextWindow": 262144,
|
|
83
|
+
"maxTokens": 32768
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "umans-qwen3.6-35b-a3b",
|
|
87
|
+
"name": "Umans Qwen3.6 35B A3B",
|
|
88
|
+
"reasoning": true,
|
|
89
|
+
"input": [
|
|
90
|
+
"text",
|
|
91
|
+
"image"
|
|
92
|
+
],
|
|
93
|
+
"cost": {
|
|
94
|
+
"input": 0,
|
|
95
|
+
"output": 0,
|
|
96
|
+
"cacheRead": 0,
|
|
97
|
+
"cacheWrite": 0
|
|
98
|
+
},
|
|
99
|
+
"contextWindow": 262144,
|
|
100
|
+
"maxTokens": 32768
|
|
101
|
+
}
|
|
102
|
+
]
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-umans-provider",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "UMANS provider extension for pi - Access Kimi K2.6, GLM 5.1, Qwen 3.6, and coding-optimized models through the UMANS API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi",
|
|
9
|
+
"extension",
|
|
10
|
+
"provider",
|
|
11
|
+
"umans",
|
|
12
|
+
"ai",
|
|
13
|
+
"llm",
|
|
14
|
+
"kimi",
|
|
15
|
+
"moonshot",
|
|
16
|
+
"glm",
|
|
17
|
+
"qwen",
|
|
18
|
+
"coding"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.ts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^25.9.2",
|
|
29
|
+
"typescript": "^6.0.3"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"clean": "echo 'nothing to clean'",
|
|
33
|
+
"build": "echo 'nothing to build'",
|
|
34
|
+
"check": "echo 'nothing to check'",
|
|
35
|
+
"update-models": "node scripts/update-models.js"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/patch.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"umans-coder": {
|
|
3
|
+
"thinkingLevelMap": { "off": "none" },
|
|
4
|
+
"compat": {
|
|
5
|
+
"supportsDeveloperRole": false,
|
|
6
|
+
"supportsReasoningEffort": true,
|
|
7
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"umans-flash": {
|
|
11
|
+
"thinkingLevelMap": { "off": "none" },
|
|
12
|
+
"compat": {
|
|
13
|
+
"supportsDeveloperRole": false,
|
|
14
|
+
"supportsReasoningEffort": true,
|
|
15
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"umans-kimi-k2.6": {
|
|
19
|
+
"thinkingLevelMap": { "off": "none" },
|
|
20
|
+
"compat": {
|
|
21
|
+
"supportsDeveloperRole": false,
|
|
22
|
+
"supportsReasoningEffort": true,
|
|
23
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"umans-kimi-k2.7": {
|
|
27
|
+
"thinkingLevelMap": { "off": "none" },
|
|
28
|
+
"compat": {
|
|
29
|
+
"supportsDeveloperRole": false,
|
|
30
|
+
"supportsReasoningEffort": true,
|
|
31
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"umans-glm-5.1": {
|
|
35
|
+
"thinkingLevelMap": { "off": "none" },
|
|
36
|
+
"compat": {
|
|
37
|
+
"supportsDeveloperRole": false,
|
|
38
|
+
"supportsReasoningEffort": true,
|
|
39
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"umans-glm-5.2": {
|
|
43
|
+
"thinkingLevelMap": { "off": "none" },
|
|
44
|
+
"compat": {
|
|
45
|
+
"supportsDeveloperRole": false,
|
|
46
|
+
"supportsReasoningEffort": true,
|
|
47
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"umans-glm-5.2-nvfp4": {
|
|
51
|
+
"thinkingLevelMap": { "off": "none" },
|
|
52
|
+
"compat": {
|
|
53
|
+
"supportsDeveloperRole": false,
|
|
54
|
+
"supportsReasoningEffort": true,
|
|
55
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"umans-qwen3.6-35b-a3b": {
|
|
59
|
+
"thinkingLevelMap": { "off": "none" },
|
|
60
|
+
"compat": {
|
|
61
|
+
"supportsDeveloperRole": false,
|
|
62
|
+
"supportsReasoningEffort": true,
|
|
63
|
+
"requiresReasoningContentOnAssistantMessages": true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Update UMANS models from API
|
|
4
|
+
*
|
|
5
|
+
* Fetches models from https://api.code.umans.ai/v1/models/info and updates:
|
|
6
|
+
* - models.json: Pure API model definitions (no patches baked in)
|
|
7
|
+
* - README.md: Model table with patch.json overrides applied
|
|
8
|
+
*
|
|
9
|
+
* models.json reflects the raw API data as-is. patch.json corrects
|
|
10
|
+
* compatibility details at runtime (index.ts) and is also applied when
|
|
11
|
+
* generating the README table so the docs reflect reality.
|
|
12
|
+
*
|
|
13
|
+
* Idempotent: re-running produces the same result if the API hasn't changed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
const MODELS_INFO_API_URL = "https://api.code.umans.ai/v1/models/info";
|
|
23
|
+
const MODELS_JSON_PATH = path.join(__dirname, "..", "models.json");
|
|
24
|
+
const PATCH_JSON_PATH = path.join(__dirname, "..", "patch.json");
|
|
25
|
+
const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, "..", "custom-models.json");
|
|
26
|
+
const README_PATH = path.join(__dirname, "..", "README.md");
|
|
27
|
+
|
|
28
|
+
// Skip deprecated aliases
|
|
29
|
+
const DEPRECATED_MODELS = new Set(["umans-flash-beta"]);
|
|
30
|
+
|
|
31
|
+
function transformApiModel(id, info) {
|
|
32
|
+
if (info.deprecation || DEPRECATED_MODELS.has(id)) return null;
|
|
33
|
+
|
|
34
|
+
const caps = info.capabilities || {};
|
|
35
|
+
const hasVision = caps.supports_vision === true;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
name: info.display_name || info.name || id,
|
|
40
|
+
reasoning: true, // All UMANS models are reasoning-capable
|
|
41
|
+
input: hasVision ? ["text", "image"] : ["text"],
|
|
42
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, // Subscription-based
|
|
43
|
+
contextWindow: caps.context_window || 131072,
|
|
44
|
+
maxTokens: caps.recommended_max_tokens || caps.max_completion_tokens || 131072,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyPatch(model, patch) {
|
|
49
|
+
const result = { ...model };
|
|
50
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
51
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
52
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
53
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
54
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
55
|
+
if (patch.cost) {
|
|
56
|
+
result.cost = {
|
|
57
|
+
input: patch.cost.input ?? result.cost.input,
|
|
58
|
+
output: patch.cost.output ?? result.cost.output,
|
|
59
|
+
cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
|
|
60
|
+
cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (patch.compat) {
|
|
64
|
+
result.compat = { ...(result.compat || {}), ...patch.compat };
|
|
65
|
+
}
|
|
66
|
+
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
67
|
+
delete result.compat.thinkingFormat;
|
|
68
|
+
}
|
|
69
|
+
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
70
|
+
delete result.compat;
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildModels(baseModels, customModels, patchData) {
|
|
76
|
+
const modelMap = new Map();
|
|
77
|
+
for (const model of baseModels) {
|
|
78
|
+
modelMap.set(model.id, model);
|
|
79
|
+
}
|
|
80
|
+
for (const [id, patchEntry] of Object.entries(patchData)) {
|
|
81
|
+
const existing = modelMap.get(id);
|
|
82
|
+
if (existing) {
|
|
83
|
+
modelMap.set(id, applyPatch(existing, patchEntry));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const model of customModels) {
|
|
87
|
+
const existing = modelMap.get(model.id);
|
|
88
|
+
const patchEntry = patchData[model.id];
|
|
89
|
+
if (existing && patchEntry) {
|
|
90
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
91
|
+
} else if (existing) {
|
|
92
|
+
modelMap.set(model.id, model);
|
|
93
|
+
} else if (patchEntry) {
|
|
94
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
95
|
+
} else {
|
|
96
|
+
modelMap.set(model.id, model);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Array.from(modelMap.values());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadJson(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
105
|
+
} catch {
|
|
106
|
+
return Array.isArray(filePath) ? [] : {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatContextWindow(n) {
|
|
111
|
+
if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
112
|
+
if (n >= 1000) return `${Math.round(n / 1000)}K`;
|
|
113
|
+
return n.toString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function generateReadmeTable(models) {
|
|
117
|
+
const lines = [
|
|
118
|
+
"| Model | Base | Context | Vision | Reasoning | Max Output |",
|
|
119
|
+
"|-------|------|---------|--------|-----------|------------|",
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const model of models) {
|
|
123
|
+
const name = model.name.replace(/^Umans\s+/, "");
|
|
124
|
+
const base = model._meta?.baseModel || "—";
|
|
125
|
+
const context = formatContextWindow(model.contextWindow);
|
|
126
|
+
const vision = model.input.includes("image") ? "✅" : "❌";
|
|
127
|
+
const reasoning = model.reasoning ? "✅" : "❌";
|
|
128
|
+
const maxTokens = formatContextWindow(model.maxTokens);
|
|
129
|
+
|
|
130
|
+
lines.push(`| ${name} | ${base} | ${context} | ${vision} | ${reasoning} | ${maxTokens} |`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function updateReadme(models) {
|
|
137
|
+
let readme = fs.readFileSync(README_PATH, "utf8");
|
|
138
|
+
const newTable = generateReadmeTable(models);
|
|
139
|
+
|
|
140
|
+
const tableRegex =
|
|
141
|
+
/(## Available Models\n\n)\| Model \| Base \| Context \| Vision \| Reasoning \| Max Output \|\n\|[-| ]+\|(\n\|[^\n]+\|)*\n*/;
|
|
142
|
+
|
|
143
|
+
if (tableRegex.test(readme)) {
|
|
144
|
+
readme = readme.replace(tableRegex, (match, header) => `${header}${newTable}\n\n`);
|
|
145
|
+
fs.writeFileSync(README_PATH, readme);
|
|
146
|
+
console.log("✓ Updated README.md");
|
|
147
|
+
} else {
|
|
148
|
+
console.warn('⚠ Could not find model table in "## Available Models" section');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cleanModelForJson(model) {
|
|
153
|
+
const { _meta, ...cleanModel } = model;
|
|
154
|
+
return cleanModel;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function main() {
|
|
158
|
+
console.log(`Fetching models from ${MODELS_INFO_API_URL}...`);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetch(MODELS_INFO_API_URL);
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const apiData = await response.json();
|
|
167
|
+
|
|
168
|
+
if (!apiData || typeof apiData !== "object") {
|
|
169
|
+
throw new Error("API response is not a valid object");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Load existing models.json — source of truth for curated specs
|
|
173
|
+
let existingModels = [];
|
|
174
|
+
try {
|
|
175
|
+
existingModels = JSON.parse(fs.readFileSync(MODELS_JSON_PATH, "utf8"));
|
|
176
|
+
} catch {}
|
|
177
|
+
const existingModelsMap = {};
|
|
178
|
+
for (const m of existingModels) {
|
|
179
|
+
existingModelsMap[m.id] = m;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Transform models from API
|
|
183
|
+
let apiTransformed = Object.entries(apiData)
|
|
184
|
+
.map(([id, info]) => transformApiModel(id, info))
|
|
185
|
+
.filter((m) => m !== null);
|
|
186
|
+
|
|
187
|
+
// Enrich with base model info for README
|
|
188
|
+
apiTransformed = apiTransformed.map((model) => {
|
|
189
|
+
const info = apiData[model.id];
|
|
190
|
+
const baseModel = info?.base_model?.oss_base || info?.base_model?.name || "—";
|
|
191
|
+
return { ...model, _meta: { baseModel } };
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Sort models alphabetically by name
|
|
195
|
+
apiTransformed.sort((a, b) => a.name.localeCompare(b.name));
|
|
196
|
+
|
|
197
|
+
console.log(`✓ Fetched and transformed ${apiTransformed.length} models from API`);
|
|
198
|
+
|
|
199
|
+
// Preserve existing curated data (compat, reasoning flags, etc.)
|
|
200
|
+
apiTransformed = apiTransformed.map((model) => {
|
|
201
|
+
const existing = existingModelsMap[model.id];
|
|
202
|
+
if (existing) {
|
|
203
|
+
return {
|
|
204
|
+
...model,
|
|
205
|
+
// Preserve curated fields that the API doesn't provide
|
|
206
|
+
reasoning: typeof existing.reasoning === "boolean" ? existing.reasoning : model.reasoning,
|
|
207
|
+
input: model.input,
|
|
208
|
+
maxTokens: existing.maxTokens || model.maxTokens,
|
|
209
|
+
_meta: model._meta, // Keep API-derived _meta for README
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return model;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Update models.json — pure API data, no patches baked in
|
|
216
|
+
const cleanModels = apiTransformed.map(cleanModelForJson);
|
|
217
|
+
fs.writeFileSync(MODELS_JSON_PATH, JSON.stringify(cleanModels, null, 2) + "\n");
|
|
218
|
+
console.log("✓ Updated models.json (pure API data)");
|
|
219
|
+
|
|
220
|
+
// Load patch.json and apply for README generation
|
|
221
|
+
let patch = {};
|
|
222
|
+
try {
|
|
223
|
+
patch = JSON.parse(fs.readFileSync(PATCH_JSON_PATH, "utf8"));
|
|
224
|
+
console.log(`✓ Loaded ${Object.keys(patch).length} patch overrides from patch.json`);
|
|
225
|
+
} catch {
|
|
226
|
+
console.warn("⚠ Could not load patch.json, README may show incorrect compat flags");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Build full model list for README: base → patch → custom
|
|
230
|
+
const customModels = loadJson(CUSTOM_MODELS_JSON_PATH);
|
|
231
|
+
const readmeModels = buildModels(apiTransformed, Array.isArray(customModels) ? customModels : [], patch);
|
|
232
|
+
readmeModels.sort((a, b) => a.name.localeCompare(b.name));
|
|
233
|
+
console.log("✓ Built model list (base → patch → custom) for README");
|
|
234
|
+
|
|
235
|
+
// Update README.md with patched data
|
|
236
|
+
updateReadme(readmeModels);
|
|
237
|
+
|
|
238
|
+
// Summary
|
|
239
|
+
console.log("\n--- Summary ---");
|
|
240
|
+
console.log(`Total models: ${readmeModels.length}`);
|
|
241
|
+
console.log(`Reasoning models: ${readmeModels.filter((m) => m.reasoning).length}`);
|
|
242
|
+
console.log(`Vision models: ${readmeModels.filter((m) => m.input.includes("image")).length}`);
|
|
243
|
+
console.log(`Subscription-based: All models $0/M (included in plan)`);
|
|
244
|
+
|
|
245
|
+
const oldIds = new Set(existingModels.map((m) => m.id));
|
|
246
|
+
const newIds = new Set(apiTransformed.map((m) => m.id));
|
|
247
|
+
|
|
248
|
+
const added = [...newIds].filter((id) => !oldIds.has(id));
|
|
249
|
+
const removed = [...oldIds].filter((id) => !newIds.has(id));
|
|
250
|
+
|
|
251
|
+
if (added.length > 0) {
|
|
252
|
+
console.log(`\nNew models: ${added.join(", ")}`);
|
|
253
|
+
}
|
|
254
|
+
if (removed.length > 0) {
|
|
255
|
+
console.log(`\nRemoved models: ${removed.join(", ")}`);
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("Error:", error.message);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main();
|