Prhub

#53043 [Rust Frontend] [Renderer]Fix Kimi K3 reasoning_effort="none" handling

原始 PR 作者 reidliu41 合并时间 2026-08-21 10:47 文件变更 2 提交数 3 评论 8 代码增减 +50 / -9

执行摘要

修复 Rust 渲染器对 reasoning_effort=none 的拒绝

PR body 明确指出:Rust renderer 拒绝包含 chat_template_kwargs 中 reasoning_effort="none" 的请求,报错 unsupported thinking_effort="none";而 Kimi K3 原生只接受 low、high、max 作为思考级别,none 是标准兼容值用于关闭思考,Python renderer 已按此处理。该变更是为了让合法兼容请求不再失败,同时保留 Kimi K3 原生思考控制的优先级。

值得精读:PR 体量小,但展示了“标准兼容值 vs 模型原生参数”的优先级设计,以及 review 驱动下依据官方 tokenizer 源码修正 fallback 行为的过程。对关注 Rust 前端与 Python 前端一致性工作的读者有参考价值。

讨论亮点

两条核心 review 线索:

  1. sagearc 的 fallback 质疑(已解决):在 tests.rs 第 363 行附近,sagearc 提出当 thinking=true 覆盖 reasoning_effort="none" 时是否应回退到 thinking_effort=max,并引用官方 tokenization_kimi.py L378-L379 作为依据。作者采纳该建议,第二个 commit 实现了 fallback 逻辑。
  2. Codex P2 校验绕过质疑(未解决):在 encoding.rs 的 thinking_enabled 修改处,Codex 指出当同时给出 reasoning_effort="none" 和非法原生值(如 thinking_effort="medium")时,thinking 被禁用会使渲染流程跳过 thinking_effort 校验,请求被静默接受;而 Python renderer 无论思考开关如何都会校验 thinking_effort。该评论未看到对应修复。

实现拆解

实现分三步完成:

  1. 入口与核心函数:变更集中在 rust/src/chat/src/renderer/kimi_k3/encoding.rs 的 thinking_enabled 和 thinking_effort 两个函数,它们共同决定渲染输出的思考开关和 effort 值。
  2. thinking_enabled 改造:把 typed chat_options.reasoning_effort 的判断从原来的 map(...).unwrap_or(true) 链中拆出并提前 return;随后新增对 template_kwargs["reasoning_effort"] == "none" 的检查,使 kwarg 来源的兼容值也能单独禁用思考。优先级链变为:thinking > enable_thinking > typed reasoning_effort > kwarg reasoning_effort="none"。
  3. thinking_effort 归一化:无论 effort 来自 typed 字段还是 kwarg,只要其值为 none 就映射为 DEFAULT_THINKING_EFFORT(测试断言渲染结果为 thinking_effort=max),避免 none 被 VALID_THINKING_EFFORTS 校验拒绝;当 thinking=true 覆盖 reasoning_effort="none" 时仍能输出合法 effort。
  4. 测试配套:rust/src/chat/src/renderer/kimi_k3/tests.rs 中,standard_none_disables_thinking 改为仅凭 reasoning_effort="none" 禁用思考;新增 native_thinking_true_overrides_standard_none 与 enable_thinking_true_overrides_standard_none 两个测试,验证原生开关覆盖兼容值的语义;typed_none_disables_thinking 和 rejects_removed_medium_thinking_effort 等既有行为保持不变。
  5. 演进:第一个 commit 仅处理禁用思考,第二个 commit 依据 sagearc 的 review 反馈补上 fallback 到 thinking_effort="max" 的逻辑。
文件 模块 状态 重要度
rust/src/chat/src/renderer/kimi_k3/encoding.rs 渲染器 modified 6.04
rust/src/chat/src/renderer/kimi_k3/tests.rs 渲染器 modified 6.2

关键符号

thinking_enabled thinking_effort standard_none_disables_thinking native_thinking_true_overrides_standard_none enable_thinking_true_overrides_standard_none

关键源码片段

rust/src/chat/src/renderer/kimi_k3/encoding.rs core-logic

核心逻辑所在:thinking_enabled 与 thinking_effort 两个函数共同定义了 reasoning_effort="none" 的归一化语义与参数优先级。

// 判断 Kimi K3 请求是否启用思考。
// 优先级从高到低:thinking > enable_thinking > typed reasoning_effort > kwarg reasoning_effort。
// 兼容值 reasoning_effort="none" 用于禁用思考,与 Python renderer 语义一致。
fn thinking_enabled(request: &ChatRequest) -> Result<bool> {
    // 原生控制参数优先:thinking 显式给出时直接决定开关
    if let Some(thinking) = request.parse_template_bool("thinking")? {
        return Ok(thinking);
    }
    if let Some(enable_thinking) = request.parse_template_bool("enable_thinking")? {
        return Ok(enable_thinking);
    }
    // OpenAI 兼容 typed 字段:None 表示禁用思考,其余级别保持启用
    if let Some(reasoning_effort) = request.chat_options.reasoning_effort {
        return Ok(reasoning_effort != crate::request::ReasoningEffort::None);
    }
    // chat_template_kwargs 中的字符串 "none" 同样视为禁用;非字符串按未提供处理
    Ok(request
        .chat_options
        .template_kwargs
        .get("reasoning_effort")
        .and_then(Value::as_str)
        != Some("none"))
}
​
// 解析最终输出的 thinking effort。有效级别为 low / high / max。
// "none" 是标准兼容值,不是 K3 原生级别,因此在这里归一化为默认级别。
fn thinking_effort(request: &ChatRequest) -> Result<String> {
    let effort = if let Some(value) = request.chat_options.template_kwargs.get("thinking_effort") {
        value.as_str().ok_or_else(|| {
            Error::ChatTemplate(format!(
                "template kwarg `thinking_effort` must be a string, got {value}"
            ))
        })?
    } else if let Some(effort) = request.chat_options.reasoning_effort {
        // typed reasoning_effort 为 None 时回退到默认级别,避免 "none" 被误判为非法值
        if effort == crate::request::ReasoningEffort::None {
            DEFAULT_THINKING_EFFORT
        } else {
            effort.as_str()
        }
    } else if let Some(value) = request.chat_options.template_kwargs.get("reasoning_effort") {
        let effort = value.as_str().ok_or_else(|| {
            Error::ChatTemplate(format!(
                "template kwarg `reasoning_effort` must be a string, got {value}"
            ))
        })?;
        // kwarg 来源的 "none" 同样归一化:当 thinking=true 覆盖它时仍输出合法 effort
        if effort == "none" {
            DEFAULT_THINKING_EFFORT
        } else {
            effort
        }
    } else {
        DEFAULT_THINKING_EFFORT
    };
​
    // 最终只校验 K3 原生级别;默认级别通常为 "max"(测试断言 thinking_effort=max)
    if !VALID_THINKING_EFFORTS.contains(&effort) {
        return Err(Error::ChatTemplate(format!(
            "unsupported thinking_effort={effort:?}; supported values are `low`, `high`, and `max`"
        )));
    }
    Ok(effort.to_string())
}
rust/src/chat/src/renderer/kimi_k3/tests.rs test-coverage

保存了此次兼容性语义的行为契约:修改 standard_none_disables_thinking,并新增两个原生开关覆盖 none 的测试。

// 单独的 reasoning_effort="none" 即可禁用思考,无需依赖 enable_thinking=false
#[test]
fn standard_none_disables_thinking() {
    let mut request = crate::request::ChatRequest::for_test();
    request
        .chat_options
        .template_kwargs
        .insert("reasoning_effort".to_string(), json!("none"));
​
    let rendered = render_request(&request);
​
    // 不输出 thinking-effort 标签,且以 response 标签结尾,表示思考已关闭
    assert!(!rendered.contains("type=\"thinking-effort\""));
    assert!(rendered.ends_with("<|open|>response<|sep|>"));
}
​
// 原生 thinking=true 覆盖兼容值 none:保持思考,并回退到默认 effort=max
#[test]
fn native_thinking_true_overrides_standard_none() {
    let mut request = crate::request::ChatRequest::for_test();
    request.chat_options.template_kwargs.extend([
        ("thinking".to_string(), json!(true)),
        ("reasoning_effort".to_string(), json!("none")),
    ]);
​
    let rendered = render_request(&request);
​
    assert!(rendered.contains("thinking_effort=max"));
    assert!(rendered.ends_with("<|open|>think<|sep|>"));
}
​
// enable_thinking=true 同样覆盖兼容值 none
#[test]
fn enable_thinking_true_overrides_standard_none() {
    let mut request = crate::request::ChatRequest::for_test();
    request.chat_options.template_kwargs.extend([
        ("enable_thinking".to_string(), json!(true)),
        ("reasoning_effort".to_string(), json!("none")),
    ]);
​
    let rendered = render_request(&request);
​
    assert!(rendered.contains("thinking_effort=max"));
    assert!(rendered.ends_with("<|open|>think<|sep|>"));
}

评论区精华

native thinking 开关覆盖 none 时应回退到 thinking_effort=max 设计

sagearc 在 tests.rs 第 363 行附近提问:当 `thinking=true` 覆盖 `reasoning_effort="none"` 时,是否应回退到 `thinking_effort=max`,并引用官方 tokenization_kimi.py L378-L379 作为行为依据。

结论:作者采纳建议,第二个 commit 实现 fallback:thinking=true 或 enable_thinking=true 覆盖 none 时渲染输出 thinking_effort=max 并保持 think 标签。 · 已解决

思考被禁用时非法原生 effort 校验被跳过 正确性

Codex P2 级别评论指出:同时给出 `reasoning_effort="none"` 与非法原生值(如 `thinking_effort="medium"`)时,thinking 被禁用使渲染流程跳过 `thinking_effort` 校验,请求被静默接受;Python renderer 仍会校验,存在行为不一致。

结论:未在本次 PR 中修复,Rust renderer 与 Python renderer 在“禁用思考时是否仍校验原生 effort”上存在差异,属于遗留问题。 · unresolved

风险与影响

  1. Rust/Python 行为不一致:Codex 指出的校验绕过问题未被修复,reasoning_effort="none" 与 thinking_effort="medium" 组合时 Rust renderer 静默接受,Python renderer 会报错,存在跨前端行为漂移风险。
  2. 组合语义含糊:thinking_effort kwarg 优先级高于 reasoning_effort,但当 reasoning_effort="none" 关闭思考时,显式提供的 thinking_effort 既不输出也不校验,可能出现“给了 effort 但思考关闭”的怪语义。
  3. 回归面较小:low/high/max 与非法值的主路径均有测试覆盖,typed_none_disables_thinking 与 rejects_removed_medium_thinking_effort 保持原断言。

用户侧:通过 Rust 前端以 OpenAI 兼容 reasoning_effort="none" 调用 Kimi K3 的请求从报错变为正常禁用思考;系统侧:Rust renderer 与 Python renderer 在推理控制参数上完成一次语义对齐;团队侧:这是 Rust 前端持续缩小与 Python 前端行为差距的兼容性修复线,后续仍需关注校验路径的差异。

Rust/Python 校验行为不一致 非法原生 effort 校验可能被绕过 新 Rust 前端兼容性路径

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论