pi-hypercharm-provider 1.0.3
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/.pi/messenger/session-id +1 -0
- package/AGENTS.md +56 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/custom-models.json +1 -0
- package/index.ts +371 -0
- package/models.json +360 -0
- package/package.json +37 -0
- package/patch.json +155 -0
- package/scripts/update-models.js +362 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Update HyperCharm models from API
|
|
5
|
+
*
|
|
6
|
+
* Fetches models from https://hyper.charm.land/v1/models and updates:
|
|
7
|
+
* - models.json: Model definitions with curated reasoning/vision flags + API pricing
|
|
8
|
+
* - README.md: Model table with patch.json overrides applied
|
|
9
|
+
*
|
|
10
|
+
* The HyperCharm API provides: id, display_name, supports_reasoning,
|
|
11
|
+
* supports_reasoning_effort, supports_attachments, context_window,
|
|
12
|
+
* max_output_tokens, and cost.usd pricing.
|
|
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.
|
|
18
|
+
*
|
|
19
|
+
* Merge order for README: models.json → apply patch.json → merge custom-models.json
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
|
|
28
|
+
const MODELS_API_URL = 'https://hyper.charm.land/v1/models';
|
|
29
|
+
const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
|
|
30
|
+
const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
|
|
31
|
+
const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
|
|
32
|
+
const README_PATH = path.join(__dirname, '..', 'README.md');
|
|
33
|
+
|
|
34
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function loadJson(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveJson(filePath, data) {
|
|
45
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function convertPricing(v) {
|
|
49
|
+
if (!v) return 0;
|
|
50
|
+
const n = typeof v === 'string' ? parseFloat(v) : v;
|
|
51
|
+
// API returns $/M directly; round to 6 decimals to preserve sub-cent cache prices.
|
|
52
|
+
return Math.round(n * 1e6) / 1e6;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Patch application ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function applyPatch(model, patch) {
|
|
58
|
+
const result = { ...model };
|
|
59
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
60
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
61
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
62
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
63
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
64
|
+
if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = { ...patch.thinkingLevelMap };
|
|
65
|
+
if (patch.cost) {
|
|
66
|
+
result.cost = {
|
|
67
|
+
input: patch.cost.input ?? result.cost.input,
|
|
68
|
+
output: patch.cost.output ?? result.cost.output,
|
|
69
|
+
cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
|
|
70
|
+
cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (patch.compat) {
|
|
74
|
+
result.compat = { ...(result.compat || {}), ...patch.compat };
|
|
75
|
+
}
|
|
76
|
+
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
77
|
+
delete result.compat.thinkingFormat;
|
|
78
|
+
}
|
|
79
|
+
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
80
|
+
delete result.compat;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildModels(baseModels, customModels, patchData) {
|
|
86
|
+
const modelMap = new Map();
|
|
87
|
+
for (const model of baseModels) modelMap.set(model.id, model);
|
|
88
|
+
for (const [id, patchEntry] of Object.entries(patchData)) {
|
|
89
|
+
const existing = modelMap.get(id);
|
|
90
|
+
if (existing) modelMap.set(id, applyPatch(existing, patchEntry));
|
|
91
|
+
}
|
|
92
|
+
for (const model of customModels) {
|
|
93
|
+
const existing = modelMap.get(model.id);
|
|
94
|
+
const patchEntry = patchData[model.id];
|
|
95
|
+
if (existing && patchEntry) modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
96
|
+
else if (existing) modelMap.set(model.id, model);
|
|
97
|
+
else if (patchEntry) modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
98
|
+
else modelMap.set(model.id, model);
|
|
99
|
+
}
|
|
100
|
+
return Array.from(modelMap.values());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Model transformation ─────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
// Known non-reasoning models (API incorrectly reports supports_reasoning: true)
|
|
106
|
+
const NON_REASONING_IDS = new Set([
|
|
107
|
+
'llama-3.3-70b-instruct',
|
|
108
|
+
'llama-4-maverick-17b-128e-instruct-fp8',
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
// Known vision models
|
|
112
|
+
const VISION_IDS = new Set([
|
|
113
|
+
'kimi-k2.5',
|
|
114
|
+
'kimi-k2.6',
|
|
115
|
+
'glm-5.1',
|
|
116
|
+
'gemma-4-26b-a4b-it',
|
|
117
|
+
'qwen3.6-flash',
|
|
118
|
+
'qwen3.6-max',
|
|
119
|
+
'qwen3.6-plus',
|
|
120
|
+
'qwen3.7-max',
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
function transformModel(apiModel, existingModelsMap) {
|
|
124
|
+
const modelId = apiModel.id;
|
|
125
|
+
|
|
126
|
+
// Preserve existing curated data
|
|
127
|
+
if (existingModelsMap[modelId]) {
|
|
128
|
+
const existing = { ...existingModelsMap[modelId] };
|
|
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;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// New model — build from API data + curated defaults
|
|
152
|
+
const cost = apiModel.cost?.usd || {};
|
|
153
|
+
const isReasoning = apiModel.supports_reasoning === true && !NON_REASONING_IDS.has(modelId);
|
|
154
|
+
const isVision = VISION_IDS.has(modelId);
|
|
155
|
+
const isDeepSeek = /^deepseek-v/.test(modelId);
|
|
156
|
+
|
|
157
|
+
const model = {
|
|
158
|
+
id: modelId,
|
|
159
|
+
name: apiModel.display_name || modelId,
|
|
160
|
+
reasoning: isReasoning,
|
|
161
|
+
input: isVision ? ['text', 'image'] : ['text'],
|
|
162
|
+
cost: {
|
|
163
|
+
input: convertPricing(cost['1m_in']),
|
|
164
|
+
output: convertPricing(cost['1m_out']),
|
|
165
|
+
cacheRead: convertPricing(cost['1m_in_cache']),
|
|
166
|
+
cacheWrite: convertPricing(cost['1m_out_cache']),
|
|
167
|
+
},
|
|
168
|
+
contextWindow: apiModel.context_window || 0,
|
|
169
|
+
maxTokens: apiModel.max_output_tokens || 0,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// DeepSeek models: override maxTokens (API reports 8000, actual is 384K)
|
|
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', xhigh: 'max',
|
|
178
|
+
};
|
|
179
|
+
model.compat = {
|
|
180
|
+
thinkingFormat: 'deepseek',
|
|
181
|
+
maxTokensField: 'max_tokens',
|
|
182
|
+
supportsDeveloperRole: true,
|
|
183
|
+
supportsStore: false,
|
|
184
|
+
supportsReasoningEffort: true,
|
|
185
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return model;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── README generation ────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function formatCost(cost) {
|
|
195
|
+
if (cost === 0) return 'Free';
|
|
196
|
+
if (cost === null || cost === undefined) return '-';
|
|
197
|
+
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
198
|
+
return `$${cost.toFixed(2)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatNumber(num) {
|
|
202
|
+
if (num === null || num === undefined) return '-';
|
|
203
|
+
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
|
204
|
+
if (num >= 1000) return `${Math.round(num / 1000)}K`;
|
|
205
|
+
return num.toString();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getInputTypes(inputTypes) {
|
|
209
|
+
const types = inputTypes || ['text'];
|
|
210
|
+
if (types.includes('image') && types.includes('text')) return 'Text + Image';
|
|
211
|
+
if (types.includes('image')) return 'Image';
|
|
212
|
+
return 'Text';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function generateReadmeRow(model) {
|
|
216
|
+
const cost = model.cost || {};
|
|
217
|
+
return `| ${model.name} | ${getInputTypes(model.input)} | ${formatNumber(model.contextWindow)} | ${formatNumber(model.maxTokens)} | ${formatCost(cost.input)} | ${formatCost(cost.output)} |`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateReadme(models) {
|
|
221
|
+
let readme = fs.readFileSync(README_PATH, 'utf8');
|
|
222
|
+
|
|
223
|
+
const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name));
|
|
224
|
+
const tableRows = sortedModels.map(generateReadmeRow).join('\n');
|
|
225
|
+
const newTable = `| Model | Type | Context | Max Tokens | Input Cost | Output Cost |
|
|
226
|
+
|-------|------|---------|------------|------------|-------------|
|
|
227
|
+
${tableRows}`;
|
|
228
|
+
|
|
229
|
+
const tableRegex = /\| Model \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
|
|
230
|
+
readme = readme.replace(tableRegex, newTable);
|
|
231
|
+
|
|
232
|
+
readme = readme.replace(/\*\*\d+\+ AI Models\*\*/, `**${models.length}+ AI Models**`);
|
|
233
|
+
|
|
234
|
+
fs.writeFileSync(README_PATH, readme);
|
|
235
|
+
console.log(`✓ Updated README.md with ${models.length} models`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
async function main() {
|
|
241
|
+
const apiKey = process.env.HYPERCHARM_API_KEY;
|
|
242
|
+
if (!apiKey) {
|
|
243
|
+
console.error('Error: HYPERCHARM_API_KEY environment variable is required');
|
|
244
|
+
console.error('Usage: HYPERCHARM_API_KEY=your-key node scripts/update-models.js');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(`Fetching models from ${MODELS_API_URL}...`);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(MODELS_API_URL, {
|
|
252
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
253
|
+
});
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const apiResponse = await response.json();
|
|
259
|
+
const apiModels = apiResponse.data || apiResponse;
|
|
260
|
+
|
|
261
|
+
if (!Array.isArray(apiModels)) {
|
|
262
|
+
throw new Error('API response does not contain an array of models');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(`✓ Fetched ${apiModels.length} models from API`);
|
|
266
|
+
|
|
267
|
+
// Load existing models.json — source of truth for curated specs
|
|
268
|
+
let existingModels = [];
|
|
269
|
+
try {
|
|
270
|
+
existingModels = JSON.parse(fs.readFileSync(MODELS_JSON_PATH, 'utf8'));
|
|
271
|
+
} catch {
|
|
272
|
+
// File might not exist yet
|
|
273
|
+
}
|
|
274
|
+
const existingModelsMap = {};
|
|
275
|
+
for (const m of existingModels) {
|
|
276
|
+
existingModelsMap[m.id] = m;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Transform models from API, preserving existing curated data
|
|
280
|
+
let apiTransformed = apiModels.map(m => transformModel(m, existingModelsMap));
|
|
281
|
+
apiTransformed.sort((a, b) => a.name.localeCompare(b.name));
|
|
282
|
+
|
|
283
|
+
// Log new models (not in patch.json)
|
|
284
|
+
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
|
+
|
|
291
|
+
// Update models.json — curated API data
|
|
292
|
+
fs.writeFileSync(MODELS_JSON_PATH, JSON.stringify(apiTransformed, null, 2) + '\n');
|
|
293
|
+
console.log(`✓ Updated models.json (${apiTransformed.length} models)`);
|
|
294
|
+
|
|
295
|
+
// Load custom-models.json
|
|
296
|
+
const customModels = Array.isArray(loadJson(CUSTOM_MODELS_JSON_PATH))
|
|
297
|
+
? loadJson(CUSTOM_MODELS_JSON_PATH)
|
|
298
|
+
: [];
|
|
299
|
+
|
|
300
|
+
// Check for custom models now available upstream (remove duplicates)
|
|
301
|
+
const upstreamIds = new Set(apiTransformed.map(m => m.id));
|
|
302
|
+
const duplicates = customModels.filter(m => upstreamIds.has(m.id));
|
|
303
|
+
if (duplicates.length > 0) {
|
|
304
|
+
console.log(`\nFound ${duplicates.length} custom model(s) now available upstream:`);
|
|
305
|
+
for (const dup of duplicates) {
|
|
306
|
+
console.log(` - ${dup.id} (${dup.name})`);
|
|
307
|
+
}
|
|
308
|
+
const cleaned = customModels.filter(m => !upstreamIds.has(m.id));
|
|
309
|
+
saveJson(CUSTOM_MODELS_JSON_PATH, cleaned);
|
|
310
|
+
console.log(`✓ Removed ${duplicates.length} duplicate(s) from custom-models.json`);
|
|
311
|
+
customModels.length = 0;
|
|
312
|
+
customModels.push(...cleaned);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Build merged models with patches for README
|
|
316
|
+
const readmeModels = buildModels(apiTransformed, customModels, patch);
|
|
317
|
+
readmeModels.sort((a, b) => a.name.localeCompare(b.name));
|
|
318
|
+
|
|
319
|
+
// Update README
|
|
320
|
+
updateReadme(readmeModels);
|
|
321
|
+
|
|
322
|
+
// Summary
|
|
323
|
+
console.log('\n--- Summary ---');
|
|
324
|
+
console.log(`Total models: ${readmeModels.length}`);
|
|
325
|
+
console.log(`Reasoning models: ${readmeModels.filter(m => m.reasoning).length}`);
|
|
326
|
+
console.log(`Vision models: ${readmeModels.filter(m => m.input.includes('image')).length}`);
|
|
327
|
+
|
|
328
|
+
const newIds = new Set(apiTransformed.map(m => m.id));
|
|
329
|
+
const oldIds = new Set(existingModels.map(m => m.id));
|
|
330
|
+
|
|
331
|
+
const added = [...newIds].filter(id => !oldIds.has(id));
|
|
332
|
+
const removed = [...oldIds].filter(id => !newIds.has(id));
|
|
333
|
+
|
|
334
|
+
if (added.length > 0) console.log(`\nNew models: ${added.join(', ')}`);
|
|
335
|
+
if (removed.length > 0) console.log(`\nRemoved models: ${removed.join(', ')}`);
|
|
336
|
+
|
|
337
|
+
// Show pricing changes
|
|
338
|
+
for (const model of apiTransformed) {
|
|
339
|
+
const oldModel = existingModels.find(m => m.id === model.id);
|
|
340
|
+
if (oldModel) {
|
|
341
|
+
const oldInput = oldModel.cost?.input || 0;
|
|
342
|
+
const oldOutput = oldModel.cost?.output || 0;
|
|
343
|
+
if (oldInput !== model.cost.input || oldOutput !== model.cost.output) {
|
|
344
|
+
console.log(`\nPricing change for ${model.id}:`);
|
|
345
|
+
if (oldInput !== model.cost.input) {
|
|
346
|
+
console.log(` Input: $${oldInput}/M → $${model.cost.input}/M`);
|
|
347
|
+
}
|
|
348
|
+
if (oldOutput !== model.cost.output) {
|
|
349
|
+
console.log(` Output: $${oldOutput}/M → $${model.cost.output}/M`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log('\nDone!');
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('Error:', error.message);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
main();
|