From aa60331f7fdfec79cff5e898a29099db9d79cbca Mon Sep 17 00:00:00 2001 From: Jeuner <62662523+Jeuners@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:59:03 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20UI=20w=C3=A4hlt=20empfohlenes=20deepseek?= =?UTF-8?q?-Default=20vor=20statt=20gedrosseltem=20Free-Modell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ursache des 429-'es hängt': Die UI wählte beim Laden das erste Top-Free-Modell vor; OpenRouter-Free-Modelle sind hart gedrosselt -> Lauf lief in 5x Retry + Abbruch. Jetzt: - /api/modelle stellt den Default (deepseek-v4-flash) als 'empfohlen' voran. - UI wählt das empfohlene Modell vor, markiert Free als 'oft gedrosselt' und stellt ein gemerktes Free-Modell NICHT automatisch wieder her. - Server-seitiges Fortschritts-Logging ([Stufe 2] Batch X/Y) fürs Live-Log. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/angebote/web.py | 35 +++++++++++++++++++++++++++--- src/angebote/web_static/index.html | 18 +++++++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/angebote/web.py b/src/angebote/web.py index 2ddd52b..436bd8a 100644 --- a/src/angebote/web.py +++ b/src/angebote/web.py @@ -48,16 +48,33 @@ def index() -> str: @app.get("/api/modelle") def api_modelle(q: str = "") -> list[dict]: - """Modell-Liste für das Dropdown: Suche oder Top-Free. 'Aktualisieren' = neu rufen.""" + """Modell-Liste fürs Dropdown. Ohne Suche: das EMPFOHLENE Default-Modell + (günstig, nicht gedrosselt) zuerst, dann die Top-Free. + + Hintergrund: Free-Modelle sind bei OpenRouter hart gedrosselt (429). Würde + die UI eines davon vorwählen, läuft der erste Lauf ins Rate-Limit. Deshalb + steht der Default (deepseek-v4-flash) vorne und wird vorgewählt. + """ + from .kategorisieren import _DEFAULT_MODELLE from .modelle import lade_modelle, suche, top_free try: alle = lade_modelle() except Exception as e: # Netz/SSL -> ehrlich melden, nicht raten raise HTTPException(status_code=502, detail=f"Modelle nicht abrufbar: {e}") - treffer = suche(alle, q) if q else top_free(alle, 8) + + default_id = _DEFAULT_MODELLE.get("openrouter") + if q: + treffer = suche(alle, q) + else: + default_mi = next((m for m in alle if m.id == default_id), None) + top = [m for m in top_free(alle, 8) if m.id != default_id] + treffer = ([default_mi] if default_mi else []) + top return [ - {"id": m.id, "frei": m.frei, "tools": m.tools, "context": m.context} + { + "id": m.id, "frei": m.frei, "tools": m.tools, "context": m.context, + "empfohlen": m.id == default_id, + } for m in treffer[:25] ] @@ -223,10 +240,17 @@ def _run_kategorisieren(job_id, plz, fetch, modell, anbieter, key) -> None: # Tatsächlich genutztes Modell (Default je Anbieter aufgelöst) -- für die # sichtbare Herkunft im Ergebnis. modell_genutzt = getattr(kt, "_modell", modell) + print( + f"[Stufe 2] start · PLZ {plz} · {anbieter}/{modell_genutzt} · " + f"{len(fetch.angebote)} Angebote", + flush=True, + ) def fort(done, total): job["done"] = done job["total"] = total + if done == total or done % 5 == 0: # nicht jede Batch -> Log lesbar + print(f"[Stufe 2] PLZ {plz} · Batch {done}/{total}", flush=True) kat = kategorisiere(list(fetch.angebote), kt, fortschritt=fort) @@ -235,9 +259,14 @@ def _run_kategorisieren(job_id, plz, fetch, modell, anbieter, key) -> None: ) job["status"] = "fertig" _ergebnis_cache[(plz, anbieter, modell)] = job["ergebnis"] + print( + f"[Stufe 2] fertig · PLZ {plz} · {job['ergebnis']['unsicher']} unsicher", + flush=True, + ) except AbbruchFehler as e: job["status"] = "fehler" job["fehler"] = e.als_text() + print(f"[Stufe 2] ABBRUCH · PLZ {plz} · {e.schwelle}: {e.ursache}", flush=True) except Exception as e: # nichts verstecken -- ehrliche Fehlermeldung job["status"] = "fehler" job["fehler"] = f"Unerwarteter Fehler: {e}" diff --git a/src/angebote/web_static/index.html b/src/angebote/web_static/index.html index 38603e0..d763e8d 100644 --- a/src/angebote/web_static/index.html +++ b/src/angebote/web_static/index.html @@ -383,17 +383,27 @@ async function ladeModelle(q=""){ for (const m of liste){ const o = document.createElement("option"); o.value = m.id; - const tag = ab === "ollama" ? "lokal" : (m.frei ? "FREE" : "paid"); + o.dataset.frei = m.frei ? "1" : "0"; + o.dataset.empf = m.empfohlen ? "1" : "0"; + let tag; + if (ab === "ollama") tag = "lokal"; + else if (m.empfohlen) tag = "★ empfohlen"; + else tag = m.frei ? "FREE · oft gedrosselt" : "paid"; const tools = m.tools ? "" : " ⚠ kein tool-calling"; o.textContent = `${m.id} [${tag}]${tools}`; if (!m.tools) o.disabled = true; sel.appendChild(o); } - // gemerktes Modell für diesen Anbieter bevorzugen, sonst erstes brauchbares + // Vorauswahl: gemerktes Modell -- aber bei OpenRouter NICHT automatisch ein + // gedrosseltes Free-Modell (führt zu 429); dann lieber das empfohlene. const opts = [...sel.options]; const gemerkt = gemerktesModell(ab); - if (gemerkt && opts.some(o => o.value === gemerkt && !o.disabled)){ - sel.value = gemerkt; + const gemerktOpt = gemerkt ? opts.find(o => o.value === gemerkt && !o.disabled) : null; + const empfOpt = opts.find(o => o.dataset.empf === "1" && !o.disabled); + if (gemerktOpt && !(ab !== "ollama" && gemerktOpt.dataset.frei === "1")){ + sel.value = gemerktOpt.value; + } else if (empfOpt){ + sel.value = empfOpt.value; } else { const ok = opts.find(o => !o.disabled && o.value); if (ok) sel.value = ok.value;