llm-checker 3.1.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/LICENSE +21 -0
- package/README.md +418 -0
- package/analyzer/compatibility.js +584 -0
- package/analyzer/performance.js +505 -0
- package/bin/CLAUDE.md +12 -0
- package/bin/enhanced_cli.js +3118 -0
- package/bin/test-deterministic.js +41 -0
- package/package.json +96 -0
- package/src/CLAUDE.md +12 -0
- package/src/ai/intelligent-selector.js +615 -0
- package/src/ai/model-selector.js +312 -0
- package/src/ai/multi-objective-selector.js +820 -0
- package/src/commands/check.js +58 -0
- package/src/data/CLAUDE.md +11 -0
- package/src/data/model-database.js +637 -0
- package/src/data/sync-manager.js +279 -0
- package/src/hardware/CLAUDE.md +12 -0
- package/src/hardware/backends/CLAUDE.md +11 -0
- package/src/hardware/backends/apple-silicon.js +318 -0
- package/src/hardware/backends/cpu-detector.js +490 -0
- package/src/hardware/backends/cuda-detector.js +417 -0
- package/src/hardware/backends/intel-detector.js +436 -0
- package/src/hardware/backends/rocm-detector.js +440 -0
- package/src/hardware/detector.js +573 -0
- package/src/hardware/pc-optimizer.js +635 -0
- package/src/hardware/specs.js +286 -0
- package/src/hardware/unified-detector.js +442 -0
- package/src/index.js +2289 -0
- package/src/models/CLAUDE.md +17 -0
- package/src/models/ai-check-selector.js +806 -0
- package/src/models/catalog.json +426 -0
- package/src/models/deterministic-selector.js +1145 -0
- package/src/models/expanded_database.js +1142 -0
- package/src/models/intelligent-selector.js +532 -0
- package/src/models/requirements.js +310 -0
- package/src/models/scoring-config.js +57 -0
- package/src/models/scoring-engine.js +715 -0
- package/src/ollama/.cache/README.md +33 -0
- package/src/ollama/CLAUDE.md +24 -0
- package/src/ollama/client.js +438 -0
- package/src/ollama/enhanced-client.js +113 -0
- package/src/ollama/enhanced-scraper.js +634 -0
- package/src/ollama/manager.js +357 -0
- package/src/ollama/native-scraper.js +776 -0
- package/src/plugins/CLAUDE.md +11 -0
- package/src/plugins/examples/custom_model_plugin.js +87 -0
- package/src/plugins/index.js +295 -0
- package/src/utils/CLAUDE.md +11 -0
- package/src/utils/config.js +359 -0
- package/src/utils/formatter.js +315 -0
- package/src/utils/logger.js +272 -0
- package/src/utils/model-classifier.js +167 -0
- package/src/utils/verbose-progress.js +266 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Manager - Coordinates scraping and database updates
|
|
3
|
+
* Handles initial sync and incremental updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ModelDatabase = require('./model-database');
|
|
7
|
+
const EnhancedOllamaScraper = require('../ollama/enhanced-scraper');
|
|
8
|
+
|
|
9
|
+
class SyncManager {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.db = options.database || new ModelDatabase();
|
|
12
|
+
this.scraper = options.scraper || new EnhancedOllamaScraper({
|
|
13
|
+
concurrency: options.concurrency || 5,
|
|
14
|
+
rateLimitMs: options.rateLimitMs || 200,
|
|
15
|
+
onProgress: options.onProgress || this.defaultOnProgress.bind(this),
|
|
16
|
+
onError: options.onError || console.error
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
this.onProgress = options.onProgress || this.defaultOnProgress.bind(this);
|
|
20
|
+
this.onError = options.onError || console.error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default progress handler
|
|
25
|
+
*/
|
|
26
|
+
defaultOnProgress(info) {
|
|
27
|
+
if (info.phase === 'details' && info.current && info.total) {
|
|
28
|
+
const pct = Math.round((info.current / info.total) * 100);
|
|
29
|
+
process.stdout.write(`\r[${pct}%] ${info.message} `);
|
|
30
|
+
} else {
|
|
31
|
+
console.log(info.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize database
|
|
37
|
+
*/
|
|
38
|
+
async init() {
|
|
39
|
+
await this.db.initialize();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Perform full sync from scratch
|
|
44
|
+
*/
|
|
45
|
+
async fullSync() {
|
|
46
|
+
await this.init();
|
|
47
|
+
|
|
48
|
+
this.onProgress({ phase: 'start', message: 'Starting full sync...' });
|
|
49
|
+
|
|
50
|
+
// Clear existing data
|
|
51
|
+
this.db.clear();
|
|
52
|
+
|
|
53
|
+
// Scrape all models
|
|
54
|
+
const result = await this.scraper.scrapeAll((model, variants) => {
|
|
55
|
+
// Save model as we go
|
|
56
|
+
this.db.upsertModel(model);
|
|
57
|
+
|
|
58
|
+
// Save variants
|
|
59
|
+
for (const variant of variants) {
|
|
60
|
+
this.db.upsertVariant(variant);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Update sync timestamp
|
|
65
|
+
this.db.setLastSync(new Date().toISOString());
|
|
66
|
+
|
|
67
|
+
const stats = this.db.getStats();
|
|
68
|
+
|
|
69
|
+
this.onProgress({
|
|
70
|
+
phase: 'complete',
|
|
71
|
+
message: `Sync complete: ${stats.models} models, ${stats.variants} variants`,
|
|
72
|
+
stats
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return stats;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Perform incremental sync (update only changed models)
|
|
80
|
+
*/
|
|
81
|
+
async incrementalSync() {
|
|
82
|
+
await this.init();
|
|
83
|
+
|
|
84
|
+
const lastSync = this.db.getLastSync();
|
|
85
|
+
|
|
86
|
+
if (!lastSync) {
|
|
87
|
+
// No previous sync, do full sync
|
|
88
|
+
return this.fullSync();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.onProgress({ phase: 'start', message: 'Starting incremental sync...' });
|
|
92
|
+
|
|
93
|
+
// Get current model list
|
|
94
|
+
const modelList = await this.scraper.scrapeModelList();
|
|
95
|
+
|
|
96
|
+
// Get existing models from DB
|
|
97
|
+
const existingModels = new Set(
|
|
98
|
+
this.db.all(`SELECT id FROM models`).map(m => m.id)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Find new and potentially updated models
|
|
102
|
+
const newModels = modelList.filter(m => !existingModels.has(m.id));
|
|
103
|
+
const toUpdate = modelList.filter(m => existingModels.has(m.id));
|
|
104
|
+
|
|
105
|
+
this.onProgress({
|
|
106
|
+
phase: 'incremental',
|
|
107
|
+
message: `Found ${newModels.length} new models, checking ${toUpdate.length} for updates...`
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let updated = 0;
|
|
111
|
+
let added = 0;
|
|
112
|
+
|
|
113
|
+
// Process new models
|
|
114
|
+
for (const { id } of newModels) {
|
|
115
|
+
try {
|
|
116
|
+
const model = await this.scraper.scrapeModelDetails(id);
|
|
117
|
+
if (model) {
|
|
118
|
+
this.db.upsertModel(model);
|
|
119
|
+
added++;
|
|
120
|
+
|
|
121
|
+
const variants = await this.scraper.scrapeModelTags(id);
|
|
122
|
+
for (const variant of variants) {
|
|
123
|
+
this.db.upsertVariant(variant);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await this.sleep(200);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
this.onError(`Error syncing ${id}: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check for updates in existing models (sample top 50 by pulls)
|
|
134
|
+
const topModels = toUpdate.sort((a, b) => (b.pulls || 0) - (a.pulls || 0)).slice(0, 50);
|
|
135
|
+
|
|
136
|
+
for (const { id } of topModels) {
|
|
137
|
+
try {
|
|
138
|
+
const model = await this.scraper.scrapeModelDetails(id);
|
|
139
|
+
if (model) {
|
|
140
|
+
const existing = this.db.get(`SELECT pulls, tags_count FROM models WHERE id = ?`, [id]);
|
|
141
|
+
|
|
142
|
+
// Update if pulls or tags changed significantly
|
|
143
|
+
if (!existing ||
|
|
144
|
+
Math.abs((existing.pulls || 0) - (model.pulls || 0)) > 1000 ||
|
|
145
|
+
(existing.tags_count || 0) !== (model.tags_count || 0)) {
|
|
146
|
+
|
|
147
|
+
this.db.upsertModel(model);
|
|
148
|
+
|
|
149
|
+
const variants = await this.scraper.scrapeModelTags(id);
|
|
150
|
+
for (const variant of variants) {
|
|
151
|
+
this.db.upsertVariant(variant);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updated++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await this.sleep(100);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// Ignore errors during incremental update
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Update sync timestamp
|
|
165
|
+
this.db.setLastSync(new Date().toISOString());
|
|
166
|
+
|
|
167
|
+
const stats = this.db.getStats();
|
|
168
|
+
|
|
169
|
+
this.onProgress({
|
|
170
|
+
phase: 'complete',
|
|
171
|
+
message: `Incremental sync complete: ${added} added, ${updated} updated`,
|
|
172
|
+
stats
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return { added, updated, stats };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Smart sync - chooses full or incremental based on conditions
|
|
180
|
+
*/
|
|
181
|
+
async sync(options = {}) {
|
|
182
|
+
await this.init();
|
|
183
|
+
|
|
184
|
+
const force = options.force || false;
|
|
185
|
+
const lastSync = this.db.getLastSync();
|
|
186
|
+
const modelCount = this.db.getModelCount();
|
|
187
|
+
|
|
188
|
+
// Full sync if:
|
|
189
|
+
// - Force flag is set
|
|
190
|
+
// - No previous sync
|
|
191
|
+
// - Less than 100 models (probably incomplete)
|
|
192
|
+
// - Last sync was more than 7 days ago
|
|
193
|
+
if (force || !lastSync || modelCount < 100) {
|
|
194
|
+
return this.fullSync();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const lastSyncDate = new Date(lastSync);
|
|
198
|
+
const daysSinceSync = (Date.now() - lastSyncDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
199
|
+
|
|
200
|
+
if (daysSinceSync > 7) {
|
|
201
|
+
return this.fullSync();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this.incrementalSync();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if sync is needed
|
|
209
|
+
*/
|
|
210
|
+
async needsSync() {
|
|
211
|
+
await this.init();
|
|
212
|
+
|
|
213
|
+
const lastSync = this.db.getLastSync();
|
|
214
|
+
const modelCount = this.db.getModelCount();
|
|
215
|
+
|
|
216
|
+
if (!lastSync || modelCount < 100) {
|
|
217
|
+
return { needed: true, reason: 'No previous sync or incomplete data' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const lastSyncDate = new Date(lastSync);
|
|
221
|
+
const hoursSinceSync = (Date.now() - lastSyncDate.getTime()) / (1000 * 60 * 60);
|
|
222
|
+
|
|
223
|
+
if (hoursSinceSync > 24) {
|
|
224
|
+
return { needed: true, reason: `Last sync was ${Math.round(hoursSinceSync)} hours ago` };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { needed: false, stats: this.db.getStats() };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get database stats
|
|
232
|
+
*/
|
|
233
|
+
async getStats() {
|
|
234
|
+
await this.init();
|
|
235
|
+
return this.db.getStats();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Search models
|
|
240
|
+
*/
|
|
241
|
+
async search(query, filters = {}) {
|
|
242
|
+
await this.init();
|
|
243
|
+
return this.db.searchModels(query, filters);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Search for variants (not just models)
|
|
248
|
+
*/
|
|
249
|
+
async searchVariants(query, filters = {}) {
|
|
250
|
+
await this.init();
|
|
251
|
+
return this.db.searchVariants(query, filters);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get variants for hardware constraints
|
|
256
|
+
*/
|
|
257
|
+
async getCompatibleVariants(maxSizeGB, filters = {}) {
|
|
258
|
+
await this.init();
|
|
259
|
+
return this.db.getVariantsForHardware(maxSizeGB, filters);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Sleep utility
|
|
264
|
+
*/
|
|
265
|
+
sleep(ms) {
|
|
266
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Close database connection
|
|
271
|
+
*/
|
|
272
|
+
close() {
|
|
273
|
+
if (this.db) {
|
|
274
|
+
this.db.close();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = SyncManager;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 12, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #3490 | 10:24 PM | 🔵 | Hardware Detector Cache Implementation - 5-Minute TTL Without Force Refresh Option | ~536 |
|
|
11
|
+
| #3440 | 9:58 PM | 🔵 | Hardware Detection System - Multi-GPU Support with Intelligent Selection | ~611 |
|
|
12
|
+
</claude-mem-context>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<claude-mem-context>
|
|
2
|
+
# Recent Activity
|
|
3
|
+
|
|
4
|
+
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
|
5
|
+
|
|
6
|
+
### Feb 12, 2026
|
|
7
|
+
|
|
8
|
+
| ID | Time | T | Title | Read |
|
|
9
|
+
|----|------|---|-------|------|
|
|
10
|
+
| #3453 | 10:01 PM | 🔵 | CUDA Detector Implementation - NVIDIA GPU Detection via nvidia-smi | ~497 |
|
|
11
|
+
</claude-mem-context>
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Silicon Detector
|
|
3
|
+
* Detects M1, M2, M3, M4 chips and their capabilities
|
|
4
|
+
* Uses sysctl and system_profiler for accurate detection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
class AppleSiliconDetector {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.cache = null;
|
|
12
|
+
this.isSupported = process.platform === 'darwin' && process.arch === 'arm64';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect if running on Apple Silicon
|
|
17
|
+
*/
|
|
18
|
+
detect() {
|
|
19
|
+
if (!this.isSupported) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (this.cache) {
|
|
24
|
+
return this.cache;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const info = this.getChipInfo();
|
|
29
|
+
this.cache = info;
|
|
30
|
+
return info;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get detailed chip information using sysctl
|
|
38
|
+
*/
|
|
39
|
+
getChipInfo() {
|
|
40
|
+
const result = {
|
|
41
|
+
chip: null,
|
|
42
|
+
variant: null, // base, pro, max, ultra
|
|
43
|
+
generation: null, // 1, 2, 3, 4
|
|
44
|
+
cores: {
|
|
45
|
+
performance: 0,
|
|
46
|
+
efficiency: 0,
|
|
47
|
+
total: 0
|
|
48
|
+
},
|
|
49
|
+
gpu: {
|
|
50
|
+
cores: 0,
|
|
51
|
+
model: null
|
|
52
|
+
},
|
|
53
|
+
neuralEngine: {
|
|
54
|
+
cores: 0
|
|
55
|
+
},
|
|
56
|
+
memory: {
|
|
57
|
+
unified: 0,
|
|
58
|
+
bandwidth: null
|
|
59
|
+
},
|
|
60
|
+
capabilities: {
|
|
61
|
+
metal: true,
|
|
62
|
+
metalVersion: null,
|
|
63
|
+
fp16: true,
|
|
64
|
+
int8: true,
|
|
65
|
+
amx: true
|
|
66
|
+
},
|
|
67
|
+
backend: 'metal',
|
|
68
|
+
speedCoefficient: 0
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Get chip brand
|
|
72
|
+
try {
|
|
73
|
+
const brand = execSync('sysctl -n machdep.cpu.brand_string', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
74
|
+
result.chip = brand;
|
|
75
|
+
|
|
76
|
+
// Parse chip variant and generation
|
|
77
|
+
const parsed = this.parseChipBrand(brand);
|
|
78
|
+
result.variant = parsed.variant;
|
|
79
|
+
result.generation = parsed.generation;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Fallback
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get CPU cores
|
|
85
|
+
try {
|
|
86
|
+
result.cores.total = parseInt(execSync('sysctl -n hw.ncpu', { encoding: 'utf8', timeout: 5000 }).trim());
|
|
87
|
+
result.cores.performance = parseInt(execSync('sysctl -n hw.perflevel0.logicalcpu', { encoding: 'utf8', timeout: 5000 }).trim()) || Math.ceil(result.cores.total / 2);
|
|
88
|
+
result.cores.efficiency = parseInt(execSync('sysctl -n hw.perflevel1.logicalcpu', { encoding: 'utf8', timeout: 5000 }).trim()) || Math.floor(result.cores.total / 2);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
result.cores.total = require('os').cpus().length;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get memory (unified memory on Apple Silicon)
|
|
94
|
+
try {
|
|
95
|
+
const memBytes = parseInt(execSync('sysctl -n hw.memsize', { encoding: 'utf8', timeout: 5000 }).trim());
|
|
96
|
+
result.memory.unified = Math.round(memBytes / (1024 ** 3));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
result.memory.unified = Math.round(require('os').totalmem() / (1024 ** 3));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get GPU cores from system_profiler
|
|
102
|
+
try {
|
|
103
|
+
const gpuInfo = execSync('system_profiler SPDisplaysDataType -json', {
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
timeout: 5000
|
|
106
|
+
});
|
|
107
|
+
const parsed = JSON.parse(gpuInfo);
|
|
108
|
+
const displays = parsed.SPDisplaysDataType || [];
|
|
109
|
+
|
|
110
|
+
if (displays.length > 0) {
|
|
111
|
+
const gpu = displays[0];
|
|
112
|
+
result.gpu.model = gpu.sppci_model || result.chip;
|
|
113
|
+
|
|
114
|
+
// Parse GPU cores from model name or estimate
|
|
115
|
+
const coreMatch = gpu.sppci_cores?.match(/(\d+)/);
|
|
116
|
+
if (coreMatch) {
|
|
117
|
+
result.gpu.cores = parseInt(coreMatch[1]);
|
|
118
|
+
} else {
|
|
119
|
+
result.gpu.cores = this.estimateGPUCores(result.variant, result.generation);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
result.gpu.cores = this.estimateGPUCores(result.variant, result.generation);
|
|
124
|
+
result.gpu.model = result.chip;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get Metal version
|
|
128
|
+
try {
|
|
129
|
+
const metalVersion = execSync('system_profiler SPDisplaysDataType | grep "Metal Support"', {
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
timeout: 5000
|
|
132
|
+
});
|
|
133
|
+
const match = metalVersion.match(/Metal\s*([\d.]+|Family)/i);
|
|
134
|
+
if (match) {
|
|
135
|
+
result.capabilities.metalVersion = match[1];
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
result.capabilities.metalVersion = '3'; // Apple Silicon supports Metal 3
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Calculate speed coefficient for LLM inference
|
|
142
|
+
result.speedCoefficient = this.calculateSpeedCoefficient(result);
|
|
143
|
+
result.memory.bandwidth = this.estimateMemoryBandwidth(result.variant, result.generation);
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse chip brand string to extract variant and generation
|
|
150
|
+
*/
|
|
151
|
+
parseChipBrand(brand) {
|
|
152
|
+
const result = { variant: 'base', generation: 1 };
|
|
153
|
+
|
|
154
|
+
const brandLower = brand.toLowerCase();
|
|
155
|
+
|
|
156
|
+
// Detect generation
|
|
157
|
+
if (brandLower.includes('m4')) result.generation = 4;
|
|
158
|
+
else if (brandLower.includes('m3')) result.generation = 3;
|
|
159
|
+
else if (brandLower.includes('m2')) result.generation = 2;
|
|
160
|
+
else if (brandLower.includes('m1')) result.generation = 1;
|
|
161
|
+
|
|
162
|
+
// Detect variant
|
|
163
|
+
if (brandLower.includes('ultra')) result.variant = 'ultra';
|
|
164
|
+
else if (brandLower.includes('max')) result.variant = 'max';
|
|
165
|
+
else if (brandLower.includes('pro')) result.variant = 'pro';
|
|
166
|
+
else result.variant = 'base';
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Estimate GPU cores based on variant and generation
|
|
173
|
+
*/
|
|
174
|
+
estimateGPUCores(variant, generation) {
|
|
175
|
+
const coreMap = {
|
|
176
|
+
// M1 series
|
|
177
|
+
'1-base': 8,
|
|
178
|
+
'1-pro': 16,
|
|
179
|
+
'1-max': 32,
|
|
180
|
+
'1-ultra': 64,
|
|
181
|
+
// M2 series
|
|
182
|
+
'2-base': 10,
|
|
183
|
+
'2-pro': 19,
|
|
184
|
+
'2-max': 38,
|
|
185
|
+
'2-ultra': 76,
|
|
186
|
+
// M3 series
|
|
187
|
+
'3-base': 10,
|
|
188
|
+
'3-pro': 18,
|
|
189
|
+
'3-max': 40,
|
|
190
|
+
'3-ultra': 80,
|
|
191
|
+
// M4 series
|
|
192
|
+
'4-base': 10,
|
|
193
|
+
'4-pro': 20,
|
|
194
|
+
'4-max': 40,
|
|
195
|
+
'4-ultra': 80
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return coreMap[`${generation}-${variant}`] || 10;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Estimate memory bandwidth in GB/s
|
|
203
|
+
*/
|
|
204
|
+
estimateMemoryBandwidth(variant, generation) {
|
|
205
|
+
const bandwidthMap = {
|
|
206
|
+
// M1 series
|
|
207
|
+
'1-base': 68,
|
|
208
|
+
'1-pro': 200,
|
|
209
|
+
'1-max': 400,
|
|
210
|
+
'1-ultra': 800,
|
|
211
|
+
// M2 series
|
|
212
|
+
'2-base': 100,
|
|
213
|
+
'2-pro': 200,
|
|
214
|
+
'2-max': 400,
|
|
215
|
+
'2-ultra': 800,
|
|
216
|
+
// M3 series
|
|
217
|
+
'3-base': 100,
|
|
218
|
+
'3-pro': 150,
|
|
219
|
+
'3-max': 400,
|
|
220
|
+
'3-ultra': 800,
|
|
221
|
+
// M4 series
|
|
222
|
+
'4-base': 120,
|
|
223
|
+
'4-pro': 273,
|
|
224
|
+
'4-max': 546,
|
|
225
|
+
'4-ultra': 800
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return bandwidthMap[`${generation}-${variant}`] || 100;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Calculate speed coefficient for LLM inference (tokens/sec per B params)
|
|
233
|
+
*/
|
|
234
|
+
calculateSpeedCoefficient(info) {
|
|
235
|
+
// Base coefficient by generation and variant
|
|
236
|
+
const baseCoefficients = {
|
|
237
|
+
'1-base': 180,
|
|
238
|
+
'1-pro': 200,
|
|
239
|
+
'1-max': 220,
|
|
240
|
+
'1-ultra': 240,
|
|
241
|
+
'2-base': 200,
|
|
242
|
+
'2-pro': 220,
|
|
243
|
+
'2-max': 240,
|
|
244
|
+
'2-ultra': 260,
|
|
245
|
+
'3-base': 220,
|
|
246
|
+
'3-pro': 240,
|
|
247
|
+
'3-max': 260,
|
|
248
|
+
'3-ultra': 280,
|
|
249
|
+
'4-base': 240,
|
|
250
|
+
'4-pro': 270,
|
|
251
|
+
'4-max': 300,
|
|
252
|
+
'4-ultra': 320
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const key = `${info.generation}-${info.variant}`;
|
|
256
|
+
return baseCoefficients[key] || 180;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if Ollama is using Metal backend
|
|
261
|
+
*/
|
|
262
|
+
async checkMetalSupport() {
|
|
263
|
+
if (!this.isSupported) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Check if Metal framework is available
|
|
269
|
+
const metalCheck = execSync('ls /System/Library/Frameworks/Metal.framework', {
|
|
270
|
+
encoding: 'utf8',
|
|
271
|
+
timeout: 2000
|
|
272
|
+
});
|
|
273
|
+
return metalCheck.includes('Metal');
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return true; // Assume Metal is available on Apple Silicon
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get hardware fingerprint for benchmarks
|
|
281
|
+
*/
|
|
282
|
+
getFingerprint() {
|
|
283
|
+
const info = this.detect();
|
|
284
|
+
if (!info) return null;
|
|
285
|
+
|
|
286
|
+
return `apple-${info.chip.toLowerCase().replace(/\s+/g, '-')}-${info.memory.unified}gb`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Estimate inference speed for a model size
|
|
291
|
+
*/
|
|
292
|
+
estimateTokensPerSecond(paramsB, quantization = 'Q4_K_M') {
|
|
293
|
+
const info = this.detect();
|
|
294
|
+
if (!info) return 0;
|
|
295
|
+
|
|
296
|
+
// Quantization multipliers (how much faster vs FP16)
|
|
297
|
+
const quantMult = {
|
|
298
|
+
'FP16': 1.0,
|
|
299
|
+
'Q8_0': 1.5,
|
|
300
|
+
'Q6_K': 1.8,
|
|
301
|
+
'Q5_K_M': 2.0,
|
|
302
|
+
'Q5_0': 2.0,
|
|
303
|
+
'Q4_K_M': 2.5,
|
|
304
|
+
'Q4_0': 2.8,
|
|
305
|
+
'Q3_K_M': 3.0,
|
|
306
|
+
'Q2_K': 3.5,
|
|
307
|
+
'IQ4_XS': 2.6,
|
|
308
|
+
'IQ3_XXS': 3.2
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const mult = quantMult[quantization] || 2.0;
|
|
312
|
+
const baseSpeed = info.speedCoefficient / paramsB * mult;
|
|
313
|
+
|
|
314
|
+
return Math.round(baseSpeed);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = AppleSiliconDetector;
|