pi-hypercharm-provider 1.1.3 → 1.1.5
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 +1 -0
- package/README.md +37 -48
- package/deprecated-models.json +1 -0
- package/index.ts +100 -78
- package/models.json +462 -104
- package/package.json +1 -1
- package/patch.json +1 -155
- package/scripts/update-models.js +136 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hypercharm-provider",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "HyperCharm provider extension for pi - Access DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, and GPT-OSS models through the Charm Hyper API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/patch.json
CHANGED
|
@@ -1,155 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"deepseek-v4-flash": {
|
|
3
|
-
"maxTokens": 384000,
|
|
4
|
-
"thinkingLevelMap": {
|
|
5
|
-
"minimal": null,
|
|
6
|
-
"low": null,
|
|
7
|
-
"medium": null,
|
|
8
|
-
"high": "high",
|
|
9
|
-
"max": "max"
|
|
10
|
-
},
|
|
11
|
-
"compat": {
|
|
12
|
-
"thinkingFormat": "deepseek",
|
|
13
|
-
"maxTokensField": "max_tokens",
|
|
14
|
-
"supportsDeveloperRole": true,
|
|
15
|
-
"supportsStore": false,
|
|
16
|
-
"supportsReasoningEffort": true,
|
|
17
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"deepseek-v4-pro": {
|
|
21
|
-
"maxTokens": 384000,
|
|
22
|
-
"thinkingLevelMap": {
|
|
23
|
-
"minimal": null,
|
|
24
|
-
"low": null,
|
|
25
|
-
"medium": null,
|
|
26
|
-
"high": "high",
|
|
27
|
-
"max": "max"
|
|
28
|
-
},
|
|
29
|
-
"compat": {
|
|
30
|
-
"thinkingFormat": "deepseek",
|
|
31
|
-
"maxTokensField": "max_tokens",
|
|
32
|
-
"supportsDeveloperRole": true,
|
|
33
|
-
"supportsStore": false,
|
|
34
|
-
"supportsReasoningEffort": true,
|
|
35
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"gemma-4-26b-a4b-it": {
|
|
39
|
-
"compat": {
|
|
40
|
-
"thinkingFormat": "openai",
|
|
41
|
-
"maxTokensField": "max_tokens",
|
|
42
|
-
"supportsDeveloperRole": true,
|
|
43
|
-
"supportsStore": false,
|
|
44
|
-
"supportsReasoningEffort": true
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
"glm-5": {
|
|
48
|
-
"compat": {
|
|
49
|
-
"thinkingFormat": "openai",
|
|
50
|
-
"maxTokensField": "max_tokens",
|
|
51
|
-
"supportsDeveloperRole": true,
|
|
52
|
-
"supportsStore": false,
|
|
53
|
-
"supportsReasoningEffort": true
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"glm-5.1": {
|
|
57
|
-
"compat": {
|
|
58
|
-
"thinkingFormat": "openai",
|
|
59
|
-
"maxTokensField": "max_tokens",
|
|
60
|
-
"supportsDeveloperRole": true,
|
|
61
|
-
"supportsStore": false,
|
|
62
|
-
"supportsReasoningEffort": true
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
"gpt-oss-120b": {
|
|
66
|
-
"compat": {
|
|
67
|
-
"thinkingFormat": "openai",
|
|
68
|
-
"maxTokensField": "max_tokens",
|
|
69
|
-
"supportsDeveloperRole": true,
|
|
70
|
-
"supportsStore": false,
|
|
71
|
-
"supportsReasoningEffort": true
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"kimi-k2.5": {
|
|
75
|
-
"compat": {
|
|
76
|
-
"thinkingFormat": "openai",
|
|
77
|
-
"maxTokensField": "max_tokens",
|
|
78
|
-
"supportsDeveloperRole": true,
|
|
79
|
-
"supportsStore": false,
|
|
80
|
-
"supportsReasoningEffort": true
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
"kimi-k2.6": {
|
|
84
|
-
"compat": {
|
|
85
|
-
"thinkingFormat": "openai",
|
|
86
|
-
"maxTokensField": "max_tokens",
|
|
87
|
-
"supportsDeveloperRole": true,
|
|
88
|
-
"supportsStore": false,
|
|
89
|
-
"supportsReasoningEffort": true
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
"minimax-m2.7": {
|
|
93
|
-
"compat": {
|
|
94
|
-
"thinkingFormat": "openai",
|
|
95
|
-
"maxTokensField": "max_tokens",
|
|
96
|
-
"supportsDeveloperRole": true,
|
|
97
|
-
"supportsStore": false,
|
|
98
|
-
"supportsReasoningEffort": true
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
"qwen3.6-flash": {
|
|
102
|
-
"compat": {
|
|
103
|
-
"thinkingFormat": "openai",
|
|
104
|
-
"maxTokensField": "max_tokens",
|
|
105
|
-
"supportsDeveloperRole": true,
|
|
106
|
-
"supportsStore": false,
|
|
107
|
-
"supportsReasoningEffort": true
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
"qwen3.6-max": {
|
|
111
|
-
"compat": {
|
|
112
|
-
"thinkingFormat": "openai",
|
|
113
|
-
"maxTokensField": "max_tokens",
|
|
114
|
-
"supportsDeveloperRole": true,
|
|
115
|
-
"supportsStore": false,
|
|
116
|
-
"supportsReasoningEffort": true
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
"qwen3.6-plus": {
|
|
120
|
-
"compat": {
|
|
121
|
-
"thinkingFormat": "openai",
|
|
122
|
-
"maxTokensField": "max_tokens",
|
|
123
|
-
"supportsDeveloperRole": true,
|
|
124
|
-
"supportsStore": false,
|
|
125
|
-
"supportsReasoningEffort": true
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
"qwen3.7-max": {
|
|
129
|
-
"compat": {
|
|
130
|
-
"thinkingFormat": "openai",
|
|
131
|
-
"maxTokensField": "max_tokens",
|
|
132
|
-
"supportsDeveloperRole": true,
|
|
133
|
-
"supportsStore": false,
|
|
134
|
-
"supportsReasoningEffort": true
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
"qwen3-coder-480b-a35b-instruct-int4-mixed-ar": {
|
|
138
|
-
"compat": {
|
|
139
|
-
"thinkingFormat": "openai",
|
|
140
|
-
"maxTokensField": "max_tokens",
|
|
141
|
-
"supportsDeveloperRole": true,
|
|
142
|
-
"supportsStore": false,
|
|
143
|
-
"supportsReasoningEffort": true
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
"qwen3-next-80b-a3b-instruct": {
|
|
147
|
-
"compat": {
|
|
148
|
-
"thinkingFormat": "openai",
|
|
149
|
-
"maxTokensField": "max_tokens",
|
|
150
|
-
"supportsDeveloperRole": true,
|
|
151
|
-
"supportsStore": false,
|
|
152
|
-
"supportsReasoningEffort": true
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
1
|
+
{}
|
package/scripts/update-models.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Update HyperCharm models from
|
|
4
|
+
* Update HyperCharm models from Charm's official typed provider catalog
|
|
5
5
|
*
|
|
6
|
-
* Fetches models from https://hyper.charm.land/v1/
|
|
7
|
-
* - models.json:
|
|
8
|
-
* - README.md:
|
|
6
|
+
* Fetches models from https://hyper.charm.land/v1/provider and updates:
|
|
7
|
+
* - models.json: canonical API-owned metadata used by @charmland/pi-hyper-provider
|
|
8
|
+
* - README.md: model table with patch.json overrides applied
|
|
9
9
|
*
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Note: supports_reasoning is unreliable for some models (reports true for
|
|
15
|
-
* Llama 3.3 70B which doesn't support extended thinking). models.json
|
|
16
|
-
* curates reasoning flags based on known model capabilities; patch.json
|
|
17
|
-
* adds compat flags and corrections.
|
|
10
|
+
* The endpoint provides canonical names, $/M pricing, context/output limits,
|
|
11
|
+
* can_reason, optional reasoning levels, and attachment support. models.json is
|
|
12
|
+
* pure API data. patch.json is reserved for verified endpoint regressions and
|
|
13
|
+
* currently contains no overrides.
|
|
18
14
|
*
|
|
19
15
|
* Merge order for README: models.json → apply patch.json → merge custom-models.json
|
|
20
16
|
*/
|
|
@@ -25,7 +21,7 @@ import { fileURLToPath } from 'url';
|
|
|
25
21
|
|
|
26
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
23
|
|
|
28
|
-
const MODELS_API_URL = 'https://hyper.charm.land/v1/
|
|
24
|
+
const MODELS_API_URL = 'https://hyper.charm.land/v1/provider';
|
|
29
25
|
const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
|
|
30
26
|
const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
|
|
31
27
|
const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
|
|
@@ -76,6 +72,9 @@ function applyPatch(model, patch) {
|
|
|
76
72
|
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
77
73
|
delete result.compat.thinkingFormat;
|
|
78
74
|
}
|
|
75
|
+
if (!result.reasoning && result.thinkingLevelMap) {
|
|
76
|
+
delete result.thinkingLevelMap;
|
|
77
|
+
}
|
|
79
78
|
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
80
79
|
delete result.compat;
|
|
81
80
|
}
|
|
@@ -102,91 +101,68 @@ function buildModels(baseModels, customModels, patchData) {
|
|
|
102
101
|
|
|
103
102
|
// ─── Model transformation ─────────────────────────────────────────────────────
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Update fields from API that may change
|
|
131
|
-
const cost = apiModel.cost?.usd || {};
|
|
132
|
-
const inputCost = convertPricing(cost['1m_in']);
|
|
133
|
-
const outputCost = convertPricing(cost['1m_out']);
|
|
134
|
-
const cacheReadCost = convertPricing(cost['1m_in_cache']);
|
|
135
|
-
const cacheWriteCost = convertPricing(cost['1m_out_cache']);
|
|
136
|
-
|
|
137
|
-
if (inputCost > 0) existing.cost.input = inputCost;
|
|
138
|
-
if (outputCost > 0) existing.cost.output = outputCost;
|
|
139
|
-
if (cacheReadCost > 0) existing.cost.cacheRead = cacheReadCost;
|
|
140
|
-
if (cacheWriteCost > 0) existing.cost.cacheWrite = cacheWriteCost;
|
|
141
|
-
if (apiModel.context_window) existing.contextWindow = apiModel.context_window;
|
|
142
|
-
// Don't override maxTokens from API for DeepSeek — it reports 8000 but the
|
|
143
|
-
// actual max is 384K (set in models.json / patch.json)
|
|
144
|
-
if (apiModel.max_output_tokens && !/^deepseek-v/.test(modelId)) {
|
|
145
|
-
existing.maxTokens = apiModel.max_output_tokens;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return existing;
|
|
104
|
+
const PI_THINKING_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
105
|
+
|
|
106
|
+
// Charm's official extension treats a reasoning-capable model with no levels as
|
|
107
|
+
// a boolean on/off model: Pi's max level selects the single on state.
|
|
108
|
+
const ON_OFF_THINKING_LEVEL_MAP = {
|
|
109
|
+
off: 'off',
|
|
110
|
+
minimal: null,
|
|
111
|
+
low: null,
|
|
112
|
+
medium: null,
|
|
113
|
+
high: null,
|
|
114
|
+
xhigh: null,
|
|
115
|
+
max: 'max',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function buildThinkingLevelMap(levels) {
|
|
119
|
+
if (levels.length === 0) return undefined;
|
|
120
|
+
const available = new Set(levels);
|
|
121
|
+
const result = {
|
|
122
|
+
// The provider enum uses "none" for the off state on newer deployments;
|
|
123
|
+
// the official extension looked only for the older "off" spelling.
|
|
124
|
+
off: available.has('off') ? 'off' : available.has('none') ? 'none' : null,
|
|
125
|
+
};
|
|
126
|
+
for (const level of PI_THINKING_LEVELS) {
|
|
127
|
+
result[level] = available.has(level) ? level : null;
|
|
149
128
|
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
150
131
|
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
132
|
+
function transformModel(apiModel) {
|
|
133
|
+
const reasoningLevels = Array.isArray(apiModel.reasoning_levels)
|
|
134
|
+
? apiModel.reasoning_levels.filter(level => typeof level === 'string')
|
|
135
|
+
: [];
|
|
136
|
+
const supportsReasoningEffort = reasoningLevels.length > 0;
|
|
137
|
+
const thinkingLevelMap = supportsReasoningEffort
|
|
138
|
+
? buildThinkingLevelMap(reasoningLevels)
|
|
139
|
+
: apiModel.can_reason === true
|
|
140
|
+
? ON_OFF_THINKING_LEVEL_MAP
|
|
141
|
+
: undefined;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
id: apiModel.id,
|
|
145
|
+
name: apiModel.name,
|
|
146
|
+
reasoning: apiModel.can_reason === true,
|
|
147
|
+
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
|
|
148
|
+
input: apiModel.supports_attachments === true ? ['text', 'image'] : ['text'],
|
|
162
149
|
cost: {
|
|
163
|
-
input:
|
|
164
|
-
output:
|
|
165
|
-
cacheRead:
|
|
166
|
-
|
|
150
|
+
input: typeof apiModel.cost_per_1m_in === 'number' ? apiModel.cost_per_1m_in : 0,
|
|
151
|
+
output: typeof apiModel.cost_per_1m_out === 'number' ? apiModel.cost_per_1m_out : 0,
|
|
152
|
+
cacheRead: typeof apiModel.cost_per_1m_in_cached === 'number' ? apiModel.cost_per_1m_in_cached : 0,
|
|
153
|
+
// Hyper exposes discounted cached-output pricing, not cache-write pricing;
|
|
154
|
+
// pi's cost.cacheWrite field remains unused. This matches Charm's official extension.
|
|
155
|
+
cacheWrite: 0,
|
|
167
156
|
},
|
|
168
157
|
contextWindow: apiModel.context_window || 0,
|
|
169
|
-
maxTokens: apiModel.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// and add thinkingLevelMap + deepseek compat
|
|
174
|
-
if (isDeepSeek && isReasoning) {
|
|
175
|
-
model.maxTokens = 384000;
|
|
176
|
-
model.thinkingLevelMap = {
|
|
177
|
-
minimal: null, low: null, medium: null, high: 'high', max: 'max',
|
|
178
|
-
};
|
|
179
|
-
model.compat = {
|
|
158
|
+
maxTokens: apiModel.default_max_tokens || apiModel.context_window || 0,
|
|
159
|
+
compat: {
|
|
160
|
+
supportsStore: false,
|
|
161
|
+
supportsReasoningEffort,
|
|
180
162
|
thinkingFormat: 'deepseek',
|
|
181
163
|
maxTokensField: 'max_tokens',
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
supportsReasoningEffort: true,
|
|
185
|
-
requiresReasoningContentOnAssistantMessages: true,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return model;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
190
166
|
}
|
|
191
167
|
|
|
192
168
|
// ─── README generation ────────────────────────────────────────────────────────
|
|
@@ -237,6 +213,67 @@ ${tableRows}`;
|
|
|
237
213
|
|
|
238
214
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
239
215
|
|
|
216
|
+
// Grace period for delisted models: update-models.js moves models the API no
|
|
217
|
+
// longer lists into deprecated-models.json (stamped with deprecatedAt) instead
|
|
218
|
+
// of dropping them; the runtime appends them back so sessions and saved model
|
|
219
|
+
// settings keep working, and after 14 days they are evicted permanently.
|
|
220
|
+
const DEPRECATED_MODEL_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reconcile deprecated-models.json against the freshly fetched model list.
|
|
224
|
+
* - in old models.json but not the API: moved into the deprecated file
|
|
225
|
+
* (deprecatedAt = now; preserved on repeat runs so the grace clock is not reset)
|
|
226
|
+
* - back in the API: resurrected (dropped from the deprecated file)
|
|
227
|
+
* - deprecatedAt older than 14 days: evicted permanently
|
|
228
|
+
* Must run BEFORE the new models.json is written; it reads the old file itself.
|
|
229
|
+
*/
|
|
230
|
+
function updateDeprecatedModels(modelsJsonPath, newModels) {
|
|
231
|
+
const deprecatedPath = path.join(path.dirname(modelsJsonPath), 'deprecated-models.json');
|
|
232
|
+
|
|
233
|
+
let oldModels = [];
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf8'));
|
|
236
|
+
if (Array.isArray(parsed)) oldModels = parsed;
|
|
237
|
+
} catch { /* first run: no previous models.json */ }
|
|
238
|
+
|
|
239
|
+
let deprecated = {};
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
|
|
242
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
|
|
243
|
+
} catch { /* no graveyard yet */ }
|
|
244
|
+
|
|
245
|
+
const currentIds = new Set(newModels.map(m => m.id));
|
|
246
|
+
const now = new Date().toISOString();
|
|
247
|
+
const added = [];
|
|
248
|
+
const resurrected = [];
|
|
249
|
+
const evicted = [];
|
|
250
|
+
|
|
251
|
+
for (const old of oldModels) {
|
|
252
|
+
if (old && old.id && !currentIds.has(old.id) && !deprecated[old.id]) {
|
|
253
|
+
deprecated[old.id] = { ...old, deprecatedAt: now };
|
|
254
|
+
added.push(old.id);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const [id, entry] of Object.entries(deprecated)) {
|
|
259
|
+
if (currentIds.has(id)) {
|
|
260
|
+
delete deprecated[id];
|
|
261
|
+
resurrected.push(id);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const removedAt = Date.parse(entry && entry.deprecatedAt ? entry.deprecatedAt : '');
|
|
265
|
+
if (Number.isNaN(removedAt) || Date.now() - removedAt > DEPRECATED_MODEL_TTL_MS) {
|
|
266
|
+
delete deprecated[id];
|
|
267
|
+
evicted.push(id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (added.length > 0 || resurrected.length > 0 || evicted.length > 0) {
|
|
272
|
+
fs.writeFileSync(deprecatedPath, JSON.stringify(deprecated, null, 2) + '\n');
|
|
273
|
+
console.log('Updated deprecated-models.json ' + JSON.stringify({ added, resurrected, evicted }));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
240
277
|
async function main() {
|
|
241
278
|
const apiKey = process.env.HYPERCHARM_API_KEY;
|
|
242
279
|
if (!apiKey) {
|
|
@@ -256,7 +293,9 @@ async function main() {
|
|
|
256
293
|
}
|
|
257
294
|
|
|
258
295
|
const apiResponse = await response.json();
|
|
259
|
-
const apiModels =
|
|
296
|
+
const apiModels = Array.isArray(apiResponse)
|
|
297
|
+
? apiResponse
|
|
298
|
+
: (apiResponse.models || apiResponse.data || []);
|
|
260
299
|
|
|
261
300
|
if (!Array.isArray(apiModels)) {
|
|
262
301
|
throw new Error('API response does not contain an array of models');
|
|
@@ -277,18 +316,16 @@ async function main() {
|
|
|
277
316
|
}
|
|
278
317
|
|
|
279
318
|
// Transform models from API, preserving existing curated data
|
|
280
|
-
let apiTransformed = apiModels.map(m => transformModel(m
|
|
319
|
+
let apiTransformed = apiModels.map(m => transformModel(m));
|
|
281
320
|
apiTransformed.sort((a, b) => a.name.localeCompare(b.name));
|
|
282
321
|
|
|
283
|
-
//
|
|
322
|
+
// Load patch overrides for README rendering. Canonical metadata already
|
|
323
|
+
// comes from /v1/provider, so new models do not require a patch entry.
|
|
284
324
|
const patch = loadJson(PATCH_JSON_PATH);
|
|
285
|
-
for (const m of apiTransformed) {
|
|
286
|
-
if (!patch[m.id]) {
|
|
287
|
-
console.log(` 🆕 New model: ${m.id} (${m.name}) — add to patch.json for compat overrides`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
325
|
|
|
291
326
|
// Update models.json — curated API data
|
|
327
|
+
// Move delisted models to deprecated-models.json BEFORE models.json is overwritten
|
|
328
|
+
updateDeprecatedModels(MODELS_JSON_PATH, apiTransformed);
|
|
292
329
|
fs.writeFileSync(MODELS_JSON_PATH, JSON.stringify(apiTransformed, null, 2) + '\n');
|
|
293
330
|
console.log(`✓ Updated models.json (${apiTransformed.length} models)`);
|
|
294
331
|
|