Prhub

#45458 [Feature][Frontend] Report multimodal token counts in usage.prompt_tokens_details

原始 PR 作者 Sunt-ing 合并时间 2026-06-15 13:20 文件变更 3 提交数 5 评论 9 代码增减 +93 / -14

执行摘要

在 usage.prompt_tokens_details 中报告多模态 token 计数

PR body 指出: 'usage.prompt_tokens already includes the image/audio/video placeholder tokens, but clients cannot tell how many tokens each modality contributed.' 用户需要按模态拆分的 token 计数,以进行精细化计费、监控或优化。

值得精读。PR 展示了如何以最小侵入性扩展 OpenAI 兼容 API,重点关注 _make_prompt_tokens_details 中 if not enable_prompt_tokens_details / if num_cached_tokens is None and not mm_token_counts 的短路逻辑,以及 cast 安全地处理类型。设计上使用字典替代字段枚举体现了良好的可扩展性。

讨论亮点

核心讨论围绕数据结构的可扩展性。审核者 DarkLight1337 建议使用字典而非硬编码字段(image_tokens, audio_tokens, video_tokens),作者 Sunt-ing 接受并改为 multimodal_tokens: dict[str, int],使新增模态无需改动协议。后续还涉及返回类型简化(直接返回字典而非 Optional[dict])和 docstring 格式(MkDocs 用单反引号)。所有讨论均已 resolved。

实现拆解

  1. 数据结构扩展:在 vllm/entrypoints/openai/engine/protocol.py 的 PromptTokenUsageInfo 类中添加 multimodal_tokens: dict[str, int] | None 字段,并附 docstring 说明含义。
  2. 辅助函数:在 vllm/entrypoints/openai/chat_completion/serving.py 中新增 _get_mm_token_counts(engine_input),从请求的 mm_placeholders(MultiModalPlaceholders)中按模态名汇总所有 PlaceholderRange.length 之和,返回字典;新增 _make_prompt_tokens_details(enable_prompt_tokens_details, num_cached_tokens, mm_token_counts),根据开关和是否有数据构造 PromptTokenUsageInfo 实例,当功能关闭或无可报数据时返回 None。
  3. 集成到请求处理流程:在 _create_chat_completion 中,对每个 engine_input 调用 _get_mm_token_counts,并将结果传给流式与非流式生成器。生成器在构造最终 usage 时,调用 _make_prompt_tokens_details 合并 cached_tokens 与 multimodal_tokens。
  4. 单元测试:在 tests/entrypoints/openai/chat_completion/test_serving_chat.py 中添加 test_mm_prompt_tokens_details,覆盖文本请求无多模态、多模态多范围求和、功能关闭/无数据返回 None、以及零 cached_tokens 仍报告等场景。
文件 模块 状态 重要度
vllm/entrypoints/openai/chat_completion/serving.py 前端 modified 7.7
vllm/entrypoints/openai/engine/protocol.py 协议 modified 5.57
tests/entrypoints/openai/chat_completion/test_serving_chat.py 测试 modified 5.84

关键符号

_get_mm_token_counts _make_prompt_tokens_details

关键源码片段

vllm/entrypoints/openai/chat_completion/serving.py core-logic

核心实现文件,新增 `_get_mm_token_counts` 和 `_make_prompt_tokens_details` 两个辅助函数,并修改 `_create_chat_completion` 以传递多模态 token 计数给流式和非流式生成器。

def _get_mm_token_counts(engine_input: EngineInput) -> dict[str, int]:
    """Sum per-modality placeholder tokens from ``mm_placeholders``.
​
    Keyed by modality name; ``PlaceholderRange.length`` is the placeholder's
    prompt token span, so each sum matches the placeholder tokens already
    counted in ``usage.prompt_tokens``.
    """
    # `cast` 用于让类型检查器接受动态 key 读取
    mm_placeholders = cast(
        "MultiModalPlaceholders | None", engine_input.get("mm_placeholders")
    )
    # 遍历所有模态,汇总每模态所有占位符的 length
    return {
        modality: sum(p.length for p in ranges)
        for modality, ranges in (mm_placeholders or {}).items()
        if ranges # 忽略空列表
    }
​
​
def _make_prompt_tokens_details(
    enable_prompt_tokens_details: bool,
    num_cached_tokens: int | None,
    mm_token_counts: dict[str, int] | None,
) -> PromptTokenUsageInfo | None:
    """Build ``prompt_tokens_details`` from cached + multimodal token counts."""
    # 功能未开启 → 不返回
    if not enable_prompt_tokens_details:
        return None
    # 既没有缓存信息也没有多模态 token → 不返回
    if num_cached_tokens is None and not mm_token_counts:
        return None
    # 合并构造,multimodal_tokens 为 None 时字典会被 Pydantic 序列化为 null
    return PromptTokenUsageInfo(
        cached_tokens=num_cached_tokens,
        multimodal_tokens=mm_token_counts or None,
    )

评论区精华

字段设计:硬编码 vs 字典 设计

DarkLight1337 建议使用字典而非硬编码的 `image_tokens` / `audio_tokens` / `video_tokens`,理由是可扩展性更好。作者接受并改为 `multimodal_tokens: dict[str, int]`。

结论:采用字典方案,未来新增模态无需修改协议。 · 已解决

辅助函数返回类型简化 设计

DarkLight1337 认为 `_get_mm_token_counts` 可以直接返回字典(空时为空字典),无需返回 `Optional[dict]`。作者同步调整了实现。

结论:确保调用侧 _make_prompt_tokens_details 已正确处理空字典。 · 已解决

docstring 格式 style

DarkLight1337 指出项目文档使用 MkDocs (Markdown),因此 docstring 中行内代码应用单反引号而非 RST 风格的双反引号。作者修正。

结论:统一使用单反引号包裹。 · 已解决

风险与影响

低风险。新字段 multimodal_tokens 为 Optional,不存在时客户端可安全忽略。功能受 --enable-prompt-tokens-details 开关门控,默认不启用。实现仅从已有的 mm_placeholders 汇总长度,不涉及推理路径修改。主要风险在于如果 PlaceholderRange.length 定义或计算方式将来改变,需要同步更新,但当前逻辑与 prompt_tokens 计数一致,已通过测试覆盖基本场景。

对用户:使用 Chat Completions API 且启用 token details 的请求,可在响应中得到 multimodal_tokens 明细,便于调优和理解成本。对系统:新增代码量小(+93/-14 行),无性能测试表明有影响,仅增加一次字典汇总操作(O(number of placeholders))。对团队:设计上避免了硬编码,未来支持新模态(如 depth map)只需确保其 PlaceholderRange 被正确填充即可,无需修改协议定义。文本请求行为完全不变。

依赖 mm_placeholders 正确性 功能默认关闭,无默认行为变化

关联 Issue

#27122 feat: report multimodal (image/audio/video) token counts in usage.prompt_tokens_details

完整报告

参与讨论