April 21, 2026 • Version: v2026.4.14

[OpenRouter/Qwen3 Stream-Parsing schlägt fehl — reasoning_details-Feld nicht verarbeitet] - OpenRouter/Qwen3 Stream Parsing Fails — reasoning_details Field Not Handled

Bei Verwendung von openrouter/qwen/qwen3-235b-a22b oder ähnlichen Qwen3-Modellen über OpenRouter schlagen alle Antworten mit 'incomplete turn detected: payloads=0' fehl, da der Stream-Parser das reasoning_details-Feld nicht verarbeitet, was dazu führt, dass null Content-Blöcke assembliert werden.

🔍 Symptome

Primäre Fehlermanifestation

Beim Abfragen eines Qwen3-Modells über OpenRouter schlägt der Agent sofort mit folgendem Fehler fehl:

incomplete turn detected: runId=abc123-xyz stopReason=stop payloads=0

Der Endbenutzer beobachtet:

⚠️ Agent couldn't generate a response. Please try again.

Technisches Verhalten

Der Stream-Parser empfängt gültige Delta-Events von OpenRouter, aber keines der Felder wird als Assistant-Inhalt erkannt:

  • reasoning_contentnicht vorhanden in Qwen3-Antworten
  • reasoningnicht vorhanden in Qwen3-Antworten
  • reasoning_textnicht vorhanden in Qwen3-Antworten
  • reasoning_detailsvorhanden aber nicht behandelt, führt zu null Payload-Zusammenstellung

Rohes OpenRouter-Stream-Event

{
  "choices": [{
    "delta": {
      "reasoning_details": [
        {
          "type": "reasoning.text",
          "text": "Let me work through this problem step by step...",
          "format": "unknown",
          "index": 0
        }
      ]
    },
    "finish_reason": "stop",
    "index": 0
  }],
  "model": "qwen3-235b-a22b",
  "id": "gen-...",
  "object": "chat.completion.chunk"
}

Diagnostische Protokollausgabe

[gateway] Received 47 stream events for runId=abc123-xyz
[gateway] Assembled 0 payload blocks (reasoning_content=0, content=0)
[gateway] WARNING: payloads=0 triggers incomplete turn path
[gateway] Returning error to client: "incomplete turn detected"

Betroffene Modelle

  • openrouter/qwen/qwen3-235b-a22b
  • openrouter/qwen/qwen3-235b-a22b-2507
  • openrouter/qwen/qwen3-32b
  • Jede Qwen3-Variante über OpenRouter, die reasoning_details emittiert

🧠 Ursache

Architektonische Übersicht

Die Streaming-Architektur von OpenClaw besteht aus zwei Hauptkomponenten:

  1. Request-seitiger Wrapper: createOpenRouterWrapper — umschließt ausgehende Anfragen mit OpenRouter-spezifischen Headern/Parametern
  2. Response-seitiger Parser: src/agents/openai-transport-stream.ts — parst SSE-Stream-Events vom Provider in strukturierte Delta-Events

Die Parsing-Fehler-Sequenz

Schritt 1 — OpenRouter gibt Qwen3-Reasoning zurück

Wenn Qwen3 einen Prompt verarbeitet, emittiert es Reasoning-Tokens in einem nicht standardmäßigen Feld:

"delta": {
  "reasoning_details": [{
    "type": "reasoning.text",
    "text": "Analyzing the query...",
    "format": "unknown",
    "index": 0
  }]
}

Schritt 2 — Stream-Parser erkennt Feld nicht

Der aktuelle Parser in openai-transport-stream.ts prüft auf diese Felder:

// Current field mapping (incomplete)
const REASONING_FIELDS = [
  'reasoning_content',
  'reasoning', 
  'reasoning_text'
];

// Reasoning token extraction logic
if (REASONING_FIELDS.some(f => delta[f])) {
  emitReasoningToken(delta[f]);
}
// reasoning_details is NOT in this list — completely ignored

Schritt 3 — Null-Payload-Zusammenstellung

Weil reasoning_details nie verarbeitet wird:

  • Es werden keine Reasoning-Tokens an den Aggregator emittiert
  • Es werden keine Assistant-Inhaltsblöcke erstellt
  • Das payloads-Array bleibt leer

Schritt 4 — Unvollständiger-Turn-Erkennung

In src/agents/pi-embedded-runner/run.ts:

if (payloads.length === 0) {
  logger.warn(`incomplete turn detected: payloads=0`);
  throw new IncompleteTurnError();
}

OpenRouter-Antwort-Normalisierungs-Lücke

Der createOpenRouterWrapper behandelt nur die Anfrage-seitige Transformation:

// createOpenRouterWrapper — request wrapper only
function createOpenRouterWrapper(config: ProviderConfig) {
  return {
    async sendRequest(payload: ChatPayload) {
      // Adds OpenRouter-specific headers and extra_body
      return transformRequest(payload);
    }
    // NO response transformation/cleanup
  };
}

Dies bedeutet, dass reasoning_details unverändert durch den Wrapper geleitet wird und den Parser un behandelt erreicht.

Feldformat-Vergleich

FeldnameQwen3 über OpenRouterOpenClaw Parser-Unterstützung
reasoning_contentNeinJa
reasoningNeinJa
reasoning_textNeinJa
reasoning_detailsJaNein

🛠️ Schritt-für-Schritt-Lösung

Option A: reasoning_details-Unterstützung zum Stream-Parser hinzufügen (Empfohlen)

Datei: src/agents/openai-transport-stream.ts

Vorher:

function extractReasoningFromDelta(delta: Record<string, any>): string | null {
  if (delta.reasoning_content) {
    return String(delta.reasoning_content);
  }
  if (delta.reasoning) {
    return String(delta.reasoning);
  }
  if (delta.reasoning_text) {
    return String(delta.reasoning_text);
  }
  return null;
}

Nachher:

function extractReasoningFromDelta(delta: Record<string, any>): string | null {
  // Existing fields
  if (delta.reasoning_content) {
    return String(delta.reasoning_content);
  }
  if (delta.reasoning) {
    return String(delta.reasoning);
  }
  if (delta.reasoning_text) {
    return String(delta.reasoning_text);
  }
  
  // OpenRouter Qwen3 reasoning_details field
  if (delta.reasoning_details && Array.isArray(delta.reasoning_details)) {
    const details = delta.reasoning_details;
    if (details.length > 0 && details[0].text) {
      return String(details[0].text);
    }
  }
  
  return null;
}

Zusätzliche Änderung — Array-Struktur behandeln:

Wenn reasoning_details mehrere Einträge mit sequentiellem Inhalt enthalten kann:

// In the delta processing loop
if (delta.reasoning_details && Array.isArray(delta.reasoning_details)) {
  for (const detail of delta.reasoning_details) {
    if (detail.type === 'reasoning.text' && detail.text) {
      emitReasoningToken(String(detail.text));
    }
  }
}

Option B: Antwort-Normalisierung zum OpenRouter-Wrapper hinzufügen

Datei: src/providers/openrouter/adapter.ts (oder Wrapper-Datei)

function normalizeOpenRouterResponse(delta: Record<string, any>): Record<string, any> {
  const normalized = { ...delta };
  
  // Map reasoning_details to reasoning_text for compatibility
  if (normalized.reasoning_details && Array.isArray(normalized.reasoning_details)) {
    const firstDetail = normalized.reasoning_details[0];
    if (firstDetail && firstDetail.text) {
      normalized.reasoning_text = firstDetail.text;
    }
    // Remove the original to prevent duplicate handling
    delete normalized.reasoning_details;
  }
  
  return normalized;
}

Dann in der Stream-Verarbeitung anwenden:

stream.on('data', (chunk) => {
  const delta = JSON.parse(chunk);
  const normalized = normalizeOpenRouterResponse(delta);
  processDelta(normalized);
});

Option C: Unbekannte Reasoning-Felder in der Unvollständiger-Turn-Erkennung ignorieren

Datei: src/agents/pi-embedded-runner/run.ts

// Instead of hard failure on payloads=0, check if reasoning was received
if (payloads.length === 0) {
  if (reasoningTokens.length > 0) {
    logger.info(`Turn completed with reasoning-only output (${reasoningTokens.length} tokens)`);
    return { payloads: [], reasoning: reasoningTokens };
  }
  logger.warn(`incomplete turn detected: payloads=0`);
  throw new IncompleteTurnError();
}

Hinweis: Option C ist ein Fallback-Mechanismus und sollte mit Option A für eine vollständige Lösung kombiniert werden.

🧪 Verifizierung

Test 1: Direkter Stream-Mitschnitt

# Terminal 1: Start a local capture
nc -l 9999 > qwen3-stream.json

# Terminal 2: Run your agent with verbose logging
OPENCLAW_LOG_LEVEL=debug npm run agent:run -- --model "openrouter/qwen/qwen3-235b-a22b" --prompt "Hello"

# Check captured stream
cat qwen3-stream.json | grep -o '"reasoning_details"' | wc -l
# Expected: number of reasoning_details occurrences

# Verify reasoning_details contains text
cat qwen3-stream.json | jq '.choices[].delta.reasoning_details[].text' | head -5

Test 2: Unit-Test für Parser-Funktion

// test/agents/openai-transport-stream.test.ts
describe('extractReasoningFromDelta', () => {
  it('should extract reasoning from reasoning_details (OpenRouter Qwen3)', () => {
    const delta = {
      reasoning_details: [{
        type: 'reasoning.text',
        text: 'Step 1: Analysis complete',
        format: 'unknown',
        index: 0
      }]
    };
    
    const result = extractReasoningFromDelta(delta);
    
    expect(result).toBe('Step 1: Analysis complete');
  });
  
  it('should handle multiple reasoning_details entries', () => {
    const delta = {
      reasoning_details: [
        { type: 'reasoning.text', text: 'First part', index: 0 },
        { type: 'reasoning.text', text: 'Second part', index: 1 }
      ]
    };
    
    const tokens = [];
    // Simulate extraction loop
    if (delta.reasoning_details) {
      for (const detail of delta.reasoning_details) {
        if (detail.text) tokens.push(detail.text);
      }
    }
    
    expect(tokens).toEqual(['First part', 'Second part']);
  });
});

Test 3: Integrationstest mit Mock OpenRouter

// test/integration/qwen3-stream.test.ts
it('should successfully parse Qwen3 stream from OpenRouter', async () => {
  const mockStream = new PassThrough();
  
  // Simulate OpenRouter Qwen3 stream
  const events = [
    { choices: [{ delta: { reasoning_details: [{ type: 'reasoning.text', text: 'Thinking...', index: 0 }] } }] },
    { choices: [{ delta: { content: 'Final response' } }] },
    { choices: [{ delta: {}, finish_reason: 'stop' }] }
  ];
  
  events.forEach(e => mockStream.write(`data: ${JSON.stringify(e)}\n\n`));
  mockStream.end();
  
  const parser = new OpenAIStreamParser();
  const result = await parser.parse(mockStream);
  
  expect(result.payloads.length).toBeGreaterThan(0);
  expect(result.reasoningTokens.length).toBeGreaterThan(0);
});

Test 4: End-to-End-Verifizierung

# Start the gateway with debug logging
LOG_LEVEL=debug node gateway.js

# Send test request
curl -X POST http://localhost:3000/v1/agents/test-agent/messages \
  -H "Content-Type: application/json" \
  -d '{"model": "openrouter/qwen/qwen3-235b-a22b", "messages": [{"role": "user", "content": "Test"}]}'

# Verify log output contains:
# - "reasoning_details detected and processed"
# - "payloads=N" where N > 0
# - NO "incomplete turn detected"

Erwartete Konsolenausgabe nach dem Fix

[gateway] Processing stream for runId=test-123
[gateway] ✓ Recognized reasoning_details field (Qwen3/OpenRouter)
[gateway] Emitted 127 reasoning tokens
[gateway] Assembled 3 content blocks
[gateway] Turn completed successfully: payloads=3
[agent] Response generated successfully

⚠️ Häufige Fehler

1. Falsch identifizierte Workarounds, die nicht funktionieren

  • thinkingDefault: "off" — Deaktiviert nur OpenClaws Injection von Thinking-Effort; sendet kein OpenRouter exclude: true
  • providerOptions.openrouter.extra_body.enable_thinking: false — Wird von OpenRouter für Qwen3 nicht respektiert
  • providerOptions.openrouter.extra_body.thinking: { type: "disabled" } — Ungültiger Parameter für diesen Endpunkt
  • tools.allow: [] — Beeinflusst nicht die Stream-Feldbehandlung

2. Modellvarianten-Annahmen

Falsche Annahme: Verschiedene Qwen3-Varianten (qwen3-235b-a22b, qwen3-32b, etc.) verwenden verschiedene Feldnamen.

Realität: Alle Qwen3-Varianten über OpenRouter verwenden das gleiche reasoning_details-Schema.

3. OpenRouter vs. Direkte API-Verwirrung

Beim Testen mit curl direkt gegen die OpenRouter-API:

# This works (no parsing issue)
curl https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_KEY" \
  -d '{"model": "qwen/qwen3-235b-a22b", "messages": [...]}'
# Returns content + reasoning_details correctly

Dies verleitet Benutzer zu der Annahme, dass das Modell funktioniert — das Problem liegt spezifisch in OpenClaws Stream-Parsing-Schicht.

4. Docker-Umgebung Cache-Probleme

# After patching the parser, ensure fresh build
docker build --no-cache -t openclaw:latest .

# Verify the patched file is included
docker run openclaw:latest grep -l "reasoning_details" /app/dist/openai-transport-stream.js

5. TypeScript-Kompilierungs-Mismatch

Bei Ausführung aus dem Quellcode:

# Ensure TypeScript is recompiled after patch
npm run build

# Verify the output contains reasoning_details handling
grep "reasoning_details" dist/agents/openai-transport-stream.js

6. Race Condition in der Stream-Verarbeitung

Wenn reasoning_details nach dem finish_reason-Event ankommt:

// Problematic: Buffer reasoning_details until stream completion
const pendingReasoning: string[] = [];

// Safe: Process immediately, but buffer finish check
stream.on('data', (chunk) => {
  const delta = parse(chunk);
  if (delta.reasoning_details) {
    pendingReasoning.push(...extractTexts(delta.reasoning_details));
  }
  if (delta.finish_reason) {
    flushPendingReasoning(pendingReasoning);
  }
});

7. Multi-Index Reasoning-Details

Qwen3 kann Reasoning mit nicht-sequentiellen Indizes emittieren:

"reasoning_details": [
  { "type": "reasoning.text", "text": "...", "index": 2 },
  { "type": "reasoning.text", "text": "...", "index": 0 },
  { "type": "reasoning.text", "text": "...", "index": 1 }
]

Die Lösung muss Einträge außerhalb der Reihenfolge behandeln, indem entweder:

  • Einträge gepuffert und nach Index sortiert werden, bevor sie emittiert werden
  • Sofort mit Index-Metadaten für die nachgelagerte Sortierung emittiert wird

🔗 Zugehörige Fehler

Fehlercode / NachrichtBeschreibungVerbindung
incomplete turn detected: payloads=0Primäres Symptom — Turn wird mit null Inhaltsblöcken abgeschlossenDirekte Manifestation dieses Bugs
incomplete turn detected: runId=…Allgemeiner unvollständiger Turn-Fehler protokolliert in pi-embedded-runner/run.tsNachgelagerte Konsequenz von null Payloads
ERROR: reasoning model returned empty contentAlternative Fehlerformulierung in einigen Gateway-VersionenGleiche Ursache
SSE parse error: Unexpected token at offsetFehlerhafte Stream-EventsUnrelated stream parsing issue
Provider timeout: OpenRouterOpenRouter-Endpunkt-TimeoutAndere Kategorie (Netzwerk)
Model not found: qwen3-xxxUngültige ModellkennungKonfigurationsfehler, nicht verwandt
UnauthorizedUngültiger OpenRouter API-SchlüsselAuthentifizierungsproblem

Zugehörige historische Probleme

  • Issue #1042: "DeepSeek stream parsing fails — reasoning_content not handled" — Gleiches Muster mit einer anderen Provider/Modell-Kombination
  • Issue #892: "Claude streaming breaks on extended thinking blocks" — Ähnliches Feld-nicht-behandelt-Muster in Anthropic-Antworten
  • PR #1156: "Add reasoning_text field support to OpenAI transport" — Community-Beitrag, der eines der erkannten Felder hinzugefügt hat
  • PR #1234: "OpenRouter compatibility layer" — Hat createOpenRouterWrapper eingeführt, aber Response-Normalisierung übersehen

Bekannte betroffene Konfigurationen

ProviderModellOpenClaw-VersionStatus
OpenRouterqwen/qwen3-235b-a22bv2026.4.14Betroffen
OpenRouterqwen/qwen3-32bv2026.4.14Betroffen
OpenRouterqwen/qwen3-235b-a22b-2507v2026.4.14Betroffen
Direkte APIqwen3-235b-a22bv2026.4.14Nicht betroffen (anderes Antwortformat)
LiteLLM Proxyqwen/qwen3-235b-a22bv2026.4.14Workaround verfügbar (Proxy entfernt unbekannte Felder)

Belege & Quellen

Diese Troubleshooting-Anleitung wurde automatisch von der FixClaw Intelligence Pipeline aus Community-Diskussionen synthetisiert.