bilder: Hirn und Coder sehen selbst - ueber Bild-Zwillinge, Text bleibt mit Draft schnell
Ampel / ampel (push) Failing after 21s
Ampel / ampel (push) Failing after 21s
llama.cpp kann Draft-Beschleunigung und Bilder nicht zusammen (HTTP 500 "failed to process speculative batch", b11057 und b11157 geprueft; speculative.n_max=0 je Anfrage hilft nicht). Darum bekommen Hirn und Coder je einen Bild-Zwilling: gleiche Gewichte plus Projektor, ohne Draft (vision, coder-bild), in einer eigenen llama-swap-Gruppe, die den Coder nicht verdraengt. Probe 24.09.: beide 8/8 Bildmerkmale; Hirn-Zwilling 68 t/s, Coder-Zwilling 12,5 t/s. Bild-Weiche v3 im Gateway: Bild im aktuellen Schritt geht an den Zwilling der Rolle, aeltere Bilder werden einmal beschrieben (gemerkt) und als Text mitgeschickt, damit der Rest einer Agenten-Aufgabe wieder beim schnellen Modell laeuft. Qwen3-VL gibt "vision" ab, der Coder verliert den Projektor, der mit Draft nur HTTP 500 lieferte. Radar misst die Bildfaehigkeit des heutigen Modells ueber dessen Zwilling (sonst gewaenne jeder bildfaehige Kandidat mit "versteht Bilder"). Pruefstand: Coder darf vor dem Aendern lesen (Version 3). Modelle-Seite zeigt "Bilder: ja" ueber den Zwilling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2cc1e1cc47
commit
91aa16eee1
@@ -0,0 +1,100 @@
|
||||
"""Bild-Weiche v3 (24.09.2026): frische Bilder an den Bild-Zwilling, ältere als Beschreibung."""
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
from typing import ClassVar
|
||||
|
||||
from routers import gateway_proxy
|
||||
from services.router_logic import bild_ziel, bilder_aufteilen, ist_coder_alias
|
||||
|
||||
BILD_A = {"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}}
|
||||
BILD_B = {"type": "image_url", "image_url": {"url": "data:image/png;base64,BBBB"}}
|
||||
|
||||
|
||||
def verlauf() -> dict:
|
||||
"""Agenten-Ablauf: Bild A im ersten Schritt, danach Werkzeug-Aufruf, jetzt Bild B als Ergebnis."""
|
||||
return {"model": "coder", "messages": [
|
||||
{"role": "system", "content": "Du bist ein Coding-Agent."},
|
||||
{"role": "user", "content": [BILD_A, {"type": "text", "text": "Was zeigt der Fehler?"}]},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"id": "c1", "type": "function", "function": {"name": "screenshot", "arguments": "{}"}}]},
|
||||
{"role": "tool", "tool_call_id": "c1", "content": "Bildschirmfoto folgt."},
|
||||
{"role": "user", "content": [BILD_B, {"type": "text", "text": "Bildschirmfoto aus screenshot."}]},
|
||||
]}
|
||||
|
||||
|
||||
def test_frisch_ist_was_nach_der_letzten_antwort_kommt():
|
||||
frisch, alt = bilder_aufteilen(verlauf())
|
||||
assert [m["content"][i] for m, i in frisch] == [BILD_B]
|
||||
assert [m["content"][i] for m, i in alt] == [BILD_A]
|
||||
|
||||
|
||||
def test_ohne_antwort_sind_alle_bilder_frisch():
|
||||
frisch, alt = bilder_aufteilen({"messages": [{"role": "user", "content": [BILD_A, BILD_B]}]})
|
||||
assert len(frisch) == 2 and alt == []
|
||||
|
||||
|
||||
def test_coder_ziele_gehen_an_den_coder_zwilling_alles_andere_an_vision():
|
||||
assert ist_coder_alias("coder") and ist_coder_alias("heavy") and not ist_coder_alias("fast")
|
||||
assert bild_ziel("coder", "vision", "coder-bild") == "coder-bild"
|
||||
assert bild_ziel("heavy", "vision", "coder-bild") == "coder-bild"
|
||||
assert bild_ziel("fast", "vision", "coder-bild") == "vision"
|
||||
assert bild_ziel("coder", "vision", None) == "vision" # ohne Coder-Zwilling: Hirn-Zwilling
|
||||
|
||||
|
||||
class _FalscherClient:
|
||||
"""Ersetzt httpx.AsyncClient: zählt Beschreibungs-Anfragen und antwortet mit fester Beschreibung."""
|
||||
anfragen: ClassVar[list] = []
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def post(self, url, json):
|
||||
_FalscherClient.anfragen.append(json)
|
||||
|
||||
class Antwort:
|
||||
status_code = 200
|
||||
text = ""
|
||||
|
||||
@staticmethod
|
||||
def json():
|
||||
return {"choices": [{"message": {"content": "Fehlermeldung: PORT 4711 belegt"}}]}
|
||||
return Antwort()
|
||||
|
||||
|
||||
def test_aeltere_bilder_werden_einmal_beschrieben_und_gemerkt(monkeypatch):
|
||||
monkeypatch.setattr(gateway_proxy.httpx, "AsyncClient", _FalscherClient)
|
||||
monkeypatch.setattr(gateway_proxy, "_BESCHREIBUNGEN", gateway_proxy.OrderedDict())
|
||||
_FalscherClient.anfragen = []
|
||||
|
||||
body = verlauf()
|
||||
_, alt = bilder_aufteilen(body)
|
||||
assert asyncio.run(gateway_proxy._alte_bilder_beschreiben(alt, "vision")) is True
|
||||
ersetzt = body["messages"][1]["content"][0]
|
||||
assert ersetzt["type"] == "text" and "PORT 4711" in ersetzt["text"]
|
||||
assert body["messages"][4]["content"][0] == BILD_B # das frische Bild bleibt ein Bild
|
||||
anfrage = _FalscherClient.anfragen[0]
|
||||
assert anfrage["model"] == "vision" and anfrage["chat_template_kwargs"] == {"enable_thinking": False}
|
||||
|
||||
# Nächster Schritt schickt denselben Verlauf erneut: aus dem Gedächtnis, keine zweite Anfrage.
|
||||
nochmal = verlauf()
|
||||
_, alt = bilder_aufteilen(nochmal)
|
||||
assert asyncio.run(gateway_proxy._alte_bilder_beschreiben(alt, "vision")) is True
|
||||
assert len(_FalscherClient.anfragen) == 1
|
||||
|
||||
|
||||
def test_zu_viele_neue_bilder_bleiben_drin(monkeypatch):
|
||||
monkeypatch.setattr(gateway_proxy, "_BESCHREIBUNGEN", gateway_proxy.OrderedDict())
|
||||
monkeypatch.setattr(gateway_proxy, "_BILD_MAX", 1)
|
||||
body = {"messages": [{"role": "user", "content": [BILD_A, BILD_B]}, {"role": "assistant", "content": "ok"},
|
||||
{"role": "user", "content": "und jetzt?"}]}
|
||||
vorher = copy.deepcopy(body)
|
||||
_, alt = bilder_aufteilen(body)
|
||||
assert asyncio.run(gateway_proxy._alte_bilder_beschreiben(alt, "vision")) is False
|
||||
assert body == vorher
|
||||
@@ -634,3 +634,23 @@ def test_baseline_misst_neu_wenn_sich_die_proben_geaendert_haben(ps, tmp_path, m
|
||||
def test_kein_ablenker_klingt_nach_plattenplatz(ps):
|
||||
namen = {w["function"]["name"] for w in ps.WERKZEUGE_HIRN}
|
||||
assert "system_speicher" not in namen and "system_arbeitsspeicher" in namen
|
||||
|
||||
|
||||
def test_baseline_misst_bilder_ueber_den_zwilling(monkeypatch, ps):
|
||||
"""Seit 24.09. sieht das heutige Hirn über seinen Bild-Zwilling (vision) — die Baseline muss das wissen."""
|
||||
hirn = {"name": "Qwen3.6-35B-A3B", "aliases": ["hermes", "fast"], "gguf_path": "/m/hirn.gguf",
|
||||
"cmd": "llama-server -m /m/hirn.gguf --spec-type draft-dflash"}
|
||||
zwilling = {"name": "Qwen3.6-35B-A3B-Bild", "aliases": ["vision"], "gguf_path": "/m/hirn.gguf",
|
||||
"cmd": "llama-server -m /m/hirn.gguf --mmproj /m/mmproj.gguf"}
|
||||
fremd = {"name": "Qwen3-VL", "aliases": [], "gguf_path": "/m/vl.gguf", "cmd": "llama-server -m /m/vl.gguf --mmproj x"}
|
||||
monkeypatch.setattr(radar.llamaswap, "list_models", lambda: [hirn, zwilling, fremd])
|
||||
aufrufe = []
|
||||
monkeypatch.setattr(ps, "baseline", lambda rolle, pfad, **kw: aufrufe.append(kw) or {"werte": {}})
|
||||
radar._baseline("hirn", time.time() + 60)
|
||||
assert aufrufe[0]["bild"] is True and aufrufe[0]["bild_modell"] == "vision"
|
||||
|
||||
# Ohne Zwilling (Projektor auf anderen Gewichten zählt nicht): keine Bild-Probe.
|
||||
monkeypatch.setattr(radar.llamaswap, "list_models", lambda: [hirn, fremd])
|
||||
radar._baseline("hirn", time.time() + 60)
|
||||
assert aufrufe[1]["bild"] is False and aufrufe[1]["bild_modell"] is None
|
||||
assert aufrufe[0]["kennung"] != aufrufe[1]["kennung"] # neuer Zwilling → neue Baseline
|
||||
|
||||
Reference in New Issue
Block a user