fix(ux): Idiotensicherheit — 8 Audit-Fixes
Behebung der drei kritischsten UX-Schwachstellen aus dem Produkt-Audit:
1. ConnectPanel: Steps 2-4 (Modellauswahl, Tool-Picker, Config-Snippet,
Vision) werden blockiert wenn noch kein Modell installiert ist.
Stattdessen klarer Hinweis "Erst im Cookbook ein Modell holen" mit
direktem CTA. Verhindert dass Anfänger ein Snippet mit Fake-"coder"
kopieren, bevor ein echtes Modell existiert.
2. OverviewPanel: preload() schluckte Fehler still (catch {}). Zeigt
jetzt einen verständlichen Toast wenn die Engine nicht erreichbar ist.
Schnellstart ist kontextsensitiv: ohne Modelle zeigt er Onboarding-
Schritte (Modell holen → Verbinden → Guide lesen) statt sinnloser
Aktionen wie "Speicher freigeben".
3. CookbookPanel Profi-Suche: doDownload() nutzte zwei sequenzielle
API-Calls (download + register) — wenn der zweite scheiterte, blieb
das System in einem inkonsistenten Halbzustand. Jetzt ein einziger
atomarer Aufruf an /api/cookbook/install-model (wie Recipe/Discover).
Backend-Erweiterung: InstallModelReq akzeptiert jetzt 'file' und
'ctx' um Nutzer-Auswahl direkt zu übergeben.
4. Cookbook Section 1 als "Hier starten" markiert ("1 · Anwendungsfall
wählen & installieren" mit erklärendem Sub-Text).
5. Post-Download-Toasts: Alle drei Install-Pfade sagen nun einheitlich
"Modell erscheint danach automatisch im Modelle-Tab" statt nur
"siehe Aktivität".
6. too_tight-Alternative: Bei Modellen/Setups die den Speicher übersteigen
wird jetzt eine konkrete Alternative (kleinere Datei / passendes Rezept)
verlinkt statt nur "Trotzdem installieren".
7. Duplikat extract_params_b aus cookbook.py entfernt — importiert jetzt
aus hw_math (Single Source of Truth).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -115,8 +115,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 2: Model selector -->
|
<!-- Steps 2–4: nur anzeigen wenn Modelle installiert sind -->
|
||||||
{#if allModels.length}
|
{#if allModels.length}
|
||||||
|
|
||||||
|
<!-- Step 2: Model selector -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h">
|
<div class="card-h">
|
||||||
<h3>2 · Modelle auswählen</h3>
|
<h3>2 · Modelle auswählen</h3>
|
||||||
@@ -140,10 +142,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Step 3: Tool picker -->
|
<!-- Step 3: Tool picker -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h"><h3>3 · Tool wählen</h3></div>
|
<div class="card-h"><h3>3 · Tool wählen</h3></div>
|
||||||
<div class="card-sub">Für welches Werkzeug brauchst du die Konfiguration?</div>
|
<div class="card-sub">Für welches Werkzeug brauchst du die Konfiguration?</div>
|
||||||
<div class="grid grid-4" style="gap:8px">
|
<div class="grid grid-4" style="gap:8px">
|
||||||
@@ -159,10 +160,10 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 4: Config snippet -->
|
<!-- Step 4: Config snippet -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h"><h3>4 · Config-Snippet</h3></div>
|
<div class="card-h"><h3>4 · Config-Snippet</h3></div>
|
||||||
{#if selectedTool === 'zed'}
|
{#if selectedTool === 'zed'}
|
||||||
{@const block = zedBlock(url, activeIds)}
|
{@const block = zedBlock(url, activeIds)}
|
||||||
@@ -210,10 +211,10 @@
|
|||||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Vision -->
|
<!-- Vision -->
|
||||||
{#if visionModel}
|
{#if visionModel}
|
||||||
{@const snippet = `curl ${url}/chat/completions \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${visionModel}",\n "messages": [{"role":"user","content":[{"type":"text","text":"Was ist auf dem Bild falsch?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,<DEIN_BILD>"}}]}]\n}'`}
|
{@const snippet = `curl ${url}/chat/completions \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${visionModel}",\n "messages": [{"role":"user","content":[{"type":"text","text":"Was ist auf dem Bild falsch?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,<DEIN_BILD>"}}]}]\n}'`}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h">
|
<div class="card-h">
|
||||||
@@ -226,4 +227,19 @@
|
|||||||
<div class="log" style="max-height:none"><code>{snippet}</code></div>
|
<div class="log" style="max-height:none"><code>{snippet}</code></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<!-- Kein Modell installiert: Schritt 0 erklären statt leeres Snippet ausgeben -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-h"><h3>2 · Erst ein Modell installieren</h3></div>
|
||||||
|
<div class="empty-c" style="padding:8px 0 4px">
|
||||||
|
<div class="e-t">Noch kein Modell eingerichtet</div>
|
||||||
|
<div class="e-s">Bevor du ein Tool verbinden kannst, braucht die Engine mindestens ein Modell. Das Config-Snippet wird dann automatisch mit deinen echten Modellnamen befüllt.</div>
|
||||||
|
<button class="primary" style="margin-top:14px"
|
||||||
|
onclick={() => document.querySelector(".nav-item[data-view='cookbook']")?.dispatchEvent(new MouseEvent('click'))}>
|
||||||
|
→ Zum Cookbook (Modell holen)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
if (btn) { btn.disabled = true; btn.textContent = 'Starte…' }
|
if (btn) { btn.disabled = true; btn.textContent = 'Starte…' }
|
||||||
try {
|
try {
|
||||||
const r = await api('/api/cookbook/install-recipe', { method: 'POST', body: JSON.stringify({ recipe_id: id, hf_token: getHfToken() }) })
|
const r = await api('/api/cookbook/install-recipe', { method: 'POST', body: JSON.stringify({ recipe_id: id, hf_token: getHfToken() }) })
|
||||||
toast(`${r.count} Downloads gestartet — siehe Aktivität.`)
|
toast(`${r.count} Download${r.count !== 1 ? 's' : ''} läuft — Modelle erscheinen danach automatisch im Modelle-Tab.`)
|
||||||
recipeModal = null; goView('activity')
|
recipeModal = null; goView('activity')
|
||||||
} catch (e: any) { toast(e.message, true); if (btn) { btn.disabled = false; btn.textContent = 'Installieren' } }
|
} catch (e: any) { toast(e.message, true); if (btn) { btn.disabled = false; btn.textContent = 'Installieren' } }
|
||||||
}
|
}
|
||||||
@@ -196,9 +196,14 @@
|
|||||||
if (!modelModalRepo || !modelFile) return toast('Bitte eine GGUF-Datei wählen.', true)
|
if (!modelModalRepo || !modelFile) return toast('Bitte eine GGUF-Datei wählen.', true)
|
||||||
modelLoading = true
|
modelLoading = true
|
||||||
try {
|
try {
|
||||||
const res = await api('/api/download', { method: 'POST', body: JSON.stringify({ repo: modelModalRepo, file: modelFile, hf_token: getHfToken() }) })
|
const params_b = modelAnalysis?.params_b ?? 7.0
|
||||||
await api('/api/register', { method: 'POST', body: JSON.stringify({ role: modelRole, model_path: res.expected_path, ctx: modelCtx }) })
|
const quant = modelFit?.quant ?? 'Q4_K_M'
|
||||||
toast('Download gestartet — siehe Aktivität.'); modelModal = false; goView('activity')
|
await api('/api/cookbook/install-model', { method: 'POST', body: JSON.stringify({
|
||||||
|
repo: modelModalRepo, role: modelRole, params_b, quant,
|
||||||
|
file: modelFile, ctx: modelCtx, hf_token: getHfToken()
|
||||||
|
})})
|
||||||
|
toast('Download läuft — Modell erscheint danach automatisch im Modelle-Tab.')
|
||||||
|
modelModal = false; goView('activity')
|
||||||
} catch (e: any) { toast('Fehler: ' + e.message, true) }
|
} catch (e: any) { toast('Fehler: ' + e.message, true) }
|
||||||
modelLoading = false
|
modelLoading = false
|
||||||
}
|
}
|
||||||
@@ -208,7 +213,7 @@
|
|||||||
btnEl.disabled = true; btnEl.textContent = 'Starte…'
|
btnEl.disabled = true; btnEl.textContent = 'Starte…'
|
||||||
try {
|
try {
|
||||||
await api('/api/cookbook/install-model', { method: 'POST', body: JSON.stringify({ repo, role, params_b, quant: 'Q4_K_M', hf_token: getHfToken() }) })
|
await api('/api/cookbook/install-model', { method: 'POST', body: JSON.stringify({ repo, role, params_b, quant: 'Q4_K_M', hf_token: getHfToken() }) })
|
||||||
toast('Download gestartet — siehe Aktivität.'); goView('activity')
|
toast('Download läuft — Modell erscheint danach automatisch im Modelle-Tab.'); goView('activity')
|
||||||
} catch (e: any) { toast(e.message, true); btnEl.disabled = false; btnEl.textContent = 'Installieren' }
|
} catch (e: any) { toast(e.message, true); btnEl.disabled = false; btnEl.textContent = 'Installieren' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,10 +268,10 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h">
|
<div class="card-h">
|
||||||
<h3>Use-Case-Setups</h3>
|
<h3>1 · Anwendungsfall wählen & installieren</h3>
|
||||||
<button class="ghost" style="margin-left:auto" onclick={openNewRecipe}>+ Eigenes Setup</button>
|
<button class="ghost" style="margin-left:auto" onclick={openNewRecipe}>+ Eigenes Setup</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-sub">Fertige Modell-Bundles für deinen Anwendungsfall. Hardware-Ampel zeigt, ob es auf deinen PC passt.</div>
|
<div class="card-sub">Hier starten: Fertige Modell-Bundles für deinen Anwendungsfall — ein Klick installiert alles. Die Hardware-Ampel zeigt vorab, ob das Setup auf deinen PC passt.</div>
|
||||||
{#if recipeLoading}
|
{#if recipeLoading}
|
||||||
<div class="empty" style="text-align:center;padding:30px">Lade Setups…</div>
|
<div class="empty" style="text-align:center;padding:30px">Lade Setups…</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -450,9 +455,21 @@
|
|||||||
{@const maxRam = Math.max(...r.models.map((m: any) => m.fit?.req_gb ?? 0))}
|
{@const maxRam = Math.max(...r.models.map((m: any) => m.fit?.req_gb ?? 0))}
|
||||||
<div class="hint" style="margin:12px 0">Größter Spitzenbedarf: <b>~{maxRam.toFixed(1)} GB</b>. Es läuft immer nur <b>ein</b> Modell gleichzeitig — das größte bestimmt, ob das Setup passt.</div>
|
<div class="hint" style="margin:12px 0">Größter Spitzenbedarf: <b>~{maxRam.toFixed(1)} GB</b>. Es läuft immer nur <b>ein</b> Modell gleichzeitig — das größte bestimmt, ob das Setup passt.</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if r.fit_level === 'too_tight'}
|
||||||
|
{@const smallerRec = recipes.find((x: any) => !x.user && x.fit_level !== 'too_tight' && x.id !== r.id)}
|
||||||
|
<div class="hint" style="color:var(--warn);margin-bottom:10px;border-left:2px solid var(--warn);padding-left:10px">
|
||||||
|
⚠ Dieses Setup übersteigt deinen verfügbaren Speicher.
|
||||||
|
{#if smallerRec}
|
||||||
|
Alternative: <a href="#" style="color:var(--accent)"
|
||||||
|
onclick={e => { e.preventDefault(); recipeModal = null; setTimeout(() => openRecipe(smallerRec.id), 50) }}>
|
||||||
|
„{smallerRec.title}" ansehen
|
||||||
|
</a> — passt auf dein System.
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<button id="cb-r-install-btn" class="primary{r.fit_level === 'too_tight' ? ' warn' : ''}" style="width:100%"
|
<button id="cb-r-install-btn" class="primary{r.fit_level === 'too_tight' ? ' warn' : ''}" style="width:100%"
|
||||||
onclick={() => installRecipe(r.id)}>
|
onclick={() => installRecipe(r.id)}>
|
||||||
{r.fit_level === 'too_tight' ? 'Trotzdem installieren (zu groß)' : 'Komplettes Setup installieren'}
|
{r.fit_level === 'too_tight' ? 'Trotzdem installieren (nicht empfohlen)' : 'Komplettes Setup installieren'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -501,9 +518,24 @@
|
|||||||
<span class="fit-badge {fitCls(modelFit.fit.level)}">{modelFit.fit.text}</span>
|
<span class="fit-badge {fitCls(modelFit.fit.level)}">{modelFit.fit.text}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if modelFit?.fit.level === 'too_tight' && modelAnalysis?.files}
|
||||||
|
{@const better = modelAnalysis.files.find((f: any) => f.fit.level !== 'too_tight' && f.filename !== modelFile)}
|
||||||
|
{#if better}
|
||||||
|
<div class="hint" style="color:var(--warn);margin:-8px 0 14px;border-left:2px solid var(--warn);padding-left:10px">
|
||||||
|
⚠ Diese Datei ist zu groß für deinen Speicher. Kleinere Alternative:
|
||||||
|
<a href="#" style="color:var(--accent)" onclick={e => { e.preventDefault(); modelFile = better.filename; updateModelFit() }}>
|
||||||
|
{better.filename} ({fitWord(better.fit.level)})
|
||||||
|
</a> — einfach auswählen.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="hint" style="color:var(--warn);margin:-8px 0 14px;border-left:2px solid var(--warn);padding-left:10px">
|
||||||
|
⚠ Alle Varianten dieses Modells sind zu groß für deinen Speicher. Ein kleineres Modell aus den Empfehlungen wäre die bessere Wahl.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<button class="primary{modelFit?.fit.level === 'too_tight' ? ' warn' : ''}" style="width:100%" disabled={modelLoading}
|
<button class="primary{modelFit?.fit.level === 'too_tight' ? ' warn' : ''}" style="width:100%" disabled={modelLoading}
|
||||||
onclick={doDownload}>
|
onclick={doDownload}>
|
||||||
{modelLoading ? 'Starte…' : modelFit?.fit.level === 'too_tight' ? 'Trotzdem holen (zu groß)' : 'Herunterladen & Einpflegen'}
|
{modelLoading ? 'Starte…' : modelFit?.fit.level === 'too_tight' ? 'Trotzdem holen (zu groß — nicht empfohlen)' : 'Herunterladen & Einpflegen'}
|
||||||
</button>
|
</button>
|
||||||
{:else if !modelLoading}
|
{:else if !modelLoading}
|
||||||
<div class="hint">Keine GGUF-Dateien im Repo gefunden.</div>
|
<div class="hint">Keine GGUF-Dateien im Repo gefunden.</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { statusStore } from '../stores/status.svelte.js'
|
import { statusStore } from '../stores/status.svelte.js'
|
||||||
import { systemStore } from '../stores/system.svelte.js'
|
import { systemStore } from '../stores/system.svelte.js'
|
||||||
import { api } from '@core/api.js'
|
import { api } from '@core/api.js'
|
||||||
import { confirmModal, icon, fmtBytes } from '@core/ui.js'
|
import { confirmModal, toast, icon, fmtBytes } from '@core/ui.js'
|
||||||
|
|
||||||
const s = $derived(statusStore.value)
|
const s = $derived(statusStore.value)
|
||||||
const sys = $derived(systemStore.value)
|
const sys = $derived(systemStore.value)
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
async function preload(modelName: string) {
|
async function preload(modelName: string) {
|
||||||
preloadingModel = modelName
|
preloadingModel = modelName
|
||||||
try { await api('/api/preload?model=' + encodeURIComponent(modelName), { method: 'POST' }) }
|
try { await api('/api/preload?model=' + encodeURIComponent(modelName), { method: 'POST' }) }
|
||||||
catch {}
|
catch (e: any) { toast('Laden fehlgeschlagen: ' + (e.message || 'Engine nicht erreichbar?'), true) }
|
||||||
preloadingModel = null
|
preloadingModel = null
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
@@ -153,6 +153,26 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h"><h3>Schnellstart</h3></div>
|
<div class="card-h"><h3>Schnellstart</h3></div>
|
||||||
|
{#if !configuredModels.length}
|
||||||
|
<!-- Onboarding-Pfad: noch keine Modelle -->
|
||||||
|
<div class="card-sub">Noch kein Modell eingerichtet — starte hier:</div>
|
||||||
|
<button class="qa" onclick={() => go('cookbook')}>
|
||||||
|
<span class="qa-ic teal">{@html icon('search')}</span>
|
||||||
|
<span class="qa-main"><span class="qa-t">Schritt 1 · Modell holen</span><span class="qa-s">Cookbook → passendes Setup installieren</span></span>
|
||||||
|
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||||
|
</button>
|
||||||
|
<button class="qa" onclick={() => go('connect')} style="opacity:.55;pointer-events:none">
|
||||||
|
<span class="qa-ic teal">{@html icon('swap')}</span>
|
||||||
|
<span class="qa-main"><span class="qa-t">Schritt 2 · Tools verbinden</span><span class="qa-s">Erst nach dem Download verfügbar</span></span>
|
||||||
|
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||||
|
</button>
|
||||||
|
<button class="qa" onclick={() => go('guides')}>
|
||||||
|
<span class="qa-ic blue">{@html icon('file')}</span>
|
||||||
|
<span class="qa-main"><span class="qa-t">Einsteiger-Guide lesen</span><span class="qa-s">Alle Begriffe erklärt, ohne Vorwissen</span></span>
|
||||||
|
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<!-- Normalbetrieb -->
|
||||||
<div class="card-sub">Die häufigsten Aufgaben — ein Klick.</div>
|
<div class="card-sub">Die häufigsten Aufgaben — ein Klick.</div>
|
||||||
<button class="qa" onclick={() => go('cookbook')}>
|
<button class="qa" onclick={() => go('cookbook')}>
|
||||||
<span class="qa-ic teal">{@html icon('search')}</span>
|
<span class="qa-ic teal">{@html icon('search')}</span>
|
||||||
@@ -174,6 +194,7 @@
|
|||||||
<span class="qa-main"><span class="qa-t">Tools verbinden</span><span class="qa-s">Zed, OpenCode, Cline einrichten</span></span>
|
<span class="qa-main"><span class="qa-t">Tools verbinden</span><span class="qa-s">Zed, OpenCode, Cline einrichten</span></span>
|
||||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+8
-16
@@ -14,7 +14,7 @@ import psutil
|
|||||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||||
|
|
||||||
from auth import auth
|
from auth import auth
|
||||||
from hw_math import evaluate_fit, max_ctx_for
|
from hw_math import evaluate_fit, max_ctx_for, extract_params_b
|
||||||
from config import (MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL, HF_DOWNLOAD_ENV, USER_RECIPES_PATH,
|
from config import (MODELS_DIR, CMD_TEMPLATE, DEFAULT_TTL, HF_DOWNLOAD_ENV, USER_RECIPES_PATH,
|
||||||
DISCOVER_CACHE_PATH, DISCOVER_TTL, hf_bin)
|
DISCOVER_CACHE_PATH, DISCOVER_TTL, hf_bin)
|
||||||
from llamaswap import read_config, write_config, model_id_from_path, set_role_alias
|
from llamaswap import read_config, write_config, model_id_from_path, set_role_alias
|
||||||
@@ -68,6 +68,8 @@ class InstallModelReq(BaseModel):
|
|||||||
role: str
|
role: str
|
||||||
params_b: float
|
params_b: float
|
||||||
quant: str = "Q4_K_M"
|
quant: str = "Q4_K_M"
|
||||||
|
file: str | None = None # konkrete GGUF-Datei; None → _pick_gguf wählt automatisch
|
||||||
|
ctx: int | None = None # None → max_ctx_for (Hardware-Optimum)
|
||||||
hf_token: str | None = None
|
hf_token: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -85,18 +87,6 @@ class UserRecipeReq(BaseModel):
|
|||||||
icon: str = "box"
|
icon: str = "box"
|
||||||
models: list[UserRecipeModelReq]
|
models: list[UserRecipeModelReq]
|
||||||
|
|
||||||
def extract_params_b(repo_id: str) -> float:
|
|
||||||
"""Extrahiert die Parametergröße (in Milliarden) aus dem Repo-Namen."""
|
|
||||||
# z.B. Qwen2.5-Coder-32B -> 32
|
|
||||||
# 8x7B -> 56 (MoE)
|
|
||||||
moe = re.search(r"(\d+)x(\d+(?:\.\d+)?)[bB]", repo_id)
|
|
||||||
if moe:
|
|
||||||
return float(moe.group(1)) * float(moe.group(2))
|
|
||||||
m = re.search(r"(\d+(?:\.\d+)?)[bB](?![a-zA-Z])", repo_id)
|
|
||||||
if m:
|
|
||||||
return float(m.group(1))
|
|
||||||
return 7.0 # Fallback
|
|
||||||
|
|
||||||
def extract_quant(filename: str) -> str:
|
def extract_quant(filename: str) -> str:
|
||||||
m = re.search(r"(Q\d_[A-Z0-9_]+|IQ\d_[A-Z0-9_]+|FP16|BF16)", filename, re.IGNORECASE)
|
m = re.search(r"(Q\d_[A-Z0-9_]+|IQ\d_[A-Z0-9_]+|FP16|BF16)", filename, re.IGNORECASE)
|
||||||
return m.group(1).upper() if m else "Q4_K_M"
|
return m.group(1).upper() if m else "Q4_K_M"
|
||||||
@@ -531,8 +521,10 @@ def upgrades():
|
|||||||
|
|
||||||
@router.post("/install-model")
|
@router.post("/install-model")
|
||||||
def install_model(req: InstallModelReq):
|
def install_model(req: InstallModelReq):
|
||||||
"""Ein einzelnes Modell installieren (Download + Einpflegen unter 'role', optimaler ctx)."""
|
"""Ein einzelnes Modell installieren (Download + Einpflegen unter 'role', optimaler ctx).
|
||||||
file = _pick_gguf(req.repo, req.quant)
|
Wenn 'file' übergeben wird, wird genau diese GGUF-Datei geladen (Profi-Suche-Auswahl);
|
||||||
|
andernfalls wählt _pick_gguf automatisch die beste Variante für den gewünschten Quant."""
|
||||||
|
file = req.file or _pick_gguf(req.repo, req.quant)
|
||||||
if not file:
|
if not file:
|
||||||
raise HTTPException(404, "Keine GGUF-Datei im Repo gefunden.")
|
raise HTTPException(404, "Keine GGUF-Datei im Repo gefunden.")
|
||||||
mmproj = _pick_mmproj(req.repo)
|
mmproj = _pick_mmproj(req.repo)
|
||||||
@@ -550,7 +542,7 @@ def install_model(req: InstallModelReq):
|
|||||||
JOBS[jid]["result_path"] = str(target / file)
|
JOBS[jid]["result_path"] = str(target / file)
|
||||||
attach_download_progress(jid, str(target), hf_file_size(req.repo, file))
|
attach_download_progress(jid, str(target), hf_file_size(req.repo, file))
|
||||||
cfg = read_config()
|
cfg = read_config()
|
||||||
ctx = max_ctx_for(req.params_b, req.quant, ram_gb)
|
ctx = req.ctx if req.ctx else max_ctx_for(req.params_b, req.quant, ram_gb)
|
||||||
path = str(target / file)
|
path = str(target / file)
|
||||||
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
cmd = CMD_TEMPLATE.replace("{model}", path).replace("{ctx}", str(ctx))
|
||||||
if mmproj:
|
if mmproj:
|
||||||
|
|||||||
Vendored
+26
-26
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user