291 lines
17 KiB
Markdown
291 lines
17 KiB
Markdown
# llmrouter — Betrieb (`llm.lan`, seit 2026-09-05)
|
||
|
||
Der schlanke Nachfolger des LiteLLM-Gateways: ein OpenAI-kompatibler
|
||
Reverse-Proxy in Bun/TypeScript. `llm.lan` zeigt auf ihn. Er kennt vier
|
||
Eingriffe je Anfrage: Client-Key prüfen, Modellnamen auflösen, Kosten aus der
|
||
Antwort mitlesen, eine Log-Zeile schreiben. Keine Datenbank, kein Admin-UI,
|
||
keine Format-Übersetzung — alle Upstreams sprechen OpenAI-Format.
|
||
|
||
> **Wofür wichtig:** Alle Agenten (Spark, Buzz, Klara, Jessie, Inka …),
|
||
> opencode auf dem Mac und Claude Code im Terminal sprechen nur diesen
|
||
> Endpunkt. Fällt er aus, steht die Agenten-Arbeit. Provider-Keys (OpenRouter,
|
||
> Mistral, deizo) liegen nur hier.
|
||
>
|
||
> Diese Seite ist der Betriebsstand für Störung, Wartung und Nachfolge; die
|
||
> technische Referenz steht in [referenz.md](referenz.md).
|
||
|
||
## Wo läuft was
|
||
|
||
| Was | Wo |
|
||
|---|---|
|
||
| Router (`llmrouter`) | LXC **185020** (`llmlite`, `10.18.5.20`) auf `s18p1`, systemd-Unit `llmrouter`, Port **4010** |
|
||
| Alter Port **4000** | seit 2026-09-07 von Caddy auf 4010 geleitet (vorher LiteLLM). Hart verdrahtete Clients — Agenten-Images, spark-desktop, ältere Skripte — laufen darüber weiter |
|
||
| LiteLLM | **abgeschaltet am 2026-09-07** (`systemctl stop/disable llmlite`). Dateien und Container liegen unangetastet in `/srv/llmlite`, letzte Kosten-Datenbank als `spend-final-20260907.sql.gz` |
|
||
| Caddy (`llm.lan`, TLS über `acme.lan`) | derselbe LXC, **eigener oci-Dienst `caddy`** (`/srv/caddy`, systemd-Unit `caddy.service`), `reverse_proxy localhost:4010` |
|
||
| Dateien | `/srv/llmrouter/`: Binaries `llmrouter`, `llmrouter-resolve`, `llmrouter-stats`, `llmrouter-keys`; `config.json`, `key-acls.json`, `model-blocklist.json`; `keys.json` (Token → Name, 0600); `.env` (0600); `log/` |
|
||
| Docker-Image (Demo/Basis) | `git.42i.org/spine/llmrouter:latest`, gebaut von CI; `docker compose up` mit `./data`-Mount als Demo |
|
||
| Lokales Qwen | LLM-Knoten LXC 189080, `10.18.5.30:11507` |
|
||
| Sprachdienste lokal | `10.18.5.30:8803` (Qwen3-TTS + whisper.cpp), Ollama `:11434` (Embeddings) |
|
||
| Kira im Prod-RZ (deizo) | `10.6.42.111:8000/8001/8002` über den WireGuard-Peer des LXC |
|
||
|
||
Normalzustand: `curl http://llm.lan:4010/health` liefert `{"ok":true,"zuordnung":"<Zeitstempel>"}`;
|
||
der Zeitstempel ist jünger als 12 Stunden.
|
||
|
||
## Namensräume — was ein Client rufen kann
|
||
|
||
Die vollständige Tabelle der Namensräume (Stufen, Profile, Rollen, `alt/`,
|
||
Claude-Code-Pfad) steht in [referenz.md](referenz.md#namensräume). Kurzfassung:
|
||
|
||
| Name | Bedeutung |
|
||
|---|---|
|
||
| `role/<name>` | Rolle (`xo`, `architect`, `teamleader`, `worker`, `qa` …) — **Agenten kennen nur Rollen** |
|
||
| `auto/<profil>[:stufe]` | Profile auf drei Achsen: `mass`, `mass:fast`, `allround`, `coding`, `leader` je `:junior`/`:senior` |
|
||
| `42i/marvin-2609:low|medium|high|max` | konfigurierte Stufen (Index ≥ 25/45/49/53) |
|
||
| `alt/<id>[:effort]` | günstigste Alternative derselben Klasse; aufgelöst bei der ersten Anfrage, dann alle 12 h |
|
||
| `openrouter/<id>[:effort]` | 1:1 an OpenRouter |
|
||
| `deizo/kira-*`, `42i/marvin-*` | feste Ziele (Prod-RZ, lokal, Cloud-Sprachdienste) |
|
||
| alles andere | **404** — kein stiller Fallback mehr seit der LiteLLM-Abschaltung |
|
||
|
||
Ein Modellwechsel steht in `log/changes.log` (im LXC:
|
||
`/srv/llmrouter/log/changes.log`).
|
||
|
||
## Rollen und Profile — drei Ebenen
|
||
|
||
Ein Agent nennt **nie** ein Modell. Er fragt seine Rolle, die Rolle zeigt auf
|
||
ein Profil, und das Profil sucht sich das billigste Modell über den
|
||
geforderten Untergrenzen. Umgestellt wird auf jeder Ebene in `config.json` —
|
||
ohne Deploy, ohne Container anzufassen (42i/intern#1252).
|
||
|
||
| Profil | Regel | trifft am 2026-09-07 | $/1M gew. |
|
||
|---|---|---|---|
|
||
| `auto/mass` | fest: lokales Qwen, Ausweich auf `mass:fast` | qwen36:35b | 0 |
|
||
| `auto/mass:fast` | Index ≥ 25 | ling-3.0-flash | 0,27 |
|
||
| `auto/allround:junior` | Index ≥ 40 | glm-5.3-flash | 0,95 |
|
||
| `auto/allround:senior` | Index ≥ 49 | grok-4.6:high | 26,00 |
|
||
| `auto/coding:junior` | Coding ≥ 68, Agentic ≥ 45 | glm-5.3-flash | 0,95 |
|
||
| `auto/coding:senior` | Coding ≥ 76, Agentic ≥ 50 | grok-4.6:high | 26,00 |
|
||
| `auto/leader:junior` | Index ≥ 45, Agentic ≥ 50 | glm-5.3-flash | 0,95 |
|
||
| `auto/leader:senior` | Coding ≥ 74, Agentic ≥ 53 | glm-5.3:max | 15,66 |
|
||
|
||
Warum drei Achsen und nicht eine Zahl: `gemini-3.8-flash` hat Coding 76,3 und
|
||
Agentic **41,2** — es schreibt guten Text und trägt den Werkzeug-Rundlauf nicht.
|
||
Ein einzelner Index sieht diesen Unterschied strukturell nicht.
|
||
|
||
**Rollenbelegung:** `xo`, `teamleader`, `lead` → `auto/leader:junior`;
|
||
`worker`, `engineer`, `qa`, `perf` → `auto/coding:junior`; alle übrigen →
|
||
`auto/allround:junior`; `inka` fest auf `openrouter/openai/gpt-5.6-luna`.
|
||
**Keine Rolle steht auf einer Senior-Stufe** (Andreas, 2026-09-07) — hochgestuft
|
||
wird einzeln und mit Anlass, nicht vorsorglich.
|
||
|
||
**Der Ausweich** gilt für Ziele ohne eigenen Rückfall — festes Ziel wie
|
||
`auto/mass` (lokales Qwen) oder Schwelle auf einem Ausfall-Modell wie
|
||
`auto/mass:fast`. Steht der Knoten, hält eine fremde Arbeit die Karte oder
|
||
liefert der Upstream 429/5xx, weicht der Router **einmal** auf den
|
||
konfigurierten Ausweich aus; die Log-Zeile trägt dann
|
||
`auto/mass->auto/mass:fast`, der Wechsel ist also sichtbar. Die Kette:
|
||
`auto/mass` → `auto/mass:fast` (ling) → `auto/coding:junior`
|
||
(glm-5.3-flash) — ling erfüllt die coding:junior-Schwellen strukturell nie,
|
||
der Rückfall landet also garantiert auf einem tauglichen Modell. Ohne das
|
||
hinge jede Titelzeile jedes Agenten, weil `auto/mass` überall als
|
||
`small_model` hängt. Wichtig: Die Ausweich-Auflösung läuft durch die
|
||
ACL-Prüfung des aufrufenden Keys — die Container-Personas brauchen die
|
||
Kettenziele deshalb in `key-acls.json`, sonst schlägt der Fallback still
|
||
fehl (403) und der ursprüngliche Fehler geht durch.
|
||
|
||
## Wer läuft worauf
|
||
|
||
| Wer | Rolle | Ziel heute |
|
||
|---|---|---|
|
||
| agent-spark | `role/engineer` | glm-5.3-flash |
|
||
| agent-buzz | `role/ops` | glm-5.3-flash |
|
||
| agent-jessie | `role/assistant` | glm-5.3-flash |
|
||
| agent-klara | `role/service` | glm-5.3-flash |
|
||
| Leiter auf spark-desktop (xo, xo-chat, teamleiter, worker, kanon, thema, diagnose, logbuch) | `role/xo`, `role/teamleader`, `role/worker`, `role/architect`, `role/archivist`, `role/ops` | glm-5.3-flash |
|
||
| Inka — Hirn | `role/inka` | **fest** `openai/gpt-5.6-luna` |
|
||
| Inka — STT / TTS | `42i/marvin-stt-turbo` / `-tts-turbo` | Voxtral Small / Grok Voice |
|
||
| latenztolerante Masse, `small_model` | `auto/mass` | lokales Qwen, 0 $ |
|
||
|
||
`role/inka` ist bewusst ein fester Modellname statt eines Profils: Die drei
|
||
Achsen messen **keine Antwortlatenz**, und am Telefon entscheidet genau die.
|
||
Gemessen am 2026-09-07 mit Inkas Zuschnitt (17–19k Token Systemprompt, vier
|
||
Züge): Luna 0,9–1,9 s bis zum ersten Wort, `glm-5.3-flash` 1,9–5,6 s. Der Cache
|
||
greift bei beiden; die Zeit geht bei GLM für 70–133 Denk-Token drauf, und
|
||
Reasoning lässt sich dort nicht abschalten (`"Reasoning is mandatory for this
|
||
endpoint"`). Nachmessen: `tools/telefon-latenz.py` in diesem Repo.
|
||
|
||
## Keys und Rechte
|
||
|
||
- Client-Token: `keys.json` (Token → Name; im LXC
|
||
`/srv/llmrouter/keys.json`, in der Docker-Demo `./data/keys.json`). Die
|
||
Werte der Personas liegen in OpenBao `agents/<name>/litellm` Feld `key`; es
|
||
sind **dieselben Token wie bei LiteLLM**. Noch ohne Token im Router:
|
||
`inka-mx`, `nightjob`, `lab-xo`, `team-copilot` (LiteLLM kennt nur Hashes).
|
||
Anlegen:
|
||
`KEYS_FILE=keys.json bun run src/keys.ts add <name> [--token <bestehend>]`.
|
||
- Rechte: `key-acls.json` (Name → erlaubte Namen, `*` als Präfix-Muster),
|
||
versioniert in diesem Repo. Elton und Andreas dürfen `openrouter/*` und
|
||
`alt/*`, die Agenten nur ihre Rollen und `:medium`.
|
||
- Upstream-Keys in `.env` (LXC: `/srv/llmrouter/.env`): `OPENROUTER_API_KEY`,
|
||
`MISTRAL_API_KEY`, `KIRA_REASONING_KEY`, `KIRA_STT_KEY`, `KIRA_TTS_KEY`,
|
||
`ARTIFICIALANALYSIS_API_KEY`, `ADMIN_KEY`. Quellen in OpenBao:
|
||
`agents/jessie/litellm` (OpenRouter, AA, Kira) und `agents/jessie/llmrouter`
|
||
(`admin_key`).
|
||
- OpenRouter-Konto: **ZDR-Guardrail bleibt bewusst** (Andreas 05.09.2026).
|
||
Alibaba- und Meta-gehostete Modelle sind dadurch nicht wählbar; der Router
|
||
baut nur auf der Kontoliste auf und kann nichts wählen, was dagegen verstößt.
|
||
|
||
## Log und Kosten
|
||
|
||
Je Anfrage eine Zeile: Key, angefragter Name, aufgelöster Alias, tatsächliches
|
||
Modell, Effort, Tokens, **Kosten wie von OpenRouter gemeldet**. Lokale Ziele
|
||
haben keine Kosten.
|
||
|
||
```bash
|
||
# Tagesstatistik je Key
|
||
curl -s "http://llm.lan:4010/stats?days=7" \
|
||
-H "Authorization: Bearer $(~/.agents/bin/secret get jessie llmrouter admin_key)" | python3 -m json.tool
|
||
# aktuelle Zuordnung aller Stufen und alt/-Referenzen
|
||
curl -s http://llm.lan:4010/zuordnung -H "Authorization: Bearer <ADMIN_KEY>" | python3 -m json.tool
|
||
# einzelne Anfragen / Modellwechsel / Journal
|
||
ssh s18p1.lan 'pct exec 185020 -- tail -20 /srv/llmrouter/log/requests.jsonl'
|
||
ssh s18p1.lan 'pct exec 185020 -- cat /srv/llmrouter/log/changes.log'
|
||
ssh s18p1.lan 'pct exec 185020 -- journalctl -u llmrouter -n 50 --no-pager -o cat'
|
||
```
|
||
|
||
Die Antwort trägt `x-llmrouter-model`, damit ein Client sieht, was wirklich
|
||
lief. Weitere Log-Wege (SQLite, Stats-Binary): [referenz.md](referenz.md).
|
||
|
||
## Wartung und Deploy
|
||
|
||
Quelle dieses Projekts ist dieses Repo (`spine/llmrouter`). Der Prod-Einsatz
|
||
läuft weiter als statisches Binary im LXC (Stand 2026-09-08); der Umstieg aufs
|
||
Image ist **geprüft und machbar**, das Rezept steht unten — vollzogen ist er
|
||
noch nicht.
|
||
|
||
1. Änderung in diesem Repo (Config, Code), committen, pushen. CI baut und
|
||
published danach `git.42i.org/spine/llmrouter:latest` — für den
|
||
Binary-Weg irrelevant, aber der Stand, den jede Demo zieht.
|
||
2. Binaries bauen: `bun run build` (dazu
|
||
`bun build --compile --target=bun-linux-x64 src/resolver.ts --outfile dist/llmrouter-resolve`).
|
||
3. Ausrollen: `scp` nach `s18p1`, dann `pct push 185020 <datei> /srv/llmrouter/<datei>.neu`,
|
||
im LXC `mv`, `chmod 755`, `systemctl restart llmrouter`.
|
||
4. Nach Config-Änderungen an Stufen oder `alt/`: `rm /srv/llmrouter/log/state.json`
|
||
vor dem Neustart, sonst bleibt die alte Zuordnung. Der Erstlauf fährt für jede
|
||
Stufe die Last-Probe (≈210k Token je Stufe).
|
||
5. **Caddyfile-Falle:** die Datei ist als Bind-Mount im Container; ein `mv`
|
||
tauscht den Inode und `caddy reload` sieht die alte Datei. Nach Änderung
|
||
`oci restart caddy`. Quelle: [caddy/Caddyfile](caddy/Caddyfile).
|
||
|
||
### Umstieg aufs Image (geprüft am 2026-09-08, ausstehend)
|
||
|
||
Voraussetzungen alle erfüllt: podman 5.4.2 im LXC (kein Docker nötig —
|
||
Muster wie `caddy.service`: podman-compose + systemd-oneshot), das Image ist
|
||
öffentlich pullbar (anonym, wie `42i/caddy`), Multi-Arch enthält amd64.
|
||
Produktions-Layout ist **flach** (`/srv/llmrouter/*`, kein `data/`-Unterverzeichnis)
|
||
— der ganze Ordner wird als `/data` gemountet:
|
||
|
||
```yaml
|
||
services:
|
||
llmrouter:
|
||
image: git.42i.org/spine/llmrouter:latest
|
||
env_file: /srv/llmrouter/.env
|
||
environment:
|
||
# .env trägt die Host-Pfade — im Container auf /data übersteuern:
|
||
LLMROUTER_CONFIG: /data/config.json
|
||
LLMROUTER_ACLS: /data/key-acls.json
|
||
KEYS_FILE: /data/keys.json
|
||
MODEL_BLOCKLIST: /data/model-blocklist.json
|
||
ports: ["127.0.0.1:4010:4010"] # Caddy zeigt weiter auf localhost:4010
|
||
volumes: ["/srv/llmrouter:/data"]
|
||
restart: unless-stopped
|
||
```
|
||
|
||
- **`log/` im selben Mount lassen.** `state.json` (Zuordnung) lebt im
|
||
logDir; ein isoliertes Log-Verzeichnis startet ohne Zuordnung — bis das
|
||
Auffrischen durch ist (~40 s bei `LLMROUTER_LASTTEST=0`, sonst ≈210k Token)
|
||
antworten Stufen mit 503 („noch nicht aufgelöst").
|
||
- Sidecar-Binaries (`llmrouter-resolve`, `-stats`, `-keys`) bleiben auf dem
|
||
LXC; das Image enthält nur den Server.
|
||
- Ablauf: compose-Datei + Unit anlegen → `podman-compose up -d` → `/health`
|
||
prüfen (Zeitstempel aktuell, nicht 1970) → `systemctl stop/disable
|
||
llmrouter`. Rollback: Unit wieder aktivieren, Container stoppen.
|
||
- Updates danach: `podman pull` + Container neu starten; CI published
|
||
`:latest` (und `:sha-<sha>`) bei jedem Push auf main.
|
||
|
||
Prüfstand 2026-09-08 (Probelauf auf dem LXC, Produktion unberührt, Port 4011,
|
||
Log-Dir nach /tmp ausgelagert): Image startet mit der Produktions-Config (25
|
||
Aliasse, 21 Rollen, 12 Token), `/models` **byte-identisch** zur Binary, Chat
|
||
via `/chat/completions` 200 über beide Wege (`42i/marvin-2609:medium` →
|
||
`z-ai/glm-5.3-flash`), komplette Zuordnungs-Auffrischung im Container
|
||
funktioniert. Container und Image danach rückstandsfrei entfernt.
|
||
|
||
## Störung
|
||
|
||
| Symptom | Prüfen | Tun |
|
||
|---|---|---|
|
||
| Agenten bekommen 502/Timeout | `curl http://llm.lan:4010/health`; `systemctl status llmrouter` im LXC | `systemctl restart llmrouter`; Journal lesen |
|
||
| `… ist noch nicht aufgelöst (503)` | `/zuordnung` leer oder alt | `POST /auffrischen` mit Admin-Key, oder `rm log/state.json` + Neustart; braucht OpenRouter- und AA-Zugang |
|
||
| 401 für einen Agenten | Journal zeigt `401 … (Key n Zeichen)` | Token fehlt in `keys.json` → mit `keys.ts add --token` nachtragen |
|
||
| 403 `darf … nicht rufen` | `key-acls.json` | Recht ergänzen (versioniert in diesem Repo), Neustart |
|
||
| Zahlen im TTS falsch gesprochen | Sprechtext-Regel greift nur für PLZ/Nummern | Fish über OpenRouter zerlegt deutsche Beträge/Daten — deshalb Grok Voice bis Fish bei Uwe läuft (live/live#2247) |
|
||
| `llm.lan` antwortet mit LiteLLM-Fehlern statt Router | Caddy zeigt auf 4000 | Caddyfile prüfen, `oci restart caddy` |
|
||
|
||
**Rückfall ohne Router:** Caddyfile auf `localhost:4000` stellen und Caddy neu
|
||
starten — LiteLLM bedient die `2608`-Namen weiter, die `2609`-, `alt/`- und
|
||
`role/`-Namen fehlen dann.
|
||
|
||
## Zugehörige Werkzeuge
|
||
|
||
- `~/src/tools/openrouter-models.ts` (git.home `andreas/tools`): zeigt die
|
||
Stufentabelle oder die Alternative zu einer Modell-ID (`bun run ./openrouter-models.ts anthropic/claude-opus-5:medium`).
|
||
- `~/.agents/bin/opencode-normalize`: erzeugt den `llmlan`-Provider in der
|
||
opencode-Config aus `/v1/models` des Routers, Effort-Stufen als `variants`.
|
||
- `claude-lan` (Funktion in `~/.zshrc`): Claude Code im Terminal über den
|
||
Router; die Desktop-App kann das nicht (sie erzwingt `api.anthropic.com`).
|
||
- Auswahl-Regel: [src/openrouter-auswahl.ts](../src/openrouter-auswahl.ts) —
|
||
seit 2026-09-08 eine eigenständige Kopie (vendored aus `42i/agents`
|
||
`llmlite/`, wo die llmlite-Fassung für den Tier-Updater weiterlebt).
|
||
|
||
## Verlauf
|
||
|
||
- 2026-09-08: **Umstieg aufs Image geprüft.** Probelauf des CI-Images auf
|
||
dem LXC neben dem Binary (Port 4011): Konfig-/ACL-/Keys-Parität
|
||
(`/models` byte-identisch), Chat 200 über beide Wege, Zuordnungs-Refresh
|
||
läuft im Container. Rezept und Fallstricke stehen jetzt in
|
||
[Wartung und Deploy](#wartung-und-deploy); Prod läuft weiter auf der
|
||
Binary. Dazu Registry-Login-Fix in der CI (vertauschte Secret-Werte,
|
||
401er am Vormittag) und Login-Kommentar korrigiert.
|
||
- 2026-09-08: **Umzug nach `spine/llmrouter`** (git.42i.org). Das komplette
|
||
Projekt verließ das Repo `42i/agents` (Historie per subtree split bewahrt);
|
||
dazu Docker-Image, Compose-Demo mit `./data`-Mount und Gitea-Actions-CI
|
||
nach dem live-Muster. Die Referenz-Doku liegt im Repo (`README.md`,
|
||
`docs/`), die Betriebsstand-Seite in `info` bleibt als Zeiger.
|
||
- 2026-09-08: **Ausweich-Kette für die Massenarbeit** (42i/agents 4e92f17,
|
||
42i/intern#1252). Anlass: ling-3.0-flash fiel am Vorabend aus (23:20–23:52,
|
||
5× 502 nach ~270 s hängendem Upstream, 117× 429). `auto/mass:fast` hat jetzt
|
||
selbst einen Ausweich auf `auto/coding:junior`, und der Ausweich greift auch
|
||
bei 429/5xx-Antworten — vorher nur bei fetch-Fehlern und nur für fest-Ziele.
|
||
ACLs der Container-Personas um die Kettenziele ergänzt.
|
||
- 2026-09-08: **Live-Probe reasoning-tolerant** (42i/agents 312e43c). ling
|
||
denkt vor jedem Werkzeugaufruf mit variabler Länge; bei `max_tokens: 400`
|
||
scheiterte die Probe willkürlich und kippte `marvin-2609:low` und
|
||
`alt/claude-haiku-4.5` auf teurere Modelle (solar-pro4 bzw. glm-5.3-flash),
|
||
ohne dass ling defekt war. Budget auf 2000 Token, Timeout auf 90 s.
|
||
- 2026-09-07: **LiteLLM abgeschaltet, alles läuft über Rollen.** Alle Agenten
|
||
(Container spark/buzz/jessie/klara, die Leiter auf spark-desktop, der XO) rufen
|
||
`role/*` statt eines Modellnamens; der XO und das Logbuch kamen dabei vom
|
||
Claude-Max-Abo auf opencode. Der `fallback` an LiteLLM ist aus der Config
|
||
entfernt — unbekannte Namen sind jetzt 404 statt einer stillen Weiterleitung.
|
||
Keine Rolle steht mehr auf der teuren Stufe (42i/intern#1252).
|
||
- 2026-09-07: **Caddy aus dem LiteLLM-Stack gelöst** und eigener oci-Dienst
|
||
(`/srv/caddy`, Quelle `llmrouter/caddy/`, ausgerollt über
|
||
`agents-pull`). Vorher hing er in `llmlite/compose.yml` — ein `podman-compose
|
||
down` für LiteLLM hätte die TLS-Terminierung von `llm.lan` mitgenommen und den
|
||
Router von außen unerreichbar gemacht, obwohl er läuft. Vorarbeit zur
|
||
Abschaltung von LiteLLM (42i/intern#1252).
|
||
- 2026-09-05: Router gebaut und in Betrieb, `llm.lan` umgestellt; Stufen
|
||
25/45/49/53; Namensräume `openrouter/`, `alt/`, `role/`, `deizo/`; Claude-Code-Pfad.
|
||
- 2026-09-06: Sprechtext-Normalisierung (Postleitzahl); TTS-Turbo auf Grok
|
||
Voice `ara`, STT-Turbo auf Voxtral Small 24B nach Hör- und Telefontest;
|
||
Auftrag an Uwe für Fish S2 Pro und Voxtral Small im Prod-RZ (live/live#2247).
|