Feat: "Auto"-Button beim Kontext setzen - setzt den setup-bewussten Optimalwert

Beim manuellen ctx-Eintrag (Modellkarte »Ctx«) gab es nur ein leeres Eingabefeld.
Jetzt:
- Backend: GET /api/models/{id}/ctx/auto liefert den setup-bewussten Optimal-ctx fuer
  ein bestehendes Modell (Rolle/Params/Quant + aktuelles Setup) inkl. Budget-Herleitung.
  budget.py: params_of_model() + setup_aware_ctx_for_model() (DRY mit footprint_gb).
- Dialog (CustomDialog/useDialog): optionaler Auto-Button im Prompt, der den Wert eintraegt.
- Cockpit: »Ctx« holt den Optimalwert, zeigt ihn + Budget (GTT/reserviert/frei) in der
  Meldung und bietet »Auto (Nk)« zum direkten Uebernehmen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 14:32:14 +02:00
parent 6d529e3f79
commit 1e011714dd
7 changed files with 153 additions and 91 deletions
+13
View File
@@ -162,6 +162,19 @@ class CtxReq(BaseModel):
ctx: int
@router.get("/models/{model_id}/ctx/auto")
def auto_ctx(model_id: str) -> dict:
"""Setup-bewusster Optimal-ctx für ein bestehendes Modell (Rolle/Params/Quant +
aktuelles Setup). Basis für den 'Auto'-Button an der Modellkarte."""
m = next((x for x in llamaswap.list_models() if x["name"] == model_id), None)
if not m:
raise HTTPException(404, "Modell nicht gefunden")
saw = budget.setup_aware_ctx_for_model(m)
return {"model_id": model_id, "current_ctx": m.get("ctx"),
"params_b": round(budget.params_of_model(m), 1), "quant": m.get("quant"),
"role": m.get("role"), **saw}
@router.post("/models/{model_id}/ctx")
def set_model_ctx(model_id: str, body: CtxReq) -> dict:
if not llamaswap.set_ctx(model_id, body.ctx):
+23 -6
View File
@@ -40,17 +40,26 @@ def gtt_budget_gb() -> float:
return round(psutil.virtual_memory().total / (1024 ** 3) - 6.0, 1)
def footprint_gb(model: dict) -> float:
"""Loaded-Footprint eines Modells = Gewichte + kalibrierter KV-Anteil. Params robust
aus dem MAXIMUM von Namens-Schätzung und Dateigröße (deckt 'Coder-Next' ohne Größe im
Namen sowie Split-GGUFs ab, deren size_bytes nur den ersten Teil zählt)."""
def params_of_model(model: dict) -> float:
"""Robuste Params (Mrd.) eines INSTALLIERTEN Modells: MAXIMUM aus Caps-Schätzung und
Dateigröße. Deckt 'Coder-Next' ohne Größe im Namen (→ aus Datei) und Split-GGUFs
(size_bytes = nur erster Teil → ignoriert) ab."""
caps = model.get("capabilities") or {}
quant = model.get("quant") or "Q4_K_M"
bpp = QUANT_BYTES_PER_PARAM.get(quant.upper(), 0.55)
size_gb = (model.get("size_bytes") or 0) / (1024 ** 3)
pb_size = (size_gb / bpp) if size_gb > 1.0 else 0.0
return max(float(caps.get("params_b") or 0), pb_size, 7.0)
def footprint_gb(model: dict) -> float:
"""Loaded-Footprint eines Modells = Gewichte + kalibrierter KV-Anteil (bei seinem
aktuellen ctx)."""
quant = model.get("quant") or "Q4_K_M"
ctx = int(model.get("ctx") or 32768)
bpp = QUANT_BYTES_PER_PARAM.get(quant.upper(), 0.55)
size_gb = (model.get("size_bytes") or 0) / (1024 ** 3)
pb_size = (size_gb / bpp) if size_gb > 1.0 else 0.0 # Split-Teil → ignoriert
pb = max(float(caps.get("params_b") or 0), pb_size, 7.0)
pb = params_of_model(model)
weights = max(pb * bpp, size_gb)
kv = estimate_memory_gb(pb, quant, ctx) - pb * bpp
return weights + max(kv, 0.0)
@@ -122,3 +131,11 @@ def setup_aware_ctx(params_b: float, quant: str, role: str | None = None) -> dic
"budget_gb": round(budget, 1),
"mode": r["mode"],
}
def setup_aware_ctx_for_model(model: dict) -> dict:
"""Setup-bewusster Optimal-ctx für ein INSTALLIERTES Modell (aus seiner Rolle,
Params & Quant). Für den 'Auto'-Button an der Modellkarte."""
return setup_aware_ctx(params_of_model(model),
model.get("quant") or "Q4_K_M",
role=model.get("role"))
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-DGRyEXwM.js"></script>
<script type="module" crossorigin src="/assets/index-eaeyLEVq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CqYu-pXY.css">
</head>
<body>
+27 -13
View File
@@ -6,11 +6,13 @@ export interface CustomDialogProps {
title: string
message: string
defaultValue?: string
autoValue?: string
autoLabel?: string
onConfirm: (val?: string) => void
onCancel?: () => void
}
export function CustomDialog({ type, title, message, defaultValue, onConfirm, onCancel }: CustomDialogProps) {
export function CustomDialog({ type, title, message, defaultValue, autoValue, autoLabel, onConfirm, onCancel }: CustomDialogProps) {
const inputRef = useRef<HTMLInputElement>(null)
return (
<div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
@@ -27,18 +29,30 @@ export function CustomDialog({ type, title, message, defaultValue, onConfirm, on
<p className="text-xs text-muted-foreground leading-relaxed">{message}</p>
{type === "prompt" && (
<input
ref={inputRef}
type="text"
defaultValue={defaultValue}
className="w-full h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") {
onConfirm(inputRef.current?.value)
}
}}
/>
<div className="flex gap-2">
<input
ref={inputRef}
type="text"
defaultValue={defaultValue}
className="flex-1 h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") {
onConfirm(inputRef.current?.value)
}
}}
/>
{autoValue !== undefined && (
<button
type="button"
onClick={() => { if (inputRef.current) inputRef.current.value = autoValue }}
title="Setup-bewussten Optimalwert eintragen"
className="h-9 px-3 shrink-0 text-xs font-semibold text-primary border border-primary/50 bg-primary/10 hover:bg-primary/20 rounded-lg transition-colors cursor-pointer"
>
{autoLabel || "Auto"}
</button>
)}
</div>
)}
<div className="flex justify-end gap-3 pt-2">
+5 -1
View File
@@ -15,6 +15,8 @@ interface DialogState {
title: string
message: string
defaultValue?: string
autoValue?: string
autoLabel?: string
onConfirm: (val?: string) => void
onCancel?: () => void
}
@@ -41,9 +43,11 @@ export function useDialog() {
const showPrompt = useCallback(
(title: string, message: string, defaultValue: string,
onConfirm: (val?: string) => void, onCancel?: () => void) => {
onConfirm: (val?: string) => void, onCancel?: () => void,
opts?: { autoValue?: string; autoLabel?: string }) => {
setDialog({
type: "prompt", title, message, defaultValue,
autoValue: opts?.autoValue, autoLabel: opts?.autoLabel,
onConfirm: (val) => { setDialog(null); onConfirm(val) },
onCancel: () => { setDialog(null); onCancel?.() },
})
+16 -2
View File
@@ -132,9 +132,21 @@ export function Cockpit() {
}
async function handleSetCtx(name: string, cur: number | null) {
// Setup-bewussten Optimalwert holen (Rolle/Params/Quant + aktuelles Setup).
let auto: { ctx: number; gtt_gb: number; reserved_gb: number; budget_gb: number; mode: string } | null = null
try {
auto = await api(`/api/models/${encodeURIComponent(name)}/ctx/auto`)
} catch { /* Auto optional — Prompt funktioniert auch ohne */ }
const hint = auto
? `Optimal für dein Setup: ${(auto.ctx / 1024).toFixed(0)}k (${auto.ctx}) — ` +
`GTT ${auto.gtt_gb} GB reserviert ${auto.reserved_gb} GB (${auto.mode}) → ${auto.budget_gb} GB frei. ` +
`»Auto« trägt diesen Wert ein.`
: "Gib die gewünschte Kontextlänge in Tokens an:"
showPrompt(
"Kontextlänge anpassen",
"Gib die gewünschte Kontextlänge in Tokens an:",
hint,
String(cur || 32768),
async (v) => {
if (!v) return
@@ -147,7 +159,9 @@ export function Cockpit() {
} catch (e: any) {
showAlert("Fehler", `Fehler beim Setzen des Kontexts: ${e.message || e}`)
}
}
},
undefined,
auto ? { autoValue: String(auto.ctx), autoLabel: `Auto (${(auto.ctx / 1024).toFixed(0)}k)` } : undefined,
)
}