Prhub

#54883 [Rust Frontend] Report reasoning tokens in chat completion usage

原始 PR 作者 BugenZhao 合并时间 2026-09-04 07:47 文件变更 15 提交数 3 评论 15 代码增减 +735 / -88

执行摘要

为 Rust 前端 chat completion 添加 reasoning_tokens 用量报告。

为了在 Rust 前端提供与 OpenAI API 及 Python 前端一致的 reasoning_tokens 用量信息,使客户端能够区分消耗在推理(思考)与最终回答上的令牌数量。PR body 中明确指出目标是“report usage.completion_tokens_details.reasoning_tokens from the Rust frontend”。

该 PR 值得精读。它展示了一个跨 crate 的、类型安全的数据流设计:从解析器层的归因信息 (attributions),通过中间事件 (AssistantEvent) 和总线 (ChatEvent),最终抵达服务器响应模型 (Usage)。关于 token_count: Option<usize> 的讨论是典型的 API 设计权衡,值得学习。建议关注 UnifiedParserState 的累计逻辑与 ContinuousUsage 的收敛逻辑。

讨论亮点

核心讨论集中在 token_count 字段的类型设计上。初始提交中 token_count 为 usize,在 PR 评审中(由 njhill 提出)被调整为 Option<usize>,以更清晰地区分“已知为零”(Some(0))与“未测量”(None)两种情况,并避免了对文本块的虚假零值。CodeRabbit AI 则指出了 Harmony 输出路径和 push_piece_delta 函数中对空文本但非零 attributions 的处理细节问题,这些作为未来优化点被标记(TODO)。

实现拆解

  1. 引入核心数据类型 (ChatTokenUsage):在 rust/src/chat/src/event.rs 中定义 ChatTokenUsage 结构体,它包装了引擎级别的 TokenUsage 并新增 reasoning_tokens 字段。为方便转换,实现了 From<TokenUsage> 和 Deref trait。
  2. 在解析管道中携带与累计推理令牌计数:
    • 修改 AssistantEvent::TextDelta 变体,新增 token_count: Option<usize> 字段。
    • 在核心解析器 UnifiedParserState (rust/src/chat/src/output/default/unified.rs) 中新增 reasoning_tokens: usize 累计字段。当处理一个 UnifiedParserEvent::Reasoning 事件时,累加该事件携带的 attributions.len()。新增辅助函数 push_piece_delta,将 DecodedText 及其 attributions 长度作为 token_count 附加到 AssistantEvent::TextDelta 上。
    • 修改 StructuredEventState (rust/src/chat/src/output/structured.rs),将 token_count 从 AssistantEvent 透传到 ChatEvent::BlockDelta。
  3. 在事件总线 (ChatEvent) 中传递推理计数:修改 ChatEvent::BlockDelta 事件,添加 token_count: Option<usize> 字段。同时将 ChatEvent::Done 事件的 usage 字段类型从 TokenUsage 更改为 ChatTokenUsage。
  4. 在服务器层聚合并输出用量:
    • 修改 ContinuousUsage (rust/src/server/src/routes/openai/utils/usage.rs),新增 reasoning_tokens 字段和累加/设置方法。
    • 修改流式处理主循环 (rust/src/server/src/routes/openai/chat_completions.rs):当收到 BlockDelta 且 kind 为 Reasoning 时,从 token_count 累加到 ContinuousUsage。当收到 Done 事件时,将流式累计的推理令牌数收敛为 Done 事件中的权威值。
    • 修改 Usage 类型 (rust/src/server/src/routes/openai/utils/types.rs),新增 completion_tokens_details: CompletionTokenUsageInfo 字段,并调整 from_token_usage 以接受 ChatTokenUsage。
  5. 配套测试:在 vllm-chat 和 vllm-server 的 Rust 单元测试中新增用例,验证解析、流式聚合和终端用量的正确性。在 Python 端到端测试 (tests/entrypoints/openai/chat_completion/test_include_reasoning.py) 中新增断言,验证非流式响应中 reasoning_tokens 被正确报告。
文件 模块 状态 重要度
rust/src/chat/src/output/default/unified.rs 统一解析器 modified 8.81
rust/src/chat/src/event.rs 事件定义 modified 7.58
rust/src/server/src/routes/openai/chat_completions.rs Chat 路由 modified 8.65

关键符号

UnifiedParserState::process_parser_output push_piece_delta ChatEvent::Done ContinuousUsage::add_reasoning_tokens ContinuousUsage::set_reasoning_tokens Usage::from_token_usage

关键源码片段

rust/src/chat/src/output/default/unified.rs core-logic

核心解析逻辑实现,负责从 `DecodedText.attributions` 中累计推理令牌并携带到 `AssistantEvent`。

// UnifiedParserState 新增字段用于累计最终推理令牌数。
struct UnifiedParserState {
    parser: Box<dyn UnifiedParser>,
    parser_failed: bool,
    open_call_index: Option<usize>,
    /// 仅用于在 Done 事件时生成最终计数;每个 delta 的计数由其自身携带。
    reasoning_tokens: usize,
}
​
// 在处理解析器输出时,对推理片段进行计数和事件生成。
fn process_parser_output(&mut self, output: UnifiedParserOutput, events: &mut Vec<AssistantEvent>) -> Result<()> {
    for event in output.events {
        match event {
            // ...
            UnifiedParserEvent::Reasoning(piece) => {
                self.open_call_index = None;
                // piece.attributions 恰好包含客户端作为推理接收的所有令牌。
                self.reasoning_tokens += piece.attributions.len();
                push_piece_delta(events, AssistantBlockKind::Reasoning, piece);
            }
            // ...
        }
    }
    Ok(())
}
​
/// 将带归因的 delta 推入事件列表,若文本非空。
fn push_piece_delta(events: &mut Vec<AssistantEvent>, kind: AssistantBlockKind, piece: DecodedText) {
    if piece.text.is_empty() {
        return;
    }
    events.push(AssistantEvent::TextDelta {
        kind,
        delta: piece.text,
        token_count: Some(piece.attributions.len()), // 此处携带令牌计数
    });
}
rust/src/chat/src/event.rs data-contract

定义了扩展后的核心数据类型 `ChatTokenUsage` 和事件 `ChatEvent`,是管道中数据流动的契约。

/// 一次完成请求的聊天级令牌使用量。
/// 扩展了引擎级的 [`TokenUsage`],增加了聊天输出管道测量的推理归属。
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ChatTokenUsage {
    /// 引擎级令牌使用量。
    pub engine: TokenUsage,
    /// 归属于推理的生成令牌数;当配置的解析器没有推理通道时为 0。
    pub reasoning_tokens: usize,
}
​
// 实现便捷转换和解引用。
impl From<TokenUsage> for ChatTokenUsage { /* ... */ }
impl Deref for ChatTokenUsage { /* ... */ }
​
/// 流式聊天事件。
pub enum ChatEvent {
    // ...
    /// 新观察到的、针对一个打开的助手输出块的增量。
    BlockDelta {
        index: usize,
        kind: AssistantBlockKind,
        delta: String,
        /// 此增量关联的生成令牌数,当可用时。
        token_count: Option<usize>, // 新增字段
    },
    // ...
    /// 终端事件,携带最终的助手消息和完成元数据。
    Done {
        message: AssistantMessage,
        usage: ChatTokenUsage, // 从 TokenUsage 变为 ChatTokenUsage
        // ...
    },
}
rust/src/server/src/routes/openai/chat_completions.rs entrypoint

服务器端流处理主循环,负责聚合连续使用量并生成最终的 API 响应,是功能实现的入口点和关键适配层。

// 流式处理循环中,对 BlockDelta 事件进行推理令牌聚合。
Ok(ChatEvent::BlockDelta {
    kind,
    delta,
    token_count,
    ..
}) => {
    // 该计数遵循解码器时钟;即使推理增量对客户端隐藏也提供。
    if matches!(kind, AssistantBlockKind::Reasoning) && let Some(token_count) = token_count {
        continuous_usage.add_reasoning_tokens(token_count);
    }
    // ... 处理 delta 的包含逻辑不变 ...
}
// ...
// 收到 Done 事件时,将流式累计值收敛为权威的终端值。
Ok(ChatEvent::Done {
    usage: final_usage,
    finish_reason,
    ..
}) => {
    // ...
    continuous_usage.set_final_counts(
        final_usage.prompt_token_count,
        final_usage.output_token_count,
    );
    // 收敛流式推理令牌计数到权威的终端值。
    continuous_usage.set_reasoning_tokens(final_usage.reasoning_tokens);
    // ... 后续发送 usage chunk ...
}

评论区精华

token_count 字段类型设计 设计

初始实现中 `AssistantEvent::TextDelta` 的 `token_count` 为 `usize`。njhill 指出使用 `Option` 可能更合适,以区分“已知为零”与“未测量”。BugenZhao 同意并更新。

结论:将 `token_count` 字段类型从 `usize` 更改为 `Option<usize>`。 · 已解决

Harmony 输出路径的推理令牌计数缺失 正确性

CodeRabbit AI 和 codex-bot 指出 Harmony 输出路径将 `token_count` 硬编码为 0,导致 Harmony 响应的推理令牌计数始终为零。这是已知限制,作为 TODO 标记。

结论:Harmony 路径暂未集成,标记为 TODO。 · 待处理

空文本推理增量的计数处理 正确性

CodeRabbit AI 指出 `push_piece_delta` 在文本为空时丢弃,可能影响连续使用量的准确性。此问题依赖于 parser 是否会发出此类事件,PR 中作为潜在优化点。

结论:作为潜在优化点保留,未在本次修复。 · 待处理

风险与影响

主要风险是类型变更 (ChatEvent::Done.usage 从 TokenUsage 变为 ChatTokenUsage) 可能影响该 crate 的其他消费者,但通过 Deref 实现最小化了影响。次要风险是 ContinuousUsage 的聚合逻辑需要在各种边界条件下(如推理被隐藏、parser 失败)保持正确,测试覆盖了这些场景。Harmony 输出路径目前未集成推理令牌计数,需依赖后续 PR。

  1. 用户影响:Rust 前端的 API 响应现在能正确报告推理令牌用量,功能对齐 OpenAI API 和 Python 前端。对于使用推理模型(如 DeepSeek、Qwen 等)的用户,其客户端可以准确追踪 token 消耗。
  2. 系统影响:扩展了 vllm-chat 事件协议,为未来更细粒度的 token 归因提供了基础。API 响应格式增加了 completion_tokens_details 对象,这是向后兼容的新增字段。
  3. 团队影响:为 Rust 前端与 Python 前端的功能对齐提供了范例,展示了如何在新语言前端安全地引入依赖于 token 归因的新特性。
类型契约变更 跨模块数据流 Harmony 路径待完善

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论