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>
101 lines
4.2 KiB
Python
101 lines
4.2 KiB
Python
"""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
|