Modell-Auswahl in der GUI (Konfig + Start) — Design¶
Datum: 2026-06-21
Status: ratifiziert (Brainstorming abgeschlossen) — vor Implementierungsplan
Scope: Auf der „Konfig + Start"-Seite (/config) auswählen können, welche Modelle ein Eval-Lauf fährt — heute unmöglich, weil die Modelle im models:-Feld der gewählten config*.yaml verborgen sind und die Config nur als Ganzes gewählt wird.
Auslöser: Beim Bedienen der GUI fiel auf: man kann kein Modell wählen. Das ist die natürliche nächste Wand nach Ink. 8 (Modell-Vergleich) — um zwei Modelle zu vergleichen, muss man erst einen 2-Modell-Lauf starten können, was aus dem UI nicht ging (genau deshalb hatte axis=model kein echtes Bundle).
Phase-Einordnung: Erste, kleinste Stufe der Phase-2-„Variablen-Entkopplung / Steuerung brauchbar machen". Bewusst ephemer (kein Zurückschreiben in Configs) — die größere Entkopplung (Modelle/Settings/Prompts/Kriterien als eigene Bausteine, im Tool editieren) bleibt ein späterer eigener Schnitt.
1 · Ziel & Motivation¶
Das Tool ist Johannes' Labor: falsifizierbare Aussagen „Modell X auf HW Y mit Settings Z taugt für Aufgabe B" via Variablen-Kontrolle. Die Modell-Achse ist die erste Variable, die man bewusst variieren will (dasselbe Pack mit zwei Modellen). Der Vergleich (Ink. 8) zeigt das Ergebnis; diese Stufe liefert die Eingabe: aus dem UI gezielt die Modelle für einen Lauf zusammenstellen.
2 · Verifizierte Ausgangslage (Code-fundiert)¶
/config-Route (touchstone/gui/app.py) rendertconfig.htmlmit zwei Dropdowns: Pack (packs/*.yaml) und Config (config*.yaml). Kein Modell-Selektor. (judge_configs,eval_only_bundlesfür die Judge-Form.)- Modelle stehen in der Config-YAML:
Config.models: list[ModelSpec](touchstone/config.py).ModelSpec:id: str,quant: str = "",max_tokens_default: int = 400. - Start-Pfad:
/runs/eval-POST →RunRegistry.start_eval(*, pack_path, config_path, resume_dir=None)(touchstone/gui/control.py) bautargv = ["eval", "--pack", …, "--config", …, "--run-dir", …, "--emit-events", (--resume …)]und spawntpython -m touchstone eval …. eval-CLI (eval_cmd,touchstone/cli.py): Optionen--pack,--config/-c,--run-dir,--emit-events,--resume,--out. Lädt die Config (inkl.models) und fährt die Matrixfor model in config.models(iter_eval_cells). Kein Modell-Override.- Stack: FastAPI + Jinja2 + Alpine + HTMX, build-frei. Bestehendes Muster: Daten server-seitig in die Vorlage einbetten, Alpine für Interaktivität.
3 · Ratifizierte Entscheidungen¶
| # | Entscheidung | Begründung |
|---|---|---|
| M1 | Picker auf der Start-Seite: Checkboxen für die models: der gewählten Config (alle vorab an) + Ad-hoc-Zeile (id + quant) für Modelle, die in keiner Config stehen. |
Maximal flexibel; man kann sofort zwei beliebige Modelle gegen denselben Endpoint fahren und vergleichen. Stufe 1 der Entkopplung. |
| M2 | Ephemer, nur für diesen Lauf — keine Config-Datei wird geschrieben. Das Bundle (bundle.json) protokolliert, was lief. |
YAGNI; „im Tool editieren / zurückschreiben" ist ein späterer Schnitt. |
| M3 | Override ersetzt config.models (kein Merge): die Picker-Auswahl IST die Modell-Liste des Laufs. |
Vorhersagbar; „genau diese Modelle". Endpoint/seed/… bleiben aus der Config. |
| M4 | Ein Lauf = ein Endpoint (der der gewählten Config). Kein Pool über Configs. | Jede Config hat ihren eigenen Endpoint; Pooling würde die Endpoint-Zuordnung mehrdeutig machen. |
| M5 | Transport als JSON (--models-json), nicht colon-codiert. |
Erhält alle ModelSpec-Felder (inkl. max_tokens_default) und ist Pydantic-validierbar. |
| M6 | Front-end: Server bettet Modelle aller Configs als JSON ein, Alpine tauscht die Liste (Ansatz A). | Build-frei, kein neuer Endpoint, sofortige Reaktion; passt zum bestehenden Alpine-Daten-Muster. 6 kleine YAMLs pro Seitenaufruf zu parsen ist vernachlässigbar. |
| M7 | Bei resume ist der Picker aus. |
Resume fährt die festgelegten Zellen des Bundles erneut; Modelle liegen fest. |
4 · Architektur & Datenfluss¶
config.html (Alpine baut models_json aus Checkboxen + Ad-hoc)
└─ POST /runs/eval (Feld models_json)
└─ Route: models_json → list[ModelSpec] validieren (Pydantic)
├─ ungültig/leer (wenn Picker aktiv) → config.html mit Fehler, KEIN Spawn
└─ gültig → start_eval(models=…)
└─ argv += ["--models-json", <json>] → python -m touchstone eval --models-json … --pack … --config …
└─ eval_cmd: models_json → list[ModelSpec] → ersetzt config.models → Lauf
└─ bundle.json protokolliert die tatsächlichen Modelle
→ /result, /compare?axis=model können jetzt 2 Modelle zeigen
5 · Komponenten (Verantwortung · Schnittstelle · Abhängigkeit)¶
touchstone/gui/configs.py(neu, pur):config_models(path: str | Path) -> list[ModelSpec]— parst nur dasmodels:-Feld einerconfig*.yaml(YAML laden, jedes Element viaModelSpec(**m)validieren). Defensiv: Datei fehlt / YAML kaputt /modelsfehlt oder ist kein Listenelement →[]. (Nutzt absichtlich nichtload_config, damit eine Config mit Platzhalter-Endpoint trotzdem ihre Modelle zeigt.)models_by_config(files: list[str]) -> dict[str, list[dict]]—{config_path: [model.model_dump(), …]}für die Vorlage (JSON-serialisierbar).touchstone/gui/app.py:/config-Route: zusätzlichmodels_by_config(config_files)an die Vorlage geben (Keymodels_by_config)./runs/eval-POST: neues optionales Form-Feldmodels_json: str = Form(""). Beiresumewird der Override ignoriert. Sonst, wenn nicht-leer →models_from_json(...); bei JSON-/Validierungs-/Leer-Fehler →HTTPException(400), kein Spawn (codebase-konform: die Route liefert sonst JSON und nutztHTTPException, z. B. 409 bei RunInProgress — daher 400 statt HTML-Re-render; der Front-end-Submit ist bei 0 Modellen ohnehin deaktiviert). Gültige Liste →start_eval(..., models=specs).touchstone/gui/control.py—RunRegistry.start_eval: Signatur erweitern ummodels: list[ModelSpec] | None = None; falls gesetzt →argv += ["--models-json", json.dumps([m.model_dump() for m in models])].touchstone/cli.py—eval_cmd: Optionmodels_json: str = typer.Option("", "--models-json", help="JSON list[ModelSpec]; replaces config.models for this run (GUI picker)"). Wenn nicht-leer → parsen →cfg.models = [ModelSpec(**m) …]vor dem Lauf. (Helfer_apply_models_override(cfg, models_json) -> Configrein/testbar.)touchstone/gui/templates/config.html: Alpine-Block im „Eval starten"-Formular:x-datahältbyConfig(= eingebettetesmodels_by_configviatojson),selected(Config-Pfad, an<select>gebunden),picked(Set/Map der angehakten Modelle),adhoc(Liste{id, quant}).- Bei Config-Wechsel:
picked= alle Modelle der neuen Config (vorab an). - Checkbox-Liste der Config-Modelle (
id·quant), plus „+ Modell"-Zeile (zwei Inputs + Button → anadhocanhängen, entfernbar). - Verstecktes
<input name="models_json">(computed) = JSON auspicked∪adhoc({id, quant, max_tokens_default}; Ad-hocmax_tokens_default = 400). - Submit deaktiviert, wenn 0 Modelle gewählt. Bei
resumeder ganze Picker ausgeblendet (keinmodels_jsongesendet).
6 · Error-Handling¶
Genaue Semantik des models_json-Feldes (löst die „leer"-Mehrdeutigkeit auf):
- models_json = leerer String / Feld fehlt → kein Override, Lauf nutzt config.models wie heute. Deckt resume und Alt-/Nicht-GUI-Clients ab (Rückwärtskompat).
- models_json = "[]" (explizit leere Auswahl) → HTTPException(400), kein Spawn. Der Front-end-Submit ist bei 0 Modellen deaktiviert (:disabled="count() === 0"), das 400 ist Defense-in-Depth für gecraftete Posts.
- models_json = ungültiges JSON / Element mit fehlender oder leerer id → HTTPException(400), kein Spawn. (models_from_json lehnt leere/whitespace-id ab und dedupliziert nach (id, quant).)
- Kaputte Config beim Anzeigen: config_models → []; diese Config zeigt keine Modelle (defensiv), die Seite bleibt. Wird sie ohne Override gestartet, verhält sich der Lauf wie heute.
- CLI --models-json ungültig: klarer Fehler-Exit (kein halber Lauf).
7 · Teststrategie (TDD)¶
Unit (pur):
- config_models: parst Modelle aus einer echten/temporären Config; defensiv bei fehlender Datei, kaputtem YAML, fehlendem models. models_by_config: Map über mehrere Dateien, inkl. einer kaputten (→ []).
- _apply_models_override(cfg, models_json): ersetzt cfg.models (replace, kein Merge); leerer String → unverändert; ungültiges JSON / fehlende Pflichtfelder → Fehler.
Integration / Verhalten (TestClient + argv-fangender FakeLauncher):
- /config rendert den Picker: Checkboxen für die Modelle der (ersten/gewählten) Config, „+ Modell"-Zeile, verstecktes models_json-Feld, eingebettetes models_by_config-JSON. Bei ?resume=… ist der Picker aus.
- POST /runs/eval mit gültigem models_json → start_eval wird mit den geparsten models aufgerufen / argv enthält --models-json (FakeLauncher fängt argv).
- POST /runs/eval mit ungültigem/leerem models_json → 200 mit Fehler-Re-render, kein Spawn.
- start_eval(models=…): argv enthält --models-json <json>; ohne models → unverändert (kein Flag).
Rückwärtskompat: bestehende Eval-Starts ohne models_json unverändert; Kern ohne [gui] lauffähig; eval_cmd ohne --models-json unverändert.
Manuell (am Ende): GUI starten → /config → eine Config wählen → ihre Modelle erscheinen → eines abwählen, ein zweites ad-hoc eintippen → „Eval starten" → Lauf fährt genau die zwei Modelle → danach /compare/<bundle>?axis=model zeigt sie nebeneinander.
8 · Berührte / neue Dateien¶
| Datei | Änderung |
|---|---|
touchstone/gui/configs.py |
neu — config_models + models_by_config (pur) |
touchstone/gui/app.py |
/config reicht models_by_config; /runs/eval nimmt + validiert models_json, ruft start_eval(models=…) |
touchstone/gui/control.py |
start_eval(..., models=None) → --models-json in argv |
touchstone/cli.py |
eval_cmd --models-json → _apply_models_override ersetzt config.models |
touchstone/gui/templates/config.html |
Modell-Picker (Checkboxen + Ad-hoc + hidden models_json), bei resume aus |
tests/ |
neue Tests je §7 |
AGENTS.md |
kurze Notiz: GUI-Modell-Override (--models-json, ephemer, ersetzt config.models) |
9 · Scope-Grenze (YAGNI)¶
In diesem Schnitt: Picker (Config-Modelle + Ad-hoc), ephemerer Override eines Laufs, Validierung/Fehler, resume-Ausblendung.
Bewusst NICHT: Zurückschreiben in config*.yaml; Editieren bestehender quant/max_tokens; Pool/Endpoint-übergreifende Auswahl; Modelle/Settings/Prompts/Kriterien als eigene editierbare Bausteine (= späterer Entkopplungs-Schnitt); Auto-Discovery verfügbarer Modelle vom Endpoint (/v1/models) — wäre eine schöne Ergänzung, aber eigener Schnitt.