[Error de análisis de stream de Qwen3 con campo reasoning_details no manejado] - OpenRouter/Qwen3 Stream Parsing Fails — reasoning_details Field Not Handled
Al usar modelos Qwen3 similares como openrouter/qwen/qwen3-235b-a22b a través de OpenRouter, todas las respuestas fallan con 'incomplete turn detected: payloads=0' porque el analizador de stream no maneja el campo reasoning_details, lo que resulta en cero bloques de contenido ensamblados.
🔍 Síntomas
Manifestación del error principal
Al consultar cualquier modelo Qwen3 a través de OpenRouter, el agente falla inmediatamente con:
incomplete turn detected: runId=abc123-xyz stopReason=stop payloads=0El usuario final observa:
⚠️ Agent couldn't generate a response. Please try again.Comportamiento técnico
El analizador de stream recibe eventos delta válidos de OpenRouter, pero ninguno de los campos se reconoce como contenido del asistente:
reasoning_content— no presente en las respuestas de Qwen3reasoning— no presente en las respuestas de Qwen3reasoning_text— no presente en las respuestas de Qwen3reasoning_details— presente pero no manejado, causando el ensamblaje de cero payloads
Evento de stream sin procesar de OpenRouter
{
"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"
}Salida del registro de diagnóstico
[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"Modelos afectados
openrouter/qwen/qwen3-235b-a22bopenrouter/qwen/qwen3-235b-a22b-2507openrouter/qwen/qwen3-32b- Cualquier variante de Qwen3 a través de OpenRouter que emita
reasoning_details
🧠 Causa raíz
Descripción general de la arquitectura
La arquitectura de streaming de OpenClaw consiste en dos componentes principales:
- Wrapper del lado de solicitud:
createOpenRouterWrapper— envuelve las solicitudes salientes con encabezados/parámetros específicos de OpenRouter - Analizador del lado de respuesta:
src/agents/openai-transport-stream.ts— analiza los eventos de stream SSE del proveedor en eventos delta estructurados
La secuencia del fallo de análisis
Paso 1 — OpenRouter devuelve el razonamiento de Qwen3
Cuando Qwen3 procesa un prompt, emite tokens de razonamiento en un campo no estándar:
"delta": {
"reasoning_details": [{
"type": "reasoning.text",
"text": "Analyzing the query...",
"format": "unknown",
"index": 0
}]
}Paso 2 — El analizador de stream no reconoce el campo
El analizador actual en openai-transport-stream.ts verifica estos campos:
// 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 ignoredPaso 3 — Ensamblaje de cero payloads
Debido a que reasoning_details nunca se procesa:
- No se emiten tokens de razonamiento al agregador
- No se crean bloques de contenido del asistente
- El array
payloadspermanece vacío
Paso 4 — Detección de turno incompleto
En src/agents/pi-embedded-runner/run.ts:
if (payloads.length === 0) {
logger.warn(`incomplete turn detected: payloads=0`);
throw new IncompleteTurnError();
}Brecha en la normalización de respuestas de OpenRouter
El createOpenRouterWrapper solo maneja la transformación del lado de la solicitud:
// 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
};
}Esto significa que reasoning_details pasa a través del wrapper sin modificaciones y llega al analizador sin manejar.
Comparación de formato de campos
| Nombre del campo | Qwen3 vía OpenRouter | Soporte del analizador de OpenClaw |
|---|---|---|
reasoning_content | No | Sí |
reasoning | No | Sí |
reasoning_text | No | Sí |
reasoning_details | Sí | No |
🛠️ Solución paso a paso
Opción A: Agregar soporte de reasoning_details al analizador de stream (Recomendado)
Archivo: src/agents/openai-transport-stream.ts
Antes:
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;
}Después:
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;
}Cambio adicional — Manejar estructura de array:
Si reasoning_details puede contener múltiples entradas con contenido secuencial:
// 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));
}
}
}Opción B: Agregar normalización de respuesta al wrapper de OpenRouter
Archivo: src/providers/openrouter/adapter.ts (o archivo wrapper)
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;
}Luego aplicar en el procesamiento del stream:
stream.on('data', (chunk) => {
const delta = JSON.parse(chunk);
const normalized = normalizeOpenRouterResponse(delta);
processDelta(normalized);
});Opción C: Ignorar campos de razonamiento desconocidos en la detección de turno incompleto
Archivo: 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();
}Nota: La Opción C es un mecanismo de respaldo y debe combinarse con la Opción A para una resolución completa.
🧪 Verificación
Prueba 1: Captura directa de stream
# 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 -5Prueba 2: Prueba unitaria para la función del analizador
// 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']);
});
});Prueba 3: Prueba de integración con OpenRouter simulado
// 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);
});Prueba 4: Verificación de extremo a extremo
# 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"Salida esperada de la consola después de la corrección
[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⚠️ Errores comunes
1. Soluciones alternativas mal identificadas que no funcionan
thinkingDefault: "off"— Solo deshabilita la inyección de esfuerzo de pensamiento de OpenClaw; no envíaexclude: truea OpenRouterproviderOptions.openrouter.extra_body.enable_thinking: false— No es respetado por OpenRouter para Qwen3providerOptions.openrouter.extra_body.thinking: { type: "disabled" }— Parámetro inválido para este endpointtools.allow: []— No afecta el manejo de campos de stream
2. Suposiciones sobre variantes del modelo
Suposición incorrecta: Diferentes variantes de Qwen3 (qwen3-235b-a22b, qwen3-32b, etc.) usan diferentes nombres de campos.
Realidad: Todas las variantes de Qwen3 a través de OpenRouter usan el mismo esquema de reasoning_details.
3. Confusión entre OpenRouter y API directa
Al probar con curl directamente contra la API de OpenRouter:
# 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 correctlyEsto lleva a los usuarios a pensar que el modelo funciona — el problema está específicamente en la capa de análisis de stream de OpenClaw.
4. Problemas de caché en entorno Docker
# 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.js5. Desajuste de compilación TypeScript
Si se ejecuta desde el código fuente:
# Ensure TypeScript is recompiled after patch
npm run build
# Verify the output contains reasoning_details handling
grep "reasoning_details" dist/agents/openai-transport-stream.js6. Condición de carrera en el procesamiento de stream
Si reasoning_details llega después del evento finish_reason:
// 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. Detalles de razonamiento con múltiples índices
Qwen3 puede emitir razonamiento con índices no secuenciales:
"reasoning_details": [
{ "type": "reasoning.text", "text": "...", "index": 2 },
{ "type": "reasoning.text", "text": "...", "index": 0 },
{ "type": "reasoning.text", "text": "...", "index": 1 }
]La corrección debe manejar entradas fuera de orden ya sea:
- Almacenando en buffer y ordenando por índice antes de emitir
- Emitiendo inmediatamente con metadatos de índice para ordenamiento posterior
🔗 Errores relacionados
| Código de error / Mensaje | Descripción | Conexión |
|---|---|---|
incomplete turn detected: payloads=0 | Síntoma principal — turno completado con cero bloques de contenido | Manifestación directa de este bug |
incomplete turn detected: runId=… | Error general de turno incompleto registrado en pi-embedded-runner/run.ts | Consecuencia aguas abajo de cero payloads |
ERROR: reasoning model returned empty content | Formulación alternativa del error en algunas versiones del gateway | Misma causa raíz |
SSE parse error: Unexpected token at offset | Eventos de stream mal formados | Problema de análisis de stream no relacionado |
Provider timeout: OpenRouter | Timeout del endpoint de OpenRouter | Categoría diferente (red) |
Model not found: qwen3-xxx | Identificador de modelo inválido | Error de configuración, no relacionado |
Unauthorized | Clave API de OpenRouter inválida | Problema de autenticación |
Problemas históricos relacionados
- Issue #1042: "DeepSeek stream parsing fails — reasoning_content not handled" — Mismo patrón con una combinación diferente de proveedor/modelo
- Issue #892: "Claude streaming breaks on extended thinking blocks" — Patrón similar de campo no manejado en respuestas de Anthropic
- PR #1156: "Add reasoning_text field support to OpenAI transport" — Contribución de la comunidad que agregó uno de los campos reconocidos
- PR #1234: "OpenRouter compatibility layer" — Introdujo
createOpenRouterWrapperpero perdió la normalización de respuesta
Configuraciones conocidas afectadas
| Proveedor | Modelo | Versión de OpenClaw | Estado |
|---|---|---|---|
| OpenRouter | qwen/qwen3-235b-a22b | v2026.4.14 | Afectado |
| OpenRouter | qwen/qwen3-32b | v2026.4.14 | Afectado |
| OpenRouter | qwen/qwen3-235b-a22b-2507 | v2026.4.14 | Afectado |
| API directa | qwen3-235b-a22b | v2026.4.14 | No afectado (formato de respuesta diferente) |
| LiteLLM Proxy | qwen/qwen3-235b-a22b | v2026.4.14 | Solución alternativa disponible (el proxy elimina campos desconocidos) |