Fix gpt-oss-120b with reasoning effort
This commit adjusts the configuration for the `gpt-oss-120b` model to run in plain-text mode. This is necessary because when `reasoning_effort` is set to "medium", the reasoning tokens combined with schema-constrained decoding can exhaust the `max_completion_tokens` budget, leading to an empty content response. The change restores the previous behavior where `gpt-oss-120b` was configured without `response_format` or `top_p`, ensuring the request body sent to Ionos remains byte-identical to the pre-multi-backend state. This prevents regressions and maintains stability for existing production workflows.
This commit is contained in:
+82
-26
@@ -28,9 +28,17 @@ abgleichen, bevor man auf diese Notizen baut.
|
||||
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.
|
||||
- **Wichtig**: gpt-oss-120b laeuft mit `reasoning_effort: medium`
|
||||
am besten im **plain-text mode** (kein `response_format`, kein
|
||||
`top_p`, keine zweite system-Message). Reasoning-Tokens zaehlen gegen
|
||||
`max_completion_tokens`, und schema-constrained decoding zusaetzlich
|
||||
zum Reasoning kann das Budget so leersaugen, dass `content: null`
|
||||
zurueckkommt (Test T7 / Production-Failure auf Case c414cf52,
|
||||
6 recordings, ~3.8k chars). Doctate setzt deshalb fuer gpt-oss
|
||||
`use_json_schema: false` — Body ist byte-identisch zum
|
||||
pre-multi-backend-Stand (commit bf6464d).
|
||||
- Die Drei-Komponenten-Loesung ist also **Llama-spezifisch**, nicht
|
||||
universell. Backends mit Reasoning brauchen das Gegenteil: schema OFF.
|
||||
|
||||
## So kommt man an die richtige API-Info
|
||||
|
||||
@@ -238,6 +246,32 @@ verbatim (2819 chars).
|
||||
Schema + Format-Hinweis. T6 (separate `system`-Message) ist
|
||||
architektonisch sauberer und gewaehlt fuer die Doctate-Implementation.
|
||||
|
||||
#### Session 3 (gpt-oss-Regression vom Multi-Backend-Default, 30 s Timeout)
|
||||
|
||||
Modell `openai/gpt-oss-120b` mit `reasoning_effort: "medium"`,
|
||||
`max_completion_tokens: 4096`. Input: realer Doctate-Body Case
|
||||
`c414cf52` (6 Aufnahmen, 3851 char user_content, ~1900 prompt_tokens
|
||||
nach DEFAULT_SYSTEM_PROMPT).
|
||||
|
||||
| Test | response_format | Wallclock | finish | content | completion_tokens |
|
||||
|---|---|---|---|---|---|
|
||||
| Production 14:23 | `json_schema` (DEFAULT_USE_JSON_SCHEMA) | 58 s | – | **null** (`LlmError::Parse`) | – |
|
||||
| T7 (nach Fix) | weggelassen | 15.68 s | stop | sauberer Arztbrief, 3685 chars | 1692 |
|
||||
|
||||
**Ursache**: Mit `reasoning_effort: medium` zaehlen Reasoning-Tokens
|
||||
gegen `max_completion_tokens`. Schema-constrained decoding (vLLM
|
||||
grammar) belastet zusaetzlich das CPU-Budget. Bei Bodies dieser
|
||||
Groesse fressen Reasoning + Schema-Resampling die 4096 Tokens auf,
|
||||
bevor das Modell zur Inhalts-Generierung kommt — Resultat:
|
||||
`choices[0].message.content == null`.
|
||||
|
||||
**Pre-Multi-Backend** (commit bf6464d) lief gpt-oss ohne
|
||||
`response_format`, also reasoning + plain text → content kam immer
|
||||
zurueck. Der Multi-Backend-Layer hat `DEFAULT_USE_JSON_SCHEMA = true`
|
||||
universell aktiviert, was fuer Llama noetig, fuer gpt-oss aber
|
||||
schaedlich war. Fix: `gpt_oss_120b` setzt jetzt explizit
|
||||
`with_use_json_schema(false)` — Body byte-identisch zu pre-bf6464d.
|
||||
|
||||
## Loesungswege im Vergleich
|
||||
|
||||
| Loesung | Speed | Sauber | Modell-agnostisch | Kommentar |
|
||||
@@ -249,20 +283,33 @@ verbatim (2819 chars).
|
||||
| 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 +
|
||||
- **Reasoning-Modelle mit `reasoning_effort != null`** (gpt-oss-120b
|
||||
und Verwandte): **plain-text mode** — kein `response_format`, kein
|
||||
`top_p`. Reasoning + Schema kollidieren am Token-Budget, Resultat
|
||||
ist `content: null`. Doctate-Default fuer dieses Profil:
|
||||
`temperature: 0.5 + reasoning_effort: medium + max_completion_tokens:
|
||||
4096`.
|
||||
- **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.
|
||||
- **Schema-aware Nicht-Reasoning-Modelle** (vermutlich Llama 3.3 70B,
|
||||
Mistral Nemo): Default plain-text + `response_format: json_schema`
|
||||
ist der Erwartungswert, aber **noch nicht empirisch verifiziert**
|
||||
— vor Aktivierung gegen den Doctate-Body testen.
|
||||
|
||||
## Doctate-Implementation (`server/src/analyze/backend.rs`)
|
||||
|
||||
Die Drei-Komponenten-Kombi lebt als Per-Backend-Konfiguration:
|
||||
Pro-Backend-Konfiguration mit zwei verschiedenen Profilen:
|
||||
|
||||
```rust
|
||||
// gpt-oss: schema OFF, weil Reasoning + Schema das Token-Budget aufessen.
|
||||
ionos_backend("gpt_oss_120b", "GPT OSS 120b", "openai/gpt-oss-120b")
|
||||
.with_reasoning_effort("medium")
|
||||
.with_use_json_schema(false),
|
||||
|
||||
// Llama 3.1 405B FP8: Drei-Komponenten-Kombi.
|
||||
const LLAMA_TEMPERATURE: f32 = 0.6;
|
||||
const LLAMA_TOP_P: f32 = 0.9;
|
||||
const LLAMA_FORMAT_INSTRUCTION: &str = "AUSGABEFORMAT: ...";
|
||||
@@ -274,30 +321,32 @@ ionos_backend("llama_3_1_405b", "Llama 3.1 405B", "...405B-Instruct-FP8")
|
||||
.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.
|
||||
`chat_once` baut den Body dynamisch:
|
||||
- `response_format: json_schema` wird gesendet **gdw**
|
||||
`backend.use_json_schema == true` (Llama: ja, gpt-oss: nein).
|
||||
- `top_p` wird per `Option::is_none`-Skip nur gesendet wenn `Some`
|
||||
(Llama: ja, gpt-oss: nein).
|
||||
- Eine zweite `system`-Message wird ans `messages`-Array gehaengt,
|
||||
falls `format_instruction.is_some()` (Llama: ja, gpt-oss: nein).
|
||||
|
||||
Damit ist der gpt-oss-Body **byte-identisch** zum pre-Multi-Backend-
|
||||
Stand (commit bf6464d). Gepinnt durch
|
||||
`gpt_oss_runs_in_plain_text_mode_no_schema_no_top_p_no_format_hint`-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.
|
||||
`document`-Strings (Failure-Mode T4 fuer Llama) 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
|
||||
A[Neuer Backend-Profil-Eintrag in backend.rs] --> B{Reasoning-Modell?<br/>reasoning_effort != null?}
|
||||
B -->|Ja, gpt-oss-120b| C[plain-text mode:<br/>with_use_json_schema false,<br/>kein top_p, keine format_instruction<br/>Body byte-stabil zu pre-bf6464d]
|
||||
B -->|Nein| D{Schema-aware trainiert?}
|
||||
D -->|Ja, vermutlich Llama 3.3 70B,<br/>Mistral Nemo| E[Default: temp 0.5,<br/>use_json_schema=true<br/>VOR Aktivierung gegen<br/>Doctate-Body testen]
|
||||
D -->|Nein, Llama 3.1 405B FP8| F[Drei-Komponenten-Kombi:<br/>temp 0.6 + top_p 0.9 + schema<br/>+ format_instruction]
|
||||
F --> G[Worker-Empty-Guard schuetzt<br/>vor silent deletion]
|
||||
```
|
||||
|
||||
## Quellen und Verweise
|
||||
@@ -328,6 +377,13 @@ einem neuen Bug auf diese Workarounds setzt, immer zuerst pruefen, ob:
|
||||
`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.
|
||||
5. Der gpt-oss-Token-Budget-Konflikt mit `response_format: json_schema`
|
||||
weiterhin existiert: testweise `with_use_json_schema(true)` fuer
|
||||
gpt-oss setzen, einen 6-Aufnahmen-Case (~3.8k chars) triggern. Wenn
|
||||
`LlmError::Parse: missing choices[0].message.content` im Log
|
||||
erscheint, gilt diese Doku noch. Wenn der Output sauber durchkommt,
|
||||
hat Ionos das Reasoning/Schema-Budget-Verhalten geaendert und
|
||||
`use_json_schema=true` koennte fuer gpt-oss reaktiviert werden.
|
||||
|
||||
Datum oben aktualisieren, wenn man verifiziert hat, dass diese
|
||||
Dokumentation noch dem realen Backend-Verhalten entspricht.
|
||||
|
||||
@@ -95,6 +95,11 @@ impl LlmBackend {
|
||||
self
|
||||
}
|
||||
|
||||
fn with_use_json_schema(mut self, use_schema: bool) -> Self {
|
||||
self.use_json_schema = use_schema;
|
||||
self
|
||||
}
|
||||
|
||||
fn with_system_prompt(mut self, prompt: &str) -> Self {
|
||||
self.system_prompt = prompt.into();
|
||||
self
|
||||
@@ -144,8 +149,16 @@ pub fn backends() -> &'static [LlmBackend] {
|
||||
static B: OnceLock<Vec<LlmBackend>> = OnceLock::new();
|
||||
B.get_or_init(|| {
|
||||
vec![
|
||||
// gpt-oss-120b: schema OFF on purpose. With `reasoning_effort:
|
||||
// medium`, reasoning tokens count against `max_completion_tokens`
|
||||
// (4096); on large bodies (e.g. 6 recordings, ~3.8k chars) the
|
||||
// reasoning eats the budget and the model returns
|
||||
// `choices[0].message.content == null` — the LlmError::Parse path.
|
||||
// Pre-multi-backend (commit bf6464d) gpt-oss ran without schema
|
||||
// and worked fine; this restores that body byte-for-byte.
|
||||
ionos_backend("gpt_oss_120b", "GPT OSS 120b", "openai/gpt-oss-120b")
|
||||
.with_reasoning_effort("medium"),
|
||||
.with_reasoning_effort("medium")
|
||||
.with_use_json_schema(false),
|
||||
ionos_backend(
|
||||
"llama_3_1_405b",
|
||||
"Llama 3.1 405B",
|
||||
@@ -290,15 +303,21 @@ mod tests {
|
||||
assert!(b.use_json_schema, "Llama needs schema-enforced stop");
|
||||
}
|
||||
|
||||
/// gpt-oss-120b is schema-aware out of the box. Asserting that it has
|
||||
/// neither a top_p nor a format instruction guarantees the body sent to
|
||||
/// Ionos stays byte-identical to pre-Llama-fix builds — the production
|
||||
/// path that has been working for weeks must not silently change.
|
||||
/// gpt-oss-120b runs in plain-text mode (no `response_format`, no
|
||||
/// `top_p`, no second system message). Reason: with
|
||||
/// `reasoning_effort: medium`, schema-constrained decoding plus reasoning
|
||||
/// can exhaust `max_completion_tokens` on large bodies and return
|
||||
/// `content: null`. This pin guarantees the body sent to Ionos stays
|
||||
/// byte-identical to the pre-multi-backend path (commit bf6464d).
|
||||
#[test]
|
||||
fn gpt_oss_has_no_format_instruction_and_no_top_p() {
|
||||
fn gpt_oss_runs_in_plain_text_mode_no_schema_no_top_p_no_format_hint() {
|
||||
let b = find_backend("gpt_oss_120b").unwrap();
|
||||
assert!((b.temperature - 0.5).abs() < f32::EPSILON);
|
||||
assert!(b.top_p.is_none(), "gpt-oss must not send top_p");
|
||||
assert!(
|
||||
!b.use_json_schema,
|
||||
"gpt-oss must NOT use json_schema — collides with reasoning_effort budget"
|
||||
);
|
||||
assert!(
|
||||
b.format_instruction.is_none(),
|
||||
"gpt-oss must not get a second system message — body byte-stable"
|
||||
|
||||
Reference in New Issue
Block a user