Prhub

#48134 [Bugfix][Rust Frontend] Limit chat top_logprobs in responses

原始 PR 作者 reidliu41 合并时间 2026-07-16 17:41 文件变更 4 提交数 2 评论 2 代码增减 +86 / -5

执行摘要

修复 Rust 前端 chat top_logprobs 截断问题

Rust 前端在 logprobs=true 时返回了引擎解码的每一个候选 token,未按请求参数 top_logprobs 截断,导致与 Python 前端的响应不一致。PR body 明确了 Python 的三种行为:省略/0 返回空列表、正数 k 返回前 k 个、-1 返回全部。需要对齐这一行为。

该 PR 逻辑清晰、改动集中,修复了明确的 parity 问题,值得快速合并。设计上通过 ResponseOptions 传递参数并统一流式/非流式路径的方式合理,可复用同类修复。建议关注 chat_top_logprob_entries 对负数的静默处理,未来可考虑更严格的输入校验。

讨论亮点

该 PR 无人工 review 评论,仅由 BugenZhao 批准合并。BugenZhao 在两次批准中均仅表示“Thanks!”,未提出设计或代码层面的讨论。

实现拆解

  1. ResponseOptions 中新增 output_top_logprobs 字段convert.rs):在 ResponseOptions 结构体中添加 output_top_logprobs: i32,并在 prepare_chat_request 中将请求的 top_logprobs 赋值给该字段,使其沿请求处理链路传递。
  2. 修改 logprob 转换函数签名logprobs.rs):decoded_logprobs_to_openai_chatposition_to_chat_logprobs_content 新增 top_logprobs: i32 参数,替换原有的硬编码全量遍历。
  3. 新增截断迭代器函数 chat_top_logprob_entrieslogprobs.rs):根据 top_logprobs 值决定截断数(-1 取全部、0 取 0、正数 k 取前 k 个),返回 position.entries.iter().take(limit)
  4. 在路由层传递参数chat_completions.rs):在 collect_chat_completionchat_completion_chunk_stream 中拆解 output_top_logprobs 并传递给 decoded_logprobs_to_openai_chat,使流式与非流式路径一致。
  5. 单元测试与集成测试logprobs.rstests.rs):在 logprobs.rs 新增 #[cfg(test)] 模块,使用 sample_logprobs 构造测试数据,通过 chat_top_logprobs_len 辅助函数验证 top_logprobs=0/1/-1 的截断结果;在 tests.rs 的集成测试 non_stream_chat_includes_logprobs_and_prompt_logprobs 中新增断言,验证 top_logprobs 初始为空列表。
文件 模块 状态 重要度
rust/src/server/src/routes/openai/utils/logprobs.rs logprob 转换 modified 8.35
rust/src/server/src/routes/openai/chat_completions.rs 路由层 modified 5.34
rust/src/server/src/routes/tests.rs 集成测试 modified 5.17
rust/src/server/src/routes/openai/chat_completions/convert.rs 参数解析 modified 4.98

关键符号

chat_top_logprob_entries decoded_logprobs_to_openai_chat position_to_chat_logprobs_content chat_top_logprobs_len chat_logprobs_respects_requested_top_logprobs_count

关键源码片段

rust/src/server/src/routes/openai/utils/logprobs.rs core-logic

核心实现文件:新增 `chat_top_logprob_entries` 函数进行截断逻辑,修改 `decoded_logprobs_to_openai_chat` 和 `position_to_chat_logprobs_content` 函数签名以接收 `top_logprobs` 参数,并附带单元测试 `chat_logprobs_respects_requested_top_logprobs_count`。

/// 根据请求的 top_logprobs 值截断候选条目,返回迭代器。
/// - top_logprobs == -1:返回所有条目
/// - top_logprobs >= 0:返回前 top_logprobs 个(0 返回空)
fn chat_top_logprob_entries(
    position: &DecodedPositionLogprobs,
    top_logprobs: i32,
) -> impl Iterator<Item = &DecodedTokenLogprob> {
    let limit = if top_logprobs == -1 {
        position.entries.len()
    } else {
        // 负数会 unwrap_or 为 0,静默处理
        usize::try_from(top_logprobs).unwrap_or(0)
    };
    position.entries.iter().take(limit)
}/// 将解码后的 logprobs 转换为 OpenAI chat 格式,传入 top_logprobs 控制截断。
pub fn decoded_logprobs_to_openai_chat(
    logprobs: &DecodedLogprobs,
    top_logprobs: i32,
    return_tokens_as_token_ids: bool,
) -> Result<ChatLogProbs, ApiError> {
    let content = logprobs
        .positions
        .iter()
        .map(|pos| position_to_chat_logprobs_content(pos, top_logprobs, return_tokens_as_token_ids))
        .try_collect()?;    Ok(ChatLogProbs {
        content: Some(content),
    })
}#[cfg(test)]
mod tests {
    use super::*;    fn sample_logprobs() -> DecodedLogprobs {
        DecodedLogprobs {
            positions: vec![DecodedPositionLogprobs {
                entries: vec![
                    DecodedTokenLogprob { token_id: 1, token: "A".to_string(), logprob: -0.1, rank: 1 },
                    DecodedTokenLogprob { token_id: 2, token: "B".to_string(), logprob: -1.0, rank: 2 },
                    DecodedTokenLogprob { token_id: 3, token: "C".to_string(), logprob: -2.0, rank: 3 },
                ],
            }],
        }
    }    fn chat_top_logprobs_len(top_logprobs: i32) -> usize {
        let chat_logprobs =
            decoded_logprobs_to_openai_chat(&sample_logprobs(), top_logprobs, false)
                .expect("chat logprobs");
        chat_logprobs.content.expect("content")[0].top_logprobs.len()
    }    #[test]
    fn chat_logprobs_respects_requested_top_logprobs_count() {
        // top_logprobs=0 应返回空列表
        assert_eq!(chat_top_logprobs_len(0), 0);
        // top_logprobs=1 应返回 1 个条目
        assert_eq!(chat_top_logprobs_len(1), 1);
        // top_logprobs=-1 应返回所有条目
        assert_eq!(chat_top_logprobs_len(-1), 3);
    }
}

评论区精华

无人工 review 讨论 other

PR 无人工 review 评论,仅由 BugenZhao 两次批准并合并。

结论:无需额外讨论,直接合并。 · 已解决

风险与影响

  1. 向后兼容性:此前 Rust 前端返回所有候选,此 PR 后默认(top_logprobs 未指定时)行为取决于请求解析默认值,需确认 Python 前端未指定时默认值为 0(返回空列表),这可能导致依赖全量 top_logprobs 的客户端行为变化。但 PR 是对齐 Python 行为,属于预期修复。
  2. 边界情况top_logprobs 为负数且非 -1 时,chat_top_logprob_entriesusize::try_from(top_logprobs).unwrap_or(0) 会截断为 0,与 Python 的 ValueError 行为不同,但不影响实际使用(API 文档通常限制为 -1/0/正数)。
  3. 测试覆盖:单元测试仅覆盖了正数和 -1,未覆盖负数或超大值的边界,但风险较低。
  4. 影响范围:仅影响 Rust 前端的 chat completion 响应路径,不影响 Python 前端或核心引擎。

用户:使用 Rust 前端的用户现在会得到与 Python 前端一致的 top_logprobs 截断行为,API 响应更加可预测。
系统:无性能或架构影响,改动集中在请求响应序列化层。
团队:该 PR 是 Rust 前端与 Python 前端行为对齐系列的一部分,后续若有更多 response 字段差异可参考此模式。

向后兼容性注意 负数 top_logprobs 静默处理

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论