diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index c73830f..36ed698 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -667,6 +667,12 @@ const clearSessionKeys = []; for (const key of EXACT_GENERATE_PARAM_KEYS) { if (out[key] != null) { + if (!userParamIntent && defaults[key] != null && String(out[key]) !== String(defaults[key])) { + out[key] = defaults[key]; + if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) { + clearSessionKeys.push(key); + } + } continue; } if (userParamIntent && sessionExact?.[key] != null) { @@ -1140,7 +1146,7 @@ continue; } const eps = key === "steps" ? 0.5 : 0.051; - if (patch && patch[key] != null && !numClose(patch[key], want, eps)) { + if (userParamIntent && patch && patch[key] != null && !numClose(patch[key], want, eps)) { continue; } if (userParamIntent && sessionExact && sessionExact[key] != null) { @@ -2361,7 +2367,7 @@ ${patch.prompt}`; if (want == null) { return false; } - if (patch?.[k] != null && String(patch[k]) !== String(want)) { + if (userIntent && patch?.[k] != null && String(patch[k]) !== String(want)) { return false; } if (userIntent && state.sessionExact?.[k] != null) { @@ -5496,16 +5502,16 @@ ${patch.prompt}`; return new Promise((resolve) => { const model = $2("sa_model")?.value; if (!model || typeof genericRequest !== "function") { - resolve(false); + resolve({ ok: false, alreadyResident: false }); return; } if (!force && !state.llmParked) { - resolve(false); + resolve({ ok: false, alreadyResident: false }); return; } const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; let settled = false; - const finish = (ok) => { + const finish = (ok, alreadyResident = false) => { if (settled) { return; } @@ -5514,10 +5520,19 @@ ${patch.prompt}`; if (ok) { state.expectColdLoad = false; } - resolve(!!ok); + resolve({ ok: !!ok, alreadyResident: !!alreadyResident }); }; setTimeout(() => finish(false), 18e4); - genericRequest("AssistentWarmLlm", { baseUrl, model }, () => finish(true), 0, () => finish(false)); + genericRequest( + "AssistentWarmLlm", + { baseUrl, model }, + (data) => { + const skipped = !!(data && (data.skipped === "already_resident" || data.already_resident === true)); + finish(true, skipped); + }, + 0, + () => finish(false) + ); }); } function cancelWaitForNewImage() { @@ -5845,7 +5860,8 @@ ${patch.prompt}`; } state.generating = false; setInterruptVisible(state.busy); - state.expectColdLoad = true; + const parkedBeforeWarm = !!state.llmParked; + state.expectColdLoad = parkedBeforeWarm; if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) { const row = state.genResults.find((r) => r.id === state.selectedGenResultId); if (row?.patch) { @@ -5857,9 +5873,16 @@ ${patch.prompt}`; const multiDone = !!(jobs && finishedGenResultCount() > 1); const willAutoCritique = !multiDone && !!$2("sa_auto_critique")?.checked; if (state.view === "chat" && paneVisible && !willAutoCritique && epoch === state.chatEpoch) { - startBusyUi("warming"); - setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026"); - await warmLlm({ force: true }); + if (parkedBeforeWarm || state.expectColdLoad) { + startBusyUi("warming"); + setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026"); + } + const warmResult = await warmLlm({ force: true }); + if (warmResult?.alreadyResident) { + state.expectColdLoad = false; + } else if (!warmResult?.ok) { + state.expectColdLoad = true; + } } if (epoch !== state.chatEpoch) { return null; diff --git a/README.md b/README.md index ceb59c3..1b50141 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + **Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both. -**Version 0.14.1** — On Generate, force Exact turbo/raw params when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7). `detectKreaProfileName` no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects `krea_profile` + `recommended_params`. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile. - -**Version 0.14.1** — Generate always merges Exact turbo|raw `steps`/`cfg`/`sigma_shift` (client-authoritative; sparse LLM omit is safe). Park/warm skip no-op Ollama round-trips when the chat model is already (un)loaded via `/api/ps`. Builds on 0.14.0. +**Version 0.14.1** — Force Exact turbo/raw numbers on Generate when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7), including when the profile *label* is turbo/raw. detectKreaProfileName no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects krea_profile + ecommended_params. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile. Post-Generate warm still runs with park off, but skips the cold path when Ollama already has the chat model resident. **Version 0.14.0** — **Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`. diff --git a/src/app.js b/src/app.js index 747c1af..514a9fe 100644 --- a/src/app.js +++ b/src/app.js @@ -1353,7 +1353,7 @@ if (want == null) { return false; } - if (patch?.[k] != null && String(patch[k]) !== String(want)) { + if (userIntent && patch?.[k] != null && String(patch[k]) !== String(want)) { return false; } if (userIntent && state.sessionExact?.[k] != null) { @@ -4751,20 +4751,25 @@ } /** Re-load chat model into VRAM. force=true after Generate even without park — Krea often evicts Ollama. */ + /** + * Re-load chat model into VRAM. force=true after Generate even without park — + * Krea often evicts Ollama. Soft: AssistentWarmLlm skipped=already_resident + * clears expectColdLoad without a fake cold path. Resolves { ok, alreadyResident }. + */ function warmLlm({ force = false } = {}) { return new Promise((resolve) => { const model = $('sa_model')?.value; if (!model || typeof genericRequest !== 'function') { - resolve(false); + resolve({ ok: false, alreadyResident: false }); return; } if (!force && !state.llmParked) { - resolve(false); + resolve({ ok: false, alreadyResident: false }); return; } const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; let settled = false; - const finish = (ok) => { + const finish = (ok, alreadyResident = false) => { if (settled) { return; } @@ -4773,11 +4778,20 @@ if (ok) { state.expectColdLoad = false; } - resolve(!!ok); + resolve({ ok: !!ok, alreadyResident: !!alreadyResident }); }; // VL cold-load can exceed a minute — don't time out the flag early. setTimeout(() => finish(false), 180000); - genericRequest('AssistentWarmLlm', { baseUrl, model }, () => finish(true), 0, () => finish(false)); + genericRequest( + 'AssistentWarmLlm', + { baseUrl, model }, + (data) => { + const skipped = !!(data && (data.skipped === 'already_resident' || data.already_resident === true)); + finish(true, skipped); + }, + 0, + () => finish(false), + ); }); } @@ -5130,7 +5144,10 @@ state.generating = false; setInterruptVisible(state.busy); - state.expectColdLoad = true; + // Soft cold flag: only assume eviction when we parked. Otherwise wait for + // warmLlm — already_resident means keep chatting without a cold path. + const parkedBeforeWarm = !!state.llmParked; + state.expectColdLoad = parkedBeforeWarm; if (jobs && lastSrc && state.selectedGenResultId && epoch === state.chatEpoch) { const row = state.genResults.find((r) => r.id === state.selectedGenResultId); @@ -5144,9 +5161,16 @@ const multiDone = !!(jobs && finishedGenResultCount() > 1); const willAutoCritique = !multiDone && !!$('sa_auto_critique')?.checked; if (state.view === 'chat' && paneVisible && !willAutoCritique && epoch === state.chatEpoch) { - startBusyUi('warming'); - setStatus('Возвращаю LLM в GPU…'); - await warmLlm({ force: true }); + if (parkedBeforeWarm || state.expectColdLoad) { + startBusyUi('warming'); + setStatus('Возвращаю LLM в GPU…'); + } + const warmResult = await warmLlm({ force: true }); + if (warmResult?.alreadyResident) { + state.expectColdLoad = false; + } else if (!warmResult?.ok) { + state.expectColdLoad = true; + } } if (epoch !== state.chatEpoch) { diff --git a/src/kreaProfile.js b/src/kreaProfile.js index b765875..91dd651 100644 --- a/src/kreaProfile.js +++ b/src/kreaProfile.js @@ -81,10 +81,16 @@ export function exactKeysToForce(live, defaults, { if (want == null) { continue; } - const eps = key === 'steps' ? 0.5 : 0.051; - // Patch asked for a non-Exact number — honor it. - if (patch && patch[key] != null && !numClose(patch[key], want, eps)) { - continue; + const eps = key === 'steps' ? 0.5 : 0.051; + // Honor intentional non-Exact patch only when the user asked for params. + // Sparse LLM echoes of live 20/7 must not block Exact (turbo/raw label -> numbers). + if ( + userParamIntent + && patch + && patch[key] != null + && !numClose(patch[key], want, eps) + ) { + continue; } if (userParamIntent && sessionExact && sessionExact[key] != null) { continue; diff --git a/src/session.js b/src/session.js index 311e04e..58fc708 100644 --- a/src/session.js +++ b/src/session.js @@ -372,7 +372,7 @@ export function resolveExactProfileDefaults({ exact, profiles, profileName } = { } /** - * On generate, inject Exact profile steps/cfg/sigma when the patch omitted them. + * On generate, inject Exact profile steps/cfg/sigma when omitted, and overwrite foreign leftovers when the user did not ask for params. * Client is authoritative so sparse LLM deltas are safe. Honors explicit patch values * and sessionExact when the user asked for params this turn. * Returns clearSessionKeys so the UI apply path is not blocked by stale sessionExact. @@ -390,9 +390,20 @@ export function mergeExactParamsForGenerate(patch, { const defaults = resolveExactProfileDefaults({ exact, profiles, profileName }); const out = { ...patch }; const clearSessionKeys = []; - for (const key of EXACT_GENERATE_PARAM_KEYS) { - if (out[key] != null) { - continue; + for (const key of EXACT_GENERATE_PARAM_KEYS) { + if (out[key] != null) { + // Sparse LLM may echo live leftovers (20/7). Without user intent, force Exact. + if ( + !userParamIntent + && defaults[key] != null + && String(out[key]) !== String(defaults[key]) + ) { + out[key] = defaults[key]; + if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) { + clearSessionKeys.push(key); + } + } + continue; } if (userParamIntent && sessionExact?.[key] != null) { out[key] = sessionExact[key]; diff --git a/test/kreaProfile.test.js b/test/kreaProfile.test.js index f07a9e5..f4c6b1e 100644 --- a/test/kreaProfile.test.js +++ b/test/kreaProfile.test.js @@ -68,7 +68,15 @@ describe('kreaProfile', () => { ); // Intentional non-Exact patch value is kept. assert.deepEqual( - exactKeysToForce({ steps: 20, cfg: 7, sigma_shift: 1.15 }, turbo, { patch: { cfg: 7 } }), + exactKeysToForce({ steps: 20, cfg: 7, sigma_shift: 1.15 }, turbo, { patch: { cfg: 7 } }), + ['steps', 'cfg'], + ); + assert.deepEqual( + exactKeysToForce( + { steps: 20, cfg: 7, sigma_shift: 1.15 }, + turbo, + { patch: { cfg: 7 }, userParamIntent: true }, + ), ['steps'], ); assert.deepEqual( diff --git a/test/patch.test.js b/test/patch.test.js index 0f0d852..401054a 100644 --- a/test/patch.test.js +++ b/test/patch.test.js @@ -123,10 +123,22 @@ describe('session.js', () => { const explicit = mergeExactParamsForGenerate( { generate: true, steps: 12, cfg: 2 }, - { exact, profiles: exact.profiles, profileName: 'turbo' }, + { exact, profiles: exact.profiles, profileName: 'turbo', userParamIntent: true }, ); assert.equal(explicit.patch.steps, 12); - assert.equal(explicit.patch.cfg, 2); + assert.equal(explicit.patch.cfg, 2); + + const forced = mergeExactParamsForGenerate( + { generate: true, steps: 20, cfg: 7 }, + { + exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } }, + profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } }, + profileName: 'turbo', + userParamIntent: false, + }, + ); + assert.equal(forced.patch.steps, 8); + assert.equal(forced.patch.cfg, 1); }); it('resolveExactProfileDefaults picks raw over generation defaults', () => {