执行摘要
- 一句话:新增 Cohere /v2/chat API 入口及 Melody 渲染支持
- 推荐动作:值得精读。本 PR 是 vLLM 引入厂商专属前端 API 的样板实现,有四个值得关注的设计决策:一是可选依赖探针 + VLLM_ENABLE_COHERE_API 显式 opt-in 的门控组合,既保证零依赖也能启动,又避免默认暴露新端点;二是通过子类化 + SerializeAsAny + 构造钩子扩展 OpenAI 协议而非直接改写协议类,体现了协议间的最小侵入原则;三是 handler 必须模块级定义以规避 FastAPI get_type_hints 陷阱(注释里专门记录了这个易错点);四是用 CohereErrorEnvelopeMiddleware 统一错误翻译,保证所有错误路径(含中间件内部错误)都符合 Cohere wire 形状。建议同时关注两个遗留点:/cohere 鉴权前缀问题是否已被后续 PR 修复,以及 parser adjust_response 钩子的 follow-up。
功能与动机
RFC #43015 明确指出:vLLM 不支持 Cohere Chat v2 API,该协议与 chat completions 有诸多不同字段,尤其是 assistant 消息内的 citations 需要渲染进 prompt,这对 Cohere 用户至关重要。PR body 也说明实现路径为仿照 create_chat_completion 的流程,用 Melody 库的 prompt 渲染函数产出聊天模板,输出解析逻辑则沿用现有管线。Review 中 chaunceyjiang 提出此前 WoosukKwon 建议避免新增端点,因此最终通过 VLLM_ENABLE_COHERE_API 环境变量将端点默认关闭,保持缺省行为不变。
实现拆解
1. 协议与消息模型层
在 vllm/entrypoints/cohere/protocol.py 中,wire 类型尽量复用官方 cohere SDK(如 ChatMessageV2、CitationOptions、ToolV2 等),本地只拥有三样东西:顶层请求体 CohereChatV2Request(SDK 不提供该模型)、非流式响应外壳 CohereChatV2Response、以及流式事件的判别联合(SDK 只导出单个事件类型)。CohereChatV2Request 通过 field_validator 做 _validate_model、_validate_max_tokens、_validate_messages 等校验。
在 vllm/entrypoints/cohere/cohere_chat_message.py 中定义 vLLM 内部 Citation / CitationSource 表示,以及 CohereChatMessage / CohereDeltaMessage(ChatMessage / DeltaMessage 的子类,仅增加 citations 字段),避免把 Cohere 特有字段塞进 OpenAI 协议类;该模块刻意不 import cohere SDK,使不依赖 SDK 的 reasoning parser 也能引用。
2. 服务转换与流式输出层
vllm/entrypoints/cohere/serving.py(新增 1678 行)实现 CohereServingChatV2:把 v2 请求翻译成标准 ChatCompletionRequest 后委托 OpenAIServingChat;非流式路径构造 CohereChatV2Response,流式路径用 _sse / _emit 把 MessageStartEvent、ContentStartEvent、CitationStartEvent、ContentDeltaEvent、ToolCallStartEvent 等事件包成裸 data: SSE 帧,消息结束再发 _DONE_FRAME("[DONE]",由上游 OpenAPI x-fern-streaming.terminator 声明,cohere-python SDK 依赖它终止读取循环)。
3. 渲染层
vllm/renderers/cohere.py(新增 704 行)实现 CohereRenderer:通过 --tokenizer-mode cohere 启用,用 cohere_melody(Rust 绑定)的 render_cmd3 / render_cmd4 渲染 Command 系列提示词。渲染器的客户端接口是 chat_template_kwargs,需同时承接两套词汇表:CohereServingChatV2 翻译后的 v2 字段(documents、tools、safety_mode、citation_options、response_format、thinking)与直接透传的 melody 模板旋钮(cohere_format、template_id、reasoning_type、json_mode 等);其中 citation_options.mode 会被归一化为 cmd3 的 citation_quality 与 cmd4 的 grounding。显式传入 template_jinja / template 会抛 ValueError,防止绕过 trust_request_chat_template 守卫。
4. 路由接线与门控
vllm/entrypoints/cohere/api_router.py(新增 244 行)在模块加载时用 import 探针探测 cohere SDK 是否可用,不可用时 attach_router 退化为 no-op;同时要求 VLLM_ENABLE_COHERE_API=1 才注册路由。chat_v2 handler 必须定义在模块顶层(不能嵌在 attach_router 内),否则 FastAPI 的 typing.get_type_hints 解析不到 CohereChatV2Request 注解,body 参数会被静默降级为 query 参数导致全部请求 422。错误路径通过 _translate_vllm_error_body 与 CohereErrorEnvelopeMiddleware 把 vLLM 内部 ErrorResponse 统一翻译成 Cohere 的 {message, id} 形状;模型未启用该 API 时返回 501。
5. 对 OpenAI 协议的最小侵入
OpenAI serving 新增 _create_chat_message 构造方法(早期版本用 _chat_message_cls 类变量,review 中 DarkLight1337 建议改为方法),使子类能替换非流式响应中的消息类型;ChatCompletionResponseChoice.message 改为 SerializeAsAny[ChatMessage] 以便 pydantic 序列化时保留子类字段。vllm/reasoning/cohere_command_reasoning_parser.py(+149/-4)新增 _melody_sources_to_vllm / _melody_citations_to_vllm,把 melody 输出的 citations 解析为 vLLM 内部 Citation 并在流式路径发射。
6. 测试、配置与文档配套
新增 tests/entrypoints/cohere/(api_router、protocol、serving_conversion、serving_streaming、chat_v2 集成 6 项)与 tests/renderers/test_cohere.py 共 237 个测试;requirements/test 增加 cohere SDK 测试依赖;文档同步更新了 Cohere API 用法与 --tokenizer-mode cohere 说明。31 个 commit 中有 6 次 merge main,说明 PR 生命周期长、反复解决冲突,其中多次 commit(如 Fix sanitize message、Add error handling middleware、added env var 等)对应 review 反馈的迭代。
关键文件:
vllm/entrypoints/cohere/api_router.py(模块 请求路由;类别 source;类型 entrypoint;符号 attach_router, _serving, _request_id, _error_response): 新 API 入口核心文件:注册 POST /cohere/v2/chat 路由,负责 cohere SDK 可选导入探针、VLLM_ENABLE_COHERE_API 门控、错误翻译(_error_response / _translate_vllm_error_body / CohereErrorEnvelopeMiddleware)以及模型不支持时的 501 回退;handler 的模块级定义规避 FastAPI get_type_hints 陷阱。
vllm/entrypoints/cohere/serving.py(模块 服务转换;类别 source;类型 core-logic;符号 _sse, _emit, _DONE_FRAME, ContentBlockType): Cohere v2 服务转换核心(新增 1678 行):把 v2 请求翻译为 ChatCompletionRequest 后委托 OpenAIServingChat;实现非流式响应组装、SSE 事件流(含 CitationStartEvent 等判别事件)与 [DONE] 终止帧、finish reason 映射。
vllm/renderers/cohere.py(模块 提示渲染;类别 source;类型 core-logic;符号 MelodyContentType, _try_import_melody, _normalize_cmd4_grounding, _role_to_melody): 新增 CohereRenderer(704 行):用 cohere_melody Rust 绑定的 render_cmd3 / render_cmd4 渲染 Command 系列提示词,替代 Jinja;统一承接 Cohere v2 翻译字段与直接透传的 melody 旋钮两套 chat_template_kwargs 词汇表,并显式拒绝 template_jinja 防止模板注入。
vllm/entrypoints/cohere/protocol.py(模块 协议模型;类别 source;类型 core-logic;符号 CohereError, CohereChatV2Request, CohereChatV2Response, _validate_model): Cohere v2 协议模型(362 行):复用 cohere SDK 的 wire 类型,本地只拥有请求体、非流式响应外壳和流式事件判别联合;实现请求校验(model、max_tokens、messages)与错误模型 CohereError。
vllm/entrypoints/cohere/cohere_chat_message.py(模块 消息模型;类别 source;类型 core-logic;符号 CitationSource, Citation, CohereChatMessage, _serialize): 关键的协议隔离设计(147 行):定义 vLLM 内部 Citation/CitationSource,以及 CohereChatMessage/CohereDeltaMessage 子类,让 citations 不污染 OpenAI 协议类;刻意不 import cohere SDK 以便 reasoning parser 无 SDK 也能引用。
vllm/reasoning/cohere_command_reasoning_parser.py(模块 推理解析;类别 source;类型 core-logic;符号 _melody_sources_to_vllm, _melody_citations_to_vllm): 唯一被大改的既有推理解析器(+149/-4):新增 _melody_sources_to_vllm / _melody_citations_to_vllm,把 melody 输出的 citations 解析成 vLLM 内部 Citation 表示,支撑流式路径的 CitationStartEvent / CitationEndEvent 发射。
vllm/entrypoints/openai/chat_completion/serving.py(模块 聊天服务;类别 source;类型 core-logic;符号 _create_chat_message): 对 OpenAI serving 的最小侵入改动:新增 _create_chat_message 构造方法(从 _chat_message_cls 类变量演进而来),让子类可以替换非流式响应中的消息类型而不复制分支逻辑;该扩展点是 CohereChatMessage 落地的关键挂载点。
tests/entrypoints/cohere/test_api_router.py(模块 路由测试;类别 test;类型 test-coverage;符号 _enable_cohere_api, _Handler, create_chat_v2, _build_app): 覆盖新入口的核心行为:cohere SDK 缺失时 attach_router 为 no-op、VLLM_ENABLE_COHERE_API 未设时不注册路由、响应形状(JSON + SSE)、错误翻译与 501 回退;autouse fixture 统一开启环境变量。
关键符号:chat_v2, attach_router, _error_response, _translate_vllm_error_body, CohereServingChatV2.create_chat_v2, _emit, _map_finish_reason, CohereChatMessage._serialize, _melody_sources_to_vllm, _melody_citations_to_vllm, OpenAIServingChat._create_chat_message, _normalize_cmd4_grounding, _role_to_melody, _content_blocks
关键源码片段
vllm/entrypoints/cohere/serving.py
Cohere v2 服务转换核心(新增 1678 行):把 v2 请求翻译为 ChatCompletionRequest 后委托 OpenAIServingChat;实现非流式响应组装、SSE 事件流(含 CitationStartEvent 等判别事件)与 [DONE] 终止帧、finish reason 映射。
# 流式响应包帧:Cohere v2 的 SSE 使用裸 "data:" 行(不带 event: 前缀),
# 事件类型由每个 JSON 对象的 type 字段承担,因此这里只需要最小包装。
def _sse(data: str) -> str:
return f"data: {data}
"
# 流结束符 [DONE],由上游 OpenAPI 规范声明(x-fern-streaming.terminator),
# cohere-python / Fern 生成的客户端靠它终止读取循环
# (Python SDK 在 _sse.data == "[DONE]" 时 break)。
# 必须放在最后一条 message-end 事件之后发出。
_DONE_FRAME = _sse("[DONE]")
def _emit(event: BaseModel) -> str:
"""把类型化流事件序列化成 SSE 帧。"""
# protocol 模块里每个事件模型(MessageStartEvent、ContentStartEvent、
# CitationStartEvent、ToolCallStartEvent、...)都把 wire 格式的
# type 判别字段内建在模型定义上,因此 model_dump_json 天然携带
# 判别器,无需再手工拼接 type 字段。
return _sse(event.model_dump_json(exclude_none=True))
评论区精华
讨论核心集中在两条主线:一是 OpenAI 协议能否被其他 API 扩展(DarkLight1337 与作者多次交锋);二是自动化安全扫描(depthfirst-app[bot])提出的鉴权与信息泄漏问题。
- DarkLight1337 反对直接在 ChatMessage 上加 citations 字段,认为应把公共代码抽出来让两套协议各自持有协议类;作者随后改为 CohereChatMessage 子类方案,并保留少量扩展点。关于 ChatCompletionResponseChoice 等 4 个响应外壳类是否也独立,作者权衡后认为重写 4 个类及其构造钩子维护成本高,DarkLight1337 则坚持 schema 独立以免 OpenAI API 改动意外影响 Cohere,最终双方达成一致:Cohere 方承担维护责任,保持共享外壳类现状。
- depthfirst-app[bot] 报出 CRITICAL:/cohere/v2/chat 路径不在 AuthenticationMiddleware 的 GUARDED_PREFIX 中,--api-key 场景下可无凭证调用该端点;材料中未见该问题被修复或作者回应的记录(文件列表也不含 server_utils.py),需要在合并后确认。
- depthfirst 报 HIGH:用户可控的 chat_template_kwargs["template_jinja"] 直达 melody 的 Jinja 引擎,绕过仅检查 chat_template 的 trust_request_chat_template 守卫;最终代码以显式拒绝(显式设置 template_jinja / template 抛 ValueError)收敛该攻击面。
- aarnphm 提议给 parser 增加 adjust_response 钩子,让 citations 这类 parser 产出的元数据不要混进 serving 类;sfeng33 赞同但不阻塞,作者承诺在 follow-up 中推进。
-
作者在最终版本中把错误消息统一经 sanitize_message(修复了流式 SSE 错误路径未 sanitize、以及 sanitize_message 漏导入会触发 NameError 的问题),并对 AI 生成的冗余测试做了清理。
-
是否允许 OpenAI 协议类承载 Cohere 特有字段 (design): 保留子类化方案:CohereChatMessage 继承 ChatMessage 增加 citations,响应外壳类继续共享 OpenAI 的;Cohere 方承担后续维护责任。
- /cohere 前缀不在认证 GUARDED_PREFIX 中 (security): 材料中未见作者回应或 server_utils.py 改动记录,合并时是否已修复无法确认,需在合并后立即核验;若仍存在,应把 /cohere 加入 GUARDED_PREFIX 或为该路由单独加鉴权。
- chat_template_kwargs["template_jinja"] 模板注入 (security): 最终 renderer 文档明确:显式向 chat_template_kwargs 传 template_jinja / template 会抛 ValueError,以显式拒绝收敛该攻击面。
- 错误消息 sanitize 一致性(JSON 与 SSE 路径) (security): 提交 "Fix sanitize message not being called for streaming errors" 与最终代码中的 import / 调用确认 JSON 与流式两条路径均已接入 sanitize_message。
- parser 是否应提供 adjust_response 钩子 (design): 双方确认非阻塞,作者承诺在 follow-up 中实现 parser adjust_response 钩子。
- 新端点默认关闭:环境变量显式 opt-in (design): 作者新增 VLLM_ENABLE_COHERE_API=1 显式 opt-in 门控,并补充 attach_router 无 SDK 时不注册路由的双重保险。
- tests 疑似 AI 生成需清理 (testing): 作者确认清理,提交记录中也包含 "Remove trivial tests"。
风险与影响
- 风险:
- 鉴权绕过风险(高,待确认):/cohere 前缀不在 vllm/entrypoints/serve/utils/server_utils.py 的 GUARDED_PREFIX(/v1、/v2、/inference)内,配置 --api-key 时该端点可能绕过 AuthenticationMiddleware。PR 文件列表中没有 server_utils.py 的改动,也未看到作者回应,合并后是否存在该漏洞需要立即核验;若仍存在,应把 /cohere 加入 GUARDED_PREFIX 或在该路由上加独立鉴权。
- 对 OpenAI serving 核心路径的回归风险(中):_create_chat_message 构造钩子与 SerializeAsAny 改动影响所有 OpenAI chat completion 响应路径,属于高频核心代码;若子类化或序列化行为有偏差,可能影响非 Cohere 用户的响应结构。依赖现有 OpenAI 测试覆盖,建议关注后续是否有相关回归报告。
- 可选依赖门控的健壮性(低-中):import 探针只捕获 ImportError,若 cohere 包存在但版本过旧导致部分导入失败,会以 ImportError 形式在模块加载期抛错;VLLM_ENABLE_COHERE_API 门控能避免非 Cohere 部署暴露端点,但测试依赖中引入 cohere SDK 也意味着 CI 需持续跟进上游 SDK 的兼容性。
- 错误信息泄漏风险(低):最终代码在 JSON 与 SSE 两条错误路径均调用 sanitize_message,但 CohereErrorEnvelopeMiddleware 翻译 vLLM 错误体时若遗漏某个嵌套字段,仍可能把内部路径或地址透出,建议补充覆盖验证。
- 协议耦合维护风险(低):复用 OpenAI 的响应外壳类意味着 Cohere 协议会随 OpenAI schema 变动的涟漪,属于有意识的维护成本交换,测试应能兜底。
- 影响:对用户:Cohere Command 系列模型用户可通过 POST /cohere/v2/chat 使用原生 v2 协议(含 citations、documents、tools、thinking 等字段),流式与非流式均支持;端点默认关闭,需设置 VLLM_ENABLE_COHERE_API=1 并安装 cohere SDK 才能启用,因此对其他用户完全无感。对系统:新增一层前端协议转换,不触碰调度、注意力、内核等核心执行路径;主要改动集中在 entrypoints / renderers / reasoning 三个前端模块,对 OpenAI serving 仅有少量扩展点。对团队:Cohere 方(作者)明确承担该协议的后续维护;打开了一个 parser 侧 adjust_response 钩子的重构方向(aarnphm / sfeng33 均认可)。整体影响范围可控、向后兼容。
- 风险标记:鉴权前缀风险待确认, OpenAI serving 核心路径改动, 默认关闭降低暴露面, 模板注入已显式拒绝
关联脉络
- PR #48915 [Frontend][Core][Spec Decode] Per-request acceptance stats in OpenAI API responses: 同样在 OpenAI 前端协议(engine/protocol.py)与 serving 层扩展响应结构并新增可选字段,与本 PR 对 ChatMessage / 响应协议的扩展模式相呼应,可作为协议演进与兼容性维护的对照参考。
参与讨论