执行摘要
- 一句话:为 Cohere ChatV2 新增 render 端点,仅分词不生成
- 推荐动作:值得精读,尤其是
vllm/entrypoints/cohere/serving.py 中把请求转换逻辑提取为公开方法的设计:它让 render 端点在零 tokenizer/engine 依赖下复用完整转换链路,是「协议转换与生成执行解耦」的干净示例。关注 Cohere 兼容层或 render 端点扩展的同学可重点看 api_router.py 的路由错误处理与 test_chat_v2.py 的跨层契约测试。
功能与动机
PR body 明确说明目的是 "Add /cohere/v2/chat/render endpoint to match other endpoint prompt render capability",即让 Cohere Chat v2 与其他端点(如 OpenAI chat render)保持一致的 prompt 渲染能力,使调用方可以在不触发生成的情况下预检模板化与分词结果。
实现拆解
实现分四步完成:
-
提取公开转换方法(serving.py):把 CohereServingChatV2 中原本内联在 create_chat_v2 里的 _convert_v2_to_chat_completion 调用及 debug 日志封装为公开方法 to_chat_completion_request,create_chat_v2 改为调用该方法。这样 render 端点无需实例化完整 chat serving 即可复用同一套 v2→ChatCompletionRequest 转换,避免两条路径逻辑漂移。
-
新增 render 路由(api_router.py):在 Cohere 路由中新增 _serving_render(request) 辅助函数读取 app.state.serving_render;随后注册 @router.post("/cohere/v2/chat/render"),响应模型为 GenerateRequest。路由先通过 handler.to_chat_completion_request 把 CohereChatV2Request 转成 ChatCompletionRequest,再交给 ServingRender.render_chat_request,最终原样返回 GenerateRequest;对 ErrorResponse、handler 缺失(501)和异常(500)的处理与既有 /cohere/v2/chat 保持一致的 CohereError 错误格式。
-
环境变量注释同步(envs.py):更新 VLLM_ENABLE_COHERE_API 的注释,说明开启后同时暴露 chat 与 render 两个端点。行为不变,仍默认关闭。
-
测试配套:
tests/entrypoints/cohere/test_api_router.py 新增 _RenderChatHandler、_RenderHandler、_generate_request、_build_render_app 等测试脚手架,以及 TestRenderEndpoint 覆盖转换链路、ErrorResponse 透传、异常 500、校验 400/422 等分支;
tests/entrypoints/cohere/test_chat_v2.py 新增 3 个端到端用例,验证采样参数(temperature、stop_sequences)在 v2→ChatCompletionRequest→SamplingParams 链路中保留,以及通过 /detokenize 反查确认 token_ids 是真实模板化后的 prompt。
关键文件:
vllm/entrypoints/cohere/api_router.py(模块 路由层;类别 source;类型 entrypoint;符号 _serving_render, chat_v2_render): 新增 POST /cohere/v2/chat/render 路由及 _serving_render 辅助函数,是整个 PR 的入口层核心变更,负责把 Cohere v2 请求经转换后送入通用 ServingRender,并统一错误翻译。
vllm/entrypoints/cohere/serving.py(模块 服务层;类别 source;类型 core-logic;符号 to_chat_completion_request): 把 _convert_v2_to_chat_completion 封装为公开 to_chat_completion_request,create_chat_v2 改走该方法;这是让 render 端点复用转换逻辑、避免双路径漂移的关键重构。
tests/entrypoints/cohere/test_api_router.py(模块 单测;类别 test;类型 test-coverage;符号 _RenderChatHandler, to_chat_completion_request, _RenderHandler, init): 新增 render 路由的单元测试,通过假 handler 覆盖转换链路、ErrorResponse 透传、异常 500、校验错误等分支,保证路由错误语义与 chat 端点一致。
tests/entrypoints/cohere/test_chat_v2.py(模块 集成测试;类别 test;类型 test-coverage;符号 test_cohere_v2_chat_render, test_cohere_v2_chat_render_token_ids_decode_to_the_prompt, test_cohere_v2_chat_render_validation_error_returns_400): 端到端验证 render 端点返回的 token_ids 是真实模板化 prompt(通过 /detokenize 反查),并验证 v2 采样字段在跨层转换中保留,是 render 能力正确性的关键保障。
vllm/envs.py(模块 环境变量;类别 source;类型 configuration): 同步更新 VLLM_ENABLE_COHERE_API 环境变量注释,说明开启后同时暴露 chat 与 render 端点;属于文档性配置修改。
关键符号:chat_v2_render, _serving_render, to_chat_completion_request
关键源码片段
vllm/entrypoints/cohere/api_router.py
新增 POST /cohere/v2/chat/render 路由及 _serving_render 辅助函数,是整个 PR 的入口层核心变更,负责把 Cohere v2 请求经转换后送入通用 ServingRender,并统一错误翻译。
# POST /cohere/v2/chat/render 路由与 serving 读取辅助函数
# (基于 api_router.py 实际结构整理,省略请求校验中间件等外围细节)
def _serving_render(request: Request) -> ServingRender | None:
"""从 app.state 读取渲染服务实例,未注册时返回 None。"""
return getattr(request.app.state, "serving_render", None)
@router.post(
"/cohere/v2/chat/render",
dependencies=[Depends(validate_json_request)],
response_model=GenerateRequest,
)
async def chat_v2_render(
request: CohereChatV2Request, raw_request: Request
) -> GenerateRequest | JSONResponse:
"""镜像 /v1/chat/completions/render:只做分词与模板化,不运行生成。"""
handler = _serving(raw_request)
render = _serving_render(raw_request)
if handler is None or render is None:
# chat_v2 用 501 表示模型不支持,render 保持同样的语义
return JSONResponse(
status_code=HTTPStatus.NOT_IMPLEMENTED.value,
content=CohereError(
message="The model does not support the Cohere v2 chat API.",
id=_request_id(raw_request),
).model_dump(exclude_none=True),
)
try:
# 先复用 Cohere v2 的公开转换方法得到 OpenAI 请求,
# 再交给通用渲染服务,保证与 OpenAI render 端点路径一致
chat_req = handler.to_chat_completion_request(request)
result = await render.render_chat_request(chat_req)
except Exception as e: # noqa: BLE001 - 统一按 500 输出,与 chat_v2 对齐
logger.exception("Error in /cohere/v2/chat/render: %s", e)
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
content=CohereError(
message=sanitize_message(str(e)),
id=_request_id(raw_request),
).model_dump(exclude_none=True),
)
if isinstance(result, ErrorResponse):
# 转换或校验阶段产生的 vLLM 错误信封,统一翻译为 Cohere 错误格式
return _error_response(result, raw_request)
return result
评论区精华
本 PR 没有实质性的代码 review 讨论:Claude[bot] 因 PR 来自 fork 自动跳过审查;DarkLight1337 在解决冲突并触发 Buildkite CI 后直接 APPROVED。唯一的外部交互是 mergify[bot] 两次提示分支与 main 存在合并冲突、要求 rebase,作者通过两次 merge origin/main 解决。
- 合并冲突与 CI 触发 (other): 作者两次 merge origin/main 解决冲突,CI 通过后由 DarkLight1337 合并。
风险与影响
- 风险:
- ServingRender 注册依赖:
/cohere/v2/chat/render 依赖 app.state.serving_render 存在,若部署未启用 render 模块,_serving_render 返回 None。当前代码对 handler 或 render 缺失统一返回 501,但仍需确认 render 服务是否在所有启用 VLLM_ENABLE_COHERE_API 的部署中都被正确挂载,否则可能误报 501。
- create_chat_v2 重构回归:
create_chat_v2 从直接调用 _convert_v2_to_chat_completion 改为调用 to_chat_completion_request,行为等价(转换 + debug 日志),但作为既有聊天主路径,仍存在潜在的回归窗口,需依赖既有测试覆盖。
- 错误语义一致性:新端点复用了
_error_response 与 CohereError 信封,但路由中 handler is None 时返回的 message 沿用 chat 端点文案,未单独说明 render 不可用,可能对排障造成轻微混淆。
- 门控默认关闭:端点仍受
VLLM_ENABLE_COHERE_API 控制且默认关闭,对默认部署无影响,但用户若未显式开启会看不到该端点。
- 影响:影响范围集中在 Cohere 兼容 API 层:调用方现在可以在生成前通过
/cohere/v2/chat/render 获取模板化后的 token_ids 与 sampling_params,方便做 prompt 预检、缓存或前后端分离;对服务端核心路径无性能影响。团队层面看,这是 Cohere v2 协议对齐 OpenAI render 能力的重要一步,后续其他协议也可能复用同一 ServingRender 模式。由于是新增端点且门控默认关闭,对现有用户是严格向后兼容的增强。
- 风险标记:新端点依赖 serving_render 注册, create_chat_v2 调用路径重构, 门控默认关闭需显式开启
关联脉络
- PR #52467 [Misc] Use VLLMValidationError in Cohere request validation: 同属 Cohere 兼容层(protocol.py 与 tests/entrypoints/cohere/),此前已收紧请求校验语义;本 PR 进一步补齐 render 端点,形成 Cohere v2 API 的完整能力集。
参与讨论