Prhub

#47189 [Frontend] Cohere chat v2 api support

原始 PR 作者 andrewbcohere 合并时间 2026-08-01 13:53 文件变更 31 提交数 31 评论 40 代码增减 +8770 / -20

执行摘要

新增 Cohere /v2/chat API 入口及 Melody 渲染支持

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 环境变量将端点默认关闭,保持缺省行为不变。

值得精读。本 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。

讨论亮点

讨论核心集中在两条主线:一是 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 生成的冗余测试做了清理。

实现拆解

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 请求路由 added 9.17
vllm/entrypoints/cohere/serving.py 服务转换 added 8.89
vllm/renderers/cohere.py 提示渲染 added 8.98
vllm/entrypoints/cohere/protocol.py 协议模型 added 8.98
vllm/entrypoints/cohere/cohere_chat_message.py 消息模型 added 8.56
vllm/reasoning/cohere_command_reasoning_parser.py 推理解析 modified 7.45
vllm/entrypoints/openai/chat_completion/serving.py 聊天服务 modified 5.0
tests/entrypoints/cohere/test_api_router.py 路由测试 added 7.74

关键符号

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 core-logic

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}\n\n"
​
​
# 流结束符 [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 协议类承载 Cohere 特有字段 设计

DarkLight1337 反对直接在 ChatMessage 上加 citations 字段,主张抽出公共代码让两套协议各自持有协议类。作者尝试后改为子类方案,并通过 _create_chat_message 构造钩子与 SerializeAsAny 保持 OpenAI 协议类不被污染。关于 4 个响应外壳类是否也独立,作者指出需要重写 ChatCompletionResponseChoice 等类及其构造钩子,维护成本高;DarkLight1337 坚持 schema 独立以免 OpenAI 改动意外影响 Cohere,双方最终达成一致。

结论:保留子类化方案:CohereChatMessage 继承 ChatMessage 增加 citations,响应外壳类继续共享 OpenAI 的;Cohere 方承担后续维护责任。 · 已解决

/cohere 前缀不在认证 GUARDED_PREFIX 中 安全

depthfirst-app[bot] 报 CRITICAL:/cohere/v2/chat 不在 AuthenticationMiddleware 检查的 GUARDED_PREFIX(/v1、/v2、/inference)内,配置 --api-key 时该端点无需凭证即可访问,等于无鉴权的推理入口。

结论:材料中未见作者回应或 server_utils.py 改动记录,合并时是否已修复无法确认,需在合并后立即核验;若仍存在,应把 /cohere 加入 GUARDED_PREFIX 或为该路由单独加鉴权。 · unresolved

chat_template_kwargs["template_jinja"] 模板注入 安全

depthfirst-app[bot] 报 HIGH:用户可控的 template_jinja 直达 melody 的 Jinja 引擎,而既有 trust_request_chat_template 守卫只检查 chat_template 键,因此可绕过守卫执行任意模板代码。

结论:最终 renderer 文档明确:显式向 chat_template_kwargs 传 template_jinja / template 会抛 ValueError,以显式拒绝收敛该攻击面。 · 已解决

错误消息 sanitize 一致性(JSON 与 SSE 路径) 安全

depthfirst 连报三条:_error_response 未 sanitize 错误消息、SSE 流式异常用裸 str(exc) 会泄漏路径与地址、以及早期版本 sanitize_message 未导入会触发 NameError;Anthropic 处理器的同类路径已使用 sanitize_message。

结论:提交 "Fix sanitize message not being called for streaming errors" 与最终代码中的 import / 调用确认 JSON 与流式两条路径均已接入 sanitize_message。 · 已解决

parser 是否应提供 adjust_response 钩子 设计

aarnphm 在 OpenAIServingChat._create_chat_message 处提出:parser 最好也提供 adjust_response 钩子,让 citations 等 parser 产出的元数据在 parser 侧就调整好,避免 serving 类里堆砌转换逻辑;并 cc sfeng33 征求意见。sfeng33 赞同这是一个好方向,但不阻塞本 PR。

结论:双方确认非阻塞,作者承诺在 follow-up 中实现 parser adjust_response 钩子。 · deferred

新端点默认关闭:环境变量显式 opt-in 设计

chaunceyjiang 在 api_router.py 注册处提出:此前 WoosukKwon 建议避免新增端点,因此希望用默认关闭的环境变量控制路由注册,保持缺省行为不变。

结论:作者新增 VLLM_ENABLE_COHERE_API=1 显式 opt-in 门控,并补充 attach_router 无 SDK 时不注册路由的双重保险。 · 已解决

tests 疑似 AI 生成需清理 测试

DarkLight1337 在 tests/entrypoints/cohere/test_protocol.py 首行评论指出测试看起来像 AI 生成的,要求删除冗余无帮助的用例。

结论:作者确认清理,提交记录中也包含 "Remove trivial tests"。 · 已解决

风险与影响

  1. 鉴权绕过风险(高,待确认):/cohere 前缀不在 vllm/entrypoints/serve/utils/server_utils.py 的 GUARDED_PREFIX(/v1、/v2、/inference)内,配置 --api-key 时该端点可能绕过 AuthenticationMiddleware。PR 文件列表中没有 server_utils.py 的改动,也未看到作者回应,合并后是否存在该漏洞需要立即核验;若仍存在,应把 /cohere 加入 GUARDED_PREFIX 或在该路由上加独立鉴权。
  2. 对 OpenAI serving 核心路径的回归风险(中):_create_chat_message 构造钩子与 SerializeAsAny 改动影响所有 OpenAI chat completion 响应路径,属于高频核心代码;若子类化或序列化行为有偏差,可能影响非 Cohere 用户的响应结构。依赖现有 OpenAI 测试覆盖,建议关注后续是否有相关回归报告。
  3. 可选依赖门控的健壮性(低-中):import 探针只捕获 ImportError,若 cohere 包存在但版本过旧导致部分导入失败,会以 ImportError 形式在模块加载期抛错;VLLM_ENABLE_COHERE_API 门控能避免非 Cohere 部署暴露端点,但测试依赖中引入 cohere SDK 也意味着 CI 需持续跟进上游 SDK 的兼容性。
  4. 错误信息泄漏风险(低):最终代码在 JSON 与 SSE 两条错误路径均调用 sanitize_message,但 CohereErrorEnvelopeMiddleware 翻译 vLLM 错误体时若遗漏某个嵌套字段,仍可能把内部路径或地址透出,建议补充覆盖验证。
  5. 协议耦合维护风险(低):复用 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 核心路径改动 默认关闭降低暴露面 模板注入已显式拒绝

关联 Issue

#43015 [RFC]: Add support for Cohere's /v2/chat HTTP API entry point

完整报告

参与讨论