/* 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;