Skip to content

[Bug] OpenAI 兼容渠道:上游返回 OpenRouter 方言(reasoning + reasoning_details[reasoning.text])时思考内容被完全丢弃(0.41→0.42 回归) #3900

Description

@Sisyphbaous-DT-Project

你运行的 Kimi Code 版本是?

2.0.0(回归起点为 0.42.0,0.41.0 行为正常)

你使用的是哪个开放平台/订阅?

第三方 OpenAI 兼容网关(new-api 中转,Chat Completions 协议,provider type 为 openai)

你使用的是哪个模型?

deepseek-v4.1-flash(网关侧映射为 deepseek/deepseek-v4.1-flash)

你的电脑平台是?

Microsoft Windows NT 10.0.26100.0 x64

你遇到了什么问题?

开启 Thinking([thinking] enabled = true,effort=max)后,思考块在 TUI 中完全不显示——既没有流式思考文本,回合结束后也没有任何折叠的思考组件。检查会话的 wire.jsonl 发现整个会话 0 条 think 记录,即思考内容在进入渲染层之前就被丢弃了。

同一个模型、同一份配置,在 0.41.0 下思考内容显示正常,因此这是 0.42 引入的回归。

根因(已定位到代码):上游网关以 OpenRouter 方言返回思考内容,SSE delta 中同时携带:

{
  "choices": [
    {
      "delta": {
        "reasoning": "We",
        "reasoning_details": [
          { "type": "reasoning.text", "text": "We", "format": "unknown", "index": "0" }
        ]
      }
    }
  ]
}

即:字符串字段 reasoning + 数组字段 reasoning_details(元素类型为 reasoning.text),没有 reasoning_content。

在 2.0.0 的 packages/agent-core-v2/src/human/llm/requester/bases/openai/format.ts 流式解析中(约 L300 起):

  1. 只要 delta 里存在数组形态的 reasoning_details,就进入 details 分支;
  2. reasoning-key.ts 的 toReasoningDetailsElement 只接受 summary/encrypted 两种元素类型,reasoning.text 元素被跳过,extractReasoningDetails 返回空数组(而非 undefined),分支已被占用;
  3. 于是读取字符串字段的 else 分支(extractReasoning(delta),能扫到 reasoning 字符串)永远不会执行;
  4. 结果:思考内容被完整丢弃,不进 wire、不上屏。

0.41.0 的 openai-legacy.ts 只做 reasoningKeyDialect.observe(delta) 字符串扫描,会跳过数组、读到 reasoning 字符串,因此显示正常。

另外注意到 PR #3492 的描述中写的是「跳过非 summary/encrypted 元素,OpenRouter 的 reasoning.* 数组方言保持和以前一样被忽略」——但实际上以前并没有被完全忽略(字符串 reasoning 字段当时能被读到并显示),所以这是一次意外回归。PR #3735 的显示优先级修复只作用于 kimi trait,generic openai provider 的这个问题在最新 main 上依然存在。

复现步骤?

  1. 准备一个会返回上述方言的 OpenAI 兼容端点。没有现成网关的话,可用下面的最小 mock(Python 3,监听 8787):
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        self.send_response(200)
        self.send_header('Content-Type', 'text/event-stream')
        self.end_headers()
        def chunk(delta, finish=None):
            c = {"id":"x","object":"chat.completion.chunk","choices":[{"index":0,"delta":delta,"finish_reason":finish}]}
            self.wfile.write(f"data: {json.dumps(c)}\n\n".encode())
        for w in ["We", " need", " think"]:
            chunk({"role":"assistant","reasoning": w,
                   "reasoning_details":[{"type":"reasoning.text","text":w,"format":"unknown","index":"0"}]})
        chunk({"content": "3"})
        chunk({}, "stop")
        self.wfile.write(b"data: [DONE]\n\n")

HTTPServer(("127.0.0.1", 8787), H).serve_forever()
  1. config.toml 配置指向 mock 的模型并开启 Thinking:
[providers.mock]
type = "openai"
base_url = "http://127.0.0.1:8787/v1"
api_key = "sk-any"

[models."mock/t"]
provider = "mock"
model = "t"
max_context_size = 128000
capabilities = ["thinking", "tool_use"]

[thinking]
enabled = true
  1. 用该模型发起任意对话 → 思考内容完全不显示(wire.jsonl 中无 think 记录)。
  2. 对照:把 mock 中的字段名换成 reasoning_content(字符串、去掉 reasoning_details 数组)→ 思考正常显示。

真实环境补充:我的网关上游对 deepseek/deepseek-v4.1-flash 在 reasoning_effort 的 low/high/max 各档位均稳定返回上述方言(直连上游多轮实测一致);经 new-api 透传后格式不变。返回标准 reasoning_content 的模型(如 kimi-k3 官方渠道)不受影响。

期望的行为是什么?

OpenRouter 方言的思考内容应正常显示。建议任选其一:

  1. extractReasoningDetails 在数组存在但所有元素都被过滤时返回 undefined,让流程回退到字符串字段扫描(改动最小);
  2. details 分支内同时读取字符串 reasoning 字段;
  3. toReasoningDetailsElement 识别 reasoning.text 元素,取其 text 作为可见思考。

至少不应在「数组存在但无可用元素」时把字符串字段里的思考内容静默吞掉。

补充信息

  • OpenRouter 的 reasoning_details(reasoning.text 元素)是相当常见的方言,任何前置/中转 OpenRouter 风格端点的网关都会踩到这个问题。
  • 当前会话(2.0.0 下 kimi-k3 官方渠道)wire.jsonl 中 think 记录完整,可证明渲染层与配置无问题,问题确在解析层。

Contribution

  • 我愿意自己提交修复此 bug 的 PR(请先等待维护者在本 issue 中批准)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions