Der Schnitt aus CLAUDE.md ist durchgehalten: - Fetch (deterministisch, kein LLM): marktguru-Adapter mit geprüftem Ortsbezug (zipCode), Wochen-Cache, robots.txt-Respekt, ehrlicher Regel-4- Abbruch bei fehlendem Beleg statt Krücke. - Kategorisierung (einziger LLM-Ort): geschlossene Liste + Daten-Integrität als Code erzwungen; austauschbar via Protokoll (OpenRouter/Anthropic), mit Drosselung/Retry und ehrlichem Abbruch. - FastAPI-Web-UI als dünne Schicht: Modellauswahl (Liste/Suche/Refresh), Live-Fortschritt, gruppierte Ergebnisse mit Filtern, Ergebnis-Cache. - 36 Tests gegen die Architektur-Regeln (kein Auffüllen, Abbruch, Integrität, geschlossene Liste, Unsicherheit, Schnitt) und die Web-Schicht. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
136 lines
3.8 KiB
Python
136 lines
3.8 KiB
Python
"""Modell-Discovery + interaktive Auswahl -- offline, ohne Netz/Key."""
|
|
|
|
from angebote.modelle import lade_modelle, parse_modelle, suche, top_free
|
|
from angebote.modellauswahl import waehle_modell_interaktiv
|
|
|
|
FAKE = [
|
|
{
|
|
"id": "moonshotai/kimi-k2.6:free",
|
|
"name": "Kimi K2",
|
|
"context_length": 262144,
|
|
"pricing": {"prompt": "0", "completion": "0"},
|
|
"supported_parameters": ["tools", "response_format"],
|
|
},
|
|
{
|
|
"id": "meta-llama/llama-3.3-70b-instruct:free",
|
|
"name": "Llama 3.3 70B",
|
|
"context_length": 131072,
|
|
"pricing": {"prompt": "0", "completion": "0"},
|
|
"supported_parameters": ["tools"],
|
|
},
|
|
{
|
|
"id": "some/free-no-tools:free",
|
|
"name": "Free ohne Tools",
|
|
"context_length": 100000,
|
|
"pricing": {"prompt": "0", "completion": "0"},
|
|
"supported_parameters": [],
|
|
},
|
|
{
|
|
"id": "anthropic/claude-sonnet-4.6",
|
|
"name": "Claude Sonnet 4.6",
|
|
"context_length": 200000,
|
|
"pricing": {"prompt": "3", "completion": "15"},
|
|
"supported_parameters": ["tools"],
|
|
},
|
|
]
|
|
|
|
|
|
class _Resp:
|
|
def __init__(self, payload):
|
|
self._p = payload
|
|
|
|
def raise_for_status(self):
|
|
pass
|
|
|
|
def json(self):
|
|
return self._p
|
|
|
|
|
|
class _Session:
|
|
def __init__(self, data):
|
|
self._data = data
|
|
|
|
def get(self, url, timeout=None):
|
|
return _Resp({"data": self._data})
|
|
|
|
|
|
def _scripted(antworten):
|
|
it = iter(antworten)
|
|
return lambda prompt="": next(it)
|
|
|
|
|
|
# --- reine Daten-Logik -------------------------------------------------------
|
|
|
|
|
|
def test_parse_erkennt_frei_und_tools():
|
|
modelle = parse_modelle(FAKE)
|
|
nach_id = {m.id: m for m in modelle}
|
|
assert nach_id["moonshotai/kimi-k2.6:free"].frei is True
|
|
assert nach_id["moonshotai/kimi-k2.6:free"].tools is True
|
|
assert nach_id["anthropic/claude-sonnet-4.6"].frei is False
|
|
assert nach_id["some/free-no-tools:free"].tools is False
|
|
|
|
|
|
def test_top_free_nur_tools_und_rangfolge():
|
|
modelle = parse_modelle(FAKE)
|
|
top = top_free(modelle, 5, nur_tools=True)
|
|
ids = [m.id for m in top]
|
|
# paid (Sonnet) und das tool-lose Free-Modell sind raus:
|
|
assert "anthropic/claude-sonnet-4.6" not in ids
|
|
assert "some/free-no-tools:free" not in ids
|
|
# Präferenz: kimi-k2 vor llama-3.3-70b
|
|
assert ids == [
|
|
"moonshotai/kimi-k2.6:free",
|
|
"meta-llama/llama-3.3-70b-instruct:free",
|
|
]
|
|
|
|
|
|
def test_suche_findet_teilstring():
|
|
modelle = parse_modelle(FAKE)
|
|
treffer = suche(modelle, "llama")
|
|
assert [m.id for m in treffer] == ["meta-llama/llama-3.3-70b-instruct:free"]
|
|
|
|
|
|
def test_lade_modelle_ueber_session():
|
|
modelle = lade_modelle(session=_Session(FAKE))
|
|
assert len(modelle) == 4
|
|
|
|
|
|
# --- interaktiver Picker -----------------------------------------------------
|
|
|
|
|
|
def test_picker_waehlt_per_nummer():
|
|
gewaehlt = waehle_modell_interaktiv(
|
|
session=_Session(FAKE),
|
|
eingabe=_scripted(["1"]),
|
|
ausgabe=lambda s: None,
|
|
)
|
|
assert gewaehlt == "moonshotai/kimi-k2.6:free"
|
|
|
|
|
|
def test_picker_suche_dann_wahl():
|
|
gewaehlt = waehle_modell_interaktiv(
|
|
session=_Session(FAKE),
|
|
eingabe=_scripted(["s llama", "1"]),
|
|
ausgabe=lambda s: None,
|
|
)
|
|
assert gewaehlt == "meta-llama/llama-3.3-70b-instruct:free"
|
|
|
|
|
|
def test_picker_warnt_bei_modell_ohne_tools_und_bricht_ab():
|
|
# Suche bringt das tool-lose Modell in die Liste; Wahl -> Warnung -> 'n' -> q.
|
|
gewaehlt = waehle_modell_interaktiv(
|
|
session=_Session(FAKE),
|
|
eingabe=_scripted(["s no-tools", "1", "n", "q"]),
|
|
ausgabe=lambda s: None,
|
|
)
|
|
assert gewaehlt is None
|
|
|
|
|
|
def test_picker_quit_gibt_none():
|
|
gewaehlt = waehle_modell_interaktiv(
|
|
session=_Session(FAKE),
|
|
eingabe=_scripted(["q"]),
|
|
ausgabe=lambda s: None,
|
|
)
|
|
assert gewaehlt is None
|