Core-Agentic-ERP / core /localmodel.mjs
fivetech's picture
Update core/localmodel.mjs
23eaea0 verified
Raw History Blame Contribute Delete
3.19 kB
/* Modelo local MiMo en el navegador (wllama 3.5.1, WASM/CPU).
El chat de este Space no depende de ningún servicio externo de modelos:
el GGUF se descarga desde Hugging Face (aprox. 5,8 GB la primera vez) y se
reutiliza desde la caché del navegador mientras esta se conserve. */
const WLLAMA_ESM = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/index.js';
const WLLAMA_WASM = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/wasm/wllama.wasm';
const MODEL_URL = 'https://huggingface.co/bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF/resolve/main/MiMo-V2.6-Distill-Qwen-9B-Q4_K_M.gguf';
export const APLocalModel = {
state: 'idle', // idle | downloading | ready | error
error: '',
wllama: null,
label: 'MiMo-V2.6-Distill-Qwen-9B (local en este navegador)',
async cleanupCache() {
// Solo la caché del propio modelo/wllama de ESTE origen; nada más.
try {
for (const name of await caches.keys()) {
const c = await caches.open(name);
for (const req of await c.keys()) {
if (/wllama|MiMo-V2\.6/i.test(req.url)) await c.delete(req);
}
}
} catch (_) {}
},
async load(onProgress) {
if (this.state === 'ready' || this.state === 'downloading') return;
this.state = 'downloading'; this.error = '';
try {
const { Wllama } = await import(WLLAMA_ESM);
if (this.wllama) { try { await this.wllama.exit(); } catch (_) {} }
this.wllama = new Wllama({ default: WLLAMA_WASM }, { parallelDownloads: 5 });
const opts = {
n_gpu_layers: 0, // WASM/CPU: sin aceleración garantizada
n_ctx: 4096, n_batch: 512,
n_threads: Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)),
progressCallback: ({ loaded, total }) => { if (onProgress && total > 0) onProgress(loaded, total); },
};
try {
await this.wllama.loadModelFromUrl(MODEL_URL, opts);
} catch (first) {
// Caché incompleta o corrupta: limpiar y reintentar una vez.
await this.cleanupCache();
await this.wllama.loadModelFromUrl(MODEL_URL, opts);
}
this.state = 'ready';
} catch (e) {
this.state = 'error'; this.error = String(e && e.message || e).slice(0, 200);
throw e;
}
},
stop() { this._abort = true; },
async chat(messages, { maxTokens = 640, temperature = 0.2, onToken } = {}) {
if (this.state !== 'ready') throw new Error('modelo local no cargado');
this.state = 'generating'; this._abort = false;
let stream = null;
try {
stream = await this.wllama.createChatCompletion({ messages, max_tokens: maxTokens, temperature, stream: true });
let answer = '';
for await (const chunk of stream) {
if (this._abort) break; // salir del bucle cierra el iterador y frena la generación
const delta = (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) || {};
const tk = delta.content || '';
if (tk) { answer += tk; if (onToken) onToken(answer); }
}
return answer;
} finally {
this._abort = false;
if (this.state === 'generating') this.state = 'ready';
}
},
};
globalThis.APLocalModel = APLocalModel;