Files
doctate/docs/ionos-llm-api.md
T

334 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ionos LLM API: Quirks und Workarounds
**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.
## TL;DR
- Die **Marketing-Doku** unter `docs.ionos.com/cloud/ai/...` ist
unvollstaendig. Die dort gezeigten Beispiele fuer Llama 3.1 405B FP8
laufen in der Praxis in einen Gateway-Timeout, weil sie wesentliche
Parameter weglassen.
- Die **echte aktuelle API-Spezifikation** liegt unter
`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 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
### Verfuegbare Modelle abfragen
Die Marketing-Doku listet Modelle, ist aber nicht immer synchron mit dem
Backend. Authoritative Liste:
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
https://openai.inference.de-txl.ionos.com/v1/models | jq .
```
Aktuell (2026-05-03) verfuegbar:
| Modell | Kategorie | Anmerkung |
|---|---|---|
| `meta-llama/Meta-Llama-3.1-8B-Instruct` | Chat | schnell, klein |
| `meta-llama/Meta-Llama-3.1-405B-Instruct-FP8` | Chat | siehe Quirks unten |
| `meta-llama/Llama-3.3-70B-Instruct` | Chat | stoppt sauber, <1s |
| `mistralai/Mistral-Nemo-Instruct-2407` | Chat | ungetestet |
| `mistralai/Mistral-Small-24B-Instruct` | Chat | siehe Doctate-Memory |
| `openai/gpt-oss-120b` | Reasoning | stoppt sauber, vorherige Doctate-Wahl |
| `BAAI/bge-m3`, `bge-large-en-v1.5` | Embedding | |
| `sentence-transformers/paraphrase-multilingual-mpnet-base-v2` | Embedding | |
| `meta-llama/CodeLlama-13b-Instruct-hf` | Code | |
| `Qwen/Qwen3-Coder-Next` | Code | |
| `lightonai/LightOnOCR-2-1B` | OCR | |
| `black-forest-labs/FLUX.1-schnell` | Bild | |
### Aktuelle OpenAPI-Spec extrahieren
Die Redoc-Seite rendert die Spec clientseitig — `openapi.json` ist nicht
unter einer stabilen URL erreichbar, sondern liegt eingebettet im HTML
als JavaScript-Variable `__redoc_state`. Extraktion mit Brace-Balancing,
weil ein Regex auf `</script>` zu viel mitnimmt:
```python
import json, re
import urllib.request
html = urllib.request.urlopen(
"https://api.ionos.com/docs/inference-openai/v1/").read().decode()
start = html.find('{"menu":{"activeItemIdx":-1}')
depth, in_str, esc = 0, False, False
for i, c in enumerate(html[start:], start):
if in_str:
if esc: esc = False
elif c == '\\': esc = True
elif c == '"': in_str = False
else:
if c == '"': in_str = True
elif c == '{': depth += 1
elif c == '}':
depth -= 1
if depth == 0:
end = i + 1; break
spec = json.loads(html[start:end])['spec']['data']
print(json.dumps(spec['paths']['/v1/chat/completions']['post'], indent=2))
```
### Parameter-Schema fuer `chat/completions` (Stand 2026-05-03)
Wichtige Defaults und Limits aus der Spec (Auszug, fett = abweichend von
OpenAI):
| Parameter | Typ | Default | Anmerkung |
|---|---|---|---|
| `model` | string | required | |
| `messages` | array | required | |
| `temperature` | number | **1** | |
| `top_p` | number | **-1** | Sentinel fuer "deaktiviert" |
| `n` | int | 1 | |
| `stream` | bool | false | |
| `stop` | array | | **max 4 Sequenzen** |
| `max_tokens` | int | **16** | **deprecated** zugunsten `max_completion_tokens` |
| `max_completion_tokens` | int | **16** | |
| `presence_penalty` | number | 0 | |
| `frequency_penalty` | number | 0 | |
| `logit_bias` | object | | Token-ID -> Bias |
| `response_format` | object | | `text` / `json_object` / `json_schema` |
| `tools`, `tool_choice` | | | Function Calling |
**Nicht exposed** (wuerde es mit anderen vLLM-Backends geben):
`repetition_penalty`, `min_p`, `top_k`, `skip_special_tokens`,
`ignore_eos`, `stop_token_ids`. Wer diese vLLM-Backdoors braucht, ist
auf Ionos falsch.
### Warum die Marketing-Doku nicht reicht
`https://docs.ionos.com/cloud/ai/ai-model-hub/models/llms/meta-llama-3-1-405b`
zeigt das Beispiel:
```json
{ "model": "...405B-FP8", "messages": [...],
"temperature": 0.7, "max_tokens": 100 }
```
Das funktioniert nur deshalb, weil `max_tokens: 100` zufaellig klein
genug ist, um vor Quirk B (siehe unten) zurueckzukommen. Sobald man
realistische Ausgabelaengen will (z.B. 2k+ Tokens fuer einen Arztbrief),
laeuft die Combo in den Gateway-Timeout, weil die Doku den Tokenizer-Bug
nicht erwaehnt und die Empfehlungen nicht praxistauglich validiert sind.
## Llama 3.1 405B FP8: Drei Quirks
### Quirk A — `<|eot_id|>` wird als ASCII geleakt
Das Llama-3.1-Chat-Template nutzt `<|eot_id|>` (End-of-Turn) als
Stop-Marker. Auf Ionos's vLLM-Deployment ist dieses Token nicht
korrekt im Tokenizer als Special-Token registriert. Folge: das Modell
emittiert es als die einzelnen ASCII-Zeichen `a s s i s t a n t \n \n`
und labert weiter. Beobachtbar an Outputs der Form:
```
Hallo! Wie kann ich helfen?assistant\n\n
Oder moechtest du einfach plaudern?assistant\n\n
Entschuldigung, ich habe mich wiederholt...assistant\n\n
```
Konsequenz: der `stop`-Parameter mit `["<|eot_id|>"]` greift nie, weil
diese String-Sequenz nicht im Output auftaucht. Workarounds:
| Workaround | Funktioniert | Sauber? |
|---|---|---|
| `stop: ["assistant"]` zusaetzlich | ja, 0.79 s fuer "Sag hallo." | unsauber, Provider-Bug-Patch |
| System-Prompt instruiert eigenen End-Marker, `stop` matcht ihn | ja, 0.89 s | mittel, Instruction-Drift moeglich |
| `response_format: json_schema` | ja, 0.84 s | sauber, offizielles Feature |
### Quirk B — Ohne `max_tokens` laeuft das Modell bis Context-Ceiling
Da das Modell selbst keinen Stop-Token emittiert (Quirk A) und
`max_tokens` Default = 16 nur theoretisch greift, generiert FP8-405B
auf vielen Pfaden bis zum Kontext-Limit (128k Tokens) weiter. Bei
~20 tok/s Generierungsrate sind das ~107 Minuten — laenger als jeder
Gateway-Timeout. Beobachtung: HTTP 504 "stream timeout" nach ~3
Minuten, oder schon `HTTP 000` clientseitig.
Konsequenz: **immer** `max_tokens` (oder `max_completion_tokens`)
explizit setzen. Realistische Werte fuer Doctate-Arztbriefe: 2048-4096.
### Quirk C — Generierungsrate ~20-23 tok/s
Empirisch ueber 6 Runs ermittelt (siehe Tabelle unten). Das ist deutlich
langsamer als `Llama-3.3-70B-Instruct` (~1 s fuer 10 Tokens) und
`gpt-oss-120b` (~0.5 s fuer 10 Tokens) auf demselben Endpoint.
Konsequenz: praktischer Hardlimit bei ~3000 Output-Tokens, sonst
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):
| Test | Parameter | Ergebnis | Tok/s |
|---|---|---|---|
| T1 | `temp=0`, `max_tokens=8192` | Timeout 25 s | |
| T3 | `temp=0`, `max_tokens=100` | 5.1 s, finish=length, 100 Tokens | 19.6 |
| T8 | `temp=0`, `max_tokens=500` | 23 s, finish=length, 500 Tokens | 21.7 |
| T9 | `temp=0`, `max_tokens=1500` | 114 s, finish=length, 1500 Tokens | 13.2 |
| T10 | `temp=0`, `max_tokens=3000` | 183 s, finish=length, 3000 Tokens | 16.4 |
| T11 | `temp=0`, `max_tokens=4500` | Gateway-Timeout (curl 200 s) | |
| T17 | Doku-Combo + `stop:["...","assistant"]` | **0.79 s, finish=stop, 11 Tokens** | |
| T19 | Doku-Combo + System-Marker `<|12345|>` | **0.89 s, finish=stop, 14 Tokens** | |
| T21 | `response_format: json_schema` (nur `document:string`) | **0.84 s, finish=stop, 8 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.
#### 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
| Loesung | Speed | Sauber | Modell-agnostisch | Kommentar |
|---|---|---|---|---|
| `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` 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 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 Backend-Profil-Eintrag in backend.rs] --> B{Modell?}
B -->|gpt-oss-120b<br/>schema-aware| C[Default: temp 0.5,<br/>use_json_schema=true,<br/>kein top_p, keine format_instruction]
B -->|Llama 3.1 405B FP8| D[Drei-Komponenten-Kombi:<br/>temp 0.6 + top_p 0.9 + schema<br/>+ format_instruction]
B -->|Llama 3.3 70B<br/>noch nicht getestet| E[Erst gegen Doctate-Body testen,<br/>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
- API-Endpoint: `https://openai.inference.de-txl.ionos.com/v1/chat/completions`
- Authoritative API-Doku (Redoc):
`https://api.ionos.com/docs/inference-openai/v1/`
- Marketing-Doku (oft veraltet, mit Vorsicht geniessen):
`https://docs.ionos.com/cloud/ai/ai-model-hub`
- vLLM-Tokenizer-Hintergrund (warum `<|eot_id|>` als ASCII leakt):
Llama-3.1-Chat-Template + `add_special_tokens=False`-Detokenization-Pfad.
- Verwandte Doctate-Memories: `project_canary_ollama_vram_conflict`,
`project_mistral_24b_silent_deletion`,
`feedback_no_inline_llm_in_handlers`.
## Wartungshinweis
Diese Datei dokumentiert Verhalten zu einem Zeitpunkt. Bevor man bei
einem neuen Bug auf diese Workarounds setzt, immer zuerst pruefen, ob:
1. Die Modellliste sich geaendert hat (`/v1/models`).
2. `<|eot_id|>` mittlerweile im Output sauber als Token statt als
`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.