15 KiB
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\nzurueck, 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 zweitesystem-Message (sagt dem Modell WAS indocumentzu 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:
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:
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:
{ "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 | >` |
| 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:
- json_schema allein reicht nicht (T1): Production-Pfad mit
temperature: 0.5ohnetop_ptimeoutet, weil Llama Schema-Resampling als Endlos-Schleife verkraftet. - 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. - 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 Workerremove_failure_markeraufruft. Worker.rs hat seither einen Empty-Guard. - 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:
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
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:
- Die Modellliste sich geaendert hat (
/v1/models). <|eot_id|>mittlerweile im Output sauber als Token statt alsassistant\n\nankommt — dann ist Quirk A behoben und die String-Hacks koennen weg.- Die OpenAPI-Spec neue/geaenderte Parameter zeigt.
- Die Drei-Komponenten-Kombi fuer Llama 3.1 weiterhin noetig ist:
schnellster Check ist, das
format_instruction-Feld testweise aufNonezu 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.