diff --git a/docs/ionos-llm-api.md b/docs/ionos-llm-api.md
index cf8378d..1a5f7e4 100644
--- a/docs/ionos-llm-api.md
+++ b/docs/ionos-llm-api.md
@@ -1,7 +1,7 @@
# Ionos LLM API: Quirks und Workarounds
-**Stand:** 2026-05-03 — empirisch ermittelt gegen
-`https://openai.inference.de-txl.ionos.com/v1/chat/completions`.
+**Stand:** 2026-05-03 (zweite Session, in Doctate umgesetzt) — empirisch
+ermittelt gegen `https://openai.inference.de-txl.ionos.com/v1/chat/completions`.
Verhalten kann sich aendern; Datum oben mit dem aktuellen Test-Befund
abgleichen, bevor man auf diese Notizen baut.
@@ -15,15 +15,22 @@ abgleichen, bevor man auf diese Notizen baut.
`https://api.ionos.com/docs/inference-openai/v1/`. Das ist eine
Redoc-Seite mit eingebetteter OpenAPI-3.0.3-Spec — die einzige
verlaessliche Quelle fuer unterstuetzte Parameter, Defaults und Limits.
-- Llama 3.1 405B FP8 hat einen **vLLM-Tokenizer-Bug**: das
- End-of-Turn-Token `<|eot_id|>` wird als Klartext-String
- `assistant\n\n` ausgegeben statt als Special-Token. Folge: der
- offizielle `stop`-Parameter mit `<|eot_id|>` feuert nie, das Modell
- laeuft bis `max_tokens` voll.
-- **Empfohlene Loesung fuer Doctate**: `response_format: json_schema`.
- Schema-erzwungenes Decoding stoppt natuerlich am schliessenden `}`,
- funktioniert modell-agnostisch und braucht keine Provider-spezifischen
- Workarounds.
+- Llama 3.1 405B FP8 hat ein bekanntes **End-of-Turn-Termination-Problem**:
+ in der Sandbox kam `<|eot_id|>` als ASCII-`assistant\n\n` zurueck, was
+ Quelle der ersten Diagnose war. Aber das eigentliche Problem ist breiter
+ und modell-inhaerent: Llama 3.1 verfaellt mit Greedy-Decoding und/oder
+ konflikthaltigen Prompts in Endlos-Schleifen (bestaetigt von Ionos selbst,
+ HuggingFace-Discussion #32, vLLM-Issues #13530, #13828).
+- **Empfohlene Loesung fuer Doctate (Drei-Komponenten-Kombi):**
+ `temperature: 0.6 + top_p: 0.9` (Sampling) + `response_format:
+ json_schema` (Form-Garantie) + expliziter Format-Hinweis als zweite
+ `system`-Message (sagt dem Modell WAS in `document` zu schreiben ist).
+ Alle drei sind notwendig; jede einzelne weggelassen kippt in einen
+ anderen Failure-Mode (Timeout, Endlos-Schleife, oder silent
+ `{"document": ""}`).
+- **Wichtig**: gpt-oss-120b braucht weder Sampling noch Format-Hinweis —
+ es ist schema-aware trainiert und laeuft mit dem Default-Body sauber.
+ Die Drei-Komponenten-Loesung ist Llama-spezifisch, nicht universell.
## So kommt man an die richtige API-Info
@@ -176,6 +183,8 @@ Gateway-Timeout.
### Empirische Datentabelle
+#### Session 1 (Diagnose, Sandbox-Inputs)
+
Alle Tests mit `meta-llama/Meta-Llama-3.1-405B-Instruct-FP8`,
Input `"Sag hallo."` (ausser T22 = realer Doctate-Body, ~1.5k Prompt-Tokens):
@@ -193,8 +202,41 @@ Input `"Sag hallo."` (ausser T22 = realer Doctate-Body, ~1.5k Prompt-Tokens):
| T22 | T21 + realer Doctate-Body (1539 Prompt-Tokens) | **22.77 s, finish=stop, 482 Tokens** | 22.7 |
Konsistenz: **`finish_reason: "length"` heisst Modell wollte
-weiter** — kein sauberer Stop. Nur die letzten vier Loesungswege
-bekommen sauberes `"stop"`.
+weiter** — kein sauberer Stop.
+
+#### Session 2 (Reproduktion gegen Production-Pipeline, 30 s Timeout)
+
+Alle Tests gegen den realen Doctate-Body von Case `aecf5890` (236
+char user_content, ~1040-1115 Prompt-Tokens). Skripte unter
+`/tmp/doctate-llama-test/run{1..6}.py`. Production-`LLAMA_SYSTEM_PROMPT`
+verbatim (2819 chars).
+
+| Test | Sampling | json_schema | Format-Hinweis | Wallclock | finish | Output |
+|---|---|---|---|---|---|---|
+| T1 | nein (`temp=0.5`) | ja | nein | **30 s Timeout** | – | nichts geliefert |
+| T2 | nein (`temp=0.5`) | ja | im Prompt inline | 3.26 s | stop | sauber, 61 Tokens |
+| T3 | ja (`0.6 + 0.9 + freq=0.1 + stop`) | nein (Ionos-Doku verbatim) | nein | **30 s Timeout** | – | nichts geliefert |
+| T4 | ja (`0.6 + 0.9`) | ja | nein | 0.98 s | stop | **`{"document":""}` (silent deletion)** |
+| T5 | ja (`0.6 + 0.9`) | ja | im Prompt inline | 3× ⌀ 3.05 s | stop | sauber, 61 Tokens, 3× stabil |
+| T6 | ja (`0.6 + 0.9`) | ja | als zweite `system`-Message | 3× ⌀ 3.13 s | stop | sauber, 61 Tokens, 3× stabil — **Production-Konfig** |
+
+**Wichtige Befunde aus Session 2:**
+
+1. **json_schema allein reicht nicht (T1)**: Production-Pfad mit
+ `temperature: 0.5` ohne `top_p` timeoutet, weil Llama Schema-Resampling
+ als Endlos-Schleife verkraftet.
+2. **Sampling allein reicht nicht (T3)**: Selbst die exakte Ionos-Doku-
+ Empfehlung mit `stop: ["<|eot_id|>", "<|end_of_text|>"]` timeoutet
+ gegen unseren 2.8k-Char-Prompt. Die Doku-Empfehlung ist fuer kuerzere
+ Prompts validiert.
+3. **Sampling + Schema ohne Hinweis kippt in silent deletion (T4)**:
+ Modell respektiert die Form, weiss aber inhaltlich nicht was tun.
+ `{"document": ""}` waere fuer Doctate als leere Notiz im UI gelandet
+ — gefaehrlicher als Timeout, weil der Worker `remove_failure_marker`
+ aufruft. **Worker.rs hat seither einen Empty-Guard.**
+4. **Drei-Komponenten-Kombi laeuft 3x stabil (T5, T6)**: Sampling +
+ Schema + Format-Hinweis. T6 (separate `system`-Message) ist
+ architektonisch sauberer und gewaehlt fuer die Doctate-Implementation.
## Loesungswege im Vergleich
@@ -202,33 +244,60 @@ bekommen sauberes `"stop"`.
|---|---|---|---|---|
| `stop: ["assistant"]` | sehr schnell | nein | nein | Patch fuer Provider-Bug; bricht im englischen Text |
| Eigener End-Marker via System-Prompt | sehr schnell | mittel | ja | Modell kann Marker bei langem Output vergessen |
-| `response_format: json_schema` | schnell | ja | **ja** | Server erzwingt Stop ueber Grammar-Decoding |
-| Modellwechsel auf `gpt-oss-120b` / `Llama-3.3-70B` | sehr schnell | ja | – | Quirks A+B betreffen diese Modelle nicht |
+| `response_format: json_schema` allein | – | nein | nein | **In Production NICHT ausreichend** (T1, T4) — kippt entweder in Timeout oder silent deletion |
+| **Drei-Komponenten-Kombi** (Sampling + Schema + Format-Hinweis) | schnell (3 s) | ja | – | **In Doctate umgesetzt fuer Llama** (T5, T6) |
+| Modellwechsel auf `gpt-oss-120b` / `Llama-3.3-70B` | sehr schnell | ja | – | Schema-aware, brauchen die Drei-Komponenten-Kombi nicht |
-Empfehlung: **`response_format: json_schema`** als Default-Pfad fuer
-Doctate, weil derselbe Code-Pfad auf gpt-oss-120b und Llama 3.3 70B
-unveraendert weiter funktioniert. Wenn das Schema-Decoding bei
-zukuenftigen, viel groesseren Outputs Latenzprobleme macht, wechselt
-man auf einen schnelleren Modell-Endpoint, nicht zurueck auf
-String-Hacks.
+**Empfehlung pro Modell-Familie:**
+- Schema-aware Modelle (gpt-oss-120b, vermutlich Llama 3.3 70B):
+ `temperature: 0.5 + response_format: json_schema`. Body bleibt klein
+ und stabil.
+- Llama 3.1 405B FP8: nur als **Drei-Komponenten-Kombi** brauchbar —
+ `temperature: 0.6 + top_p: 0.9 + response_format: json_schema +
+ expliziter Format-Hinweis als zweite system-Message`. Wer eine
+ Komponente weglaesst, faengt sich Timeout (T1, T3) oder silent
+ deletion (T4) ein.
+
+## Doctate-Implementation (`server/src/analyze/backend.rs`)
+
+Die Drei-Komponenten-Kombi lebt als Per-Backend-Konfiguration:
+
+```rust
+const LLAMA_TEMPERATURE: f32 = 0.6;
+const LLAMA_TOP_P: f32 = 0.9;
+const LLAMA_FORMAT_INSTRUCTION: &str = "AUSGABEFORMAT: ...";
+
+ionos_backend("llama_3_1_405b", "Llama 3.1 405B", "...405B-Instruct-FP8")
+ .with_system_prompt(LLAMA_SYSTEM_PROMPT)
+ .with_temperature(LLAMA_TEMPERATURE)
+ .with_top_p(LLAMA_TOP_P)
+ .with_format_instruction(LLAMA_FORMAT_INSTRUCTION),
+```
+
+`chat_once` injiziert `format_instruction` als zweite `system`-Message
+zwischen `system_prompt` und `user`-Content, falls `Some`. Backends
+ohne `format_instruction` (gpt-oss) erzeugen den exakten Body wie vor
+dem Llama-Fix — byte-stabil, gepinnt durch
+`gpt_oss_has_no_format_instruction_and_no_top_p`-Test.
+
+`worker.rs` hat einen **Empty-Guard** nach `vocab.replace`: leere
+`document`-Strings (Failure-Mode T4) loesen einen Failure-Marker statt
+einer leeren `document.md` aus.
## Entscheidungsbaum
```mermaid
graph TD
- A[Neuer Aufruf gegen Ionos] --> B{Brauche ich strukturierten Output?}
- B -->|Ja oder unsicher| C[response_format: json_schema setzen]
- B -->|Nein, freier Text| D{Modell?}
- D -->|gpt-oss-120b oder Llama 3.3 70B| E[Standard-Aufruf reicht]
- D -->|Llama 3.1 405B FP8| F[max_tokens MUSS gesetzt sein]
- F --> G{Wie soll gestoppt werden?}
- G -->|Sauber via Schema| C
- G -->|String-Hack ok| H["stop: array enthaelt 'assistant'"]
- G -->|Eigener Marker| I[System-Prompt instruiert + stop matcht]
- C --> J[Output ist JSON, server-seitig parsen]
- E --> K[Output direkt nutzen]
- H --> K
- I --> K
+ A[Neuer Backend-Profil-Eintrag in backend.rs] --> B{Modell?}
+ B -->|gpt-oss-120b
schema-aware| C[Default: temp 0.5,
use_json_schema=true,
kein top_p, keine format_instruction]
+ B -->|Llama 3.1 405B FP8| D[Drei-Komponenten-Kombi:
temp 0.6 + top_p 0.9 + schema
+ format_instruction]
+ B -->|Llama 3.3 70B
noch nicht getestet| E[Erst gegen Doctate-Body testen,
vermutlich wie gpt-oss]
+ B -->|Anderes neues Modell| F{Schema-aware trainiert?}
+ F -->|Ja| C
+ F -->|Nein| D
+ C --> G[Body byte-stabil zu pre-Llama-Fix]
+ D --> H[Worker-Empty-Guard schuetzt vor silent deletion]
+ E --> G
```
## Quellen und Verweise
@@ -254,6 +323,11 @@ einem neuen Bug auf diese Workarounds setzt, immer zuerst pruefen, ob:
`assistant\n\n` ankommt — dann ist Quirk A behoben und die
String-Hacks koennen weg.
3. Die OpenAPI-Spec neue/geaenderte Parameter zeigt.
+4. Die Drei-Komponenten-Kombi fuer Llama 3.1 weiterhin noetig ist:
+ schnellster Check ist, das `format_instruction`-Feld testweise auf
+ `None` zu setzen und einen Doctate-Case neu zu triggern. Wenn der
+ Output sauber kommt, kann der Format-Hinweis weg. Wenn er
+ `{"document":""}` ist, gilt diese Doku noch.
Datum oben aktualisieren, wenn man verifiziert hat, dass diese
Dokumentation noch dem realen Backend-Verhalten entspricht.