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,33 @@
|
|
|
1
|
+
# Ollama Models Cache - STATIC DATABASE
|
|
2
|
+
|
|
3
|
+
## ⚠️ IMPORTANT: Static Database with Pre-Classified Categories
|
|
4
|
+
|
|
5
|
+
This directory contains a **static, pre-loaded database** of 177 Ollama models with complete category classifications.
|
|
6
|
+
|
|
7
|
+
### Current Status:
|
|
8
|
+
- **177 models** with real size data from Ollama Hub
|
|
9
|
+
- **Pre-classified categories**: coding, creative, reasoning, multimodal, embeddings, chat, safety, general
|
|
10
|
+
- **No automatic updates** - this is intentionally static for stability
|
|
11
|
+
|
|
12
|
+
### If You Need to Update the Database in the Future:
|
|
13
|
+
|
|
14
|
+
1. **Run database update process** (re-enable update functionality)
|
|
15
|
+
2. **🚨 CRITICAL: RE-ADD CATEGORIES** after update:
|
|
16
|
+
- Run `node src/utils/model-classifier.js` on all new models
|
|
17
|
+
- Apply classification rules to new entries
|
|
18
|
+
- Update cache with category data
|
|
19
|
+
3. **Test all use-case filters**:
|
|
20
|
+
- `--use-case coding`
|
|
21
|
+
- `--use-case creative`
|
|
22
|
+
- `--use-case reasoning`
|
|
23
|
+
- `--use-case multimodal`
|
|
24
|
+
- `--use-case embeddings`
|
|
25
|
+
- `--use-case talking`
|
|
26
|
+
4. **Verify category detection** works for all 7 use cases
|
|
27
|
+
|
|
28
|
+
### Files:
|
|
29
|
+
- `ollama-detailed-models.json` - Main cache with 177 models + categories
|
|
30
|
+
- `README.md` - This documentation
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
**Note**: The database update functionality was intentionally removed to maintain stability and keep the current categorization system intact.
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
| #3500 | 10:26 PM | 🔴 | pullModel() Stream Handling Improved - Success Validation Added | ~458 |
|
|
11
|
+
| #3499 | " | 🔴 | Race Condition Fixed in Ollama Availability Cache | ~440 |
|
|
12
|
+
| #3498 | 10:25 PM | 🔵 | testModelPerformance() Timeout Already Fixed | ~418 |
|
|
13
|
+
| #3497 | " | 🔴 | Timeout Fixed in deleteModel() Using AbortController | ~391 |
|
|
14
|
+
| #3496 | " | 🔴 | Timeout Fixed in testConnection() Using AbortController | ~395 |
|
|
15
|
+
| #3495 | " | 🔴 | Fixed unbounded memory growth in native scraper HTTP request handler | ~361 |
|
|
16
|
+
| #3493 | " | 🔴 | Fixed race condition in checkOllamaAvailability() with promise deduplication | ~398 |
|
|
17
|
+
| #3491 | 10:24 PM | 🔴 | Added missing clearTimeout() in testModelPerformance() | ~319 |
|
|
18
|
+
| #3489 | " | 🔴 | Fixed node-fetch timeout handling in testModelPerformance() | ~332 |
|
|
19
|
+
| #3488 | " | 🔴 | Fixed node-fetch timeout handling in testConnection() tags check | ~303 |
|
|
20
|
+
| #3486 | " | 🔴 | Fixed node-fetch timeout handling in getRunningModels() | ~308 |
|
|
21
|
+
| #3484 | 10:23 PM | 🔵 | Ollama Client Timeout Implementation - Mixed Patterns with AbortController | ~554 |
|
|
22
|
+
| #3443 | 9:59 PM | 🔵 | Ollama Native Scraper - Web Scraping with Dual Cache Strategy | ~594 |
|
|
23
|
+
| #3437 | 9:58 PM | 🔵 | Ollama Client Implementation - HTTP API Wrapper with Connection Management | ~605 |
|
|
24
|
+
</claude-mem-context>
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
|
|
3
|
+
class OllamaClient {
|
|
4
|
+
constructor(baseURL = null) {
|
|
5
|
+
// Support OLLAMA_HOST environment variable (standard Ollama configuration)
|
|
6
|
+
// Also support OLLAMA_URL for backwards compatibility
|
|
7
|
+
this.baseURL = baseURL || process.env.OLLAMA_HOST || process.env.OLLAMA_URL || 'http://localhost:11434';
|
|
8
|
+
|
|
9
|
+
// Normalize URL: ensure it has protocol and remove trailing slash
|
|
10
|
+
if (!this.baseURL.startsWith('http://') && !this.baseURL.startsWith('https://')) {
|
|
11
|
+
this.baseURL = 'http://' + this.baseURL;
|
|
12
|
+
}
|
|
13
|
+
this.baseURL = this.baseURL.replace(/\/$/, '');
|
|
14
|
+
|
|
15
|
+
this.isAvailable = null;
|
|
16
|
+
this.lastCheck = 0;
|
|
17
|
+
this.cacheTimeout = 30000;
|
|
18
|
+
this._pendingCheck = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async checkOllamaAvailability() {
|
|
22
|
+
|
|
23
|
+
if (this.isAvailable !== null && Date.now() - this.lastCheck < this.cacheTimeout) {
|
|
24
|
+
return this.isAvailable;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Prevent concurrent requests — reuse in-flight promise
|
|
28
|
+
if (this._pendingCheck) {
|
|
29
|
+
return this._pendingCheck;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this._pendingCheck = this._doAvailabilityCheck();
|
|
33
|
+
try {
|
|
34
|
+
return await this._pendingCheck;
|
|
35
|
+
} finally {
|
|
36
|
+
this._pendingCheck = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async _doAvailabilityCheck() {
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
45
|
+
|
|
46
|
+
const response = await fetch(`${this.baseURL}/api/version`, {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
headers: { 'Content-Type': 'application/json' }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
|
|
53
|
+
if (response.ok) {
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
this.isAvailable = { available: true, version: data.version || 'unknown' };
|
|
56
|
+
this.lastCheck = Date.now();
|
|
57
|
+
return this.isAvailable;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.isAvailable = { available: false, error: 'Ollama not responding properly' };
|
|
61
|
+
this.lastCheck = Date.now();
|
|
62
|
+
return this.isAvailable;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
let errorMessage;
|
|
65
|
+
let hint = '';
|
|
66
|
+
|
|
67
|
+
if (error.message.includes('ECONNREFUSED')) {
|
|
68
|
+
errorMessage = `Ollama not running at ${this.baseURL}`;
|
|
69
|
+
hint = 'Make sure Ollama is running. Try: ollama serve';
|
|
70
|
+
} else if (error.message.includes('timeout') || error.name === 'AbortError') {
|
|
71
|
+
errorMessage = `Ollama connection timeout at ${this.baseURL}`;
|
|
72
|
+
hint = 'The server is not responding. Check if Ollama is running and accessible.';
|
|
73
|
+
} else if (error.message.includes('ENOTFOUND')) {
|
|
74
|
+
errorMessage = `Cannot resolve host: ${this.baseURL}`;
|
|
75
|
+
hint = 'Check your OLLAMA_HOST environment variable or network configuration.';
|
|
76
|
+
} else {
|
|
77
|
+
errorMessage = error.message;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.isAvailable = {
|
|
81
|
+
available: false,
|
|
82
|
+
error: errorMessage,
|
|
83
|
+
hint: hint,
|
|
84
|
+
attemptedURL: this.baseURL
|
|
85
|
+
};
|
|
86
|
+
this.lastCheck = Date.now();
|
|
87
|
+
return this.isAvailable;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getLocalModels() {
|
|
92
|
+
const availability = await this.checkOllamaAvailability();
|
|
93
|
+
if (!availability.available) {
|
|
94
|
+
throw new Error(`Ollama not available: ${availability.error}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
100
|
+
|
|
101
|
+
const response = await fetch(`${this.baseURL}/api/tags`, {
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
'Accept': 'application/json'
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
clearTimeout(timeoutId);
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const errorText = await response.text();
|
|
113
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
|
|
118
|
+
if (!data.models) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const models = data.models.map(model => this.parseOllamaModel(model));
|
|
123
|
+
return models;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`Failed to fetch local models: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getRunningModels() {
|
|
130
|
+
const availability = await this.checkOllamaAvailability();
|
|
131
|
+
if (!availability.available) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
138
|
+
|
|
139
|
+
const response = await fetch(`${this.baseURL}/api/ps`, {
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
'Accept': 'application/json'
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
clearTimeout(timeoutId);
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
|
|
155
|
+
const runningModels = (data.models || []).map(model => ({
|
|
156
|
+
name: model.name,
|
|
157
|
+
model: model.model,
|
|
158
|
+
size: model.size,
|
|
159
|
+
digest: model.digest,
|
|
160
|
+
expires_at: model.expires_at,
|
|
161
|
+
size_vram: model.size_vram,
|
|
162
|
+
processor: model.processor || 'unknown'
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
return runningModels;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async testConnection() {
|
|
172
|
+
try {
|
|
173
|
+
// Test 1: Version check
|
|
174
|
+
const versionController = new AbortController();
|
|
175
|
+
const versionTimeoutId = setTimeout(() => versionController.abort(), 5000);
|
|
176
|
+
|
|
177
|
+
const versionResponse = await fetch(`${this.baseURL}/api/version`, {
|
|
178
|
+
signal: versionController.signal
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
clearTimeout(versionTimeoutId);
|
|
182
|
+
|
|
183
|
+
if (!versionResponse.ok) {
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
error: `Version check failed: ${versionResponse.status}`,
|
|
187
|
+
details: 'Ollama might not be running properly'
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const versionData = await versionResponse.json();
|
|
192
|
+
|
|
193
|
+
// Test 2: Tags check
|
|
194
|
+
const tagsController = new AbortController();
|
|
195
|
+
const tagsTimeoutId = setTimeout(() => tagsController.abort(), 10000);
|
|
196
|
+
|
|
197
|
+
const tagsResponse = await fetch(`${this.baseURL}/api/tags`, {
|
|
198
|
+
signal: tagsController.signal
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
clearTimeout(tagsTimeoutId);
|
|
202
|
+
|
|
203
|
+
if (!tagsResponse.ok) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
error: `Tags check failed: ${tagsResponse.status}`,
|
|
207
|
+
details: 'Could not access models API'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tagsText = await tagsResponse.text();
|
|
212
|
+
let tagsData;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
tagsData = JSON.parse(tagsText);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
error: 'Invalid JSON in tags response',
|
|
220
|
+
details: tagsText.substring(0, 100)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
success: true,
|
|
226
|
+
version: versionData.version,
|
|
227
|
+
modelsFound: tagsData.models ? tagsData.models.length : 0,
|
|
228
|
+
models: tagsData.models || []
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: error.message,
|
|
235
|
+
details: error.code || 'Unknown error'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
parseOllamaModel(ollamaModel) {
|
|
241
|
+
const sizeBytes = ollamaModel.size || 0;
|
|
242
|
+
const sizeGB = Math.round(sizeBytes / (1024 ** 3) * 10) / 10;
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
const [modelFamily, version] = ollamaModel.name.split(':');
|
|
246
|
+
const details = ollamaModel.details || {};
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
let estimatedParams = 'Unknown';
|
|
250
|
+
if (details.parameter_size) {
|
|
251
|
+
estimatedParams = details.parameter_size;
|
|
252
|
+
} else if (sizeGB > 0) {
|
|
253
|
+
|
|
254
|
+
if (sizeGB < 2) estimatedParams = '1B';
|
|
255
|
+
else if (sizeGB < 4) estimatedParams = '3B';
|
|
256
|
+
else if (sizeGB < 6) estimatedParams = '7B';
|
|
257
|
+
else if (sizeGB < 15) estimatedParams = '8B';
|
|
258
|
+
else if (sizeGB < 25) estimatedParams = '13B';
|
|
259
|
+
else if (sizeGB < 45) estimatedParams = '34B';
|
|
260
|
+
else estimatedParams = '70B+';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
name: ollamaModel.name,
|
|
265
|
+
displayName: `${modelFamily} ${version || 'latest'}`,
|
|
266
|
+
family: details.family || modelFamily.toLowerCase(),
|
|
267
|
+
size: estimatedParams,
|
|
268
|
+
fileSizeGB: sizeGB,
|
|
269
|
+
quantization: details.quantization_level || 'Unknown',
|
|
270
|
+
format: details.format || 'GGUF',
|
|
271
|
+
digest: ollamaModel.digest,
|
|
272
|
+
modified: ollamaModel.modified_at,
|
|
273
|
+
source: 'ollama_local',
|
|
274
|
+
details: {
|
|
275
|
+
parameter_size: details.parameter_size,
|
|
276
|
+
quantization_level: details.quantization_level,
|
|
277
|
+
families: details.families || [details.family || modelFamily],
|
|
278
|
+
parent_model: details.parent_model || ''
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async pullModel(modelName, onProgress = null) {
|
|
284
|
+
const availability = await this.checkOllamaAvailability();
|
|
285
|
+
if (!availability.available) {
|
|
286
|
+
throw new Error(`Ollama not available: ${availability.error}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetch(`${this.baseURL}/api/pull`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: { 'Content-Type': 'application/json' },
|
|
293
|
+
body: JSON.stringify({ model: modelName, stream: true })
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
throw new Error(`Failed to pull model: HTTP ${response.status}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
const reader = response.body.getReader();
|
|
302
|
+
const decoder = new TextDecoder();
|
|
303
|
+
let receivedSuccess = false;
|
|
304
|
+
|
|
305
|
+
while (true) {
|
|
306
|
+
const { done, value } = await reader.read();
|
|
307
|
+
if (done) break;
|
|
308
|
+
|
|
309
|
+
const chunk = decoder.decode(value);
|
|
310
|
+
const lines = chunk.split('\n').filter(line => line.trim());
|
|
311
|
+
|
|
312
|
+
for (const line of lines) {
|
|
313
|
+
try {
|
|
314
|
+
const data = JSON.parse(line);
|
|
315
|
+
|
|
316
|
+
if (onProgress && (data.status || data.completed !== undefined)) {
|
|
317
|
+
onProgress({
|
|
318
|
+
status: data.status,
|
|
319
|
+
completed: data.completed,
|
|
320
|
+
total: data.total,
|
|
321
|
+
percent: data.total ? Math.round((data.completed / data.total) * 100) : 0
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (data.status === 'success') {
|
|
326
|
+
receivedSuccess = true;
|
|
327
|
+
return { success: true, model: modelName };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (data.error) {
|
|
331
|
+
throw new Error(data.error);
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
if (e.message && !e.message.includes('Unexpected')) {
|
|
335
|
+
throw e; // Re-throw real errors, skip JSON parse errors
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!receivedSuccess) {
|
|
342
|
+
throw new Error('Stream ended without success confirmation');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { success: true, model: modelName };
|
|
346
|
+
} catch (error) {
|
|
347
|
+
throw new Error(`Failed to pull model: ${error.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async deleteModel(modelName) {
|
|
352
|
+
const availability = await this.checkOllamaAvailability();
|
|
353
|
+
if (!availability.available) {
|
|
354
|
+
throw new Error(`Ollama not available: ${availability.error}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const controller = new AbortController();
|
|
359
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
360
|
+
|
|
361
|
+
const response = await fetch(`${this.baseURL}/api/delete`, {
|
|
362
|
+
method: 'DELETE',
|
|
363
|
+
signal: controller.signal,
|
|
364
|
+
headers: { 'Content-Type': 'application/json' },
|
|
365
|
+
body: JSON.stringify({ model: modelName })
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
clearTimeout(timeoutId);
|
|
369
|
+
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
throw new Error(`Failed to delete model: HTTP ${response.status}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { success: true, model: modelName };
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error(`Failed to delete model: ${error.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async testModelPerformance(modelName, testPrompt = "Hello, how are you?") {
|
|
381
|
+
const availability = await this.checkOllamaAvailability();
|
|
382
|
+
if (!availability.available) {
|
|
383
|
+
throw new Error(`Ollama not available: ${availability.error}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const startTime = Date.now();
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const controller = new AbortController();
|
|
390
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
391
|
+
|
|
392
|
+
const response = await fetch(`${this.baseURL}/api/generate`, {
|
|
393
|
+
method: 'POST',
|
|
394
|
+
signal: controller.signal,
|
|
395
|
+
headers: { 'Content-Type': 'application/json' },
|
|
396
|
+
body: JSON.stringify({
|
|
397
|
+
model: modelName,
|
|
398
|
+
prompt: testPrompt,
|
|
399
|
+
stream: false,
|
|
400
|
+
options: {
|
|
401
|
+
num_predict: 50 // Limitar respuesta para test rápido
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
clearTimeout(timeoutId);
|
|
407
|
+
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
throw new Error(`Test failed: HTTP ${response.status}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const data = await response.json();
|
|
413
|
+
const endTime = Date.now();
|
|
414
|
+
|
|
415
|
+
const totalTime = endTime - startTime;
|
|
416
|
+
const tokensGenerated = data.eval_count || 50;
|
|
417
|
+
const tokensPerSecond = Math.round((tokensGenerated / (totalTime / 1000)) * 10) / 10;
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
success: true,
|
|
421
|
+
responseTime: totalTime,
|
|
422
|
+
tokensPerSecond,
|
|
423
|
+
tokensGenerated,
|
|
424
|
+
loadTime: data.load_duration ? Math.round(data.load_duration / 1000000) : null,
|
|
425
|
+
evalTime: data.eval_duration ? Math.round(data.eval_duration / 1000000) : null,
|
|
426
|
+
response: data.response
|
|
427
|
+
};
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
error: error.message,
|
|
432
|
+
responseTime: Date.now() - startTime
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = OllamaClient;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const { OllamaNativeScraper } = require('./native-scraper');
|
|
2
|
+
const OllamaClient = require('./client');
|
|
3
|
+
|
|
4
|
+
class EnhancedOllamaClient extends OllamaClient {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.scraper = new OllamaNativeScraper();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async getEnhancedCompatibleModels() {
|
|
11
|
+
try {
|
|
12
|
+
const localModels = await this.getLocalModels();
|
|
13
|
+
const compatibility = await this.scraper.findCompatibleModels(localModels);
|
|
14
|
+
|
|
15
|
+
const compatible = compatibility.compatible_models.map(match => {
|
|
16
|
+
return {
|
|
17
|
+
name: match.cloud.model_name,
|
|
18
|
+
identifier: match.cloud.model_identifier,
|
|
19
|
+
description: match.cloud.description,
|
|
20
|
+
local_name: match.local.name,
|
|
21
|
+
pulls: match.cloud.pulls,
|
|
22
|
+
url: match.cloud.url,
|
|
23
|
+
match_type: match.match_type,
|
|
24
|
+
score: this.calculateCompatibilityScore(match),
|
|
25
|
+
size: this.extractSizeFromName(match.cloud.model_identifier),
|
|
26
|
+
status: 'INSTALLED',
|
|
27
|
+
installation: {
|
|
28
|
+
ollama: `ollama pull ${match.cloud.model_identifier}`
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const allModels = await this.scraper.scrapeAllModels();
|
|
34
|
+
const recommendations = allModels.models
|
|
35
|
+
.filter(m => !compatibility.compatible_models.find(c => c.cloud.model_identifier === m.model_identifier))
|
|
36
|
+
.sort((a, b) => (b.pulls || 0) - (a.pulls || 0))
|
|
37
|
+
.slice(0, 10)
|
|
38
|
+
.map(model => ({
|
|
39
|
+
name: model.model_name,
|
|
40
|
+
identifier: model.model_identifier,
|
|
41
|
+
description: model.description,
|
|
42
|
+
pulls: model.pulls,
|
|
43
|
+
url: model.url,
|
|
44
|
+
score: this.calculateRecommendationScore(model),
|
|
45
|
+
size: this.extractSizeFromName(model.model_identifier),
|
|
46
|
+
installation: {
|
|
47
|
+
ollama: `ollama pull ${model.model_identifier}`
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
installed: localModels.length,
|
|
53
|
+
compatible: compatible.length,
|
|
54
|
+
compatible_models: compatible,
|
|
55
|
+
recommendations,
|
|
56
|
+
total_available: allModels.total_count,
|
|
57
|
+
cache_info: {
|
|
58
|
+
cached_at: allModels.cached_at,
|
|
59
|
+
expires_at: allModels.expires_at
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error in enhanced compatibility check:', error);
|
|
64
|
+
return await this.getCompatibleModels();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
calculateCompatibilityScore(match) {
|
|
69
|
+
let score = 75;
|
|
70
|
+
if (match.match_type === 'exact') score += 20;
|
|
71
|
+
const pulls = match.cloud.pulls || 0;
|
|
72
|
+
if (pulls > 1000000) score += 5;
|
|
73
|
+
if (pulls > 100000) score += 3;
|
|
74
|
+
if (pulls > 10000) score += 1;
|
|
75
|
+
return Math.min(100, score);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
calculateRecommendationScore(model) {
|
|
79
|
+
let score = 60;
|
|
80
|
+
const pulls = model.pulls || 0;
|
|
81
|
+
if (pulls > 10000000) score += 25;
|
|
82
|
+
else if (pulls > 1000000) score += 20;
|
|
83
|
+
else if (pulls > 100000) score += 15;
|
|
84
|
+
else if (pulls > 10000) score += 10;
|
|
85
|
+
if (model.model_type === 'official') score += 10;
|
|
86
|
+
return Math.min(100, score);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
extractSizeFromName(identifier) {
|
|
90
|
+
const sizeMatch = identifier.match(/(\d+\.?\d*[bm])/i);
|
|
91
|
+
return sizeMatch ? sizeMatch[1].toUpperCase() : 'Unknown';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async searchAvailableModels(query) {
|
|
95
|
+
const results = await this.scraper.searchModels(query);
|
|
96
|
+
return results.models.map(model => ({
|
|
97
|
+
name: model.model_name,
|
|
98
|
+
identifier: model.model_identifier,
|
|
99
|
+
description: model.description,
|
|
100
|
+
pulls: model.pulls,
|
|
101
|
+
url: model.url,
|
|
102
|
+
installation: {
|
|
103
|
+
ollama: `ollama pull ${model.model_identifier}`
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getOllamaStats() {
|
|
109
|
+
return await this.scraper.getStats();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = EnhancedOllamaClient;
|