mihailik commited on
Commit
fb5e6b2
·
1 Parent(s): 16221a7

Slash-loading regardless of lookup.

Browse files
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "localm",
3
- "version": "1.1.33",
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
- // Concatenate history and the new prompt into a single prompt string
27
  const combinedPrompt = promptMarkdown;
28
- // historyText ? (historyText + '\n\n' + promptMarkdown) :
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 entry = { id, model_type: null, architectures: null, classification: 'unknown', confidence: 'low', fetchStatus: 'error' };
 
 
 
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','gpt','t5','bart','pegasus'];
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
- // Some listing entries may not expose siblings; fall back to pipeline_tag heuristic
242
- if (!hasTokenizer && (!pipeline || !pipeline.toLowerCase().includes('text-generation'))) continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 up to 20 non-auth models and 20 auth-required models to avoid returning a very large list.
 
315
  const authRequired = results.filter(r => r.classification === 'auth-protected').slice(0, 50);
316
- const nonAuth = results.filter(r => r.classification !== 'auth-protected').slice(0, 50);
 
 
 
 
 
 
 
 
 
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 };