# Ionos LLM API: Quirks und Workarounds **Stand:** 2026-05-03 — 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 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. ## 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 `` 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 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. Nur die letzten vier Loesungswege bekommen sauberes `"stop"`. ## 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` | 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 | 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. ## 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 ``` ## 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. Datum oben aktualisieren, wenn man verifiziert hat, dass diese Dokumentation noch dem realen Backend-Verhalten entspricht.