feat(model): wählbares LLM-Modell pro User & pro Job

Mac-Worker:
- /api/summarize, /api/protocol, /api/preload akzeptieren optionales
  model-Feld → überschreibt den .env-Default pro Call, kein Restart nötig
- /api/models listet die lokal verfügbaren Ollama-Modelle + Default

LXC:
- User.default_model + Job.model (Migration für SQLite)
- /api/auth/me liefert default_model, PATCH speichert ihn sofort
- /api/mac/models proxiet die Liste an die UI
- Pipeline reicht job.model an Mac-Worker durch; leer = Worker-Default
- create_job & reprocess akzeptieren model-Override
- Snapshot-Meta nutzt job.model (genauer als /health-Snapshot)

UI:
- Header-Dropdown "Modell:" neben "Profil:" — sofort-speichert User-Default
- Jeder Job zeigt sein Modell als Tag (lila); leer bedeutet Worker-Default
- Reprocess öffnet einen Modal-Picker (statt simplem confirm), Auswahl
  aller verfügbaren Modelle inkl. "Worker-Default"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-16 16:48:11 +00:00
co-authored by Claude Opus 4.7
parent dbf7bca6c7
commit c6164d946c
13 changed files with 386 additions and 54 deletions
+15 -6
View File
@@ -86,6 +86,13 @@ class MacClient:
r.raise_for_status()
return r.json().get("profiles", [])
async def list_models(self) -> dict:
"""Liefert {models: [...], default: <worker .env default>}."""
async with httpx.AsyncClient(timeout=10) as c:
r = await c.get(f"{self.base_url}/api/models")
r.raise_for_status()
return r.json()
async def fetch_log(self, job_id: int, last: int = 200) -> list[str]:
"""Holt die letzten Mac-Worker-Logzeilen für einen Job (für die Diagnose-Anzeige)."""
try:
@@ -97,11 +104,12 @@ class MacClient:
log.warning("fetch_log job=%s failed: %s", job_id, e)
return []
async def preload(self, profile: str | None = None, job_id: int | None = None) -> dict:
async def preload(self, profile: str | None = None, model: str | None = None,
job_id: int | None = None) -> dict:
"""Vor-Heizen: Ollama soll das Modell laden, bevor der echte Call läuft."""
return await self._post_json_retry(
f"{self.base_url}/api/preload",
{"profile": profile},
{"profile": profile, "model": model},
job_id,
)
@@ -122,18 +130,19 @@ class MacClient:
return r.json()
async def summarize(self, transcript: str, title: str = "", profile: str | None = None,
job_id: int | None = None) -> dict:
model: str | None = None, job_id: int | None = None) -> dict:
return await self._post_json_retry(
f"{self.base_url}/api/summarize",
{"transcript": transcript, "title": title, "profile": profile},
{"transcript": transcript, "title": title, "profile": profile, "model": model},
job_id,
)
async def protocol(self, transcript: str, summary: dict, title: str = "", profile: str | None = None,
job_id: int | None = None) -> dict:
model: str | None = None, job_id: int | None = None) -> dict:
return await self._post_json_retry(
f"{self.base_url}/api/protocol",
{"transcript": transcript, "summary": summary, "title": title, "profile": profile},
{"transcript": transcript, "summary": summary, "title": title, "profile": profile,
"model": model},
job_id,
)
+10
View File
@@ -55,6 +55,16 @@ def run_migrations() -> None:
log.info("Migration: füge job.last_heartbeat_at hinzu")
conn.execute(text("ALTER TABLE job ADD COLUMN last_heartbeat_at DATETIME"))
# 6) Modell-Auswahl: user.default_model + job.model
user_cols = _existing_columns(conn, "user")
if user_cols and "default_model" not in user_cols:
log.info("Migration: füge user.default_model hinzu (leer = Worker-Default)")
conn.execute(text("ALTER TABLE user ADD COLUMN default_model VARCHAR DEFAULT ''"))
cols = _existing_columns(conn, "job")
if "model" not in cols:
log.info("Migration: füge job.model hinzu (leer = Worker-Default)")
conn.execute(text("ALTER TABLE job ADD COLUMN model VARCHAR DEFAULT ''"))
def assign_orphan_jobs_to(user_id: int) -> int:
"""Weist alle Jobs ohne owner_id dem angegebenen User zu. Gibt Anzahl zurück."""