Tweaking model list a little.
Browse files- package.json +1 -1
- src/app/model-list.js +34 -31
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "localm",
|
| 3 |
-
"version": "1.1.
|
| 4 |
"description": "Chat application",
|
| 5 |
"scripts": {
|
| 6 |
"build": "esbuild src/index.js --target=es6 --bundle --sourcemap --outfile=./index.js --format=iife --external:fs --external:path --external:child_process --external:ws --external:katex/dist/katex.min.css",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "localm",
|
| 3 |
+
"version": "1.1.21",
|
| 4 |
"description": "Chat application",
|
| 5 |
"scripts": {
|
| 6 |
"build": "esbuild src/index.js --target=es6 --bundle --sourcemap --outfile=./index.js --format=iife --external:fs --external:path --external:child_process --external:ws --external:katex/dist/katex.min.css",
|
src/app/model-list.js
CHANGED
|
@@ -56,20 +56,20 @@ export async function fetchBrowserModels() {
|
|
| 56 |
// eslint-disable-next-line no-await-in-loop
|
| 57 |
const res = await fetch(url);
|
| 58 |
if (!res.ok) {
|
| 59 |
-
console.warn(`HF batch ${i+1} returned ${res.status}; stopping further batches`);
|
| 60 |
break;
|
| 61 |
}
|
| 62 |
// eslint-disable-next-line no-await-in-loop
|
| 63 |
const batch = await res.json();
|
| 64 |
if (!Array.isArray(batch) || batch.length === 0) {
|
| 65 |
-
console.log(`HF batch ${i+1} returned 0 models; stopping`);
|
| 66 |
break;
|
| 67 |
}
|
| 68 |
-
console.log(`batch ${i+1} -> ${batch.length} models`);
|
| 69 |
allRaw = allRaw.concat(batch);
|
| 70 |
if (batch.length < batchSize) break; // last page
|
| 71 |
} catch (err) {
|
| 72 |
-
console.warn(`Error fetching HF batch ${i+1}:`, err);
|
| 73 |
break;
|
| 74 |
}
|
| 75 |
}
|
|
@@ -92,22 +92,22 @@ export async function fetchBrowserModels() {
|
|
| 92 |
hasTokenizer: !!hasTokenizer,
|
| 93 |
missingFiles: !!missingFiles,
|
| 94 |
missingReason: missingReason || '',
|
| 95 |
-
|
| 96 |
-
|
| 97 |
});
|
| 98 |
} catch (e) {
|
| 99 |
return null;
|
| 100 |
}
|
| 101 |
}).filter(m => m !== null);
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
|
| 112 |
const final = [...auth, ...pub];
|
| 113 |
|
|
@@ -132,26 +132,26 @@ export async function fetchBrowserModels() {
|
|
| 132 |
function isModelMobileCapable(model) {
|
| 133 |
// Skip if no model ID
|
| 134 |
if (!model.id) return false;
|
| 135 |
-
|
| 136 |
// Estimate model size from various indicators
|
| 137 |
const sizeEstimate = estimateModelSize(model);
|
| 138 |
-
|
| 139 |
// Skip models that are too large
|
| 140 |
if (sizeEstimate > MOBILE_SIZE_THRESHOLD) {
|
| 141 |
return false;
|
| 142 |
}
|
| 143 |
-
|
| 144 |
// Prefer models with certain pipeline tags that work well in browsers
|
| 145 |
const preferredTags = [
|
| 146 |
'text-generation',
|
| 147 |
-
'text2text-generation',
|
| 148 |
'feature-extraction',
|
| 149 |
'sentence-similarity',
|
| 150 |
'fill-mask'
|
| 151 |
];
|
| 152 |
-
|
| 153 |
const hasPreferredTag = !model.pipeline_tag || preferredTags.includes(model.pipeline_tag);
|
| 154 |
-
|
| 155 |
// Skip certain model types that are less suitable for general text generation
|
| 156 |
const excludePatterns = [
|
| 157 |
/whisper/i,
|
|
@@ -162,9 +162,9 @@ function isModelMobileCapable(model) {
|
|
| 162 |
/classification/i,
|
| 163 |
/embedding/i
|
| 164 |
];
|
| 165 |
-
|
| 166 |
const isExcluded = excludePatterns.some(pattern => pattern.test(model.id));
|
| 167 |
-
|
| 168 |
return hasPreferredTag && !isExcluded;
|
| 169 |
}
|
| 170 |
|
|
@@ -175,14 +175,14 @@ function isModelMobileCapable(model) {
|
|
| 175 |
*/
|
| 176 |
function estimateModelSize(model) {
|
| 177 |
const modelId = model.id.toLowerCase();
|
| 178 |
-
|
| 179 |
// Extract size from model name patterns
|
| 180 |
const sizePatterns = [
|
| 181 |
/(\d+\.?\d*)b\b/i, // "7b", "3.8b", etc.
|
| 182 |
/(\d+)m\b/i, // "125m" -> convert to billions
|
| 183 |
/(\d+)k\b/i // "125k" -> very small
|
| 184 |
];
|
| 185 |
-
|
| 186 |
for (const pattern of sizePatterns) {
|
| 187 |
const match = modelId.match(pattern);
|
| 188 |
if (match) {
|
|
@@ -196,7 +196,7 @@ function estimateModelSize(model) {
|
|
| 196 |
}
|
| 197 |
}
|
| 198 |
}
|
| 199 |
-
|
| 200 |
// If no size found in name, make conservative estimates based on model family
|
| 201 |
if (modelId.includes('gpt2') || modelId.includes('distil')) return 0.2;
|
| 202 |
if (modelId.includes('phi-1') || modelId.includes('phi1')) return 1.3;
|
|
@@ -206,7 +206,7 @@ function estimateModelSize(model) {
|
|
| 206 |
if (modelId.includes('qwen') && modelId.includes('7b')) return 7;
|
| 207 |
if (modelId.includes('llama') && modelId.includes('7b')) return 7;
|
| 208 |
if (modelId.includes('llama') && modelId.includes('13b')) return 13;
|
| 209 |
-
|
| 210 |
// Default conservative estimate for unknown models
|
| 211 |
return 5;
|
| 212 |
}
|
|
@@ -222,7 +222,7 @@ function processModelData(model) {
|
|
| 222 |
const vendor = extractVendor(model.id);
|
| 223 |
const name = extractModelName(model.id);
|
| 224 |
const slashCommand = generateSlashCommand(model.id);
|
| 225 |
-
|
| 226 |
return {
|
| 227 |
id: model.id,
|
| 228 |
name,
|
|
@@ -272,7 +272,7 @@ function extractVendor(modelId) {
|
|
| 272 |
function extractModelName(modelId) {
|
| 273 |
const parts = modelId.split('/');
|
| 274 |
const name = parts[parts.length - 1];
|
| 275 |
-
|
| 276 |
// Clean up common patterns
|
| 277 |
return name
|
| 278 |
.replace(/-ONNX$/, '')
|
|
@@ -291,7 +291,7 @@ function extractModelName(modelId) {
|
|
| 291 |
*/
|
| 292 |
function generateSlashCommand(modelId) {
|
| 293 |
const name = (modelId.split('/').pop() || modelId).toLowerCase();
|
| 294 |
-
|
| 295 |
// Create short, memorable commands
|
| 296 |
if (name.includes('phi-3') || name.includes('phi3')) return 'phi3';
|
| 297 |
if (name.includes('phi-1') || name.includes('phi1')) return 'phi1';
|
|
@@ -304,7 +304,7 @@ function generateSlashCommand(modelId) {
|
|
| 304 |
if (name.includes('llama')) return 'llama';
|
| 305 |
if (name.includes('gemma')) return 'gemma';
|
| 306 |
if (name.includes('flan')) return 'flant5';
|
| 307 |
-
|
| 308 |
// Generate from first few characters of model name
|
| 309 |
const clean = name.replace(/[^a-z0-9]/g, '');
|
| 310 |
return clean.substring(0, 8);
|
|
@@ -351,7 +351,10 @@ function detectRequiredFiles(model) {
|
|
| 351 |
*/
|
| 352 |
function isModelChatCapable(model) {
|
| 353 |
if (!model) return false;
|
| 354 |
-
const allowedPipelines = new Set([
|
|
|
|
|
|
|
|
|
|
| 355 |
if (model.pipeline_tag && allowedPipelines.has(model.pipeline_tag)) return true;
|
| 356 |
// tags array may contain 'conversational' or 'chat'
|
| 357 |
if (Array.isArray(model.tags)) {
|
|
@@ -362,7 +365,7 @@ function isModelChatCapable(model) {
|
|
| 362 |
// fallback heuristics in id/name: look for chat, conversational, dialog, instruct
|
| 363 |
const id = (model.id || '').toLowerCase();
|
| 364 |
const name = (model.name || '').toLowerCase();
|
| 365 |
-
const heuristics = ['chat', 'conversational', 'dialog', 'instruct', 'instruction'];
|
| 366 |
for (const h of heuristics) {
|
| 367 |
if (id.includes(h) || name.includes(h)) return true;
|
| 368 |
}
|
|
@@ -386,7 +389,7 @@ function getFallbackModels() {
|
|
| 386 |
{
|
| 387 |
id: 'mistralai/Mistral-7B-v0.1',
|
| 388 |
name: 'Mistral 7B',
|
| 389 |
-
vendor: 'Mistral AI',
|
| 390 |
size: '7.3B',
|
| 391 |
slashCommand: 'mistral',
|
| 392 |
description: 'Highly efficient, outperforms larger models with innovative architecture'
|
|
|
|
| 56 |
// eslint-disable-next-line no-await-in-loop
|
| 57 |
const res = await fetch(url);
|
| 58 |
if (!res.ok) {
|
| 59 |
+
console.warn(`HF batch ${i + 1} returned ${res.status}; stopping further batches`);
|
| 60 |
break;
|
| 61 |
}
|
| 62 |
// eslint-disable-next-line no-await-in-loop
|
| 63 |
const batch = await res.json();
|
| 64 |
if (!Array.isArray(batch) || batch.length === 0) {
|
| 65 |
+
console.log(`HF batch ${i + 1} returned 0 models; stopping`);
|
| 66 |
break;
|
| 67 |
}
|
| 68 |
+
console.log(`batch ${i + 1} -> ${batch.length} models`);
|
| 69 |
allRaw = allRaw.concat(batch);
|
| 70 |
if (batch.length < batchSize) break; // last page
|
| 71 |
} catch (err) {
|
| 72 |
+
console.warn(`Error fetching HF batch ${i + 1}:`, err);
|
| 73 |
break;
|
| 74 |
}
|
| 75 |
}
|
|
|
|
| 92 |
hasTokenizer: !!hasTokenizer,
|
| 93 |
missingFiles: !!missingFiles,
|
| 94 |
missingReason: missingReason || '',
|
| 95 |
+
downloads: m.downloads || 0,
|
| 96 |
+
tags: Array.isArray(m.tags) ? m.tags.slice() : []
|
| 97 |
});
|
| 98 |
} catch (e) {
|
| 99 |
return null;
|
| 100 |
}
|
| 101 |
}).filter(m => m !== null);
|
| 102 |
|
| 103 |
+
// Keep only models that have both ONNX and tokenizer files AND support chat
|
| 104 |
+
const withFiles = processed.filter(p => p && p.hasOnnx && p.hasTokenizer && isModelChatCapable(p));
|
| 105 |
|
| 106 |
+
// Sort by downloads desc
|
| 107 |
+
withFiles.sort((a, b) => ((b && b.downloads) || 0) - ((a && a.downloads) || 0));
|
| 108 |
|
| 109 |
+
const auth = withFiles.filter(m => m && m.requiresAuth).slice(0, 10).map(x => x);
|
| 110 |
+
const pub = withFiles.filter(m => m && !m.requiresAuth).slice(0, 10).map(x => x);
|
| 111 |
|
| 112 |
const final = [...auth, ...pub];
|
| 113 |
|
|
|
|
| 132 |
function isModelMobileCapable(model) {
|
| 133 |
// Skip if no model ID
|
| 134 |
if (!model.id) return false;
|
| 135 |
+
|
| 136 |
// Estimate model size from various indicators
|
| 137 |
const sizeEstimate = estimateModelSize(model);
|
| 138 |
+
|
| 139 |
// Skip models that are too large
|
| 140 |
if (sizeEstimate > MOBILE_SIZE_THRESHOLD) {
|
| 141 |
return false;
|
| 142 |
}
|
| 143 |
+
|
| 144 |
// Prefer models with certain pipeline tags that work well in browsers
|
| 145 |
const preferredTags = [
|
| 146 |
'text-generation',
|
| 147 |
+
'text2text-generation',
|
| 148 |
'feature-extraction',
|
| 149 |
'sentence-similarity',
|
| 150 |
'fill-mask'
|
| 151 |
];
|
| 152 |
+
|
| 153 |
const hasPreferredTag = !model.pipeline_tag || preferredTags.includes(model.pipeline_tag);
|
| 154 |
+
|
| 155 |
// Skip certain model types that are less suitable for general text generation
|
| 156 |
const excludePatterns = [
|
| 157 |
/whisper/i,
|
|
|
|
| 162 |
/classification/i,
|
| 163 |
/embedding/i
|
| 164 |
];
|
| 165 |
+
|
| 166 |
const isExcluded = excludePatterns.some(pattern => pattern.test(model.id));
|
| 167 |
+
|
| 168 |
return hasPreferredTag && !isExcluded;
|
| 169 |
}
|
| 170 |
|
|
|
|
| 175 |
*/
|
| 176 |
function estimateModelSize(model) {
|
| 177 |
const modelId = model.id.toLowerCase();
|
| 178 |
+
|
| 179 |
// Extract size from model name patterns
|
| 180 |
const sizePatterns = [
|
| 181 |
/(\d+\.?\d*)b\b/i, // "7b", "3.8b", etc.
|
| 182 |
/(\d+)m\b/i, // "125m" -> convert to billions
|
| 183 |
/(\d+)k\b/i // "125k" -> very small
|
| 184 |
];
|
| 185 |
+
|
| 186 |
for (const pattern of sizePatterns) {
|
| 187 |
const match = modelId.match(pattern);
|
| 188 |
if (match) {
|
|
|
|
| 196 |
}
|
| 197 |
}
|
| 198 |
}
|
| 199 |
+
|
| 200 |
// If no size found in name, make conservative estimates based on model family
|
| 201 |
if (modelId.includes('gpt2') || modelId.includes('distil')) return 0.2;
|
| 202 |
if (modelId.includes('phi-1') || modelId.includes('phi1')) return 1.3;
|
|
|
|
| 206 |
if (modelId.includes('qwen') && modelId.includes('7b')) return 7;
|
| 207 |
if (modelId.includes('llama') && modelId.includes('7b')) return 7;
|
| 208 |
if (modelId.includes('llama') && modelId.includes('13b')) return 13;
|
| 209 |
+
|
| 210 |
// Default conservative estimate for unknown models
|
| 211 |
return 5;
|
| 212 |
}
|
|
|
|
| 222 |
const vendor = extractVendor(model.id);
|
| 223 |
const name = extractModelName(model.id);
|
| 224 |
const slashCommand = generateSlashCommand(model.id);
|
| 225 |
+
|
| 226 |
return {
|
| 227 |
id: model.id,
|
| 228 |
name,
|
|
|
|
| 272 |
function extractModelName(modelId) {
|
| 273 |
const parts = modelId.split('/');
|
| 274 |
const name = parts[parts.length - 1];
|
| 275 |
+
|
| 276 |
// Clean up common patterns
|
| 277 |
return name
|
| 278 |
.replace(/-ONNX$/, '')
|
|
|
|
| 291 |
*/
|
| 292 |
function generateSlashCommand(modelId) {
|
| 293 |
const name = (modelId.split('/').pop() || modelId).toLowerCase();
|
| 294 |
+
|
| 295 |
// Create short, memorable commands
|
| 296 |
if (name.includes('phi-3') || name.includes('phi3')) return 'phi3';
|
| 297 |
if (name.includes('phi-1') || name.includes('phi1')) return 'phi1';
|
|
|
|
| 304 |
if (name.includes('llama')) return 'llama';
|
| 305 |
if (name.includes('gemma')) return 'gemma';
|
| 306 |
if (name.includes('flan')) return 'flant5';
|
| 307 |
+
|
| 308 |
// Generate from first few characters of model name
|
| 309 |
const clean = name.replace(/[^a-z0-9]/g, '');
|
| 310 |
return clean.substring(0, 8);
|
|
|
|
| 351 |
*/
|
| 352 |
function isModelChatCapable(model) {
|
| 353 |
if (!model) return false;
|
| 354 |
+
const allowedPipelines = new Set([
|
| 355 |
+
'text-generation', 'conversational', 'text2text-generation', 'chat',
|
| 356 |
+
'sentence'
|
| 357 |
+
]);
|
| 358 |
if (model.pipeline_tag && allowedPipelines.has(model.pipeline_tag)) return true;
|
| 359 |
// tags array may contain 'conversational' or 'chat'
|
| 360 |
if (Array.isArray(model.tags)) {
|
|
|
|
| 365 |
// fallback heuristics in id/name: look for chat, conversational, dialog, instruct
|
| 366 |
const id = (model.id || '').toLowerCase();
|
| 367 |
const name = (model.name || '').toLowerCase();
|
| 368 |
+
const heuristics = ['chat', 'conversational', 'dialog', 'instruct', 'instruction', 'sentence'];
|
| 369 |
for (const h of heuristics) {
|
| 370 |
if (id.includes(h) || name.includes(h)) return true;
|
| 371 |
}
|
|
|
|
| 389 |
{
|
| 390 |
id: 'mistralai/Mistral-7B-v0.1',
|
| 391 |
name: 'Mistral 7B',
|
| 392 |
+
vendor: 'Mistral AI',
|
| 393 |
size: '7.3B',
|
| 394 |
slashCommand: 'mistral',
|
| 395 |
description: 'Highly efficient, outperforms larger models with innovative architecture'
|