Prhub

#56392 [Frontend] create unified Cohere parser

原始 PR 作者 walterbm 合并时间 2026-09-12 16:04 文件变更 21 提交数 2 评论 5 代码增减 +1917 / -2137

执行摘要

统一 Cohere 解析器,旧 parser 降级为 shim 修复特殊 token 泄漏

PR body 的 Purpose 写明:创建统一 CohereCommandParser 并将旧的 tool / reasoning parser 转为薄 shim,采用与 mistral、gpt-oss 相同的做法;同时修复 Cohere 模型当前解析在用户可见响应中泄漏特殊 token 的问题。代码层面,旧的 reasoning 与 tool parser 各自持有完整的 cohere_melody 状态机与 convert_schema_to_structural_tags 实现,重复约 300 行且容易漂移,统一入口是消除重复与行为不一致的必要条件。

建议精读,价值在于三点:一是 DelegatingParser 统一入口 + 薄 shim 的迁移手法,可复用于其他模型家族;二是 convert_schema_to_structural_tags 对 xgrammar triggered_tags 的构造方式,是结构化输出与工具调用约束生成的关键;三是 tests/parser/cohere/utils.py 的 MockCohereTokenizer 与 token_deltas 设计,把多字节字符拆分的 U+FFFD 缓冲、多 token chunk 流式行为都纳入测试,值得在其它 parser 测试中借鉴。

讨论亮点

整个 PR 没有任何 review comments,唯一的审核动作是维护者 sfeng33 的快速批准:“Thank you for the making the migration, LGTM!”。另外 claude[bot] 提示本 PR 来自 fork,自动化 review 被禁用,需维护者用 @claude review 手动触发;Issue 评论区 5 条均为自动化消息(mergify 文档预览、/ci run、/ci retry 与两次 Buildkite 触发),说明合入前 CI 实际跑了两轮。由于没有实质技术交锋,讨论信息量有限,也侧面说明迁移方案本身争议不大。

实现拆解

  1. 新增统一解析器(vllm/parser/cohere_command.py,+687 行):CohereCommandParser 继承 DelegatingParser,一次性带入 cohere_melody 的 streaming / unary 两套 PyFilter、citations 解析(_melody_sources_to_vllm、_melody_citations_to_vllm)、结构化输出改写(adjust_request)。MODEL_TO_TAG_STYLE 以架构名为键(Cohere2ForCausalLM、Cohere2VisionForConditionalGeneration、Cohere2MoeForCausalLM)映射到各自的 CohereTagStyle,统一处理 Command A 系模型的 <|START_RESPONSE|> 与 Command A+ 系的 <|START_TEXT|> 差异。
  2. reasoning parser 降级为约 60 行 shim:vllm/reasoning/cohere_command_reasoning_parser.py 删掉约 660 行,仅保留 BaseCohereCommandReasoningParser 的 token id 初始化、is_reasoning_end、count_reasoning_tokens 与两个子类的 melody_preset = "cmd3" / "cmd4"。保留 is_reasoning_end 是因为 structured-output 引擎仍通过它做推理结束门控。
  3. tool parser 降级为 18 行 shim:vllm/tool_parsers/cohere_command_tool_parser.py 同样只保留 melody_preset,类名保持不变,保证 ParserManager 按名解析的兼容性。
  4. 注册与调用方接线:vllm/parser/parser_manager.py 注册新的统一解析器;vllm/entrypoints/cohere/serving.py、cohere_chat_message.py、vllm/renderers/cohere.py 调整引用,把 POSITION_TO_SOURCE_KEY 的 citation 位置映射等上下文经 chat_template_kwargs 注入统一解析器。
  5. 测试重组:删除 tests/reasoning/test_cohere_command_reasoning_parser.py(796 行)与 tests/tool_parsers/test_cohere_command_tool_parser.py(510 行),在 tests/parser/cohere/ 下按能力拆为 test_reasoning.py、test_tool_calls.py、test_structural_tags.py、test_citations.py,公共设施(MockCohereTokenizer、make_parser、drive_parser、stream_parser、token_deltas)收敛到 utils.py。
  6. 配套变更:requirements/test/{cpu,cuda,rocm}.txt 将 cohere-melody 升级到 >=0.14.0;docs/features/tool_calling.md 同步更新。PR body 提及用 CohereLabs/command-a-plus-05-2026-bf16 与 CohereLabs/North-Mini-Code-1.0 做了集成验证,未出现特殊 token 泄漏。
文件 模块 状态 重要度
vllm/parser/cohere_command.py 解析器 added 9.08
vllm/reasoning/cohere_command_reasoning_parser.py 推理解析 modified 8.65
vllm/tool_parsers/cohere_command_tool_parser.py 工具解析 modified 8.16
vllm/parser/parser_manager.py 解析注册 modified 6.44
tests/parser/cohere/test_tool_calls.py 工具测试 added 7.48
tests/parser/cohere/test_citations.py 引用测试 added 7.34

关键符号

CohereCommandParser.parse CohereCommandParser.parse_delta CohereCommandParser.adjust_request convert_schema_to_structural_tags collect_tool_schema _tool_definitions_to_schema_list _has_effective_tools _add_tag BaseCohereCommandReasoningParser.is_reasoning_end BaseCohereCommandReasoningParser.count_reasoning_tokens

关键源码片段

vllm/parser/cohere_command.py core-logic

新增的统一解析器核心文件,687 行,收拢全部 Cohere 解析逻辑与 xgrammar structural_tag 构造

# MODEL_TO_TAG_STYLE 定义每个 Cohere 架构要使用的结构性标签:
# json_tags 是 JSON schema 的包裹对,tools 是工具调用数组的包裹对。
# Command A 系用 <|START_RESPONSE|> / <|END_RESPONSE|>,Command A+ 系
# (Vision / MOE)额外接受 <|START_TEXT|> / <|END_TEXT|>。
COMMAND_A_TOOLS_TAG = CohereTagRegistry(
    trigger="<|START_ACTION|>",
    end="<|END_ACTION|>",
)
COMMAND_A_JSON_TAG = CohereTagRegistry(
    trigger="<|START_RESPONSE|>",
    end="<|END_RESPONSE|>",
)
COMMAND_A_PLUS_JSON_TAG = CohereTagRegistry(
    trigger="<|START_TEXT|>",
    end="<|END_TEXT|>",
)
​
MODEL_TO_TAG_STYLE: dict[str, CohereTagStyle] = {
    "Cohere2ForCausalLM": CohereTagStyle(
        json_tags=(COMMAND_A_JSON_TAG,),
        tools=COMMAND_A_TOOLS_TAG,
    ),
    "Cohere2VisionForConditionalGeneration": CohereTagStyle(
        json_tags=(COMMAND_A_JSON_TAG, COMMAND_A_PLUS_JSON_TAG),
        tools=COMMAND_A_TOOLS_TAG,
    ),
    "Cohere2MoeForCausalLM": CohereTagStyle(
        json_tags=(COMMAND_A_JSON_TAG, COMMAND_A_PLUS_JSON_TAG),
        tools=COMMAND_A_TOOLS_TAG,
    ),
}
​
​
def convert_schema_to_structural_tags(
    schema: dict | None = None,
    tools: str | list[Any] | None = None,
    model_architecture: str | None = None,
) -> str | None:
    # 返回 xgrammar 认可的 structural_tag response_format;未知架构或
    # 缺少 model_config 时返回 None,让调用方走原路径。
    if model_architecture is None or model_architecture not in MODEL_TO_TAG_STYLE:
        return None
    style = MODEL_TO_TAG_STYLE[model_architecture]
​
    tags: list[dict] = []
    triggers: list[str] = []
​
    def _add_tag(tag: CohereTagRegistry, content: dict) -> None:
        # 每个 tag 用 begin / end 标记触发范围,content 限定范围内生成约束;
        # triggers 汇总所有 begin,供 xgrammar 触发匹配。
        tags.append({"begin": tag.trigger, "content": content, "end": tag.end})
        triggers.append(tag.trigger)
​
    if schema is not None:
        # JSON schema 需要逐个包裹;MOE 同时输出 RESPONSE 与 TEXT 两套
        # 分隔符,因此 json_tags 是元组,逐个注册。
        for jt in style.json_tags:
            _add_tag(jt, {"type": "json_schema", "json_schema": schema})
​
    if _has_effective_tools(tools):
        # 工具定义可能来自 JSON 字符串(poseidon 场景)或请求体列表
        # (OpenAI Chat / Responses 格式),先归一化再生成 EBNF grammar。
        tool_schema_list = _tool_definitions_to_schema_list(tools)
        if not tool_schema_list:
            raise ValueError(
                "No valid tool definitions could be parsed from the request for "
                "structural tag conversion."
            )
        tool_grammar = collect_tool_schema(tool_schema_list)
        _add_tag(style.tools, {"type": "grammar", "grammar": tool_grammar})
​
    if not tags:
        return None
    return json.dumps(
        {
            "type": "structural_tag",
            "format": {
                "type": "triggered_tags",
                "triggers": triggers,
                "tags": tags,
            },
        }
    )
vllm/reasoning/cohere_command_reasoning_parser.py dependency-wiring

从 670 行精简到约 60 行,保留 is_reasoning_end 与 count_reasoning_tokens 供 structured-output 引擎使用

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
​
from __future__ import annotations
​
from collections.abc import Sequence
from typing import TYPE_CHECKING, ClassVar
​
from vllm.reasoning import ReasoningParser
​
if TYPE_CHECKING:
    from vllm.tokenizers import TokenizerLike
​
​
class BaseCohereCommandReasoningParser(ReasoningParser):
    """Expose is_reasoning_end for the structured-output engine.
    Everything else runs in
    :class:`vllm.parser.cohere_command.CohereCommandParser`.
    """
​
    melody_preset: ClassVar[str]
​
    def __init__(self, tokenizer: TokenizerLike, *args, **kwargs):
        super().__init__(tokenizer, *args, **kwargs)
        # 只缓存推理边界 token 的 id;真正的解析状态机已上移到
        # CohereCommandParser,这个类退化为 structured-output 引擎的钩子。
        self.start_token_id = tokenizer.convert_tokens_to_ids("<|START_THINKING|>")
        self.end_token_id = tokenizer.convert_tokens_to_ids("<|END_THINKING|>")
        self.chatbot_token_id = tokenizer.convert_tokens_to_ids("<|CHATBOT_TOKEN|>")
​
    @property
    def reasoning_start_str(self) -> str | None:
        return "<|START_THINKING|>"
​
    @property
    def reasoning_end_str(self) -> str | None:
        return "<|END_THINKING|>"
​
    def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
        # 从后向前扫描:遇到 START 时返回是否已见 END;先遇到 CHATBOT
        # 说明这段历史属于先前轮次,忽略。这样对完整 prompt / history
        # 也能把推理结束判定限定在最后一个对话轮次内。
        chatbot = self.chatbot_token_id
        start = self.start_token_id
        end = self.end_token_id
        has_end_token = False
​
        for i in reversed(range(len(input_ids))):
            tid = input_ids[i]
            if tid == start:
                return has_end_token
            if tid == chatbot:
                return False
            if tid == end:
                has_end_token = True
​
        return has_end_token
​
​
class CohereCommand3ReasoningParser(BaseCohereCommandReasoningParser):
    melody_preset = "cmd3"
​
​
class CohereCommand4ReasoningParser(BaseCohereCommandReasoningParser):
    melody_preset = "cmd4"
vllm/tool_parsers/cohere_command_tool_parser.py dependency-wiring

从 148 行精简到 18 行,仅保留 melody_preset 声明,体现 registry 兼容的 shim 模式

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
​
from typing import ClassVar
​
from vllm.tool_parsers import ToolParser
​
​
class BaseCohereCommandToolParser(ToolParser):
    # 旧实现在这里维护 melody PyFilter 与流式增量解析;统一后仅保留
    # preset 声明,实例化与解析均由 CohereCommandParser 完成。
    melody_preset: ClassVar[str]
​
​
class CohereCommand3ToolParser(BaseCohereCommandToolParser):
    melody_preset = "cmd3"
​
​
class CohereCommand4ToolParser(BaseCohereCommandToolParser):
    melody_preset = "cmd4"

评论区精华

无实质技术讨论,维护者直接批准 other

PR 来自 fork,claude[bot] 自动 review 被禁用;唯一审核意见是维护者 sfeng33 的 “Thank you for the making the migration, LGTM!”。Issue 评论 5 条均为 mergify 预览与 CI 触发。

结论:无未解决的技术疑虑;迁移方案获得维护者认可。 · 已解决

风险与影响

  • 解析行为回归(高):所有 Cohere 前端解析由两套旧实现切换到一个新实现,流式 delta 形状、tool_call_id 挂起缓存、citations 偏移等细节的细微差异都会影响线上输出;缓解靠用例整体迁移与新增 chunk_size 参数化断言。
  • 外部实例化兼容性(中):CohereCommand3ToolParser / CohereCommand4ToolParser 类名保留但不再具备解析能力,仓库外直接实例化的调用方会拿到只有 melody_preset 的空壳。
  • 依赖门槛(中):cohere-melody 从 >=0.11.1 提升到 >=0.14.0,旧环境 import 即失败;三平台测试 requirements 已同步,生产环境需留意。
  • 架构映射维护(低):MODEL_TO_TAG_STYLE 是硬编码表,未来新增 Cohere 架构变体时容易遗漏,导致 convert_schema_to_structural_tags 静默返回 None。
  • 用户:Cohere Command A / A+ / MOE / Vision 系列模型的 tool calling、reasoning、citations 与结构化输出行为统一,特殊 token 不再泄漏。
  • 系统:解析逻辑收敛到 vllm/parser/cohere_command.py 单文件,后续修 bug 或加能力只需改一处。
  • 团队:测试目录从按解析器类型组织改为按能力组织;与 mistral / gpt-oss 模式一致,可作为其他模型家族迁移模板。
  • CI:tests/parser/cohere 新旧目录切换,cohere-melody 版本约束三平台同步升级。
核心解析路径变更 大量旧解析逻辑删除 cohere-melody 版本门槛提升 外部直接实例化兼容性 架构映射表需持续维护

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论