[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-a22bopenrouter/qwen/qwen3-235b-a22b-2507openrouter/qwen/qwen3-32b- 任何通过 OpenRouter 发出
reasoning_details的 Qwen3 变体
🧠 根因分析
架构概述
OpenClaw 的流式架构由两个主要组件组成:
- 请求端包装器:
createOpenRouterWrapper— 使用 OpenRouter 特定的头部/参数包装出站请求 - 响应端解析器:
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 通过 OpenRouter | OpenClaw 解析器支持 |
|---|---|---|
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 注入的思考努力;不会发送 OpenRouterexclude: trueproviderOptions.openrouter.extra_body.enable_thinking: false— Qwen3 不支持 OpenRouter 此参数providerOptions.openrouter.extra_body.thinking: { type: "disabled" }— 此端点无效参数tools.allow: []— 不影响流字段处理
2. 模型变体假设错误
错误假设:不同的 Qwen3 变体(qwen3-235b-a22b、qwen3-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.js5. 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.js6. 流处理中的竞态条件
如果 reasoning_details 在 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. 多索引推理详情
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: OpenRouter | OpenRouter 端点超时 | 不同类别(网络) |
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 版本 | 状态 |
|---|---|---|---|
| OpenRouter | qwen/qwen3-235b-a22b | v2026.4.14 | 受影响 |
| OpenRouter | qwen/qwen3-32b | v2026.4.14 | 受影响 |
| OpenRouter | qwen/qwen3-235b-a22b-2507 | v2026.4.14 | 受影响 |
| Direct API | qwen3-235b-a22b | v2026.4.14 | 不受影响(不同的响应格式) |
| LiteLLM Proxy | qwen/qwen3-235b-a22b | v2026.4.14 | 有可用变通方案(代理剥离未知字段) |