April 21, 2026 • 版本: v2026.4.14

[OpenRouter/Qwen3流解析失败 — reasoning_details字段未处理] - OpenRouter/Qwen3 Stream Parsing Fails — reasoning_details Field Not Handled

当通过OpenRouter使用openrouter/qwen/qwen3-235b-a22b或类似Qwen3模型时,所有响应都因流解析器未处理reasoning_details字段而失败,错误提示为'incomplete turn detected: payloads=0',导致零个内容块被组装。

🔍 症状

主要错误表现

当通过 OpenRouter 查询任何 Qwen3 模型时,智能体立即失败并返回:

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

最终用户看到:

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

技术行为

流解析器从 OpenRouter 接收有效的 delta 事件,但没有任何字段被识别为助手内容:

  • reasoning_content — Qwen3 响应中不存在
  • reasoning — Qwen3 响应中不存在
  • reasoning_text — Qwen3 响应中不存在
  • reasoning_details存在但未被处理,导致零 payload 组装

原始 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"
}

诊断日志输出

[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"

受影响的模型

  • openrouter/qwen/qwen3-235b-a22b
  • openrouter/qwen/qwen3-235b-a22b-2507
  • openrouter/qwen/qwen3-32b
  • 任何通过 OpenRouter 发出 reasoning_details 的 Qwen3 变体

🧠 根因分析

架构概述

OpenClaw 的流式架构由两个主要组件组成:

  1. 请求端包装器createOpenRouterWrapper — 使用 OpenRouter 特定的头部/参数包装出站请求
  2. 响应端解析器src/agents/openai-transport-stream.ts — 将提供商返回的 SSE 流事件解析为结构化 delta 事件

解析失败序列

步骤 1 — OpenRouter 返回 Qwen3 推理

当 Qwen3 处理提示时,它在一个非标准字段中发出推理标记:

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

步骤 2 — 流解析器不识别字段

当前 openai-transport-stream.ts 中的解析器检查以下字段:

// 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

步骤 3 — 零 Payload 组装

因为 reasoning_details 从未被处理:

  • 没有推理标记被发送到聚合器
  • 没有创建助手内容块
  • payloads 数组保持为空

步骤 4 — 不完整轮次检测

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

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

OpenRouter 响应标准化差距

createOpenRouterWrapper 只处理请求端转换:

// 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
  };
}

这意味着 reasoning_details 通过包装器未修改地传递,到达解析器时未被处理。

字段格式对比

字段名Qwen3 通过 OpenRouterOpenClaw 解析器支持
reasoning_content
reasoning
reasoning_text
reasoning_details

🛠️ 逐步修复

选项 A:在流解析器中添加 reasoning_details 支持(推荐)

文件src/agents/openai-transport-stream.ts

修改前

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;
}

修改后

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;
}

额外更改 — 处理数组结构

如果 reasoning_details 可能包含具有顺序内容的多个条目:

// 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));
    }
  }
}

选项 B:在 OpenRouter 包装器中添加响应标准化

文件src/providers/openrouter/adapter.ts(或包装器文件)

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;
}

然后在流处理中应用:

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

选项 C:在不完整轮次检测中忽略未知推理字段

文件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();
}

注意:选项 C 是一种回退机制,应与选项 A 结合使用以实现完整解决方案。

🧪 验证

测试 1:直接流捕获

# 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

测试 2:解析器函数单元测试

// 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']);
  });
});

测试 3:使用模拟 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);
});

测试 4:端到端验证

# 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"

修复后的预期控制台输出

[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

⚠️ 常见陷阱

1. 误识别的无效变通方案

  • thinkingDefault: "off" — 仅禁用 OpenClaw 注入的思考努力;不会发送 OpenRouter exclude: true
  • providerOptions.openrouter.extra_body.enable_thinking: false — Qwen3 不支持 OpenRouter 此参数
  • providerOptions.openrouter.extra_body.thinking: { type: "disabled" } — 此端点无效参数
  • tools.allow: [] — 不影响流字段处理

2. 模型变体假设错误

错误假设:不同的 Qwen3 变体(qwen3-235b-a22bqwen3-32b 等)使用不同的字段名。

实际情况:所有通过 OpenRouter 的 Qwen3 变体使用相同的 reasoning_details 模式。

3. OpenRouter 与直接 API 混淆

使用 curl 直接测试 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

这误导用户认为模型工作正常——问题具体出在 OpenClaw 的流解析层。

4. 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.js

5. TypeScript 编译不匹配

如果从源代码运行:

# 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. 流处理中的竞态条件

如果 reasoning_detailsfinish_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. 多索引推理详情

Qwen3 可能发出具有非顺序索引的推理:

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

修复必须通过以下方式之一处理乱序条目:

  • 在发出前缓冲并按索引排序
  • 立即发出并附带索引元数据以便下游排序

🔗 相关错误

错误代码/消息描述关联性
incomplete turn detected: payloads=0主要症状 — 轮次完成但内容块为零此 bug 的直接表现
incomplete turn detected: runId=…pi-embedded-runner/run.ts 中记录的一般不完整轮次错误零 payload 的下游后果
ERROR: reasoning model returned empty content某些网关版本中的替代错误措辞相同根因
SSE parse error: Unexpected token at offset格式错误的流事件不相关的流解析问题
Provider timeout: OpenRouterOpenRouter 端点超时不同类别(网络)
Model not found: qwen3-xxx无效的模型标识符配置错误,无关
Unauthorized无效的 OpenRouter API 密钥认证问题

相关历史问题

  • Issue #1042:"DeepSeek 流解析失败 — reasoning_content 未处理" — 不同提供商/模型组合的相同模式
  • Issue #892:"Claude 流式处理在扩展思考块上中断" — Anthropic 响应中类似的字段未处理模式
  • PR #1156:"向 OpenAI 传输添加 reasoning_text 字段支持" — 添加了已识别字段之一的社区贡献
  • PR #1234:"OpenRouter 兼容性层" — 引入了 createOpenRouterWrapper 但遗漏了响应标准化

已知的受影响配置

提供商模型OpenClaw 版本状态
OpenRouterqwen/qwen3-235b-a22bv2026.4.14受影响
OpenRouterqwen/qwen3-32bv2026.4.14受影响
OpenRouterqwen/qwen3-235b-a22b-2507v2026.4.14受影响
Direct APIqwen3-235b-a22bv2026.4.14不受影响(不同的响应格式)
LiteLLM Proxyqwen/qwen3-235b-a22bv2026.4.14有可用变通方案(代理剥离未知字段)

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。