Slash-loading regardless of lookup.
Browse files- package.json +1 -1
- src/app/handle-prompt.js +17 -4
- src/worker/list-chat-models.js +54 -7
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.35",
|
| 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/handle-prompt.js
CHANGED
|
@@ -18,16 +18,29 @@ export async function handlePrompt({ promptMarkdown, workerConnection }) {
|
|
| 18 |
return serializer(view.state.doc);
|
| 19 |
});
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
const formatted = `**Question:**\n> ${promptMarkdown.replaceAll('\n', '\n> ')}`;
|
| 22 |
outputMessage(formatted);
|
| 23 |
|
| 24 |
outputMessage('Processing your request...');
|
| 25 |
try {
|
| 26 |
-
|
| 27 |
const combinedPrompt = promptMarkdown;
|
| 28 |
-
|
| 29 |
-
// promptMarkdown;
|
| 30 |
-
const promptOutput = await workerConnection.runPrompt(combinedPrompt);
|
| 31 |
outputMessage('**Reply:**\n' + promptOutput);
|
| 32 |
} catch (error) {
|
| 33 |
outputMessage('**Error:** ' + error.message);
|
|
|
|
| 18 |
return serializer(view.state.doc);
|
| 19 |
});
|
| 20 |
|
| 21 |
+
// If the user typed a slash command like `/owner/model-name`, treat it as a direct
|
| 22 |
+
// load-model request and do not treat it as a chat prompt.
|
| 23 |
+
const trimmed = (promptMarkdown || '').trim();
|
| 24 |
+
if (trimmed.startsWith('/') && trimmed.length > 1) {
|
| 25 |
+
const modelId = trimmed.slice(1).trim();
|
| 26 |
+
outputMessage(`Loading model: ${modelId}...`);
|
| 27 |
+
try {
|
| 28 |
+
await workerConnection.loadModel(modelId);
|
| 29 |
+
outputMessage(`Model ${modelId} loaded successfully!`);
|
| 30 |
+
} catch (error) {
|
| 31 |
+
outputMessage(`Error loading model ${modelId}: ${error.message}`);
|
| 32 |
+
}
|
| 33 |
+
return;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
const formatted = `**Question:**\n> ${promptMarkdown.replaceAll('\n', '\n> ')}`;
|
| 37 |
outputMessage(formatted);
|
| 38 |
|
| 39 |
outputMessage('Processing your request...');
|
| 40 |
try {
|
| 41 |
+
// Concatenate history and the new prompt into a single prompt string
|
| 42 |
const combinedPrompt = promptMarkdown;
|
| 43 |
+
const promptOutput = await workerConnection.runPrompt(combinedPrompt);
|
|
|
|
|
|
|
| 44 |
outputMessage('**Reply:**\n' + promptOutput);
|
| 45 |
} catch (error) {
|
| 46 |
outputMessage('**Error:** ' + error.message);
|
src/worker/list-chat-models.js
CHANGED
|
@@ -135,7 +135,10 @@ export async function* listChatModelsIterator(params = {}) {
|
|
| 135 |
|
| 136 |
function classifyModel(rawModel, fetchResult) {
|
| 137 |
const id = rawModel.modelId || rawModel.id || rawModel.model || rawModel.modelId;
|
| 138 |
-
const
|
|
|
|
|
|
|
|
|
|
| 139 |
if (!fetchResult) return entry;
|
| 140 |
if (fetchResult.status === 'auth') {
|
| 141 |
entry.classification = 'auth-protected';
|
|
@@ -148,9 +151,17 @@ export async function* listChatModelsIterator(params = {}) {
|
|
| 148 |
entry.architectures = Array.isArray(fetchResult.architectures) ? fetchResult.architectures : null;
|
| 149 |
entry.fetchStatus = 'ok';
|
| 150 |
const deny = ['bert','roberta','distilbert','electra','albert','deberta','mobilebert','convbert','sentence-transformers'];
|
| 151 |
-
const allow = ['gpt2','gptj','gpt_neox','llama','qwen','mistral','phi','
|
| 152 |
if (entry.model_type && deny.includes(entry.model_type)) { entry.classification = 'encoder'; entry.confidence = 'high'; return entry; }
|
| 153 |
if (entry.model_type && allow.includes(entry.model_type)) { entry.classification = 'gen'; entry.confidence = 'high'; return entry; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
const arch = entry.architectures;
|
| 155 |
if (arch && Array.isArray(arch)) {
|
| 156 |
for (let i = 0; i < arch.length; i++) {
|
|
@@ -238,8 +249,34 @@ export async function* listChatModelsIterator(params = {}) {
|
|
| 238 |
return /tokenizer|vocab|merges|sentencepiece/i.test(String(name));
|
| 239 |
});
|
| 240 |
|
| 241 |
-
//
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
survivors.push(m);
|
| 245 |
}
|
|
@@ -311,11 +348,21 @@ export async function* listChatModelsIterator(params = {}) {
|
|
| 311 |
await Promise.all(pool);
|
| 312 |
|
| 313 |
// final
|
| 314 |
-
// Select
|
|
|
|
| 315 |
const authRequired = results.filter(r => r.classification === 'auth-protected').slice(0, 50);
|
| 316 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
const selected = nonAuth.concat(authRequired);
|
| 318 |
-
const models = selected.map(r => ({ id: r.id, model_type: r.model_type, architectures: r.architectures, classification: r.classification, confidence: r.confidence, fetchStatus: r.fetchStatus }));
|
| 319 |
const meta = { fetched: listing.length, filtered: survivors.length, errors, selected: { nonAuth: nonAuth.length, authRequired: authRequired.length, total: models.length } };
|
| 320 |
if (params && params.debug) meta.counters = Object.assign({}, counters);
|
| 321 |
yield { status: 'done', models, meta };
|
|
|
|
| 135 |
|
| 136 |
function classifyModel(rawModel, fetchResult) {
|
| 137 |
const id = rawModel.modelId || rawModel.id || rawModel.model || rawModel.modelId;
|
| 138 |
+
const hasTokenizer = rawModel.hasTokenizer || false;
|
| 139 |
+
const hasOnnxModel = rawModel.hasOnnxModel || false;
|
| 140 |
+
const isTransformersJsReady = rawModel.isTransformersJsReady || false;
|
| 141 |
+
const entry = { id, model_type: null, architectures: null, classification: 'unknown', confidence: 'low', fetchStatus: 'error', hasTokenizer, hasOnnxModel, isTransformersJsReady };
|
| 142 |
if (!fetchResult) return entry;
|
| 143 |
if (fetchResult.status === 'auth') {
|
| 144 |
entry.classification = 'auth-protected';
|
|
|
|
| 151 |
entry.architectures = Array.isArray(fetchResult.architectures) ? fetchResult.architectures : null;
|
| 152 |
entry.fetchStatus = 'ok';
|
| 153 |
const deny = ['bert','roberta','distilbert','electra','albert','deberta','mobilebert','convbert','sentence-transformers'];
|
| 154 |
+
const allow = ['gpt2','gptj','gpt_neox','llama','qwen','qwen2','mistral','phi','phi3','t5','bart','pegasus','gemma','gemma2','gemma3','falcon','bloom','lfm2'];
|
| 155 |
if (entry.model_type && deny.includes(entry.model_type)) { entry.classification = 'encoder'; entry.confidence = 'high'; return entry; }
|
| 156 |
if (entry.model_type && allow.includes(entry.model_type)) { entry.classification = 'gen'; entry.confidence = 'high'; return entry; }
|
| 157 |
+
// Also check for model_type variations with underscores/dashes
|
| 158 |
+
const normalizedModelType = entry.model_type && entry.model_type.replace(/[-_]/g, '');
|
| 159 |
+
if (normalizedModelType) {
|
| 160 |
+
const normalizedAllow = allow.map(t => t.replace(/[-_]/g, ''));
|
| 161 |
+
const normalizedDeny = deny.map(t => t.replace(/[-_]/g, ''));
|
| 162 |
+
if (normalizedDeny.includes(normalizedModelType)) { entry.classification = 'encoder'; entry.confidence = 'high'; return entry; }
|
| 163 |
+
if (normalizedAllow.includes(normalizedModelType)) { entry.classification = 'gen'; entry.confidence = 'high'; return entry; }
|
| 164 |
+
}
|
| 165 |
const arch = entry.architectures;
|
| 166 |
if (arch && Array.isArray(arch)) {
|
| 167 |
for (let i = 0; i < arch.length; i++) {
|
|
|
|
| 249 |
return /tokenizer|vocab|merges|sentencepiece/i.test(String(name));
|
| 250 |
});
|
| 251 |
|
| 252 |
+
// Check for ONNX model files that transformers.js needs
|
| 253 |
+
const hasOnnxModel = siblings.some((s) => {
|
| 254 |
+
if (!s) return false;
|
| 255 |
+
let name = null;
|
| 256 |
+
if (typeof s === 'string') name = s;
|
| 257 |
+
else if (typeof s === 'object') name = s.rfilename || s.name || s.path || s.filename || s.repo_file || s.file || null;
|
| 258 |
+
if (!name) return false;
|
| 259 |
+
// Look for ONNX files - transformers.js needs various ONNX model files
|
| 260 |
+
return /onnx\/.*\.onnx|onnx\\.*\.onnx|.*model.*\.onnx|.*decoder.*\.onnx/i.test(String(name));
|
| 261 |
+
});
|
| 262 |
+
|
| 263 |
+
// Filter out models that lack required files
|
| 264 |
+
// Models must have both tokenizer files AND ONNX model files, OR be auth-protected
|
| 265 |
+
if (!hasTokenizer || !hasOnnxModel) {
|
| 266 |
+
// Only keep if it's likely auth-protected or has text-generation pipeline with both files
|
| 267 |
+
if (!pipeline || !pipeline.toLowerCase().includes('text-generation')) continue;
|
| 268 |
+
if (!hasTokenizer) continue; // Always require tokenizer
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
// Check if model explicitly supports transformers.js
|
| 272 |
+
const isTransformersJsReady = (m.library_name === 'transformers.js') ||
|
| 273 |
+
(Array.isArray(m.tags) && m.tags.includes('transformers.js')) ||
|
| 274 |
+
(Array.isArray(m.tags) && m.tags.includes('onnx'));
|
| 275 |
+
|
| 276 |
+
// Preserve flags for later filtering
|
| 277 |
+
m.hasTokenizer = hasTokenizer;
|
| 278 |
+
m.hasOnnxModel = hasOnnxModel;
|
| 279 |
+
m.isTransformersJsReady = isTransformersJsReady;
|
| 280 |
|
| 281 |
survivors.push(m);
|
| 282 |
}
|
|
|
|
| 348 |
await Promise.all(pool);
|
| 349 |
|
| 350 |
// final
|
| 351 |
+
// Select models: auth-protected regardless of classification, or generation-capable with both tokenizers and ONNX files
|
| 352 |
+
// Prioritize transformers.js-ready models
|
| 353 |
const authRequired = results.filter(r => r.classification === 'auth-protected').slice(0, 50);
|
| 354 |
+
const genCapable = results.filter(r => r.classification === 'gen' && r.hasTokenizer && r.hasOnnxModel);
|
| 355 |
+
|
| 356 |
+
// Sort generation-capable models: transformers.js-ready first, then others
|
| 357 |
+
genCapable.sort((a, b) => {
|
| 358 |
+
if (a.isTransformersJsReady && !b.isTransformersJsReady) return -1;
|
| 359 |
+
if (!a.isTransformersJsReady && b.isTransformersJsReady) return 1;
|
| 360 |
+
return 0;
|
| 361 |
+
});
|
| 362 |
+
|
| 363 |
+
const nonAuth = genCapable.slice(0, 50);
|
| 364 |
const selected = nonAuth.concat(authRequired);
|
| 365 |
+
const models = selected.map(r => ({ id: r.id, model_type: r.model_type, architectures: r.architectures, classification: r.classification, confidence: r.confidence, fetchStatus: r.fetchStatus, hasTokenizer: r.hasTokenizer, hasOnnxModel: r.hasOnnxModel, isTransformersJsReady: r.isTransformersJsReady }));
|
| 366 |
const meta = { fetched: listing.length, filtered: survivors.length, errors, selected: { nonAuth: nonAuth.length, authRequired: authRequired.length, total: models.length } };
|
| 367 |
if (params && params.debug) meta.counters = Object.assign({}, counters);
|
| 368 |
yield { status: 'done', models, meta };
|